feat!: replace folder naming convention for actor discovery with explicit config file - #96
feat!: replace folder naming convention for actor discovery with explicit config file#96ruocco-l wants to merge 37 commits into
Conversation
…eric builder token
metalwarrior665
left a comment
There was a problem hiding this comment.
I think this is good and obviously needed change. It won't be that big of a hassle for other teams since adding new Actors is quite rare. We need to polish this though.
| '.actor/', | ||
| // In root .actor/ mode, .actor/ changes must trigger builds | ||
| ...(isSingleActorRepo ? [] : ['.actor/']), |
There was a problem hiding this comment.
We can remove this completely, there is no reason we should commit changes to top level .actor in multiactor
|
|
||
| let cachedBuilderUsername: string | undefined; | ||
|
|
||
| export const resolveBuilderTokenUsername = async (): Promise<string> => { |
There was a problem hiding this comment.
What is the use-case to need this? Just so you don't have to think about the username?
| }; | ||
|
|
||
| const readActorName = async (actorJsonPath: string): Promise<string> => { | ||
| const actorJson: { name?: string } = JSON.parse(await fs.readFile(actorJsonPath, 'utf-8')); |
There was a problem hiding this comment.
I would rather introduce a new JSON file for our own custom needs than stitch non-spec fields to actor.json.
There was a problem hiding this comment.
that would also enable us to cover miniactors, monorepos or single actor stuff without doing the
user-name_actor-name convention.
It could all be config stuff
| actorName: fullName, | ||
| folder: actorDir, | ||
| isStandalone: folderType === 'standalone-actors', | ||
| tokenEnvVar, |
There was a problem hiding this comment.
I'm not a big fan of passing the token around because it is more likely to leak, we should be able to just resolve it in place like we did no?
There was a problem hiding this comment.
This doesn't pass the token, just the name of the env variable where it's stored
|
We should also sync with @gullmar about other repos. The monorepo where each Actor has its own src is also probably not ideally supported. |
| const resolveOwner = async (folderName: string): Promise<string> => { | ||
| const ownerMatch = folderName.match(/^(.+)_[^_]+$/); | ||
| if (ownerMatch) return ownerMatch[1]; | ||
| return resolveBuilderTokenUsername(); | ||
| }; |
There was a problem hiding this comment.
So the owner still has to be defined in the folder name 😅 That kind of defeats the purpose of this entire effort 🫠
I would either want the conventionally named folder names only (owner + name in folder name), or to have both the owner and actor name in actor.json, but not both the conventionally named folder names for owners and actor.json for names at once 🙏
There was a problem hiding this comment.
No. IF the owner is there we automatically assign and look for the appropriate token. If not we fallback to whatever is the owner of the BUILDER token. This is still useful if you have a default account where there are all the miniactors (for which is ok to fallback the BUILDER token) and have one miniactor that is build under a different account (for whatever reason), in which case you will specify it with the folder name owner_whatever-actor-name-since-it's-not-used.
But I agree that just adding the owner to the actor.json is much cleaner
There was a problem hiding this comment.
I agree that for most repos, the single BUILDER token would work fine, since we usually have everything under one account. So, for the most part, this would only have to cover edge cases. Even then it seems like there should be a better way to define the edge case then by folder name - be it actor.json or a global config file 🤔
| }; | ||
|
|
||
| const readActorName = async (actorJsonPath: string): Promise<string> => { | ||
| const actorJson: { name?: string } = JSON.parse(await fs.readFile(actorJsonPath, 'utf-8')); |
|
Alright alright, let's do the config |
|
One more related long-term thing. When we designed this library, the core idea was "convention over configuration". We wanted everything to be at hardcoded places since that is always better than having to config and it worked well for that use-case. Now if we want many (or arbitrary) different setups, we cannot keep patching it like this because we would have to keep adding On the other hand, the library has to remain somewhat opinionated, like what file changes trigger build etc, otherwise it has basically no value, it would just be some configs + few API calls. I think for now, this PR is ok because top-level .actor is the template thing, but there probably already are things in the "changed Actors" that break. But for anything more, I would wait for th full rewrite. That should also make it truly open-source usable. |
metalwarrior665
left a comment
There was a problem hiding this comment.
I gave it another thought and I think it wouldn't be too hard to go all the way for really configurable config. Otherwise, we would have to potentially do another breaking change.
We should support (let's look of we can find more) at least 3 reference repos:
- typical Store monorepo
- template-like repo (e.g. WCC)
- monorepo with packages and actors importing them - e-commerce
I'm thinking it could work like this:
- Each Actor config points to the folder where .actor is (like now) or directly to the .actor folder.
- Then from actor.json, we read
dockerContextDirand that contains all folders/files that can influence the changes for that Actor. We can allow config to haveoverrideActorContextif they want to e.g. narrow it down. - Then instead of the current logic that if "code changes" we add mark all Actors as changed, we just do the changed files logic for each mini Actor independently, that cleans up the logic as well.
This way, we handle current Store monorepo (Actor folder + top-level context), template (just top-level), e-commerce (actor folder with its code, packages, shared)
What do you think?
| { | ||
| "actors": [ | ||
| { | ||
| "folder": "actors/web-scraper", |
There was a problem hiding this comment.
I would add actorName too rather than deriving it from the folder
There was a problem hiding this comment.
I thought about it, but I think can cause some confusion when you (usually when you publish the actor) play a little with the naming for SEO reasons (or similar). I think whatever is in the actor.json should be respected as the truth and not be overridden by some obscure logic from the testing package.
There was a problem hiding this comment.
The testing lib will not set or change the name, it just check that it exists. The name.in actor.json is not a single source of truth so I would not use it, better to have all here
| }); | ||
| }, | ||
| ) | ||
| .command( |
There was a problem hiding this comment.
I would remove this, Claude will one-shot this and you will want to manually check it anyway.
There was a problem hiding this comment.
I think this sort of CLI initialization utility perfectly fits into a library like here 🤔
However unless we make this a completely seamless end to end migration command, then I'm also against it. Right now as far as I can tell, it doesn't really migrate the ENV vars. I will open up a separate comment for that.
There was a problem hiding this comment.
metalwarrior665
left a comment
There was a problem hiding this comment.
Thanks for another rewrite! I think we are almost done.
| ); | ||
| } | ||
|
|
||
| if (!contextPaths.some((contextPath) => isPathWithinScope(folder, contextPath))) { |
There was a problem hiding this comment.
Do we need this? We carry over actorConfig.folder so we always know where our .actor lives. So we shouldn't need it inside the context paths
There was a problem hiding this comment.
I think of the context as "I need to track the files in these paths because that's what this actor care about". With this logic it looks correct to me that the .actor folder should always be listed/in the scope.
The middle ground for this would be to always forcefully add the folder path to the overridden context list, what do you think?
There was a problem hiding this comment.
I would add it automatically. I take as a special type of context that is always included but if you need it add explicitly, then for sure
|
|
||
| if (lowercaseFilePath.endsWith('changelog.md')) { | ||
| return { impact: 'cosmetic', semanticallyVerified: false, includes: 'all-actors' }; | ||
| if (dockerIgnoreMatcher(originalFilePath)) { |
There was a problem hiding this comment.
.actor can be legit inside .dockerignore as it is evaluated by Apify platform before Docker build. That can be used to make Dockerfile more cacheable if you don't want to COPY individual folders like we d now. So either we skip it from dockerIgnore or we handle it before/special path.
| } | ||
| if (lowercaseFilePath.endsWith('readme.md')) { | ||
| return { impact: 'cosmetic', semanticallyVerified: false, includes: actorConfigChanged }; | ||
| const lowerFolder = actorConfig.folder.toLowerCase(); |
There was a problem hiding this comment.
| const lowerFolder = actorConfig.folder.toLowerCase(); | |
| const lowerCaseFolder = actorConfig.folder.toLowerCase(); |
| * 1. Context matching (actorConfig.contextPaths) → outside-context if no match | ||
| * 2. Hardcoded ignore list, checked against the path hoisted relative to the matched context entry → ignored | ||
| * 3. .dockerignore filtering (patterns relative to dockerContextDir) → ignored if matched | ||
| * 4. README/CHANGELOG by filename → cosmetic if inside the actor's own folder, otherwise ignored |
There was a problem hiding this comment.
The Changelog change is wrong, in Store Actors, there is single shared changelog so it works differently from readme.
Instead of speculating what's the right setup, let's also do this more properly for all the Actor-only files. File is specific Actor related if:
- Is inside Actor folder (inside .actor or next to it) - We already handle this
- Is referenced from actor.json - if the file is outside of Actor folder but is referenced from actor.json, then it belongs to this Actor (and independenctly can belong to other Actor). So top-level Changelog.md will independently trigger change for every Actor (that points to it). We should parse this at the config file read. I think we can have a hardcoded list of files supported this way (readme, changelog, all schemas)
The ignored/cosmetic/functional logic depending on being test vs release and JSON file changes stays
EDIT: I'm dragging you through this PR a bit so feel free to just hotfix this with the old semantics and make TODO + issue for next iteration
| const isInActorFolder = isPathWithinScope(lowercaseFilePath, lowerFolder); | ||
|
|
||
| if (lowercaseFilePath.endsWith('readme.md') || lowercaseFilePath.endsWith('changelog.md')) { | ||
| return isInActorFolder ? { impact: 'cosmetic', semanticallyVerified: false } : { impact: 'ignored' }; |
|
Also pls update the PR title :) |
| * | ||
| * Returns a no-op matcher (always returns false) when the file is absent. | ||
| */ | ||
| export const loadDockerIgnore = (dockerContextDir: string): DockerIgnoreMatcher => { |
There was a problem hiding this comment.
I found this bug. loadDockerIgnore doesn't normalize a leading ./ (e.g. ./node_modules) before passing patterns to the ignore package, so any .dockerignore written in that common style (like ./node_modules, ./test) silently matches nothing :/
| afterEach(() => vi.restoreAllMocks()); | ||
|
|
||
| describe('loadDockerIgnore', () => { | ||
| it('returns no-op matcher when .dockerignore is absent', () => { |
There was a problem hiding this comment.
lets add a unit test for ./-prefixed pattern
There was a problem hiding this comment.
also edge cases like trailing slash
6ab8638 to
0483a8d
Compare
0483a8d to
48b7847
Compare
@metalwarrior665 what would be the usecase of |
With this folder structure you will have to have the dockercontext to look at the whole |
This is not about building but about deciding if a change in a file should rebuilt & retest a specific Actor. In e-commerce monorepo, most folders are totally unrelated even if they are in build context |
metalwarrior665
left a comment
There was a problem hiding this comment.
Great, I consider it done! Thanks for going through all.
Now the release will be a bit tricky. I published a beta apify-test-tools 0.9.0-beta.0. We cannot merge this because we automatically use latest and would break everyone. So until all repos migrate to this, we will have to keep rebasing this on top of master. But the migration should be simple.
Before starting the Store-wide migration, let's wait a few days if @Patai5 @JuanGalilea @oklinov will have some remarks.
I would not release this as 1.0. I would reserve that for the final public API that will be easy to use but any publisher and we will not break that for long time to come.
| try { | ||
| actorName = await readActorName('./.actor/actor.json'); | ||
| } catch { | ||
| return []; |
There was a problem hiding this comment.
what do you think about adding some error handling here (logging or throw)? looks like it may fail silently
There was a problem hiding this comment.
I guess you forgot to remove these comments from a month ago before approving :D Thank you for your review!
| actorName: `${circleActor.username}/${circleActor.name}`, | ||
| folder: actorConfigFound.folder, | ||
| isStandalone: actorConfigFound.isStandalone, | ||
| tokenEnvVar: 'APIFY_TOKEN_CIRC_LE', |
There was a problem hiding this comment.
should it be APIFY_TOKEN_CIRC_LE but not APIFY_TOKEN_CIRCLE? 🤔
| actorName: `${circleActor.username}/${circleActor.name}`, | ||
| folder: actorConfigFound.folder, | ||
| isStandalone: actorConfigFound.isStandalone, | ||
| tokenEnvVar: 'APIFY_TOKEN_CIRC_LE', |
There was a problem hiding this comment.
btw 'APIFY_TOKEN_CIRC_LE' looks like it could be a const instead of a hardcoded string
Closes #84
The CI tooling used to derive actor names and ownership entirely from folder naming conventions (
owner_actor-name). This PR replaces that convention with an explicit, mandatory config file (apify-test-tools.config.json) at the repo root, and reworks change detection to scope by each actor's Docker build context instead of hardcodedactors//standalone-actors/path regexes.The config file
Each repo declares its actors in
apify-test-tools.config.json, one entry per actor:folder(where the relevant .actor folder is),actorFullName(owner/name, now the sole source of truth — no longer reconstructed from the folder),tokenEnvVar(no fallback, no derivation from owner/repo organization fails hard if unset),overrideActorContextfor actors depending on paths outside their own folder.See the README for the full schema.
Why change detection is scoped by Docker context instead of top-level directories
The old detection matched changed files against hardcoded
actors//standalone-actors/prefixes, which couldn't express "this actor also depends onpackages/shared" or work for a single-actor repo with noactors/directory at all. Scoping by each actor'sdockerContextDir(withoverrideActorContextas an escape hatch) ties change detection to the same boundary Docker itself uses to build the actor, so the two can't drift apart..dockerignorefiltering follows the same reasoning: if Docker wouldn't see a file, it shouldn't trigger a rebuild either.Other design decisions
actorNamewas used ambiguously for both the fullowner/namestring and the platform-internal ID, which produced hard-to-catch bugs when the wrong one was used. Split intoactorFullName,actorRawId, andactorIdthroughout so each call site is unambiguous about which one it needs.ApifyBuilder.fromActorConfigreplacesfromActorName, which re-derived the token independently from the actor name rather than trusting the already-resolved config.Breaking change
Repos must add
apify-test-tools.config.jsonat the root, listing every actor with itsfolder,actorFullName, andtokenEnvVar(see README for the exact schema). The old folder-naming convention andBUILDER_APIFY_TOKENfallback are both gone — there's no compatibility path for repos that don't add the config file. Special-case support forCIRC_LEaccounts is also discontinued.