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

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 (90) hide show
  1. package/.github/cli/apify-test-tools.mjs +125 -127
  2. package/.github/scripts/run-with-apify-tokens.mjs +1 -1
  3. package/CHANGELOG.md +5 -0
  4. package/CONTRIBUTING.md +4 -1
  5. package/README.md +69 -71
  6. package/bin/affected-actors.ts +132 -91
  7. package/bin/build.ts +0 -2
  8. package/bin/diff-changes.ts +7 -6
  9. package/bin/diff-json-schema.ts +5 -6
  10. package/bin/git.ts +21 -55
  11. package/bin/main.ts +7 -97
  12. package/bin/slack.ts +14 -27
  13. package/bin/types.ts +6 -0
  14. package/dist/bin/affected-actors.d.ts +23 -22
  15. package/dist/bin/affected-actors.d.ts.map +1 -1
  16. package/dist/bin/affected-actors.js +108 -60
  17. package/dist/bin/affected-actors.js.map +1 -1
  18. package/dist/bin/build.d.ts +0 -1
  19. package/dist/bin/build.d.ts.map +1 -1
  20. package/dist/bin/build.js +0 -2
  21. package/dist/bin/build.js.map +1 -1
  22. package/dist/bin/diff-changes.d.ts +4 -3
  23. package/dist/bin/diff-changes.d.ts.map +1 -1
  24. package/dist/bin/diff-changes.js +4 -4
  25. package/dist/bin/diff-changes.js.map +1 -1
  26. package/dist/bin/diff-json-schema.d.ts +2 -2
  27. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  28. package/dist/bin/diff-json-schema.js +1 -4
  29. package/dist/bin/diff-json-schema.js.map +1 -1
  30. package/dist/bin/git.d.ts +6 -9
  31. package/dist/bin/git.d.ts.map +1 -1
  32. package/dist/bin/git.js +20 -48
  33. package/dist/bin/git.js.map +1 -1
  34. package/dist/bin/main.js +7 -77
  35. package/dist/bin/main.js.map +1 -1
  36. package/dist/bin/slack.d.ts +2 -7
  37. package/dist/bin/slack.d.ts.map +1 -1
  38. package/dist/bin/slack.js +12 -18
  39. package/dist/bin/slack.js.map +1 -1
  40. package/dist/bin/types.d.ts +5 -0
  41. package/dist/bin/types.d.ts.map +1 -1
  42. package/dist/test/bundle/cli-bundle.test.js +14 -16
  43. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  44. package/dist/test/e2e/cli-bundle.e2e.test.js +22 -63
  45. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  46. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  47. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  48. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  49. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  50. package/dist/test/e2e/helpers.d.ts +37 -0
  51. package/dist/test/e2e/helpers.d.ts.map +1 -0
  52. package/dist/test/e2e/helpers.js +57 -0
  53. package/dist/test/e2e/helpers.js.map +1 -0
  54. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  55. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  56. package/dist/test/unit/bin/affected-actors-cli.test.js +161 -0
  57. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  58. package/dist/test/unit/bin/affected-actors.test.js +65 -78
  59. package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
  60. package/dist/test/unit/bin/build-cli.test.js +5 -13
  61. package/dist/test/unit/bin/build-cli.test.js.map +1 -1
  62. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  63. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  64. package/dist/test/unit/bin/compiled-cli.js +16 -0
  65. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  66. package/dist/test/unit/bin/diff-changes.test.js +55 -55
  67. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  68. package/dist/test/unit/bin/diff.test.js +25 -28
  69. package/dist/test/unit/bin/diff.test.js.map +1 -1
  70. package/dist/test/unit/bin/git.test.js +29 -97
  71. package/dist/test/unit/bin/git.test.js.map +1 -1
  72. package/dist/test/unit/bin/slack.test.js +6 -27
  73. package/dist/test/unit/bin/slack.test.js.map +1 -1
  74. package/dist/test/unit/should-built-and-test.test.js +18 -21
  75. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +1 -1
  78. package/test/bundle/cli-bundle.test.ts +14 -24
  79. package/test/e2e/cli-bundle.e2e.test.ts +39 -84
  80. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  81. package/test/e2e/helpers.ts +82 -0
  82. package/test/unit/bin/affected-actors-cli.test.ts +193 -0
  83. package/test/unit/bin/affected-actors.test.ts +83 -85
  84. package/test/unit/bin/build-cli.test.ts +6 -12
  85. package/test/unit/bin/compiled-cli.ts +17 -0
  86. package/test/unit/bin/diff-changes.test.ts +55 -55
  87. package/test/unit/bin/diff.test.ts +25 -28
  88. package/test/unit/bin/git.test.ts +37 -120
  89. package/test/unit/bin/slack.test.ts +6 -30
  90. package/test/unit/should-built-and-test.test.ts +19 -22
@@ -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-for-test --target-branch ...
4
+ // jq ... | node .github/scripts/run-with-apify-tokens.mjs apify-test-tools build --version-number 0.99
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
package/CHANGELOG.md CHANGED
@@ -10,12 +10,17 @@ All notable changes to this project will be documented in this file.
10
10
  - **ci:** Ship the workflows' 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
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)
13
+ - **get-affected-actors:** Three-dot diff, narrowed per Actor with --since ([#204](https://github.com/apify/apify-test-tools/pull/204)) ([fa9a0e7](https://github.com/apify/apify-test-tools/commit/fa9a0e7518764dd4722ec9ecbf67b2a26a5a062e)) by [@metalwarrior665](https://github.com/metalwarrior665)
13
14
 
14
15
  ### 🐛 Bug Fixes
15
16
 
16
17
  - **toBeObject:** Reject null ([#191](https://github.com/apify/apify-test-tools/pull/191)) ([6bdae49](https://github.com/apify/apify-test-tools/commit/6bdae4905756e0ae8dd6146942e6fc04dbead48e)) by [@JuanGalilea](https://github.com/JuanGalilea), closes [#88](https://github.com/apify/apify-test-tools/issues/88)
17
18
  - Build and test on test-only changes even when .dockerignore lists the tests ([#177](https://github.com/apify/apify-test-tools/pull/177)) ([5cb1b5a](https://github.com/apify/apify-test-tools/commit/5cb1b5ab5f6f43ec279f9624b402e73d55a5974a)) by [@artogahr](https://github.com/artogahr), closes [#145](https://github.com/apify/apify-test-tools/issues/145)
18
19
 
20
+ ### 🚜 Refactor
21
+
22
+ - [**breaking**] Remove build-for-test and release composite commands ([#203](https://github.com/apify/apify-test-tools/pull/203)) ([85efc54](https://github.com/apify/apify-test-tools/commit/85efc54697e98208c827c4f450906e00eebcd9ca)) by [@metalwarrior665](https://github.com/metalwarrior665)
23
+
19
24
 
20
25
  <!-- git-cliff-unreleased-end -->
21
26
  ## [0.10.1](https://github.com/apify/apify-test-tools/releases/tag/v0.10.1) (2026-10-13)
package/CONTRIBUTING.md CHANGED
@@ -94,9 +94,12 @@ npm release relate — the short version:
94
94
  - the CLI runs from `.github/cli/apify-test-tools.mjs`, a bundle the pre-commit hook rebuilds with
95
95
  `npm run bundle`. If you commit with `--no-verify`, run it yourself: the `CLI bundle` check fails
96
96
  on a stale bundle, and a stale bundle would hold the tag.
97
- - `npm test` covers the bundle without network (`test/bundle/`). `npm run test:e2e` runs it against two
97
+ - `npm test` covers the bundle without network (`test/bundle/`). `npm run test:e2e` runs it against
98
98
  dummy Actors in an isolated sandbox account (`test/e2e/`) and skips itself unless
99
99
  `E2E_TEST_APIFY_TOKEN_DBYPODOIHUZABNQTR` is set. CI passes that secret only to the `E2E tests` job.
100
+ The git `build` and release pipeline tests clone
101
+ [testing-repo-for-github-actions](https://github.com/apify-store/testing-repo-for-github-actions) and
102
+ use its `e2e/base` and `e2e/feature` branches as fixtures, so never rebase or delete those.
100
103
 
101
104
  `npm run lint` and `actionlint` (via the `Code checks` workflow) both gate master, so run them before
102
105
  pushing workflow changes.
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-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.
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: `get-affected-actors --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`; `get-affected-actors` 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,8 @@ jobs:
226
226
  secrets: inherit
227
227
  ```
228
228
 
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
229
+ This workflow runs one step per command: `get-affected-actors --since ${{ github.event.before }}`, then
230
+ `build --version-number default` on every changed Actor, then `notify-release`. See [Releasing Actors](#releasing-actors) for how the
232
231
  released range is determined and what to pass if you call the CLI from somewhere else.
233
232
 
234
233
  ### `claude-review.yaml`
@@ -274,11 +273,13 @@ secrets to pass on:
274
273
 
275
274
  ```yaml
276
275
  - name: Build
276
+ shell: bash
277
277
  env:
278
278
  ALL_SECRETS: ${{ toJSON(secrets) }}
279
279
  run: |
280
- node "${{ steps.setup.outputs.scripts-path }}/run-with-apify-tokens.mjs" \
281
- npx apify-test-tools build-for-test --target-branch ...
280
+ jq '{ actors: .functionallyChangedActors }' affected.json \
281
+ | node "${{ steps.setup.outputs.scripts-path }}/run-with-apify-tokens.mjs" \
282
+ npx apify-test-tools build --version-number 0.99
282
283
  ```
283
284
 
284
285
  The wrapper passes only the tokens the config declares and drops `ALL_SECRETS`, so neither `npx` nor
@@ -569,12 +570,12 @@ The package includes a CLI binary used by CI workflows to build Actors, detect c
569
570
 
570
571
  ### Dry runs (`--dry-run`)
571
572
 
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:
573
+ Every command that changes something (`build`, `build-from-local`, `notify-release`, `delete-old-builds`, `report-tests`) takes `--dry-run`. A dry run:
573
574
 
574
575
  - **Changes nothing.** No versions or builds are created, nothing is uploaded or deleted, and no Slack message is posted.
575
576
  - **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`).
576
577
  - **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.
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.
578
+ - **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.
578
579
 
579
580
  ### Running locally
580
581
 
@@ -599,16 +600,15 @@ If you want to test vs existing src code, you can skip this and instead construc
599
600
  Requires `APIFY_TOKEN_<USERNAME>` for all Apify users that own your Actors (e.g. `apify`, `compass`, `lukaskrivka` users). The username is derived from the actor name — uppercased with non-word chars replaced by `_` (e.g. Actor `john.doe/my-actor` needs `APIFY_TOKEN_JOHN_DOE`).
600
601
 
601
602
  ```bash
602
- APIFY_TOKEN_JOHN_DOE=<token> \
603
- npx apify-test-tools build-for-test \
604
- --target-branch origin/master \
605
- --dry-run
603
+ npx apify-test-tools get-affected-actors --base origin/master \
604
+ | jq '{ actors: .functionallyChangedActors }' \
605
+ | APIFY_TOKEN_JOHN_DOE=<token> npx apify-test-tools build --version-number 0.99 --dry-run
606
606
  ```
607
607
 
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:
608
+ Run it with your dummy branch checked out: it builds the Actors that branch changed functionally, from the branch
609
+ as it is on the remote, into the test version `0.99`. With `--dry-run`, it prints the plan (the version and Git URL
610
+ each Actor would be built from). Remove `--dry-run` to actually trigger builds, and update `--base` to the branch
611
+ you compare against. `build` outputs a JSON array of build objects to stdout:
612
612
 
613
613
  ```json
614
614
  [{ "buildId": "...", "actorRawId": "...", "buildNumber": "...", "actorFullName": "john.doe/my-actor" }]
@@ -647,14 +647,13 @@ TESTER_APIFY_TOKEN=<token> \
647
647
 
648
648
  #### Full example
649
649
 
650
- Tests only run for Actors in `ACTOR_BUILDS`, so a custom workflow has to build (and pass) the Actors even when only tests changed. Pass `--test-files-glob` so test changes count even when `.dockerignore` lists them.
650
+ Tests only run for Actors in `ACTOR_BUILDS`, so a custom workflow has to build (and pass) the Actors even when only tests changed. Pass `--test-files-glob` to `get-affected-actors` so test changes count even when `.dockerignore` lists them.
651
651
 
652
652
  ```bash
653
653
  # Build and capture output
654
- BUILDS=$(APIFY_TOKEN_JOHN_DOE=apify_api_xxx \
655
- npx apify-test-tools build-for-test \
656
- --target-branch origin/master \
657
- --test-files-glob 'test/platform/**')
654
+ BUILDS=$(npx apify-test-tools get-affected-actors --base origin/master --test-files-glob 'test/platform/**' \
655
+ | jq '{ actors: .functionallyChangedActors }' \
656
+ | APIFY_TOKEN_JOHN_DOE=apify_api_xxx npx apify-test-tools build --version-number 0.99)
658
657
 
659
658
  # Run tests with the builds
660
659
  ACTOR_BUILDS="$BUILDS" \
@@ -669,7 +668,7 @@ TESTER_APIFY_TOKEN=apify_api_yyy \
669
668
  The range always ends at the checked-out `HEAD`, so check out the branch you want to inspect first.
670
669
 
671
670
  ```bash
672
- # A PR: the branch's own commits, after its last validated commit
671
+ # A PR: the branch's own changes, narrowed to the Actors that changed since its last validated commit
673
672
  npx apify-test-tools get-affected-actors --base origin/master --since <sha>
674
673
 
675
674
  # A release: everything after the last released commit
@@ -678,23 +677,31 @@ npx apify-test-tools get-affected-actors --since <sha>
678
677
 
679
678
  | Flag | Default | Meaning |
680
679
  | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
681
- | `--base` | | Only commits on `HEAD` that are not on this branch count |
682
- | `--since` | | Only commits after this one count (see below) |
680
+ | `--base` | | Only what `HEAD` changes compared to this branch counts, like `git diff <base>...HEAD` |
681
+ | `--since` | | Only changes after this commit count (see below) |
683
682
  | `--test-files-glob` | | Test files (glob from the repo root) that count as changes even when `.dockerignore` lists them, see [Change detection](#change-detection) |
684
683
  | `--actors` | all | Only report these Actors |
685
684
  | `--ignore` | none | Never report these Actors |
686
685
 
687
686
  Pass `--base`, `--since`, or both:
688
687
 
689
- - **`--base` only** inspects the whole branch. If the branch merged `--base` in and has no functional changes
690
- of its own, no Actor is reported as functional, since `--base` is already validated (and none as cosmetic
691
- either, if the branch has no changes of its own at all).
692
- - **`--base` and `--since`** inspects the branch after its last validated commit. If `--since` is not on the
693
- branch (force-push), or it is the head (a rerun), the whole branch is inspected instead.
688
+ - **`--base` only** inspects what the branch changes, the same files GitHub's "Files changed" tab shows: the diff
689
+ from where the branch forked from `--base`, or the last commit of `--base` it merged in. What it merged in from
690
+ `--base` is already validated there, so it doesn't count.
691
+ - **`--base` and `--since`** also inspects what changed after the branch's last validated commit, and reports only
692
+ the Actors that changed in both. An Actor in only one of them is already validated, by this branch or on `--base`.
693
+ When both changed an Actor, the changes can interact, so it is reported, but as functional only when both changes
694
+ are functional. A validated functional change followed by README edits is cosmetic, and so is a functional
695
+ change merged in from `--base` that meets only cosmetic changes of the branch. `--since` doesn't have to be on
696
+ the branch, so a rebase only re-checks the Actors that `--base` changed too. If `--since` is not in the local
697
+ history (a force-push), or it is `HEAD` (a rerun), it is ignored and the whole branch is checked.
694
698
  - **`--since` only** inspects everything after it up to `HEAD`. It must be an ancestor of `HEAD` and fails
695
- otherwise, exactly like [`--base-commit` of `release`](#--base-commit). If `HEAD` is `--since`, nothing is
699
+ otherwise, see [Releasing Actors](#releasing-actors). If `HEAD` is `--since`, nothing is
696
700
  reported.
697
701
 
702
+ `commits`, `changedFiles` and `changelog` describe the whole range, `--base...HEAD` or `--since..HEAD`: `--since`
703
+ together with `--base` only narrows the Actor lists.
704
+
698
705
  The output is a JSON document:
699
706
 
700
707
  ```json
@@ -711,25 +718,23 @@ The output is a JSON document:
711
718
 
712
719
  Each changed Actor is in exactly one of the lists. `functionallyChangedActors` have at least one functional change, so
713
720
  they need a new build and tests. `cosmeticallyChangedActors` only have cosmetic changes (see
714
- [Change detection](#change-detection)), so they need a new build only when released: `build-for-test` builds
715
- `functionallyChangedActors`, `release` builds both.
716
-
717
- `build-for-test` and `release` use the same detection internally.
721
+ [Change detection](#change-detection)), so they need a new build only when released: the PR workflow builds
722
+ `functionallyChangedActors`, the release workflow builds both.
718
723
 
719
724
  ### Building Actors (`build`)
720
725
 
721
726
  `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
727
+ prints the builds as the same JSON array `build-from-local` prints. A detached HEAD is an
723
728
  error, since there is no branch to build from. It doesn't look at git history: which Actors to build is
724
729
  up to you, usually picked from the output of [`get-affected-actors`](#finding-affected-actors-get-affected-actors).
725
730
 
726
731
  ```bash
727
- # What build-for-test does: the Actors a branch changed functionally, built into the test version
732
+ # A PR: the Actors a branch changed functionally, built into the test version
728
733
  npx apify-test-tools get-affected-actors --base origin/master \
729
734
  | jq '{ actors: .functionallyChangedActors }' \
730
735
  | npx apify-test-tools build --version-number 0.99
731
736
 
732
- # What release builds: every Actor changed since the last release, into the version users run
737
+ # A release: every Actor changed since the last release, into the version users run
733
738
  npx apify-test-tools get-affected-actors --since <sha> \
734
739
  | jq '{ actors: (.functionallyChangedActors + .cosmeticallyChangedActors) }' \
735
740
  | npx apify-test-tools build --version-number default
@@ -751,39 +756,33 @@ Actions, give such a step `shell: bash` so the pipe also fails on its own (the d
751
756
  so building the default version moves its tag (usually `latest`) to the new build, and building a version
752
757
  without a tag leaves every tag where it is. Builds always use the Docker cache.
753
758
 
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.
759
+ ### Releasing Actors
758
760
 
759
- ### Releasing Actors (`release`)
760
-
761
- `release` builds the `latest` (default) version of every Actor changed since the last release and
762
- posts release notes to Slack. It reads everything else from git: the branch from the checked-out
763
- `HEAD`, the commits and changed files from the range `<base-commit>..HEAD`, and the repository from
764
- the `origin` remote.
761
+ A release is three commands: find what changed since the last released commit, build those Actors into
762
+ their default version (the one users run), and announce it.
765
763
 
766
764
  ```bash
767
- npx apify-test-tools release --base-commit <sha> --release-slack-channel "#releases" --dry-run
768
- ```
765
+ npx apify-test-tools get-affected-actors --since <sha> > affected.json
769
766
 
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`.
767
+ jq '{ actors: (.functionallyChangedActors + .cosmeticallyChangedActors) }' affected.json \
768
+ | npx apify-test-tools build --version-number default --dry-run
773
769
 
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
770
+ npx apify-test-tools notify-release --release-slack-channel "#releases" --dry-run < affected.json
777
771
  ```
778
772
 
779
- #### `--base-commit`
773
+ `notify-release` reads the output of `get-affected-actors` on stdin. It posts the changelog additions to
774
+ `--release-slack-channel` and the commits and changed files to `--report-slack-channel`, and posts nothing when
775
+ there are no new commits. It names the release after the `origin` remote (or `--repo-url`) and the author of the
776
+ newest commit, and needs `SLACK_TOKEN_RELEASES_BOT`.
777
+
778
+ #### The last released commit (`--since`)
780
779
 
781
- The last commit that is **already released**. Everything after it, up to `HEAD`, is released. It is
782
- required: git alone can't tell which commits a push brought, and guessing wrong silently leaves Actors
783
- on old code.
780
+ The last commit that is **already released**. Everything after it, up to `HEAD`, is released. Git alone
781
+ can't tell which commits a push brought, and guessing wrong silently leaves Actors on old code, so it has
782
+ to be passed.
784
783
 
785
- The changed files come from diffing `<base-commit>` and `HEAD` directly, so this works for every
786
- merge strategy as long as the base commit is the branch tip from **before** the push:
784
+ The changed files come from diffing `--since` and `HEAD` directly, so this works for every merge strategy as
785
+ long as `--since` is the branch tip from **before** the push:
787
786
 
788
787
  | Merge strategy | What lands on the branch | What to pass |
789
788
  | ------------------------------ | --------------------------------- | ---------------------------------------------------------------------- |
@@ -793,26 +792,25 @@ merge strategy as long as the base commit is the branch tip from **before** the
793
792
 
794
793
  On GitHub, the push's "before" commit is `${{ github.event.before }}`, which the reusable workflow
795
794
  passes for you. On other CIs use the equivalent, e.g. `$CI_COMMIT_BEFORE_SHA` on GitLab. The checkout
796
- needs the full history (`fetch-depth: 0` in `actions/checkout`) so the base commit is available locally.
795
+ needs the full history (`fetch-depth: 0` in `actions/checkout`) so the commit is available locally.
797
796
 
798
- `release` fails instead of guessing when:
797
+ `get-affected-actors --since` without `--base` fails instead of guessing when:
799
798
 
800
- - the base commit is all zeros (GitHub sends this for a push that created the branch)
801
- - the base commit isn't in the local history (shallow checkout) or isn't an ancestor of `HEAD`
799
+ - the commit is all zeros (GitHub sends this for a push that created the branch)
800
+ - the commit isn't in the local history (shallow checkout) or isn't an ancestor of `HEAD`
802
801
  (the branch was force-pushed)
803
802
 
804
- In those cases, rerun `release` with a `--base-commit` you pick yourself: an ancestor of `HEAD` from
805
- before the changes you want to release (for a shallow checkout, fetch the full history instead).
806
- `--actors` only narrows which of the changed Actors get released; it never forces a build, so on its
807
- own it doesn't get past these errors.
808
- If `HEAD` is the base commit, there is nothing to release and the command exits successfully.
803
+ In those cases, rerun it with a `--since` you pick yourself: an ancestor of `HEAD` from before the changes
804
+ you want to release (for a shallow checkout, fetch the full history instead). `--actors` only narrows which
805
+ of the changed Actors are reported; it never forces a build, so on its own it doesn't get past these errors.
806
+ If `HEAD` is `--since`, nothing is reported, so nothing is built or announced.
809
807
 
810
808
  A failed release isn't retried by the next push: that push's base commit is the failed one's head, so
811
809
  its changes are skipped. Re-run the failed workflow run, or release the Actors manually.
812
810
 
813
811
  #### Repository check
814
812
 
815
- `release` and `build` point each Actor's version at the `origin` remote. Because a build overwrites the
813
+ `build` points each Actor's version at the `origin` remote. Because a build overwrites the
816
814
  version's Git URL, building from a fork or a mirror would repoint the Actor. So, before building, the
817
815
  remote is compared with the Git URL of the Actor's default version (the one its default build tag
818
816
  points to), and the command fails if they differ or if the default version isn't built from a Git
@@ -1,27 +1,31 @@
1
1
  import { type ChangedActors, getChangedActors } from './diff-changes.js';
2
2
  import {
3
- getBranchOnlyChangedFiles,
3
+ findCommitSha,
4
4
  getChangedFilesBetween,
5
5
  getChangelogAdditions,
6
- getCommits,
7
6
  getCommitsBetween,
8
- hasMergeFromTarget,
7
+ getMergeBase,
9
8
  resolveCommitSha,
10
9
  resolveReleaseBaseCommit,
11
10
  } from './git.js';
12
11
  import { logger } from './logger.js';
13
- import type { ActorConfig, Commit } from './types.js';
12
+ import type { ActorConfig, Commit, GitRange } from './types.js';
14
13
  import { readConfigFile } from './utils/config/load-config.js';
15
14
 
16
15
  // The range always ends at the checked-out commit: check out what you want to inspect
17
16
  const HEAD = 'HEAD';
18
17
 
19
18
  interface AffectedActorsOptions {
20
- /** Only the commits on HEAD that are not on base count, i.e. the branch's own changes. */
19
+ /**
20
+ * Base usually points to the master branch
21
+ * Only what HEAD changes compared to base counts, like `git diff base...HEAD` or GitHub's "Files changed" tab:
22
+ * the branch's own changes, not what it merged in from base.
23
+ */
21
24
  base?: string;
22
25
  /**
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).
26
+ * With `base` it's the last validated commit of the branch: Actors that didn't change after it are left out.
27
+ * It's ignored when it is HEAD (a rerun) or isn't in the local history. Without `base` everything after it
28
+ * counts, and it must be an ancestor of HEAD (e.g. the last release).
25
29
  */
26
30
  since?: string;
27
31
  /** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
@@ -30,77 +34,113 @@ interface AffectedActorsOptions {
30
34
  ignore: string[];
31
35
  }
32
36
 
37
+ /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
33
38
  interface AffectedActors {
34
- headSha: string;
39
+ version: 1;
40
+ head: string;
41
+ /** The commits, changed files and changelog describe the whole range, `--since` with `--base` only narrows the Actors. */
35
42
  commits: Commit[];
36
43
  changedFiles: string[];
37
44
  changelog: string | null;
38
- /** Actors with at least one functional change. They need a new build and tests. */
39
- functionallyChangedActors: ActorConfig[];
45
+ /**
46
+ * Actors with at least one functional change. They need a new build and tests. Only the names: the config file
47
+ * stays the source of truth for the rest.
48
+ */
49
+ functionallyChangedActors: { actorFullName: string }[];
40
50
  /** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
41
- cosmeticallyChangedActors: ActorConfig[];
42
- }
43
-
44
- interface CommitRange {
45
- commits: Commit[];
46
- fromRef: string;
47
- toRef: string;
51
+ cosmeticallyChangedActors: { actorFullName: string }[];
48
52
  }
49
53
 
50
- /** The branch's own commits, after `since` when it is on the branch (see getCommits). */
51
- const getBranchRange = async (base: string, since: string | undefined): Promise<CommitRange> => {
52
- const commits = await getCommits({ sourceBranch: HEAD, targetBranch: base, baseCommit: since });
53
- return { commits, fromRef: `${commits[0].sha}~`, toRef: commits[commits.length - 1].sha };
54
- };
54
+ const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
55
55
 
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);
59
- const commits = sinceSha === headSha ? [] : await getCommitsBetween(sinceSha, headSha);
60
- return { commits, fromRef: sinceSha, toRef: headSha };
56
+ /** The branch's last validated commit, unless it can't narrow anything down. */
57
+ const findValidatedSha = async (since: string, headSha: string): Promise<string | undefined> => {
58
+ const sinceSha = await findCommitSha(since);
59
+ if (!sinceSha) {
60
+ logger.info(
61
+ `Last validated commit ${since} is not in the local history (force-push?), checking the whole branch`,
62
+ );
63
+ return undefined;
64
+ }
65
+ if (sinceSha === headSha) {
66
+ logger.info('Last validated commit is HEAD, so this is a rerun: checking the whole branch from scratch');
67
+ return undefined;
68
+ }
69
+ return sinceSha;
61
70
  };
62
71
 
63
72
  /**
64
- * This is an optimization for the common case where a branch only has cosmetic changes but had to merge in
65
- * functional changes from base (being up-to-date is a CI requirement). Base is already validated, and
66
- * since the branch has no functional changes of its own, there is nothing new to validate.
67
- * Exception: if the branch has any functional changes alongside the merge, we must re-test — even
68
- * individually validated changes can have novel interactions when combined.
69
- * Returns the Actors the branch's own commits changed, or undefined when it didn't merge base in.
73
+ * This is Juan's magical algorithm where we need just 2 git diffs to determine functional changes on Actors.
74
+ *
75
+ * Naively, we could just check branch HEAD changes vs base or parent of first commit and be done but
76
+ * we do this jiu-jitsu code to apply 2 optimizations:
77
+ * 1. On our PR branch, we track last validated commit (since) that was tested and so we only need to test new commits.
78
+ * 2. We also merge changes from the base branch (usually master) and we can consider those already validated as well.
79
+ *
80
+ * But there is annoying problem: even if both our branch and base are validated, they could have novel interactions and
81
+ * we need to retest those.
82
+ *
83
+ * So we look at:
84
+ * 1. The changes introduced by our branch since the last validated commit (`since..HEAD`).
85
+ * 2. The changes introduced by the branch compared to the base branch (`base...HEAD`). The important part is that this
86
+ * diff does check the whole branch, discarding the last validated commit (since) so it checks those novel interactions problem.
87
+ *
88
+ * The magic is that both need to contain changes for an Actor to be retested:
89
+ * 1. If we have changes only in since..HEAD (and not in base...HEAD), it means those changes came from merging the base alone
90
+ * which is already validated and there were actually no real changes introduced by our branch alone.
91
+ * 2. If we have changes only in base...HEAD (and not in since..HEAD), it means there are no new changes on our branch
92
+ * since it was last validated. base...HEAD has changes that we already validated.
93
+ *
94
+ * This enables to simply do those 2 diffs for each Actor's context separately and only Actors that have changes
95
+ * in both diffs need to be retested.
70
96
  *
71
- * TODO: Apply this per Actor instead of to the whole branch. Changes merged in from base only interact with the
72
- * branch's own changes in Actors whose context both touch, so an Actor that only base changed is already validated
73
- * and can be skipped even when the branch changes other Actors functionally. Today any functional change of the
74
- * branch's own keeps every Actor, and the log messages below describe the whole branch.
97
+ * THIS ASSUMES BRANCH IS UP TO DATE WITH BASE WHEN IT MERGES, which we currently require. Only then is HEAD what lands on
98
+ * base, and base's commits validated. Otherwise what lands is HEAD combined with base's newer commits, which nobody
99
+ * tested, however the Actors are picked.
100
+ *
101
+ * In each example the branch's functional change to Actor A was validated at `since`, then one more push follows:
102
+ *
103
+ * Example scenarios:
104
+ * - Our own branch only: a commit changes A's README and B's code.
105
+ * `since..HEAD`: A cosmetic, B functional. `base...HEAD`: A and B functional.
106
+ * → A cosmetic, B functional: only B is tested, A's code is still the validated one.
107
+ * - Cosmetic merge: the branch merges base, which only changed READMEs and the CHANGELOG.
108
+ * `since..HEAD`: every Actor cosmetic (CHANGELOG counts for all). `base...HEAD`: A functional, since the merged
109
+ * files now match base and drop out of it.
110
+ * → A cosmetic, the other Actors left out: nothing is tested.
111
+ * - Functional merge: the branch merges base, which changed code of A and of C.
112
+ * `since..HEAD`: A and C functional. `base...HEAD`: A functional.
113
+ * → A functional, C left out: A is tested, since base's change can interact with the branch's, while C only has
114
+ * base's change, which base already validated.
75
115
  */
76
- const getOwnChangesIfMergedFromBase = async (
77
- base: string,
78
- actorConfigs: ActorConfig[],
79
- testFilesGlob: string | undefined,
80
- ): Promise<ChangedActors | undefined> => {
81
- if (!(await hasMergeFromTarget(HEAD, base))) return undefined;
116
+ const intersectChangedInBranchVsSince = (branch: ChangedActors, sinceValidated: ChangedActors): ChangedActors => {
117
+ const narrowed: ChangedActors = { functional: [], cosmetic: [] };
118
+ // All checks are done for each Actor separately, that's cleanest and most accurate
119
+ for (const actor of [...branch.functional, ...branch.cosmetic]) {
120
+ // base...HEAD
121
+ const changedFunctionallyOnBranch = branch.functional.includes(actor);
122
+ // since..HEAD
123
+ const changedFunctionallySince = sinceValidated.functional.includes(actor);
124
+
125
+ // changedSince is used for release. Release cares also about cosmetic.
126
+ const changedSince = changedFunctionallySince || sinceValidated.cosmetic.includes(actor);
127
+
128
+ // Nothing changed since the last validation, so the branch's change to this Actor is already validated
129
+ if (!changedSince) continue;
82
130
 
83
- logger.info(
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',
85
- );
86
- const own = await getChangedActors({
87
- filepathsChanged: await getBranchOnlyChangedFiles(HEAD, base),
88
- actorConfigs,
89
- // Omit since to get full branch history. Validated functional commits can still interact with merged ones
90
- commits: await getCommits({ sourceBranch: HEAD, targetBranch: base }),
91
- testFilesGlob,
92
- });
93
- logger.info(
94
- own.functional.length > 0
95
- ? `[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${own.functional.length} functional changes, cannot optimize, we continue with full check`
96
- : '[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests',
97
- );
98
- return own;
131
+ // As we said, only intersection counts as functional, otherwise it's cosmetic
132
+ if (changedFunctionallyOnBranch && changedFunctionallySince) {
133
+ narrowed.functional.push(actor);
134
+ } else {
135
+ narrowed.cosmetic.push(actor);
136
+ }
137
+ }
138
+ return narrowed;
99
139
  };
100
140
 
101
141
  /**
102
- * Finds the Actors whose context changed in the given range. `base` selects the branch's own commits
103
- * (a PR), `since` the commits after a known one (a release, or the last validated commit of a PR).
142
+ * Finds the Actors whose context changed in the given range. `base` selects the branch's own changes
143
+ * (a PR), `since` the changes after a known commit (a release, or the last validated commit of a PR).
104
144
  */
105
145
  export const getAffectedActors = async ({
106
146
  base,
@@ -115,43 +155,44 @@ export const getAffectedActors = async ({
115
155
  throw new Error('Pass --base, --since, or both to select the commits to inspect.');
116
156
  }
117
157
 
118
- const headSha = await resolveCommitSha(HEAD);
119
- const { commits, fromRef, toRef } = base ? await getBranchRange(base, since) : await getRangeSince(headSha, since!);
120
158
  const actorConfigs = await readConfigFile({ actors, ignore });
159
+ const getChangedInRange = async (range: GitRange) => {
160
+ const changedFiles = await getChangedFilesBetween(range.fromRef, range.toRef);
161
+ return {
162
+ changedFiles,
163
+ changedActors: await getChangedActors({
164
+ filepathsChanged: changedFiles,
165
+ actorConfigs,
166
+ range,
167
+ testFilesGlob,
168
+ }),
169
+ };
170
+ };
121
171
 
122
- // What the branch only merged in from base is already validated. Without functional changes of its own
123
- // the branch has nothing to test, and without any changes of its own nothing to release either.
124
- const own = base ? await getOwnChangesIfMergedFromBase(base, actorConfigs, testFilesGlob) : undefined;
125
- const changedFiles = await getChangedFilesBetween(fromRef, toRef);
126
- const changed = await getChangedActors({ filepathsChanged: changedFiles, actorConfigs, commits, testFilesGlob });
127
-
128
- return {
129
- headSha,
130
- commits,
131
- changedFiles,
132
- changelog: await getChangelogAdditions(fromRef, toRef, changedFiles),
133
- functionallyChangedActors: own && own.functional.length === 0 ? [] : changed.functional,
134
- cosmeticallyChangedActors: own && own.functional.length + own.cosmetic.length === 0 ? [] : changed.cosmetic,
172
+ const headSha = await resolveCommitSha(HEAD);
173
+ const rangeVsBase: GitRange = {
174
+ // merge base = PR, release base = push to master (release)
175
+ fromRef: base ? await getMergeBase(base, HEAD) : await resolveReleaseBaseCommit(since!),
176
+ toRef: headSha,
135
177
  };
136
- };
137
178
 
138
- /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
139
- export const toAffectedActorsDocument = ({
140
- headSha,
141
- commits,
142
- changedFiles,
143
- changelog,
144
- functionallyChangedActors,
145
- cosmeticallyChangedActors,
146
- }: AffectedActors) => {
147
- const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
179
+ // Falls back to rangeVsBase.fromRef when there's no (usable) last validated commit — same as
180
+ // intersecting the branch's changes with themselves, which is a no-op (see this function's docs).
181
+ const validatedSha = base && since ? await findValidatedSha(since, headSha) : undefined;
182
+ const sinceFromRef = validatedSha ?? rangeVsBase.fromRef;
183
+
184
+ const { changedFiles, changedActors: changedVsBase } = await getChangedInRange(rangeVsBase);
185
+ const { changedActors: changedSinceActors } = await getChangedInRange({ fromRef: sinceFromRef, toRef: headSha });
186
+ // See this function's docs to understand the algorithm: only intersection counts as functional, otherwise it's cosmetic.
187
+ const changedActors = intersectChangedInBranchVsSince(changedVsBase, changedSinceActors);
188
+
148
189
  return {
149
190
  version: 1,
150
191
  head: headSha,
151
- commits,
192
+ commits: await getCommitsBetween(rangeVsBase.fromRef, rangeVsBase.toRef),
152
193
  changedFiles,
153
- changelog,
154
- functionallyChangedActors: names(functionallyChangedActors),
155
- cosmeticallyChangedActors: names(cosmeticallyChangedActors),
194
+ changelog: await getChangelogAdditions(rangeVsBase.fromRef, rangeVsBase.toRef, changedFiles),
195
+ functionallyChangedActors: names(changedActors.functional),
196
+ cosmeticallyChangedActors: names(changedActors.cosmetic),
156
197
  };
157
198
  };
package/bin/build.ts CHANGED
@@ -14,8 +14,6 @@ type BuildPrActorOptions = {
14
14
 
15
15
  // Fixed version number used to build from local source files, since there is no real version to track.
16
16
  export const LOCAL_SOURCE_VERSION_NUMBER = '0.98';
17
- // Version that build-for-test builds, so tests never touch the version users run
18
- export const TEST_VERSION_NUMBER = '0.99';
19
17
  // Stands for each Actor's default version: the one its default build tag points to
20
18
  export const DEFAULT_VERSION_KEYWORD = 'default';
21
19