apify-test-tools 0.9.1-beta.8 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.github/actions/checkout-restore-dependencies/action.yaml +3 -29
  2. package/.github/scripts/check-major-tag-refs.mjs +1 -1
  3. package/.github/workflows/_move_major_tag.yaml +11 -3
  4. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +2 -2
  5. package/.github/workflows/public_platform-tests.yaml +1 -13
  6. package/.github/workflows/public_pr-build-test.yaml +4 -26
  7. package/.github/workflows/public_push-build-latest.yaml +4 -15
  8. package/.github/workflows/public_review.yaml +1 -8
  9. package/.github/workflows-package-version +1 -1
  10. package/CHANGELOG.md +8 -3
  11. package/CONTRIBUTING.md +11 -7
  12. package/README.md +169 -56
  13. package/bin/build-from-local.ts +15 -11
  14. package/bin/build.ts +127 -67
  15. package/bin/git.ts +140 -0
  16. package/bin/main.ts +128 -91
  17. package/bin/slack.ts +5 -2
  18. package/bin/types.ts +0 -73
  19. package/bin/utils/config/parser.ts +6 -5
  20. package/bin/utils/config/structures/base.ts +5 -1
  21. package/bin/utils/config/structures/grouped.ts +40 -0
  22. package/bin/utils/config/structures/legacy.ts +3 -1
  23. package/bin/utils.ts +7 -15
  24. package/dist/bin/build-from-local.d.ts +2 -2
  25. package/dist/bin/build-from-local.d.ts.map +1 -1
  26. package/dist/bin/build-from-local.js +11 -6
  27. package/dist/bin/build-from-local.js.map +1 -1
  28. package/dist/bin/build.d.ts +26 -4
  29. package/dist/bin/build.d.ts.map +1 -1
  30. package/dist/bin/build.js +90 -47
  31. package/dist/bin/build.js.map +1 -1
  32. package/dist/bin/git.d.ts +40 -0
  33. package/dist/bin/git.d.ts.map +1 -1
  34. package/dist/bin/git.js +118 -0
  35. package/dist/bin/git.js.map +1 -1
  36. package/dist/bin/main.d.ts +1 -20
  37. package/dist/bin/main.d.ts.map +1 -1
  38. package/dist/bin/main.js +65 -62
  39. package/dist/bin/main.js.map +1 -1
  40. package/dist/bin/slack.d.ts.map +1 -1
  41. package/dist/bin/slack.js +4 -2
  42. package/dist/bin/slack.js.map +1 -1
  43. package/dist/bin/types.d.ts +0 -64
  44. package/dist/bin/types.d.ts.map +1 -1
  45. package/dist/bin/utils/config/parser.d.ts.map +1 -1
  46. package/dist/bin/utils/config/parser.js +6 -5
  47. package/dist/bin/utils/config/parser.js.map +1 -1
  48. package/dist/bin/utils/config/structures/base.d.ts +3 -2
  49. package/dist/bin/utils/config/structures/base.d.ts.map +1 -1
  50. package/dist/bin/utils/config/structures/base.js +1 -0
  51. package/dist/bin/utils/config/structures/base.js.map +1 -1
  52. package/dist/bin/utils/config/structures/grouped.d.ts +6 -0
  53. package/dist/bin/utils/config/structures/grouped.d.ts.map +1 -0
  54. package/dist/bin/utils/config/structures/grouped.js +29 -0
  55. package/dist/bin/utils/config/structures/grouped.js.map +1 -0
  56. package/dist/bin/utils/config/structures/legacy.d.ts +1 -1
  57. package/dist/bin/utils/config/structures/legacy.d.ts.map +1 -1
  58. package/dist/bin/utils/config/structures/legacy.js +1 -1
  59. package/dist/bin/utils/config/structures/legacy.js.map +1 -1
  60. package/dist/bin/utils.d.ts +5 -2
  61. package/dist/bin/utils.d.ts.map +1 -1
  62. package/dist/bin/utils.js +6 -12
  63. package/dist/bin/utils.js.map +1 -1
  64. package/dist/test/unit/bin/build.test.d.ts +2 -0
  65. package/dist/test/unit/bin/build.test.d.ts.map +1 -0
  66. package/dist/test/unit/bin/build.test.js +46 -0
  67. package/dist/test/unit/bin/build.test.js.map +1 -0
  68. package/dist/test/unit/bin/git.test.js +104 -1
  69. package/dist/test/unit/bin/git.test.js.map +1 -1
  70. package/dist/test/unit/bin/utils/config/load-config.test.js +1 -0
  71. package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
  72. package/dist/test/unit/bin/utils/config/parser.test.js +23 -5
  73. package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
  74. package/dist/test/unit/bin/utils/config/structures/grouped.test.d.ts +2 -0
  75. package/dist/test/unit/bin/utils/config/structures/grouped.test.d.ts.map +1 -0
  76. package/dist/test/unit/bin/utils/config/structures/grouped.test.js +75 -0
  77. package/dist/test/unit/bin/utils/config/structures/grouped.test.js.map +1 -0
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/package.json +65 -63
  80. package/test/unit/bin/build.test.ts +68 -0
  81. package/test/unit/bin/git.test.ts +122 -0
  82. package/test/unit/bin/utils/config/load-config.test.ts +1 -0
  83. package/test/unit/bin/utils/config/parser.test.ts +26 -5
  84. package/test/unit/bin/utils/config/structures/grouped.test.ts +90 -0
  85. package/bin/github.ts +0 -69
  86. package/dist/bin/github.d.ts +0 -11
  87. package/dist/bin/github.d.ts.map +0 -1
  88. package/dist/bin/github.js +0 -59
  89. package/dist/bin/github.js.map +0 -1
package/README.md CHANGED
@@ -12,37 +12,15 @@ npm i -D apify-test-tools
12
12
 
13
13
  - Requires `vitest` version `3.2.0` or later (uses [annotate](https://vitest.dev/guide/test-context.html#annotate))
14
14
  - Make sure `target` and `module` in your `tsconfig.json`'s `compilerOptions` are set to `ES2022`
15
+ - Every Actor must already have at least one build under its default build tag (usually `latest`), built
16
+ from a Git repository version whose URL points to this repository. Build it once manually on the
17
+ platform before CI takes over. See [Repository check](#repository-check).
15
18
 
16
19
  ### 2. Create the config file
17
20
 
18
- Every repo that uses `apify-test-tools` must have an `apify-test-tools.config.json` file at the root. This file tells the tool which actors live in the repo, how to identify them, and which token to use.
21
+ Every repo that uses `apify-test-tools` must have an `apify-test-tools.config.json` file at the root. It lists the actors in the repo, their folders, their full names on the platform, and which env var holds the token to use for each. A top-level `mode` field picks the file's layout; leave it out to use the default `"legacy"` layout.
19
22
 
20
- ```json
21
- {
22
- "actors": [
23
- {
24
- "folder": "actors/web-scraper",
25
- "actorFullName": "myteam/web-scraper",
26
- "tokenEnvVar": "APIFY_TOKEN_MYTEAM"
27
- },
28
- {
29
- "folder": "actors/email-sender",
30
- "actorFullName": "myteam/email-sender",
31
- "tokenEnvVar": "APIFY_TOKEN_MYTEAM",
32
- "overrideActorContext": ["actors/email-sender", "packages/shared"]
33
- }
34
- ]
35
- }
36
- ```
37
-
38
- Each entry has:
39
-
40
- | Field | Required | Description |
41
- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | `folder` | yes | Relative path from repo root to the actor's own project directory — the folder that directly contains `.actor/actor.json` (i.e. `<folder>/.actor/actor.json`), the actor's README/CHANGELOG, and its source. Use `"."` for a single-actor repo where `.actor/` is at the root. |
43
- | `actorFullName` | yes | Full actor identifier in `owner/name` format (e.g. `"apify/web-scraper"`). This is the source of truth for the actor name — the `name` field in `actor.json` is not used. |
44
- | `tokenEnvVar` | yes | Name of the environment variable holding the Apify API token for this actor. No fallback — if the env var is not set at build time, the build fails. |
45
- | `overrideActorContext` | no | Array of paths (relative to repo root) that define which files are relevant to this actor. When set, replaces the `dockerContextDir` from `actor.json` for change detection. Useful when an actor depends on shared packages outside its Docker build context. Entries must not be prefixes of one another (e.g. `["", "code"]` or `["actors", "actors/foo"]` are rejected). The actor's own `folder` is always part of its context — if none of the listed entries reach it, it's added automatically. |
23
+ See [Config file](#config-file) for the available modes and every field.
46
24
 
47
25
  ### 3. Set up actor folders
48
26
 
@@ -97,11 +75,87 @@ mkdir -p test/platform/core
97
75
 
98
76
  See the [GitHub workflows](#github-worklows) section below.
99
77
 
78
+ ## Config file
79
+
80
+ `apify-test-tools.config.json` lives at the repo root and tells the tool which actors live in the repo, how to identify them, and which token to use.
81
+
82
+ ### Actor fields
83
+
84
+ Every actor in the config is described by these fields, whatever the mode:
85
+
86
+ | Field | Required | Description |
87
+ | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `folder` | yes | Relative path from repo root to the actor's own project directory — the folder that directly contains `.actor/actor.json` (i.e. `<folder>/.actor/actor.json`), the actor's README/CHANGELOG, and its source. Use `"."` for a single-actor repo where `.actor/` is at the root. |
89
+ | `actorFullName` | yes | Full actor identifier in `owner/name` format (e.g. `"apify/web-scraper"`). This is the source of truth for the actor name — the `name` field in `actor.json` is not used. |
90
+ | `tokenEnvVar` | yes | Name of the environment variable holding the Apify API token for this actor. In [grouped mode](#grouped-mode) it's inherited from the group unless the actor sets its own. No fallback — if the env var is not set at build time, the build fails. |
91
+ | `overrideActorContext` | no | Array of paths (relative to repo root) that define which files are relevant to this actor. When set, replaces the `dockerContextDir` from `actor.json` for change detection. Useful when an actor depends on shared packages outside its Docker build context. Entries must not be prefixes of one another (e.g. `["", "code"]` or `["actors", "actors/foo"]` are rejected). The actor's own `folder` is always part of its context — if none of the listed entries reach it, it's added automatically. |
92
+
93
+ ### Modes
94
+
95
+ The file's layout is picked by the top-level `mode` field. Each mode is a different way of writing the same thing: every mode resolves to a list of actors with the fields above, and the same rules apply to all of them — no two actors may share a `folder`, and no two entries may point at the same `actorFullName`. If `mode` is left out, [`"legacy"`](#legacy-mode) is used, so existing config files keep working unchanged.
96
+
97
+ ### Legacy mode
98
+
99
+ The default. A flat `actors` array where every entry carries all of its settings. Good for repos with a few actors, or where every actor has different settings.
100
+
101
+ ```json
102
+ {
103
+ "mode": "legacy",
104
+ "actors": [
105
+ {
106
+ "folder": "actors/web-scraper",
107
+ "actorFullName": "myteam/web-scraper",
108
+ "tokenEnvVar": "APIFY_TOKEN_MYTEAM"
109
+ },
110
+ {
111
+ "folder": "actors/email-sender",
112
+ "actorFullName": "myteam/email-sender",
113
+ "tokenEnvVar": "APIFY_TOKEN_MYTEAM",
114
+ "overrideActorContext": ["actors/email-sender", "packages/shared"]
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ `actors` must hold at least one entry. Every entry takes all the [actor fields](#actor-fields) directly, and `tokenEnvVar` is required on each one.
121
+
122
+ ### Grouped mode
123
+
124
+ Actors are organized into `groups`, and each group sets `tokenEnvVar` and `overrideActorContext` once for all of its actors. Good for repos where many actors share the same token or context.
125
+
126
+ ```json
127
+ {
128
+ "mode": "grouped",
129
+ "groups": {
130
+ "myteam": {
131
+ "tokenEnvVar": "APIFY_TOKEN_MYTEAM",
132
+ "overrideActorContext": ["packages/shared"],
133
+ "actors": [
134
+ { "folder": "actors/web-scraper", "actorFullName": "myteam/web-scraper" },
135
+ {
136
+ "folder": "actors/email-sender",
137
+ "actorFullName": "myteam/email-sender",
138
+ "overrideActorContext": ["actors/email-sender", "packages/mailer"]
139
+ }
140
+ ]
141
+ },
142
+ "partner": {
143
+ "tokenEnvVar": "APIFY_TOKEN_PARTNER",
144
+ "actors": [{ "folder": "actors/partner-crawler", "actorFullName": "partner/crawler" }]
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ `groups` is an object with at least one group. The group key (`myteam`, `partner` above) is only a label for readability; it has no effect on the result, and it does not need to match any folder or username. Each group needs a `tokenEnvVar` and at least one actor, and can optionally set `overrideActorContext`.
151
+
152
+ An actor inside a group can set its own `tokenEnvVar` or `overrideActorContext`, and that value replaces the group's for that actor only. Values are replaced, not merged: in the example above, `myteam/email-sender` gets `["actors/email-sender", "packages/mailer"]`, not `packages/shared` as well.
153
+
100
154
  ## Github worklows
101
155
 
102
156
  The reusable workflows live in this repo, alongside the package they call. They are the
103
157
  `public_`-prefixed files in `.github/workflows`; everything else there is this repo's own CI.
104
- Reference them at the `@v0` major tag, never at `@master` — see
158
+ Reference them at the `@workflows-v0` major tag, never at `@master` — see
105
159
  [Versioning and releases](#versioning-and-releases).
106
160
 
107
161
  There should be 4 GH workflow files in `.github/workflows`, plus an optional fifth for Claude reviews.
@@ -119,7 +173,7 @@ on:
119
173
 
120
174
  jobs:
121
175
  platformTestsCore:
122
- uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@v0
176
+ uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
123
177
  with:
124
178
  subtest: core
125
179
  secrets: inherit
@@ -138,7 +192,7 @@ on:
138
192
 
139
193
  jobs:
140
194
  platformTestsDaily:
141
- uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@v0
195
+ uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
142
196
  secrets: inherit
143
197
  ```
144
198
 
@@ -153,7 +207,7 @@ on:
153
207
 
154
208
  jobs:
155
209
  buildDevelAndTest:
156
- uses: apify/apify-test-tools/.github/workflows/public_pr-build-test.yaml@v0
210
+ uses: apify/apify-test-tools/.github/workflows/public_pr-build-test.yaml@workflows-v0
157
211
  secrets: inherit
158
212
  ```
159
213
 
@@ -168,10 +222,14 @@ on:
168
222
 
169
223
  jobs:
170
224
  buildLatest:
171
- uses: apify/apify-test-tools/.github/workflows/public_push-build-latest.yaml@v0
225
+ uses: apify/apify-test-tools/.github/workflows/public_push-build-latest.yaml@workflows-v0
172
226
  secrets: inherit
173
227
  ```
174
228
 
229
+ This workflow runs `apify-test-tools release --base-commit ${{ github.event.before }}`. See
230
+ [Releasing Actors](#releasing-actors-release) for how the released range is determined and what to
231
+ pass if you call the CLI from somewhere else.
232
+
175
233
  ### `claude-review.yaml`
176
234
 
177
235
  Optional. Reviews a PR against the shared guidelines when you add the trigger label, and again on
@@ -186,13 +244,13 @@ on:
186
244
 
187
245
  jobs:
188
246
  review:
189
- uses: apify/apify-test-tools/.github/workflows/public_review.yaml@v0
247
+ uses: apify/apify-test-tools/.github/workflows/public_review.yaml@workflows-v0
190
248
  secrets: inherit
191
249
  ```
192
250
 
193
251
  The review instructions live in `.github/review-prompt.md` in this repo and are fetched at run time,
194
252
  because a reusable workflow doesn't get its own repo checked out. `prompt-ref` selects which ref to
195
- fetch them from and defaults to `v0`, so the instructions match the workflow you're calling — point
253
+ fetch them from and defaults to `workflows-v0`, so the instructions match the workflow you're calling — point
196
254
  it at a branch only to test a prompt change.
197
255
 
198
256
  ### Secrets
@@ -256,7 +314,7 @@ decide what a consumer repo actually runs:
256
314
 
257
315
  | Pointer | What it selects | Moves when |
258
316
  | ----------------------------------- | ---------------------------------------------- | -------------------------------------------------------- |
259
- | the `@v0` tag in `uses:` | which workflows run | a master push, once the version below is published |
317
+ | the `@workflows-v0` tag in `uses:` | which workflows run | a master push, once the version below is published |
260
318
  | `.github/workflows-package-version` | which `apify-test-tools` the workflows install | a stable release writes it; you may set it ahead of time |
261
319
 
262
320
  The setup action installs that **exact** version — not a range. A tag is therefore a complete
@@ -270,31 +328,33 @@ action, which is how branch testing works.
270
328
 
271
329
  Nothing is coupled that doesn't need to be:
272
330
 
273
- - **Workflow-only change** — merge it. `v0` moves, it goes live, no release needed.
331
+ - **Workflow-only change** — merge it. `workflows-v0` moves, it goes live, no release needed.
274
332
  - **Package-only change** — merge it, then cut a release when you want it out. The workflows are
275
333
  unchanged, so consumers see nothing until the release lands.
276
334
  - **A workflow that calls a new CLI feature** — the one case that can break consumers, and the only
277
335
  one with any ceremony. Put the package change, the workflow change, and the new pin in one PR. On
278
- merge the tag is **held**: the CI job reports that the pinned version isn't on npm and leaves `v0`
336
+ merge the tag is **held**: the CI job reports that the pinned version isn't on npm and leaves `workflows-v0`
279
337
  where it is, so consumers keep running the previous workflows. Cut a stable release, and the tag
280
338
  moves on its own. Run **Move major version tag** if you don't want to wait for the next master
281
339
  push.
282
340
 
283
- `v0` moving on every master push means `@v0` is as live as `@master` was — there's no staging step,
284
- just a gate on the package version. What the tag buys you is a `v1` for breaking workflow changes,
285
- so repos migrate one at a time instead of all at once, and a way to roll back by pointing the tag at
286
- an earlier commit. Bump `MAJOR_TAG` in `.github/workflows/_move_major_tag.yaml` to cut the next
287
- major; the old tag then freezes where it is and keeps working.
341
+ `workflows-v0` moving on every master push means `@workflows-v0` is as live as `@master` was —
342
+ there's no staging step, just a gate on the package version. What the tag buys you is a
343
+ `workflows-v1` for breaking workflow changes, so repos migrate one at a time instead of all at once,
344
+ and a way to roll back by pointing the tag at an earlier commit. Bump `MAJOR_TAG` in
345
+ `.github/workflows/_move_major_tag.yaml` to cut the next major; the old tag then freezes where it is
346
+ and keeps working.
288
347
 
289
- Freezing is real, which is the whole reason the version is pinned rather than a floor. A frozen `v0`
348
+ Freezing is real, which is the whole reason the version is pinned rather than a floor. A frozen `workflows-v0`
290
349
  points at a commit whose pinned version never changes, so it keeps installing the library it was
291
350
  tested against no matter how far the package moves on. Had the workflows installed `>=<floor>`, a
292
- frozen `v0` would still resolve to whatever is newest — and since the release that enables `v1` is
293
- usually the same release that breaks `v0`, the migration window would have been zero.
351
+ frozen `workflows-v0` would still resolve to whatever is newest — and since the release that enables
352
+ `workflows-v1` is usually the same release that breaks `workflows-v0`, the migration window would
353
+ have been zero.
294
354
 
295
355
  The tag tracks the **workflows'** contract, not the npm package version. They move independently on
296
- purpose, so `@v0` is expected to stay `@v0` after the package reaches 1.0 — bump it when a workflow
297
- breaks its callers, not when the package does. Because `uses:` cannot take an expression, every ref
356
+ purpose, so `@workflows-v0` is expected to stay `@workflows-v0` after the package reaches 1.0 —
357
+ bump it when a workflow breaks its callers, not when the package does. Because `uses:` cannot take an expression, every ref
298
358
  into this repo repeats the tag literally; `check-major-tag-refs.mjs` fails the build if `MAJOR_TAG`
299
359
  and those refs disagree, which is the mistake that would otherwise ship silently during a bump.
300
360
 
@@ -535,7 +595,7 @@ The main local flow is:
535
595
  4. Build Actors on Apify (with your new code)
536
596
  5. Run tests against those builds. You can change tests and run on the same builds.
537
597
 
538
- `cd` into the actor repository you want to work with (or use `--workspace`).
598
+ Run every command from the root of the actor repository you want to work with (or point `--workspace` at it).
539
599
 
540
600
  #### 4. Build affected Actors
541
601
 
@@ -545,7 +605,6 @@ Requires `APIFY_TOKEN_<USERNAME>` for all Apify users that own your Actors (e.g.
545
605
 
546
606
  ```bash
547
607
  APIFY_TOKEN_JOHN_DOE=<token> \
548
- GITHUB_WORKSPACE=. \
549
608
  npx apify-test-tools build \
550
609
  --target-branch origin/master \
551
610
  --source-branch origin/my-dummy-branch \
@@ -560,11 +619,10 @@ Remove `--dry-run` to actually trigger builds and update the branch names/ The c
560
619
 
561
620
  #### Build from local source (no push needed)
562
621
 
563
- If you don't want to push a dummy branch just to test a change and wait for all the tests to finish, `build-from-local` builds Actors directly from your local files (zipped and uploaded as `SOURCE_FILES`), skipping steps 1-4 above.
622
+ If you don't want to push a dummy branch just to test a change and wait for all the tests to finish, `build-from-local` builds Actors directly from your local files (zipped into the Actor's `actor-<id>-source` key-value store and built as `TARBALL`, like `apify push`), skipping steps 1-4 above.
564
623
 
565
624
  ```bash
566
625
  APIFY_TOKEN_JOHN_DOE=<token> \
567
- GITHUB_WORKSPACE=. \
568
626
  npx apify-test-tools build-from-local --actors john.doe/my-actor
569
627
  ```
570
628
 
@@ -573,8 +631,7 @@ Pass a hardcoded actor name via `--actors` to build only that Actor (comma-separ
573
631
  ```bash
574
632
  # Build from local source and capture output
575
633
  BUILDS=$(APIFY_TOKEN_JOHN_DOE=apify_api_xxx \
576
- GITHUB_WORKSPACE=. \
577
- npx apify-test-tools build-from-local --actors apify/my-actor)
634
+ npx apify-test-tools build-from-local --actors apify/my-actor)
578
635
  ```
579
636
 
580
637
  Since you already scoped the build to just the Actor(s) you care about, point vitest at a specific test file (or a `-t` name filter) instead of the whole `test/platform` directory — you get feedback on that one test without waiting for the full suite to run.
@@ -596,8 +653,7 @@ TESTER_APIFY_TOKEN=<token> \
596
653
  ```bash
597
654
  # Build and capture output
598
655
  BUILDS=$(APIFY_TOKEN_JOHN_DOE=apify_api_xxx \
599
- GITHUB_WORKSPACE=. \
600
- npx apify-test-tools build \
656
+ npx apify-test-tools build \
601
657
  --target-branch origin/master \
602
658
  --source-branch origin/my-dummy-branch)
603
659
 
@@ -607,10 +663,67 @@ TESTER_APIFY_TOKEN=apify_api_yyy \
607
663
  npx vitest --run --maxConcurrency 20 --fileParallelism=true --maxWorkers 100 test/platform
608
664
  ```
609
665
 
666
+ ### Releasing Actors (`release`)
667
+
668
+ `release` builds the `latest` (default) version of every Actor changed since the last release and
669
+ posts release notes to Slack. It reads everything else from git: the branch from the checked-out
670
+ `HEAD`, the commits and changed files from the range `<base-commit>..HEAD`, and the repository from
671
+ the `origin` remote.
672
+
673
+ ```bash
674
+ npx apify-test-tools release --base-commit <sha> --release-slack-channel "#releases" --dry-run
675
+ ```
676
+
677
+ #### `--base-commit`
678
+
679
+ The last commit that is **already released**. Everything after it, up to `HEAD`, is released. It is
680
+ required: git alone can't tell which commits a push brought, and guessing wrong silently leaves Actors
681
+ on old code.
682
+
683
+ The changed files come from diffing `<base-commit>` and `HEAD` directly, so this works for every
684
+ merge strategy as long as the base commit is the branch tip from **before** the push:
685
+
686
+ | Merge strategy | What lands on the branch | What to pass |
687
+ | ------------------------------ | --------------------------------- | ---------------------------------------------------------------------- |
688
+ | Squash and merge | 1 new commit | the push's "before" commit (`HEAD~1` also works) |
689
+ | Merge commit | the PR's commits + 1 merge commit | the push's "before" commit (`HEAD^1`, the first parent, also works) |
690
+ | Rebase and merge / direct push | N new commits | the push's "before" commit (`HEAD~1` would miss the first N-1 commits) |
691
+
692
+ On GitHub, the push's "before" commit is `${{ github.event.before }}`, which the reusable workflow
693
+ passes for you. On other CIs use the equivalent, e.g. `$CI_COMMIT_BEFORE_SHA` on GitLab. The checkout
694
+ needs the full history (`fetch-depth: 0` in `actions/checkout`) so the base commit is available locally.
695
+
696
+ `release` fails instead of guessing when:
697
+
698
+ - the base commit is all zeros (GitHub sends this for a push that created the branch)
699
+ - the base commit isn't in the local history (shallow checkout) or isn't an ancestor of `HEAD`
700
+ (the branch was force-pushed)
701
+
702
+ In those cases, rerun `release` with a `--base-commit` you pick yourself: an ancestor of `HEAD` from
703
+ before the changes you want to release (for a shallow checkout, fetch the full history instead).
704
+ `--actors` only narrows which of the changed Actors get released; it never forces a build, so on its
705
+ own it doesn't get past these errors.
706
+ If `HEAD` is the base commit, there is nothing to release and the command exits successfully.
707
+
708
+ A failed release isn't retried by the next push: that push's base commit is the failed one's head, so
709
+ its changes are skipped. Re-run the failed workflow run, or release the Actors manually.
710
+
711
+ #### Repository check
712
+
713
+ `release` and `build` point each Actor's version at the `origin` remote. Because a build overwrites the
714
+ version's Git URL, building from a fork or a mirror would repoint the Actor. So, before building, the
715
+ remote is compared with the Git URL of the Actor's default version (the one its default build tag
716
+ points to), and the command fails if they differ or if the default version isn't built from a Git
717
+ repository. The URL form doesn't matter (`https://github.com/org/repo` equals
718
+ `git@github.com:org/repo.git`).
719
+
720
+ To build from a different repository on purpose, e.g. when moving an Actor, pass `--repo-url`. That
721
+ skips the check.
722
+
610
723
  #### Dev mode
611
724
 
612
725
  For development on `apify-test-tools` itself, use `tsx` directly:
613
726
 
614
727
  ```bash
615
- GITHUB_WORKSPACE=local-clone tsx bin/main.ts get-actor-configs
728
+ tsx bin/main.ts --workspace local-clone get-actor-configs
616
729
  ```
@@ -2,14 +2,16 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
- import type { ActorVersion, ActorVersionSourceFile } from 'apify-client';
5
+ import AdmZip from 'adm-zip';
6
+ import type * as ApifyClientTypes from 'apify-client';
6
7
  import { ActorSourceType } from 'apify-client';
7
8
 
8
9
  import { dryRunBuildData, LOCAL_SOURCE_VERSION_NUMBER, runAndSummarizeBuilds } from './build.js';
9
10
  import { buildDockerIgnoreMatcher } from './dockerignore.js';
10
11
  import { isPathWithinScope } from './path-utils.js';
11
12
  import type { ActorConfig, BuildData } from './types.js';
12
- import { getGitignoredPaths, isOutsideDir, listRepoFilePaths, toActorVersionSourceFile } from './utils.js';
13
+ import type { SourceFile } from './utils.js';
14
+ import { getGitignoredPaths, isOutsideDir, listRepoFilePaths, readSourceFile } from './utils.js';
13
15
 
14
16
  // JUST IN CASE. File patterns that commonly hold credentials — never ship these into a build, regardless
15
17
  // of sourceType or of whether the repo's .gitignore happens to list them. Everything else that should be
@@ -18,7 +20,7 @@ import { getGitignoredPaths, isOutsideDir, listRepoFilePaths, toActorVersionSour
18
20
  const SKIP_FILE_PATTERNS = [/^\.env(\..+)?$/, /\.pem$/, /\.key$/, /\.pfx$/, /\.p12$/];
19
21
  const isSecretFile = (fileName: string): boolean => SKIP_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
20
22
 
21
- export const collectSourceFiles = async (actorName: string, actorDir: string): Promise<ActorVersionSourceFile[]> => {
23
+ export const collectSourceFiles = async (actorName: string, actorDir: string): Promise<SourceFile[]> => {
22
24
  const repoRoot = process.cwd();
23
25
  const absActorDir = path.resolve(actorDir);
24
26
 
@@ -35,9 +37,7 @@ export const collectSourceFiles = async (actorName: string, actorDir: string): P
35
37
  const keptFilePaths = collectNonIgnoredFiles(dockerContextDirAbs, repoRoot);
36
38
 
37
39
  if (!isMonorepoActor) {
38
- return Promise.all(
39
- keptFilePaths.map(async (filePath) => toActorVersionSourceFile(filePath, dockerContextDirAbs)),
40
- );
40
+ return Promise.all(keptFilePaths.map(async (filePath) => readSourceFile(filePath, dockerContextDirAbs)));
41
41
  }
42
42
 
43
43
  const { tempDir, filePaths } = await flattenMonorepoContext(
@@ -48,7 +48,7 @@ export const collectSourceFiles = async (actorName: string, actorDir: string): P
48
48
  keptFilePaths,
49
49
  );
50
50
  try {
51
- return await Promise.all(filePaths.map(async (filePath) => toActorVersionSourceFile(filePath, tempDir)));
51
+ return await Promise.all(filePaths.map(async (filePath) => readSourceFile(filePath, tempDir)));
52
52
  } finally {
53
53
  // Only the flattened copy is temporary — never delete the actor's own directory.
54
54
  await fs.rm(tempDir, { recursive: true, force: true });
@@ -202,11 +202,15 @@ export const runBuildsFromLocal = async ({
202
202
  }
203
203
 
204
204
  return runAndSummarizeBuilds(actorConfigs, 'LOCAL BUILDS', async (actorConfig, builder) => {
205
- const sourceFiles = await collectSourceFiles(actorConfig.actorFullName, actorConfig.folder);
206
- const actorVersion: ActorVersion = {
205
+ // Zipped and uploaded like `apify push` does, since inline SOURCE_FILES are capped at ~9 MB.
206
+ const zip = new AdmZip();
207
+ for (const { name, content } of await collectSourceFiles(actorConfig.actorFullName, actorConfig.folder)) {
208
+ zip.addFile(name, content);
209
+ }
210
+ const actorVersion: ApifyClientTypes.ActorVersion = {
207
211
  versionNumber: LOCAL_SOURCE_VERSION_NUMBER,
208
- sourceFiles,
209
- sourceType: ActorSourceType.SourceFiles,
212
+ tarballUrl: await builder.uploadSourceZip(LOCAL_SOURCE_VERSION_NUMBER, zip.toBuffer()),
213
+ sourceType: ActorSourceType.Tarball,
210
214
  };
211
215
  return builder.createVersionAndBuild(LOCAL_SOURCE_VERSION_NUMBER, actorVersion, false);
212
216
  });