apify-test-tools 0.10.2-beta.3 → 0.10.2-beta.4

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 (64) hide show
  1. package/.github/cli/apify-test-tools.mjs +126 -125
  2. package/.github/scripts/run-with-apify-tokens.mjs +1 -1
  3. package/.github/workflows/public_pr-build-test.yaml +25 -25
  4. package/.github/workflows/public_push-build-latest.yaml +20 -4
  5. package/CHANGELOG.md +1 -0
  6. package/README.md +67 -17
  7. package/bin/affected-actors.ts +16 -25
  8. package/bin/build.ts +55 -38
  9. package/bin/git.ts +3 -14
  10. package/bin/main.ts +96 -40
  11. package/bin/slack.ts +24 -0
  12. package/bin/stdin-input.ts +65 -0
  13. package/dist/bin/affected-actors.d.ts +4 -9
  14. package/dist/bin/affected-actors.d.ts.map +1 -1
  15. package/dist/bin/affected-actors.js +17 -19
  16. package/dist/bin/affected-actors.js.map +1 -1
  17. package/dist/bin/build.d.ts +12 -5
  18. package/dist/bin/build.d.ts.map +1 -1
  19. package/dist/bin/build.js +45 -23
  20. package/dist/bin/build.js.map +1 -1
  21. package/dist/bin/git.d.ts +1 -6
  22. package/dist/bin/git.d.ts.map +1 -1
  23. package/dist/bin/git.js +3 -13
  24. package/dist/bin/git.js.map +1 -1
  25. package/dist/bin/main.js +78 -40
  26. package/dist/bin/main.js.map +1 -1
  27. package/dist/bin/slack.d.ts +8 -0
  28. package/dist/bin/slack.d.ts.map +1 -1
  29. package/dist/bin/slack.js +18 -0
  30. package/dist/bin/slack.js.map +1 -1
  31. package/dist/bin/stdin-input.d.ts +17 -0
  32. package/dist/bin/stdin-input.d.ts.map +1 -0
  33. package/dist/bin/stdin-input.js +54 -0
  34. package/dist/bin/stdin-input.js.map +1 -0
  35. package/dist/test/bundle/cli-bundle.test.js +27 -4
  36. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  37. package/dist/test/e2e/cli-bundle.e2e.test.js +16 -9
  38. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  39. package/dist/test/unit/bin/affected-actors.test.js +7 -13
  40. package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
  41. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  42. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  43. package/dist/test/unit/bin/build-cli.test.js +52 -0
  44. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  45. package/dist/test/unit/bin/build.test.js +61 -4
  46. package/dist/test/unit/bin/build.test.js.map +1 -1
  47. package/dist/test/unit/bin/git.test.js +2 -14
  48. package/dist/test/unit/bin/git.test.js.map +1 -1
  49. package/dist/test/unit/bin/slack.test.js +39 -1
  50. package/dist/test/unit/bin/slack.test.js.map +1 -1
  51. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  52. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  53. package/dist/test/unit/bin/stdin-input.test.js +50 -0
  54. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +1 -1
  57. package/test/bundle/cli-bundle.test.ts +50 -4
  58. package/test/e2e/cli-bundle.e2e.test.ts +26 -11
  59. package/test/unit/bin/affected-actors.test.ts +7 -13
  60. package/test/unit/bin/build-cli.test.ts +61 -0
  61. package/test/unit/bin/build.test.ts +71 -3
  62. package/test/unit/bin/git.test.ts +1 -15
  63. package/test/unit/bin/slack.test.ts +44 -1
  64. package/test/unit/bin/stdin-input.test.ts +61 -0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Runs apify-test-tools and passes it only secrets needed for the Actors, no other secrets are leaked.
3
3
  //
4
- // node .github/scripts/run-with-apify-tokens.mjs apify-test-tools build --target-branch ...
4
+ // node .github/scripts/run-with-apify-tokens.mjs apify-test-tools build-for-test --target-branch ...
5
5
  //
6
6
  // The step passes the whole secrets map as ALL_SECRETS. The token names come from `tokenEnvVar` in
7
7
  // the repo's apify-test-tools.config.json, which is the same file apify-test-tools itself reads, so
@@ -41,31 +41,39 @@ jobs:
41
41
  # This is needed to force overriding the cache with new entry at the end
42
42
  restore-keys: base_commit-
43
43
 
44
- # run-with-apify-tokens.mjs reads the token names from in apify-test-tools.config.json
45
- # and hands the build those secrets and nothing else, so the tokens stay in this one command instead of the whole job.
46
- # The library never sees secrets it doesn't need
47
- - name: Build
48
- id: build
44
+ # Needs no tokens, it only reads git and apify-test-tools.config.json. The setup action checks out
45
+ # the PR branch itself (not GitHub's merge commit), so the range ends at the branch's HEAD.
46
+ - name: Find affected Actors
49
47
  env:
50
- ALL_SECRETS: ${{ toJSON(secrets) }}
51
48
  # Branch names are attacker-controlled on fork PRs, so they reach the script as
52
49
  # environment variables rather than being interpolated into it.
53
- HEAD_REF: ${{ github.head_ref }}
54
50
  BASE_REF: ${{ github.base_ref }}
55
- SCRIPTS_PATH: ${{ steps.setup.outputs.scripts-path }}
56
51
  run: |
57
52
  base_commit=$(cat base_commit.txt 2>/dev/null || echo "")
58
53
  echo "Last validated (base) commit to be used for build & test: $base_commit"
59
54
  args=(
60
- --source-branch "origin/$HEAD_REF"
61
- --target-branch "origin/$BASE_REF"
55
+ --base "origin/$BASE_REF"
62
56
  # TODO: Temporary hotfix until build glob matching aligns with Vitest's test selection glob.
63
57
  --test-files-glob 'test/platform/**'
64
- --use-docker-cache
65
58
  )
66
- [ -n "$base_commit" ] && args+=(--base-commit "$base_commit")
67
- actor_builds=$(node "$SCRIPTS_PATH/run-with-apify-tokens.mjs" \
68
- apify-test-tools build "${args[@]}")
59
+ [ -n "$base_commit" ] && args+=(--since "$base_commit")
60
+ apify-test-tools get-affected-actors "${args[@]}" > affected.json
61
+
62
+ # Cosmetic-only changes need no tests, so only the functionally changed Actors are built, into the
63
+ # test version 0.99 that users never run.
64
+ # run-with-apify-tokens.mjs reads the token names from in apify-test-tools.config.json
65
+ # and hands the build those secrets and nothing else, so the tokens stay in this one command instead of the whole job.
66
+ # The library never sees secrets it doesn't need
67
+ - name: Build
68
+ id: build
69
+ # bash rather than the default shell, for pipefail
70
+ shell: bash
71
+ env:
72
+ ALL_SECRETS: ${{ toJSON(secrets) }}
73
+ SCRIPTS_PATH: ${{ steps.setup.outputs.scripts-path }}
74
+ run: |
75
+ actor_builds=$(jq '{ actors: .functionallyChangedActors }' affected.json \
76
+ | node "$SCRIPTS_PATH/run-with-apify-tokens.mjs" apify-test-tools build --version-number 0.99)
69
77
  echo "actor_builds=$actor_builds" | tee -a "$GITHUB_OUTPUT"
70
78
 
71
79
  # Runs the repo's test code and its whole import graph, so it gets the tester token only.
@@ -77,18 +85,10 @@ jobs:
77
85
 
78
86
  # NOTE: This is an optimization that if we did functional changes and later only cosmetic changes (e.g. dev readme), we will compare changes files only for the last commit. This must run after tests because we want to cache the latest commit only if the tests are successful
79
87
  - name: Store last validated commit to cache
80
- env:
81
- HEAD_REF: ${{ github.head_ref }}
82
- BASE_REF: ${{ github.base_ref }}
83
88
  run: |
84
- base_commit=$(cat base_commit.txt 2>/dev/null || echo "")
85
- echo "Old last validated (base) commit: $base_commit"
86
- args=(
87
- --source-branch "origin/$HEAD_REF"
88
- --target-branch "origin/$BASE_REF"
89
- )
90
- [ -n "$base_commit" ] && args+=(--base-commit "$base_commit")
91
- apify-test-tools get-latest-commit "${args[@]}" > ./base_commit.txt
89
+ echo "Old last validated (base) commit: $(cat base_commit.txt 2>/dev/null || echo "")"
90
+ # The HEAD that was just built and tested
91
+ jq -r .head affected.json > base_commit.txt
92
92
  echo "New last validated (base) commit: $(cat base_commit.txt)"
93
93
 
94
94
  # Pure static checks against the repo. Needs no Apify or Slack credentials, only the npm
@@ -25,18 +25,34 @@ jobs:
25
25
  with:
26
26
  npm-token: ${{ secrets.NPM_TOKEN }}
27
27
 
28
- # --base-commit is the last commit that is already released: the push's "before" commit.
28
+ # --since is the last commit that is already released: the push's "before" commit.
29
29
  # Everything after it up to HEAD gets released. See the "Releasing Actors" section of the README.
30
+ # Needs no tokens, it only reads git and apify-test-tools.config.json.
31
+ - name: Find changes since the last release
32
+ run: |
33
+ apify-test-tools get-affected-actors --since ${{ github.event.before }} > affected.json
34
+
35
+ # A release ships cosmetic changes too (README, input schema descriptions), so both lists are built,
36
+ # into each Actor's default version: the one its default build tag points to, which users run.
30
37
  # run-with-apify-tokens.mjs reads the token names from in apify-test-tools.config.json
31
38
  # and hands the build those secrets and nothing else, so the tokens stay in this one command instead of the whole job.
32
39
  # The library never sees secrets it doesn't need
33
- - name: Release Actors and notify
40
+ - name: Build the default versions
41
+ # bash rather than the default shell, for pipefail
42
+ shell: bash
34
43
  env:
35
44
  ALL_SECRETS: ${{ toJSON(secrets) }}
45
+ run: |
46
+ jq '{ actors: (.functionallyChangedActors + .cosmeticallyChangedActors) }' affected.json \
47
+ | node "${{ steps.setup.outputs.scripts-path }}/run-with-apify-tokens.mjs" \
48
+ apify-test-tools build --version-number default
49
+
50
+ # Skips the announcement when the push brought no new commits
51
+ - name: Announce the release on Slack
52
+ env:
36
53
  SLACK_TOKEN_RELEASES_BOT: ${{ secrets.SLACK_TOKEN_RELEASES_BOT }}
37
54
  run: |
38
- node "${{ steps.setup.outputs.scripts-path }}/run-with-apify-tokens.mjs" \
39
- apify-test-tools release --base-commit ${{ github.event.before }} --release-slack-channel "#delivery-public-actors" --report-slack-channel "${{ inputs.report-slack-channel || format('#notif-{0}', github.event.repository.name) }}" --use-docker-cache
55
+ apify-test-tools notify-release --release-slack-channel "#delivery-public-actors" --report-slack-channel "${{ inputs.report-slack-channel || format('#notif-{0}', github.event.repository.name) }}" < affected.json
40
56
 
41
57
  - name: Delete old builds
42
58
  env:
package/CHANGELOG.md CHANGED
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
9
9
 
10
10
  - **ci:** Ship the workflows&#x27; CLI as a bundle in the action instead of installing it ([#200](https://github.com/apify/apify-test-tools/pull/200)) ([a0ef975](https://github.com/apify/apify-test-tools/commit/a0ef9756123a30a8210590b080a2a289c6a9c17c)) by [@metalwarrior665](https://github.com/metalwarrior665)
11
11
  - [**breaking**] Revamp get-affected-actors to be a core logic command ([#190](https://github.com/apify/apify-test-tools/pull/190)) ([ea5f244](https://github.com/apify/apify-test-tools/commit/ea5f244cfd42bf3fa9ffd38cd2ad499dde2e83f8)) by [@metalwarrior665](https://github.com/metalwarrior665)
12
+ - [**breaking**] Add logic-less build command and use command chaining in workflows ([#193](https://github.com/apify/apify-test-tools/pull/193)) ([e5b0b86](https://github.com/apify/apify-test-tools/commit/e5b0b86ee06fca51d4be0ddc81b0a4a469d31603)) by [@metalwarrior665](https://github.com/metalwarrior665)
12
13
 
13
14
  ### 🐛 Bug Fixes
14
15
 
package/README.md CHANGED
@@ -57,7 +57,7 @@ When a PR is opened or code is pushed, the tool determines which actors need to
57
57
  2. **CHANGELOG classification** — a `CHANGELOG.md` file is always `cosmetic` (only triggers a release build, not tests), for every actor, regardless of context or folder. (See [issue #106](https://github.com/apify/apify-test-tools/issues/106).)
58
58
  3. **Context matching** — the file must fall within one of the actor's context paths (`dockerContextDir` from `actor.json` by default, or `overrideActorContext` from config if set). Files outside every context path are skipped.
59
59
  4. **Hardcoded ignore list, context-aware** — the file path is first "hoisted" relative to the context path it matched (e.g. a standalone actor's own `.eslintrc` is checked as just `.eslintrc`, not the full repo-root-relative path), then checked against repo-level dev file patterns (`.vscode/`, `.gitignore`, `.husky/`, `.eslintrc`, `eslint.config.mjs`, `.prettierrc`, `.editorconfig`). There's no hardcoded special-casing for legacy `code/`/`shared/` layouts — repos that need those directories treated as top-level must list them explicitly in `overrideActorContext`.
60
- 5. **`.dockerignore` filtering** — if a `.dockerignore` exists at the root of the actor's `dockerContextDir`, matching files are ignored. Patterns are resolved relative to `dockerContextDir`, matching Docker's own behavior. Test files are the exception: `build --test-files-glob <glob>` (relative to the repo root) skips this check for matching files, so a changed test triggers a build, and with it its tests, even when `.dockerignore` lists it. The PR workflow passes `test/platform/**`. Context matching still applies, so tests outside an actor's `dockerContextDir` (e.g. a root `test/` with standalone actors) need to be listed in its `overrideActorContext`; `build` logs a warning for changed test files outside every actor's context.
60
+ 5. **`.dockerignore` filtering** — if a `.dockerignore` exists at the root of the actor's `dockerContextDir`, matching files are ignored. Patterns are resolved relative to `dockerContextDir`, matching Docker's own behavior. Test files are the exception: `build-for-test --test-files-glob <glob>` (relative to the repo root, also taken by `get-affected-actors`) skips this check for matching files, so a changed test triggers a build, and with it its tests, even when `.dockerignore` lists it. The PR workflow passes `test/platform/**`. Context matching still applies, so tests outside an actor's `dockerContextDir` (e.g. a root `test/` with standalone actors) need to be listed in its `overrideActorContext`; `build-for-test` logs a warning for changed test files outside every actor's context.
61
61
  6. **README classification** — a `README.md` file is `cosmetic` (only triggers a release build, not tests) if it lives inside the actor's own `folder`; otherwise it's ignored entirely, since it isn't documentation for this actor.
62
62
  7. **Cosmetic JSON classification** — `.json` files inside the actor's own `.actor/` directory with only cosmetic schema changes (whitespace, key ordering) only trigger a release build.
63
63
  8. **Functional** — everything else triggers both build and tests.
@@ -226,9 +226,10 @@ jobs:
226
226
  secrets: inherit
227
227
  ```
228
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.
229
+ This workflow does what `apify-test-tools release --base-commit ${{ github.event.before }}` does, one
230
+ step per command: `get-affected-actors --since ${{ github.event.before }}`, then `build --version-number default`
231
+ on every changed Actor, then `notify-release`. See [Releasing Actors](#releasing-actors-release) for how the
232
+ released range is determined and what to pass if you call the CLI from somewhere else.
232
233
 
233
234
  ### `claude-review.yaml`
234
235
 
@@ -261,9 +262,9 @@ environment variables, so a secret is only visible to the step that needs it:
261
262
  | Secret | Reaches |
262
263
  | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
263
264
  | `NPM_TOKEN` | dependency install steps only, as both `NPM_TOKEN` and `NODE_AUTH_TOKEN`. Use a read-only token: npm granular tokens can be read-only, classic automation tokens can publish. |
264
- | the Actor tokens named by `tokenEnvVar` in `apify-test-tools.config.json` | the `build`, `release`, and `delete-old-builds` steps only |
265
+ | the Actor tokens named by `tokenEnvVar` in `apify-test-tools.config.json` | the build and `delete-old-builds` steps only |
265
266
  | `TESTER_APIFY_TOKEN` | the vitest step, and the Claude investigation step: the Apify MCP server's bearer token for reading the failing run, its log and its storages, and Claude's token for re-running it to measure how often the failure happens |
266
- | `SLACK_TOKEN_TESTS_BOT` / `SLACK_TOKEN_RELEASES_BOT` | the reporting and release steps only |
267
+ | `SLACK_TOKEN_TESTS_BOT` / `SLACK_TOKEN_RELEASES_BOT` | the reporting and announcement steps only |
267
268
 
268
269
  The Actor tokens are the one set that cannot be listed in the workflow, because each Actor names its
269
270
  own token via `tokenEnvVar` in `apify-test-tools.config.json`. Those steps pass
@@ -277,7 +278,7 @@ secrets to pass on:
277
278
  ALL_SECRETS: ${{ toJSON(secrets) }}
278
279
  run: |
279
280
  node "${{ steps.setup.outputs.scripts-path }}/run-with-apify-tokens.mjs" \
280
- npx apify-test-tools build --target-branch ...
281
+ npx apify-test-tools build-for-test --target-branch ...
281
282
  ```
282
283
 
283
284
  The wrapper passes only the tokens the config declares and drops `ALL_SECRETS`, so neither `npx` nor
@@ -568,12 +569,12 @@ The package includes a CLI binary used by CI workflows to build Actors, detect c
568
569
 
569
570
  ### Dry runs (`--dry-run`)
570
571
 
571
- Every command that changes something (`build`, `build-from-local`, `release`, `delete-old-builds`, `report-tests`) takes `--dry-run`. A dry run:
572
+ Every command that changes something (`build`, `build-for-test`, `build-from-local`, `release`, `notify-release`, `delete-old-builds`, `report-tests`) takes `--dry-run`. A dry run:
572
573
 
573
574
  - **Changes nothing.** No versions or builds are created, nothing is uploaded or deleted, and no Slack message is posted.
574
575
  - **Does every read and check the real run does**, so it fails wherever the real run would: change detection, the Apify token and Actor lookup, the [repository check](#repository-check), collecting and zipping local sources, and the Slack token (verified with Slack's read-only `auth.test`).
575
576
  - **Needs the same tokens as the real run.** Read-only tokens are enough, since nothing is written. A dry run can't tell whether a token is also allowed to write, because the Apify API has no dry-run mode of its own.
576
- - **Prints a plan, not results.** Logs start with `[DRY RUN] Would …`. `build` and `build-from-local` write `{ "dryRun": true, "builds": [...] }` to stdout instead of the build array, so the plan can't be passed to tests as `ACTOR_BUILDS` by mistake.
577
+ - **Prints a plan, not results.** Logs start with `[DRY RUN] Would …`. `build`, `build-for-test` and `build-from-local` write `{ "dryRun": true, "builds": [...] }` to stdout instead of the build array, so the plan can't be passed to tests as `ACTOR_BUILDS` by mistake.
577
578
 
578
579
  ### Running locally
579
580
 
@@ -599,13 +600,15 @@ Requires `APIFY_TOKEN_<USERNAME>` for all Apify users that own your Actors (e.g.
599
600
 
600
601
  ```bash
601
602
  APIFY_TOKEN_JOHN_DOE=<token> \
602
- npx apify-test-tools build \
603
+ npx apify-test-tools build-for-test \
603
604
  --target-branch origin/master \
604
- --source-branch origin/my-dummy-branch \
605
605
  --dry-run
606
606
  ```
607
607
 
608
- With `--dry-run`, it prints the plan (the version and Git URL each Actor would be built from). Remove `--dry-run` to actually trigger builds and update the branch names. The command outputs a JSON array of build objects to stdout:
608
+ Run it with your dummy branch checked out: it builds the Actors that branch changed, from the branch as it is on
609
+ the remote. With `--dry-run`, it prints the plan (the version and Git URL each Actor would be built from). Remove
610
+ `--dry-run` to actually trigger builds and update the target branch name. The command outputs a JSON array of build
611
+ objects to stdout:
609
612
 
610
613
  ```json
611
614
  [{ "buildId": "...", "actorRawId": "...", "buildNumber": "...", "actorFullName": "john.doe/my-actor" }]
@@ -649,9 +652,8 @@ Tests only run for Actors in `ACTOR_BUILDS`, so a custom workflow has to build (
649
652
  ```bash
650
653
  # Build and capture output
651
654
  BUILDS=$(APIFY_TOKEN_JOHN_DOE=apify_api_xxx \
652
- npx apify-test-tools build \
655
+ npx apify-test-tools build-for-test \
653
656
  --target-branch origin/master \
654
- --source-branch origin/my-dummy-branch \
655
657
  --test-files-glob 'test/platform/**')
656
658
 
657
659
  # Run tests with the builds
@@ -698,7 +700,6 @@ The output is a JSON document:
698
700
  ```json
699
701
  {
700
702
  "version": 1,
701
- "ref": "my-branch",
702
703
  "head": "<sha of HEAD>",
703
704
  "commits": [{ "sha": "…", "author": "…", "date": "…", "message": "…" }],
704
705
  "changedFiles": ["src/main.ts", "CHANGELOG.md"],
@@ -710,10 +711,50 @@ The output is a JSON document:
710
711
 
711
712
  Each changed Actor is in exactly one of the lists. `functionallyChangedActors` have at least one functional change, so
712
713
  they need a new build and tests. `cosmeticallyChangedActors` only have cosmetic changes (see
713
- [Change detection](#change-detection)), so they need a new build only when released: `build` builds
714
+ [Change detection](#change-detection)), so they need a new build only when released: `build-for-test` builds
714
715
  `functionallyChangedActors`, `release` builds both.
715
716
 
716
- `ref` is the checked-out branch, or `null` for a detached HEAD. `build` and `release` use the same detection internally.
717
+ `build-for-test` and `release` use the same detection internally.
718
+
719
+ ### Building Actors (`build`)
720
+
721
+ `build` builds exactly the Actors it is given on stdin, from the checked-out branch as it is on the remote, and
722
+ prints the builds as the same JSON array `build-for-test` and `build-from-local` print. A detached HEAD is an
723
+ error, since there is no branch to build from. It doesn't look at git history: which Actors to build is
724
+ up to you, usually picked from the output of [`get-affected-actors`](#finding-affected-actors-get-affected-actors).
725
+
726
+ ```bash
727
+ # What build-for-test does: the Actors a branch changed functionally, built into the test version
728
+ npx apify-test-tools get-affected-actors --base origin/master \
729
+ | jq '{ actors: .functionallyChangedActors }' \
730
+ | npx apify-test-tools build --version-number 0.99
731
+
732
+ # What release builds: every Actor changed since the last release, into the version users run
733
+ npx apify-test-tools get-affected-actors --since <sha> \
734
+ | jq '{ actors: (.functionallyChangedActors + .cosmeticallyChangedActors) }' \
735
+ | npx apify-test-tools build --version-number default
736
+
737
+ # One Actor, whatever changed
738
+ echo '{ "actors": [{ "actorFullName": "john.doe/my-actor" }] }' | npx apify-test-tools build --version-number 0.99
739
+ ```
740
+
741
+ The input is `{ "actors": [{ "actorFullName": "<owner/name>" }] }`; other fields are ignored. An empty `actors` list builds nothing and prints `[]`. Empty or invalid input is an error,
742
+ so a failed command earlier in the pipe can't turn into a successful run that built nothing. In GitHub
743
+ Actions, give such a step `shell: bash` so the pipe also fails on its own (the default shell has no `pipefail`).
744
+
745
+ | Flag | Default | Meaning |
746
+ | ------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------- |
747
+ | `--version-number` | required | Version to build, like `0.99`, or `default` for each Actor's default version (the one its default build tag points to) |
748
+ | `--repo-url` | the `origin` remote | Repository to build from, see [Repository check](#repository-check) |
749
+
750
+ `build` never sets or changes build tags. A build gets the tag its version is configured with on the platform,
751
+ so building the default version moves its tag (usually `latest`) to the new build, and building a version
752
+ without a tag leaves every tag where it is. Builds always use the Docker cache.
753
+
754
+ `build-for-test` and `release` are shortcuts for the two pipelines above, with the flags they always had.
755
+ `build-for-test` takes `--target-branch`, `--base-commit` and `--test-files-glob`, inspects and builds the
756
+ checked-out branch, and builds into version `0.99`. `release` takes `--base-commit`, and announces the release on Slack after building, the same
757
+ way `notify-release` does.
717
758
 
718
759
  ### Releasing Actors (`release`)
719
760
 
@@ -726,6 +767,15 @@ the `origin` remote.
726
767
  npx apify-test-tools release --base-commit <sha> --release-slack-channel "#releases" --dry-run
727
768
  ```
728
769
 
770
+ The announcement is also its own command, `notify-release`, which reads the output of `get-affected-actors` on
771
+ stdin. It posts the changelog additions to `--release-slack-channel` and the commits and changed files to
772
+ `--report-slack-channel`, and posts nothing when there are no new commits. It needs `SLACK_TOKEN_RELEASES_BOT`.
773
+
774
+ ```bash
775
+ npx apify-test-tools get-affected-actors --since <sha> \
776
+ | npx apify-test-tools notify-release --release-slack-channel "#releases" --dry-run
777
+ ```
778
+
729
779
  #### `--base-commit`
730
780
 
731
781
  The last commit that is **already released**. Everything after it, up to `HEAD`, is released. It is
@@ -6,7 +6,6 @@ import {
6
6
  getCommits,
7
7
  getCommitsBetween,
8
8
  hasMergeFromTarget,
9
- resolveBranchName,
10
9
  resolveCommitSha,
11
10
  resolveReleaseBaseCommit,
12
11
  } from './git.js';
@@ -14,14 +13,15 @@ import { logger } from './logger.js';
14
13
  import type { ActorConfig, Commit } from './types.js';
15
14
  import { readConfigFile } from './utils/config/load-config.js';
16
15
 
16
+ // The range always ends at the checked-out commit: check out what you want to inspect
17
+ const HEAD = 'HEAD';
18
+
17
19
  interface AffectedActorsOptions {
18
- /** End of the range. */
19
- head: string;
20
- /** Only the commits on head that are not on base count, i.e. the branch's own changes. */
20
+ /** Only the commits on HEAD that are not on base count, i.e. the branch's own changes. */
21
21
  base?: string;
22
22
  /**
23
23
  * Only changes after this commit count. With `base` it's the last validated commit of the branch and is
24
- * ignored when it isn't on the branch. Without `base` it must be an ancestor of head (e.g. the last release).
24
+ * ignored when it isn't on the branch. Without `base` it must be an ancestor of HEAD (e.g. the last release).
25
25
  */
26
26
  since?: string;
27
27
  /** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
@@ -32,8 +32,6 @@ interface AffectedActorsOptions {
32
32
 
33
33
  interface AffectedActors {
34
34
  headSha: string;
35
- /** Branch head points to, which is what git builds are made from. Null for a SHA or a detached HEAD. */
36
- ref: string | null;
37
35
  commits: Commit[];
38
36
  changedFiles: string[];
39
37
  changelog: string | null;
@@ -50,14 +48,14 @@ interface CommitRange {
50
48
  }
51
49
 
52
50
  /** The branch's own commits, after `since` when it is on the branch (see getCommits). */
53
- const getBranchRange = async (head: string, base: string, since: string | undefined): Promise<CommitRange> => {
54
- const commits = await getCommits({ sourceBranch: head, targetBranch: base, baseCommit: since });
51
+ const getBranchRange = async (base: string, since: string | undefined): Promise<CommitRange> => {
52
+ const commits = await getCommits({ sourceBranch: HEAD, targetBranch: base, baseCommit: since });
55
53
  return { commits, fromRef: `${commits[0].sha}~`, toRef: commits[commits.length - 1].sha };
56
54
  };
57
55
 
58
- /** Everything after `since` up to head. Unlike on a branch, `since` must be an ancestor of head. */
59
- const getRangeSince = async (head: string, headSha: string, since: string): Promise<CommitRange> => {
60
- const sinceSha = await resolveReleaseBaseCommit(since, head);
56
+ /** Everything after `since` up to HEAD. Unlike on a branch, `since` must be an ancestor of HEAD. */
57
+ const getRangeSince = async (headSha: string, since: string): Promise<CommitRange> => {
58
+ const sinceSha = await resolveReleaseBaseCommit(since);
61
59
  const commits = sinceSha === headSha ? [] : await getCommitsBetween(sinceSha, headSha);
62
60
  return { commits, fromRef: sinceSha, toRef: headSha };
63
61
  };
@@ -76,21 +74,20 @@ const getRangeSince = async (head: string, headSha: string, since: string): Prom
76
74
  * branch's own keeps every Actor, and the log messages below describe the whole branch.
77
75
  */
78
76
  const getOwnChangesIfMergedFromBase = async (
79
- head: string,
80
77
  base: string,
81
78
  actorConfigs: ActorConfig[],
82
79
  testFilesGlob: string | undefined,
83
80
  ): Promise<ChangedActors | undefined> => {
84
- if (!(await hasMergeFromTarget(head, base))) return undefined;
81
+ if (!(await hasMergeFromTarget(HEAD, base))) return undefined;
85
82
 
86
83
  logger.info(
87
84
  '[MERGE-FROM-TARGET-OPTIMIZATION]: There is merge from target branch, checking if there are no functional changes in our own branch. If so, we can skip tests',
88
85
  );
89
86
  const own = await getChangedActors({
90
- filepathsChanged: await getBranchOnlyChangedFiles(head, base),
87
+ filepathsChanged: await getBranchOnlyChangedFiles(HEAD, base),
91
88
  actorConfigs,
92
89
  // Omit since to get full branch history. Validated functional commits can still interact with merged ones
93
- commits: await getCommits({ sourceBranch: head, targetBranch: base }),
90
+ commits: await getCommits({ sourceBranch: HEAD, targetBranch: base }),
94
91
  testFilesGlob,
95
92
  });
96
93
  logger.info(
@@ -106,7 +103,6 @@ const getOwnChangesIfMergedFromBase = async (
106
103
  * (a PR), `since` the commits after a known one (a release, or the last validated commit of a PR).
107
104
  */
108
105
  export const getAffectedActors = async ({
109
- head,
110
106
  base,
111
107
  since: sinceOption,
112
108
  testFilesGlob,
@@ -119,21 +115,18 @@ export const getAffectedActors = async ({
119
115
  throw new Error('Pass --base, --since, or both to select the commits to inspect.');
120
116
  }
121
117
 
122
- const headSha = await resolveCommitSha(head);
123
- const { commits, fromRef, toRef } = base
124
- ? await getBranchRange(head, base, since)
125
- : await getRangeSince(head, headSha, since!);
118
+ const headSha = await resolveCommitSha(HEAD);
119
+ const { commits, fromRef, toRef } = base ? await getBranchRange(base, since) : await getRangeSince(headSha, since!);
126
120
  const actorConfigs = await readConfigFile({ actors, ignore });
127
121
 
128
122
  // What the branch only merged in from base is already validated. Without functional changes of its own
129
123
  // the branch has nothing to test, and without any changes of its own nothing to release either.
130
- const own = base ? await getOwnChangesIfMergedFromBase(head, base, actorConfigs, testFilesGlob) : undefined;
124
+ const own = base ? await getOwnChangesIfMergedFromBase(base, actorConfigs, testFilesGlob) : undefined;
131
125
  const changedFiles = await getChangedFilesBetween(fromRef, toRef);
132
126
  const changed = await getChangedActors({ filepathsChanged: changedFiles, actorConfigs, commits, testFilesGlob });
133
127
 
134
128
  return {
135
129
  headSha,
136
- ref: await resolveBranchName(head),
137
130
  commits,
138
131
  changedFiles,
139
132
  changelog: await getChangelogAdditions(fromRef, toRef, changedFiles),
@@ -145,7 +138,6 @@ export const getAffectedActors = async ({
145
138
  /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
146
139
  export const toAffectedActorsDocument = ({
147
140
  headSha,
148
- ref,
149
141
  commits,
150
142
  changedFiles,
151
143
  changelog,
@@ -155,7 +147,6 @@ export const toAffectedActorsDocument = ({
155
147
  const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
156
148
  return {
157
149
  version: 1,
158
- ref,
159
150
  head: headSha,
160
151
  commits,
161
152
  changedFiles,
package/bin/build.ts CHANGED
@@ -1,22 +1,23 @@
1
1
  import type * as ApifyClientTypes from 'apify-client';
2
2
  import { ActorSourceType, ApifyClient } from 'apify-client';
3
3
 
4
- import { normalizeRepoUrl } from './git.js';
4
+ import { getCurrentBranch, normalizeRepoUrl, resolveRepoUrl } from './git.js';
5
5
  import { logger } from './logger.js';
6
6
  import type { ActorConfig, BuildData, DryRunPlan } from './types.js';
7
7
 
8
8
  type BuildPrActorOptions = {
9
- buildTag?: string;
10
9
  versionNumber: string;
11
10
  gitRepoUrl: string;
12
11
  actorConfig: ActorConfig;
13
12
  actorInfo: ApifyClientTypes.Actor;
14
- useDockerCache: boolean;
15
13
  };
16
14
 
17
15
  // Fixed version number used to build from local source files, since there is no real version to track.
18
16
  export const LOCAL_SOURCE_VERSION_NUMBER = '0.98';
19
- const DEFAULT_TEST_VERSION_NUMBER = '0.99';
17
+ // Version that build-for-test builds, so tests never touch the version users run
18
+ export const TEST_VERSION_NUMBER = '0.99';
19
+ // Stands for each Actor's default version: the one its default build tag points to
20
+ export const DEFAULT_VERSION_KEYWORD = 'default';
20
21
 
21
22
  // Shown whenever an Actor isn't set up the way CI builds need it
22
23
  const ACTOR_SETUP_REQUIREMENT =
@@ -319,20 +320,18 @@ export const runAndSummarizeBuilds = async (
319
320
  type GitBuildPlan = {
320
321
  actorFullName: string;
321
322
  versionNumber: string;
322
- buildTag?: string;
323
323
  gitRepoUrl: string;
324
- useDockerCache: boolean;
325
324
  };
326
325
 
327
326
  type RunBuildsOptions = {
328
327
  actorConfigs: ActorConfig[];
329
- isLatest?: boolean;
328
+ // A version number like "0.99", or DEFAULT_VERSION_KEYWORD for each Actor's default version
329
+ versionNumber: string;
330
330
  repoUrl: string;
331
331
  // False when the repo URL was passed explicitly, see assertRepoUrlMatchesDefaultVersion
332
332
  shouldVerifyRepoUrl: boolean;
333
333
  branch: string;
334
334
  dryRun: boolean;
335
- useDockerCache: boolean;
336
335
  };
337
336
 
338
337
  export const runBuilds = async ({
@@ -340,9 +339,8 @@ export const runBuilds = async ({
340
339
  shouldVerifyRepoUrl,
341
340
  branch,
342
341
  actorConfigs,
343
- isLatest = false,
342
+ versionNumber,
344
343
  dryRun,
345
- useDockerCache,
346
344
  }: RunBuildsOptions): Promise<BuildData[] | DryRunPlan<GitBuildPlan>> => {
347
345
  // Fetched once per Actor and reused for the checks, the version numbers and the build itself
348
346
  const buildConfigs: BuildPrActorOptions[] = await Promise.all(
@@ -352,6 +350,15 @@ export const runBuilds = async ({
352
350
  actorConfig.actorFullName,
353
351
  actorInfo,
354
352
  );
353
+ // Builds keep the tags their version gives them, so a default version without the default build tag
354
+ // would build fine but leave users on the previous build
355
+ if (versionNumber === DEFAULT_VERSION_KEYWORD && defaultVersion?.buildTag !== defaultBuildTag) {
356
+ logger.warn(
357
+ `[${actorConfig.actorFullName}] Default version ${defaultVersionNumber} has build tag ` +
358
+ `"${defaultVersion?.buildTag ?? 'none'}", not "${defaultBuildTag}", so users keep running the previous build. ` +
359
+ 'Set the build tag of the version on the platform to change that.',
360
+ );
361
+ }
355
362
  if (shouldVerifyRepoUrl) {
356
363
  assertRepoUrlMatchesDefaultVersion(actorConfig.actorFullName, defaultVersion, repoUrl);
357
364
  }
@@ -365,9 +372,7 @@ export const runBuilds = async ({
365
372
  actorConfig,
366
373
  actorInfo,
367
374
  gitRepoUrl,
368
- versionNumber: isLatest ? defaultVersionNumber : DEFAULT_TEST_VERSION_NUMBER,
369
- buildTag: isLatest ? defaultBuildTag : undefined,
370
- useDockerCache,
375
+ versionNumber: versionNumber === DEFAULT_VERSION_KEYWORD ? defaultVersionNumber : versionNumber,
371
376
  };
372
377
  }),
373
378
  );
@@ -375,21 +380,15 @@ export const runBuilds = async ({
375
380
  // Everything above only reads, so a dry run has checked what the real run would, short of writing
376
381
  if (dryRun) {
377
382
  logger.info('[DRY RUN] Would build:');
378
- for (const { actorConfig, versionNumber, gitRepoUrl } of buildConfigs) {
379
- logger.info(` ${actorConfig.actorFullName} (${versionNumber}) from ${gitRepoUrl}`);
383
+ const builds = buildConfigs.map((buildConfig) => ({
384
+ actorFullName: buildConfig.actorConfig.actorFullName,
385
+ versionNumber: buildConfig.versionNumber,
386
+ gitRepoUrl: buildConfig.gitRepoUrl,
387
+ }));
388
+ for (const build of builds) {
389
+ logger.info(` ${build.actorFullName} (${build.versionNumber}) from ${build.gitRepoUrl}`);
380
390
  }
381
- return {
382
- dryRun: true,
383
- builds: buildConfigs.map(
384
- ({ actorConfig, versionNumber, buildTag, gitRepoUrl, useDockerCache: useCache }) => ({
385
- actorFullName: actorConfig.actorFullName,
386
- versionNumber,
387
- buildTag,
388
- gitRepoUrl,
389
- useDockerCache: useCache,
390
- }),
391
- ),
392
- };
391
+ return { dryRun: true, builds };
393
392
  }
394
393
 
395
394
  const buildConfigsByActorFullName = new Map(
@@ -397,20 +396,38 @@ export const runBuilds = async ({
397
396
  );
398
397
 
399
398
  return runAndSummarizeBuilds(actorConfigs, 'BUILDS', async (actorConfig, builder) => {
400
- const {
401
- actorInfo,
402
- buildTag,
403
- versionNumber,
404
- gitRepoUrl,
405
- useDockerCache: useCache,
406
- } = buildConfigsByActorFullName.get(actorConfig.actorFullName)!;
399
+ const buildConfig = buildConfigsByActorFullName.get(actorConfig.actorFullName)!;
400
+ // No buildTag: the version keeps the one it has, so its build gets the tag the version already gives it
407
401
  const actorVersion: ApifyClientTypes.ActorVersion = {
408
- buildTag,
409
- versionNumber,
410
- gitRepoUrl,
402
+ versionNumber: buildConfig.versionNumber,
403
+ gitRepoUrl: buildConfig.gitRepoUrl,
411
404
  sourceType: ActorSourceType.GitRepo,
412
405
  };
413
- return builder.createVersionAndBuild(versionNumber, actorVersion, useCache, actorInfo);
406
+ // Always with the Docker cache: CI always asked for it, and a clean build can still be started on the platform
407
+ return builder.createVersionAndBuild(buildConfig.versionNumber, actorVersion, true, buildConfig.actorInfo);
408
+ });
409
+ };
410
+
411
+ type BuildActorsOptions = Omit<RunBuildsOptions, 'repoUrl' | 'shouldVerifyRepoUrl' | 'branch'> & {
412
+ // Defaults to the `origin` remote, see resolveRepoUrl
413
+ repoUrl?: string;
414
+ };
415
+
416
+ /**
417
+ * Builds exactly the given Actors from the checked-out Git branch, as it is on the remote. Fails on a detached HEAD,
418
+ * where there is no branch to build. Used by `build` and the commands built on top of it.
419
+ */
420
+ export const buildActors = async ({ actorConfigs, repoUrl, ...options }: BuildActorsOptions) => {
421
+ if (actorConfigs.length === 0) {
422
+ logger.info('No Actors to build');
423
+ // Still the dry-run shape in a dry run, so it can't pass for a real (empty) result
424
+ return options.dryRun ? { dryRun: true as const, builds: [] } : [];
425
+ }
426
+ return runBuilds({
427
+ ...options,
428
+ ...(await resolveRepoUrl(repoUrl)),
429
+ actorConfigs,
430
+ branch: await getCurrentBranch(),
414
431
  });
415
432
  };
416
433
 
package/bin/git.ts CHANGED
@@ -181,16 +181,16 @@ export const parseCommit = (commitString: string): Commit => {
181
181
  };
182
182
 
183
183
  /**
184
- * Returns the currently checked-out branch. Release builds point the Actor version at this branch,
184
+ * Returns the currently checked-out branch. Builds point the Actor version at this branch,
185
185
  * so a detached HEAD (no branch to point at) is an error rather than a guess.
186
186
  */
187
187
  export const getCurrentBranch = async (): Promise<string> => {
188
188
  const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
189
- operation: 'determine the current branch for release',
189
+ operation: 'determine the checked-out branch',
190
190
  });
191
191
  if (branch === 'HEAD') {
192
192
  throw new Error(
193
- 'Cannot determine the branch to release: HEAD is detached. Check out the branch you want to release.',
193
+ 'Cannot determine the branch to build from: HEAD is detached. Check out the branch you want to build.',
194
194
  );
195
195
  }
196
196
  return branch;
@@ -200,17 +200,6 @@ export const getCurrentBranch = async (): Promise<string> => {
200
200
  export const resolveCommitSha = async (ref: string): Promise<string> =>
201
201
  runGitCommand(['rev-parse', '--verify', `${ref}^{commit}`], { operation: `resolve ${ref} to a commit` });
202
202
 
203
- /**
204
- * Branch name a ref points to, without the remote: `origin/foo` and `foo` both give `foo`, and `HEAD`
205
- * gives the checked-out branch. Null when there is no branch to point at (a SHA, a tag, a detached HEAD).
206
- */
207
- export const resolveBranchName = async (ref: string): Promise<string | null> => {
208
- const fullName = await runGitCommand(['rev-parse', '--symbolic-full-name', ref], {
209
- operation: `resolve the branch name of ${ref}`,
210
- });
211
- return fullName.match(/^refs\/(?:heads|remotes\/[^/]+)\/(.+)$/)?.[1] ?? null;
212
- };
213
-
214
203
  /**
215
204
  * Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
216
205
  * uses for Git repo sources, e.g. git@github.com:apify-store/google-maps