apify-test-tools 0.10.2-beta.19 → 0.10.2-beta.20

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 (75) hide show
  1. package/.github/cli/apify-test-tools.mjs +114 -114
  2. package/.github/scripts/run-with-apify-tokens.mjs +6 -6
  3. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +1 -1
  4. package/.github/workflows/public_platform-tests.yaml +6 -8
  5. package/.github/workflows/public_pr-build-test.yaml +11 -11
  6. package/.github/workflows/public_push-build-latest.yaml +1 -0
  7. package/CHANGELOG.md +1 -0
  8. package/CONTRIBUTING.md +1 -1
  9. package/README.md +10 -6
  10. package/bin/affected-actors.ts +20 -7
  11. package/bin/diff-changes.ts +40 -16
  12. package/bin/main.ts +12 -19
  13. package/bin/run-tests.ts +29 -4
  14. package/bin/test-files-glob.ts +126 -0
  15. package/bin/utils/config/load-config.ts +4 -1
  16. package/bin/utils/config/parser.ts +24 -1
  17. package/dist/bin/affected-actors.d.ts +13 -4
  18. package/dist/bin/affected-actors.d.ts.map +1 -1
  19. package/dist/bin/affected-actors.js +11 -4
  20. package/dist/bin/affected-actors.js.map +1 -1
  21. package/dist/bin/diff-changes.d.ts +12 -3
  22. package/dist/bin/diff-changes.d.ts.map +1 -1
  23. package/dist/bin/diff-changes.js +25 -12
  24. package/dist/bin/diff-changes.js.map +1 -1
  25. package/dist/bin/main.js +11 -17
  26. package/dist/bin/main.js.map +1 -1
  27. package/dist/bin/run-tests.d.ts +3 -3
  28. package/dist/bin/run-tests.d.ts.map +1 -1
  29. package/dist/bin/run-tests.js +24 -2
  30. package/dist/bin/run-tests.js.map +1 -1
  31. package/dist/bin/test-files-glob.d.ts +18 -0
  32. package/dist/bin/test-files-glob.d.ts.map +1 -0
  33. package/dist/bin/test-files-glob.js +108 -0
  34. package/dist/bin/test-files-glob.js.map +1 -0
  35. package/dist/bin/utils/config/load-config.d.ts +3 -0
  36. package/dist/bin/utils/config/load-config.d.ts.map +1 -1
  37. package/dist/bin/utils/config/load-config.js +3 -1
  38. package/dist/bin/utils/config/load-config.js.map +1 -1
  39. package/dist/bin/utils/config/parser.d.ts +9 -0
  40. package/dist/bin/utils/config/parser.d.ts.map +1 -1
  41. package/dist/bin/utils/config/parser.js +13 -1
  42. package/dist/bin/utils/config/parser.js.map +1 -1
  43. package/dist/test/bundle/cli-bundle.test.js +1 -1
  44. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  45. package/dist/test/e2e/cli-bundle.e2e.test.js +1 -1
  46. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  47. package/dist/test/unit/bin/actor-filtering.test.js +7 -0
  48. package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
  49. package/dist/test/unit/bin/affected-actors-cli.test.js +59 -2
  50. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -1
  51. package/dist/test/unit/bin/affected-actors.test.js +18 -6
  52. package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
  53. package/dist/test/unit/bin/diff-changes.test.js +44 -19
  54. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  55. package/dist/test/unit/bin/test-cli.test.js +34 -3
  56. package/dist/test/unit/bin/test-cli.test.js.map +1 -1
  57. package/dist/test/unit/bin/test-files-glob.test.d.ts +2 -0
  58. package/dist/test/unit/bin/test-files-glob.test.d.ts.map +1 -0
  59. package/dist/test/unit/bin/test-files-glob.test.js +102 -0
  60. package/dist/test/unit/bin/test-files-glob.test.js.map +1 -0
  61. package/dist/test/unit/bin/utils/config/parser.test.js +16 -1
  62. package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
  63. package/dist/tsconfig.tsbuildinfo +1 -1
  64. package/docs/cli.md +46 -29
  65. package/docs/workflows.md +53 -20
  66. package/package.json +1 -1
  67. package/test/bundle/cli-bundle.test.ts +1 -1
  68. package/test/e2e/cli-bundle.e2e.test.ts +9 -5
  69. package/test/unit/bin/actor-filtering.test.ts +8 -0
  70. package/test/unit/bin/affected-actors-cli.test.ts +73 -3
  71. package/test/unit/bin/affected-actors.test.ts +49 -10
  72. package/test/unit/bin/diff-changes.test.ts +48 -19
  73. package/test/unit/bin/test-cli.test.ts +41 -3
  74. package/test/unit/bin/test-files-glob.test.ts +118 -0
  75. package/test/unit/bin/utils/config/parser.test.ts +24 -1
@@ -22,7 +22,7 @@ name: 'Platform tests: Claude failure investigation and fix'
22
22
  # platformTestsCore:
23
23
  # uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
24
24
  # with:
25
- # subtest: core
25
+ # test-files-glob: 'test/platform/hourly/**'
26
26
  # report-slack-channel: '#notif-my-repo'
27
27
  # secrets:
28
28
  # TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
@@ -3,15 +3,10 @@ name: Platform tests
3
3
  on:
4
4
  workflow_call:
5
5
  inputs:
6
- # Deprecated, use `test-files-glob`
7
- subtest:
8
- type: string
9
- default: ''
10
- required: false
11
- # Specify what tests you want to run, relative to `test/platform`
6
+ # Glob of the test files to run, from the repo root, e.g. `test/platform/{hourly,hourly-without-pr}/**`
12
7
  test-files-glob:
13
8
  type: string
14
- default: ''
9
+ default: 'test/platform/**'
15
10
  required: false
16
11
  # Slack channel for failed tests. Empty skips Slack.
17
12
  report-slack-channel:
@@ -64,9 +59,12 @@ jobs:
64
59
  shell: bash
65
60
  env:
66
61
  TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
62
+ # Passed through the environment and quoted, so the shell doesn't expand the glob itself
63
+ # An empty input, e.g. from an expression that picks the glob, runs everything, like the default
64
+ TEST_FILES_GLOB: ${{ inputs.test-files-glob || 'test/platform/**' }}
67
65
  run: |
68
66
  apify-test-tools get-actor-configs \
69
- | apify-test-tools test ./test/platform/${{ inputs.test-files-glob || inputs.subtest }} \
67
+ | apify-test-tools test --test-files-glob "$TEST_FILES_GLOB" \
70
68
  --reporter github-actions --vitest-output-file ./test-output.json > test-report.json
71
69
 
72
70
  # Posts only when the tests failed, but checks the Slack token on every run that has a channel. Runs
@@ -7,10 +7,11 @@ on:
7
7
  type: boolean
8
8
  default: false
9
9
  required: false
10
- # Specify what tests you want to run, relative to `test/platform`
10
+ # Glob of the test files to run, from the repo root, e.g. `test/platform/{daily,hourly}/**`. Which changed
11
+ # files count as test changes is `platformTestsGlob` in apify-test-tools.config.json, not this.
11
12
  test-files-glob:
12
13
  type: string
13
- default: ''
14
+ default: 'test/platform/**'
14
15
  required: false
15
16
  # Fail the build unless the env var names of version 0.99 match the default version's
16
17
  require-env-vars-in-sync:
@@ -86,16 +87,12 @@ jobs:
86
87
  run: |
87
88
  base_commit=$(cat base_commit.txt 2>/dev/null || echo "")
88
89
  echo "Last validated (base) commit to be used for build & test: $base_commit"
89
- args=(
90
- --base "origin/$BASE_REF"
91
- # TODO: Temporary hotfix until build glob matching aligns with Vitest's test selection glob.
92
- --test-files-glob 'test/platform/**'
93
- )
90
+ args=(--base "origin/$BASE_REF")
94
91
  [ -n "$base_commit" ] && args+=(--since "$base_commit")
95
92
  apify-test-tools get-affected-actors "${args[@]}" > affected.json
96
93
 
97
- # Cosmetic-only changes need no tests, so only the functionally changed Actors are built, into the
98
- # test version 0.99 that users never run.
94
+ # Cosmetic-only changes need no tests, so only the Actors with functional or platform test changes are
95
+ # built, into the test version 0.99 that users never run.
99
96
  # run-with-apify-tokens.mjs reads the token names from in apify-test-tools.config.json
100
97
  # and hands the build those secrets and nothing else, so the tokens stay in this one command instead of the whole job.
101
98
  # The library never sees secrets it doesn't need
@@ -107,7 +104,7 @@ jobs:
107
104
  SCRIPTS_PATH: ${{ steps.setup.outputs.scripts-path }}
108
105
  REQUIRE_ENV_VARS_IN_SYNC: ${{ inputs.require-env-vars-in-sync }}
109
106
  run: |
110
- jq '{ actors: .functionallyChangedActors }' affected.json \
107
+ jq '{ actors: (.functionallyChangedActors + .testChangedActors) }' affected.json \
111
108
  | node "$SCRIPTS_PATH/run-with-apify-tokens.mjs" apify-test-tools build --version-number 0.99 \
112
109
  --require-env-vars-in-sync="$REQUIRE_ENV_VARS_IN_SYNC" \
113
110
  | tee builds.json
@@ -116,9 +113,12 @@ jobs:
116
113
  - name: Test
117
114
  env:
118
115
  TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
116
+ # Passed through the environment and quoted, so the shell doesn't expand the glob itself
117
+ # An empty input, e.g. from an expression that picks the glob, runs everything, like the default
118
+ TEST_FILES_GLOB: ${{ inputs.test-files-glob || 'test/platform/**' }}
119
119
  # The github-actions reporter annotates the PR with the failing tests
120
120
  run: |
121
- apify-test-tools test ./test/platform/${{ inputs.test-files-glob }} --reporter github-actions \
121
+ apify-test-tools test --test-files-glob "$TEST_FILES_GLOB" --reporter github-actions \
122
122
  < builds.json > test-report.json
123
123
 
124
124
  # 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
@@ -52,6 +52,7 @@ jobs:
52
52
 
53
53
  # A release ships cosmetic changes too (README, input schema descriptions), so both lists are built,
54
54
  # into each Actor's default version: the one its default build tag points to, which users run.
55
+ # testChangedActors is left out: a change to platform tests alone needs no release.
55
56
  # run-with-apify-tokens.mjs reads the token names from in apify-test-tools.config.json
56
57
  # and hands the build those secrets and nothing else, so the tokens stay in this one command instead of the whole job.
57
58
  # The library never sees secrets it doesn't need
package/CHANGELOG.md CHANGED
@@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
18
18
  - Add impact &amp; priority assessment to claude investigator ([#216](https://github.com/apify/apify-test-tools/pull/216)) ([fd16622](https://github.com/apify/apify-test-tools/commit/fd16622b910ec6e9b5515ea0bd26b0a41c22bd23)) by [@foxt451](https://github.com/foxt451), closes [#208](https://github.com/apify/apify-test-tools/issues/208)
19
19
  - Stale issue&#x2F;pr workflow ([#236](https://github.com/apify/apify-test-tools/pull/236)) ([aff1fa7](https://github.com/apify/apify-test-tools/commit/aff1fa778adb846a60fdacdd26aa13a71729613d)) by [@JuanGalilea](https://github.com/JuanGalilea), closes [#35](https://github.com/apify/apify-test-tools/issues/35)
20
20
  - **workflows:** [**breaking**] Skip Slack when report-slack-channel is empty ([#238](https://github.com/apify/apify-test-tools/pull/238)) ([9a15b56](https://github.com/apify/apify-test-tools/commit/9a15b56de9ca42fb5ea753dc478116f9cd9c2c94)) by [@metalwarrior665](https://github.com/metalwarrior665)
21
+ - [**breaking**] Repo-root test-files-glob, platformTestsGlob in the config, and drop subtest ([#239](https://github.com/apify/apify-test-tools/pull/239)) ([640bc97](https://github.com/apify/apify-test-tools/commit/640bc97d9f16cfa07501bc4eae400cd2b1bd0c9c)) by [@metalwarrior665](https://github.com/metalwarrior665), closes [#192](https://github.com/apify/apify-test-tools/issues/192)
21
22
 
22
23
  ### 🐛 Bug Fixes
23
24
 
package/CONTRIBUTING.md CHANGED
@@ -172,7 +172,7 @@ brought:
172
172
  env:
173
173
  WORKFLOW_SECRETS: ${{ toJSON(secrets) }}
174
174
  run: |
175
- jq '{ actors: .functionallyChangedActors }' affected.json \
175
+ jq '{ actors: (.functionallyChangedActors + .testChangedActors) }' affected.json \
176
176
  | node "${{ steps.setup.outputs.scripts-path }}/run-with-apify-tokens.mjs" \
177
177
  apify-test-tools build --version-number 0.99
178
178
  ```
package/README.md CHANGED
@@ -16,7 +16,7 @@ Build, test and release Apify Actors from a Git repository or local source code.
16
16
 
17
17
  ```bash
18
18
  npx apify-test-tools get-affected-actors --base origin/master \
19
- | jq '{ actors: .functionallyChangedActors }' \
19
+ | jq '{ actors: (.functionallyChangedActors + .testChangedActors) }' \
20
20
  | npx apify-test-tools build --version-number 0.99
21
21
  ```
22
22
 
@@ -180,14 +180,15 @@ group needs a `tokenEnvVar` and at least one Actor. Add another group for Actors
180
180
  Actors owned by another account.
181
181
 
182
182
  The orchestrator sets `overrideActorContext` because its tests live outside its own folder: without it, a change to
183
- only `test/platform/orchestrator` wouldn't build and test it. The other two already have the whole repo, tests
184
- included, as their context.
183
+ only `test/platform/orchestrator` wouldn't test it. The other two already have the whole repo, tests included, as
184
+ their context.
185
185
 
186
186
  | Field | Where | Required | Description |
187
187
  | ---------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
188
188
  | `folder` | Actor | 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. |
189
189
  | `actorFullName` | Actor | 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. |
190
190
  | `tokenEnvVar` | group or Actor | yes | Name of the environment variable holding the Apify API token that builds the group's Actors. No fallback — if the env var is not set at build time, the build fails. |
191
+ | `platformTestsGlob` | top level | no | Glob from the repo root of the platform tests and their helpers. Default `test/platform/**`. A changed file that matches it is a test change, also when `.dockerignore` lists it: the PR workflow builds and tests the Actor whose context holds it, and a release skips it. See [Change detection](docs/cli.md#change-detection). |
191
192
  | `overrideActorContext` | group or Actor | no | Array of paths (relative to repo root) that define which files are relevant to the 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. |
192
193
 
193
194
  A value set on an Actor replaces the group's for that Actor only. Values are replaced, not merged: if the `myteam`
@@ -259,7 +260,8 @@ jobs:
259
260
 
260
261
  ### Scheduled tests (optional)
261
262
 
262
- How often to run which tests is up to you. For example, you can run every test once a day:
263
+ How often to run which tests is up to you. `test-files-glob` is a glob from the repo root, see
264
+ [Splitting the tests](docs/workflows.md#splitting-the-tests). For example, you can run the daily tests once a day:
263
265
 
264
266
  ```yaml
265
267
  name: Platform tests - Daily
@@ -273,12 +275,14 @@ on:
273
275
  jobs:
274
276
  platformTestsDaily:
275
277
  uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
278
+ with:
279
+ test-files-glob: 'test/platform/{daily,daily-without-pr}/**'
276
280
  secrets:
277
281
  TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
278
282
  SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
279
283
  ```
280
284
 
281
- and a few critical tests, kept in e.g. `test/platform/hourly`, every hour:
285
+ and a few critical tests every hour:
282
286
 
283
287
  ```yaml
284
288
  name: Platform tests - Hourly
@@ -293,7 +297,7 @@ jobs:
293
297
  platformTestsHourly:
294
298
  uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
295
299
  with:
296
- test-files-glob: hourly
300
+ test-files-glob: 'test/platform/{hourly,hourly-without-pr}/**'
297
301
  secrets:
298
302
  TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
299
303
  SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -10,7 +10,7 @@ import {
10
10
  } from './git.js';
11
11
  import { logger } from './logger.js';
12
12
  import type { ActorConfig, Commit, GitRange } from './types.js';
13
- import { readConfigFile } from './utils/config/load-config.js';
13
+ import { readConfigFile, readRepoSettings } from './utils/config/load-config.js';
14
14
 
15
15
  // The range always ends at the checked-out commit: check out what you want to inspect
16
16
  const HEAD = 'HEAD';
@@ -28,8 +28,6 @@ interface AffectedActorsOptions {
28
28
  * counts, and it must be an ancestor of HEAD (e.g. the last release).
29
29
  */
30
30
  since?: string;
31
- /** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
32
- testFilesGlob?: string;
33
31
  actors: string[];
34
32
  ignore: string[];
35
33
  }
@@ -47,8 +45,17 @@ interface AffectedActors {
47
45
  * stays the source of truth for the rest.
48
46
  */
49
47
  functionallyChangedActors: { actorFullName: string }[];
50
- /** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
48
+ /**
49
+ * Actors with cosmetic changes (README, CHANGELOG, cosmetic .actor JSON) and no functional one. They only need a
50
+ * release build.
51
+ */
51
52
  cosmeticallyChangedActors: { actorFullName: string }[];
53
+ /**
54
+ * Actors whose platform tests (`platformTestsGlob` in the config) changed in their context, whatever else
55
+ * changed. Their tests need to run, but a test change alone needs no release. An Actor can also be in one of the
56
+ * lists above.
57
+ */
58
+ testChangedActors: { actorFullName: string }[];
52
59
  }
53
60
 
54
61
  const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
@@ -114,7 +121,12 @@ const findValidatedSha = async (since: string, headSha: string): Promise<string
114
121
  * base's change, which base already validated.
115
122
  */
116
123
  const intersectChangedInBranchVsSince = (branch: ChangedActors, sinceValidated: ChangedActors): ChangedActors => {
117
- const narrowed: ChangedActors = { functional: [], cosmetic: [] };
124
+ const narrowed: ChangedActors = {
125
+ functional: [],
126
+ cosmetic: [],
127
+ // The same rule for tests: the branch changed them, and they changed again since they were last validated
128
+ test: branch.test.filter((actor) => sinceValidated.test.includes(actor)),
129
+ };
118
130
  // All checks are done for each Actor separately, that's cleanest and most accurate
119
131
  for (const actor of [...branch.functional, ...branch.cosmetic]) {
120
132
  // base...HEAD
@@ -145,7 +157,6 @@ const intersectChangedInBranchVsSince = (branch: ChangedActors, sinceValidated:
145
157
  export const getAffectedActors = async ({
146
158
  base,
147
159
  since: sinceOption,
148
- testFilesGlob,
149
160
  actors,
150
161
  ignore,
151
162
  }: AffectedActorsOptions): Promise<AffectedActors> => {
@@ -156,6 +167,7 @@ export const getAffectedActors = async ({
156
167
  }
157
168
 
158
169
  const actorConfigs = await readConfigFile({ actors, ignore });
170
+ const { platformTestsGlob } = await readRepoSettings();
159
171
  const getChangedInRange = async (range: GitRange) => {
160
172
  const changedFiles = await getChangedFilesBetween(range.fromRef, range.toRef);
161
173
  return {
@@ -164,7 +176,7 @@ export const getAffectedActors = async ({
164
176
  filepathsChanged: changedFiles,
165
177
  actorConfigs,
166
178
  range,
167
- testFilesGlob,
179
+ platformTestsGlob,
168
180
  }),
169
181
  };
170
182
  };
@@ -194,5 +206,6 @@ export const getAffectedActors = async ({
194
206
  changelog: await getChangelogAdditions(rangeVsBase.fromRef, rangeVsBase.toRef, changedFiles),
195
207
  functionallyChangedActors: names(changedActors.functional),
196
208
  cosmeticallyChangedActors: names(changedActors.cosmetic),
209
+ testChangedActors: names(changedActors.test),
197
210
  };
198
211
  };
@@ -1,8 +1,7 @@
1
- import picomatch from 'picomatch';
2
-
3
1
  import { isCosmeticOnlyJsonSchemaChange } from './diff-json-schema.js';
4
2
  import { type DockerIgnoreMatcher, loadDockerIgnore } from './dockerignore.js';
5
3
  import { logger } from './logger.js';
4
+ import { createTestFilesMatcher } from './test-files-glob.js';
6
5
  import type { ActorConfig, GitRange } from './types.js';
7
6
  import { type RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
8
7
 
@@ -11,8 +10,11 @@ interface ShouldBuildAndTestOptions {
11
10
  actorConfigs: ActorConfig[];
12
11
  /** The range the files changed in, to compare `.actor` JSON files across. */
13
12
  range: GitRange;
14
- // Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them.
15
- testFilesGlob?: string;
13
+ /**
14
+ * Glob of the platform test files, from the repo root (`platformTestsGlob` in the config). A change to a matching
15
+ * file is a test change, even when .dockerignore lists it.
16
+ */
17
+ platformTestsGlob?: string;
16
18
  }
17
19
 
18
20
  const IGNORED_TOP_LEVEL_FILES = [
@@ -34,6 +36,7 @@ type FileChangeForActor =
34
36
  | { impact: 'ignored' }
35
37
  | { impact: 'outside-context' }
36
38
  | { impact: 'cosmetic'; semanticallyVerified: boolean }
39
+ | { impact: 'test' }
37
40
  | { impact: 'functional' };
38
41
 
39
42
  /**
@@ -44,11 +47,13 @@ type FileChangeForActor =
44
47
  * not one per actor, so it applies to every actor regardless of context/folder.
45
48
  * 2. Context matching (actorConfig.contextPaths) → outside-context if no match
46
49
  * 3. Hardcoded ignore list, checked against the path hoisted relative to the matched context entry → ignored
47
- * 4. .dockerignore filtering (patterns relative to dockerContextDir), skipped for the actor's own `.actor/`
48
- * dir and for test files → ignored if matched
49
- * 5. README.md by filename → cosmetic if inside the actor's own folder, otherwise ignored
50
- * 6. .json inside the actor's own `.actor/` dir with only cosmetic schema diffs → cosmetic (semantically verified)
51
- * 7. Everything else → functional
50
+ * 4. Platform test files (`platformTestsGlob`) → test, even when .dockerignore lists them. A test change needs the
51
+ * tests to run, but no release.
52
+ * 5. .dockerignore filtering (patterns relative to dockerContextDir), skipped for the actor's own `.actor/`
53
+ * dir → ignored if matched
54
+ * 6. README.md by filename → cosmetic if inside the actor's own folder, otherwise ignored
55
+ * 7. .json inside the actor's own `.actor/` dir with only cosmetic schema diffs → cosmetic (semantically verified)
56
+ * 8. Everything else → functional
52
57
  */
53
58
  const classifyFileChange = async (
54
59
  changedFile: RelativeFile,
@@ -75,13 +80,17 @@ const classifyFileChange = async (
75
80
  return { impact: 'ignored' };
76
81
  }
77
82
 
83
+ // Test files are often in .dockerignore, but a change to them must still run the tests
84
+ if (isTestFile(changedFile.path)) {
85
+ return { impact: 'test' };
86
+ }
87
+
78
88
  const isUnderActorDotDir = actorConfig.actorJson.file.parent.contains(changedFile);
79
89
 
80
90
  // .actor/ can legitimately be listed in .dockerignore (the Apify platform evaluates it before
81
91
  // the Docker build, so excluding it from the build context is a valid caching optimization) —
82
92
  // that shouldn't cause changes to .actor/ itself to be ignored here.
83
- // Test files are often in .dockerignore too, but a change to them must still build and test.
84
- if (!isUnderActorDotDir && !isTestFile(changedFile.path) && dockerIgnoreMatcher(changedFile.path)) {
93
+ if (!isUnderActorDotDir && dockerIgnoreMatcher(changedFile.path)) {
85
94
  return { impact: 'ignored' };
86
95
  }
87
96
 
@@ -113,6 +122,8 @@ type ActorChangeEntry = {
113
122
  actorConfig: ActorConfig;
114
123
  files: string[];
115
124
  hasFunctionalChange: boolean;
125
+ hasCosmeticChange: boolean;
126
+ hasTestChange: boolean;
116
127
  };
117
128
 
118
129
  type ChangeGroup = { actors: string[]; files: string[] };
@@ -168,8 +179,13 @@ const logChangeGroups = (groups: ChangeGroup[]): void => {
168
179
  export interface ChangedActors {
169
180
  /** Actors with at least one functional change. They need a new build and tests. */
170
181
  functional: ActorConfig[];
171
- /** Actors whose changes are all cosmetic. They only need a new build when released. */
182
+ /** Actors with cosmetic changes and no functional one. They only need a new build when released. */
172
183
  cosmetic: ActorConfig[];
184
+ /**
185
+ * Actors with at least one platform test change, whatever else changed. Their tests need to run, but test changes
186
+ * alone need no release. An Actor can be in this list and in one of the others.
187
+ */
188
+ test: ActorConfig[];
173
189
  }
174
190
 
175
191
  /** Finds the Actors the changed files belong to, split by the most impactful change each Actor has. */
@@ -177,11 +193,11 @@ export const getChangedActors = async ({
177
193
  filepathsChanged,
178
194
  actorConfigs,
179
195
  range,
180
- testFilesGlob,
196
+ platformTestsGlob,
181
197
  }: ShouldBuildAndTestOptions): Promise<ChangedActors> => {
182
198
  const actorsChangedMap = new Map<string, ActorChangeEntry>();
183
199
  const testFilesInContext = new Set<string>();
184
- const isTestFile = testFilesGlob ? picomatch(testFilesGlob, { nocase: true, dot: true }) : () => false;
200
+ const isTestFile = createTestFilesMatcher(platformTestsGlob);
185
201
 
186
202
  for (const actorConfig of actorConfigs) {
187
203
  const dockerIgnoreMatcher = loadDockerIgnore(actorConfig.dockerContextDir);
@@ -204,9 +220,13 @@ export const getChangedActors = async ({
204
220
  actorConfig,
205
221
  files: [],
206
222
  hasFunctionalChange: false,
223
+ hasCosmeticChange: false,
224
+ hasTestChange: false,
207
225
  };
208
226
  entry.files.push(originalFilePath);
209
227
  entry.hasFunctionalChange ||= change.impact === 'functional';
228
+ entry.hasCosmeticChange ||= change.impact === 'cosmetic';
229
+ entry.hasTestChange ||= change.impact === 'test';
210
230
  actorsChangedMap.set(actorConfig.folder.path, entry);
211
231
  }
212
232
  }
@@ -226,11 +246,15 @@ export const getChangedActors = async ({
226
246
 
227
247
  const entries = Array.from(actorsChangedMap.values());
228
248
  const functional = entries.filter((entry) => entry.hasFunctionalChange).map((entry) => entry.actorConfig);
229
- const cosmetic = entries.filter((entry) => !entry.hasFunctionalChange).map((entry) => entry.actorConfig);
249
+ const cosmetic = entries
250
+ .filter((entry) => !entry.hasFunctionalChange && entry.hasCosmeticChange)
251
+ .map((entry) => entry.actorConfig);
252
+ const test = entries.filter((entry) => entry.hasTestChange).map((entry) => entry.actorConfig);
230
253
 
231
254
  const names = (configs: ActorConfig[]) => configs.map((config) => config.actorFullName).join(', ') || 'none';
232
255
  logger.info(`[DIFF]: Actors with functional changes: ${names(functional)}`);
233
256
  logger.info(`[DIFF]: Actors with only cosmetic changes: ${names(cosmetic)}`);
257
+ logger.info(`[DIFF]: Actors with platform test changes: ${names(test)}`);
234
258
 
235
- return { functional, cosmetic };
259
+ return { functional, cosmetic, test };
236
260
  };
package/bin/main.ts CHANGED
@@ -56,10 +56,6 @@ const affectedRangeOptions = {
56
56
  type: 'string',
57
57
  describe: 'Only count commits after this one. Ignored with --base if it is not on the branch',
58
58
  },
59
- 'test-files-glob': {
60
- type: 'string',
61
- describe: 'Glob (from the repo root) of test files that count as changes even when .dockerignore lists them',
62
- },
63
59
  } as const satisfies Record<string, Options>;
64
60
 
65
61
  /**
@@ -103,15 +99,19 @@ const envVarsSyncOptions = {
103
99
 
104
100
  /** Flags of `test`, which runs the repository's vitest on the Actors piped in, e.g. the builds from `build`. */
105
101
  const testOptions = {
102
+ 'test-files-glob': {
103
+ type: 'string',
104
+ default: 'test/platform/**',
105
+ describe: 'Glob (from the repo root) of the test files to run. Quote it, so the shell does not expand it',
106
+ },
106
107
  'vitest-output-file': { type: 'string', describe: "Also keep vitest's own JSON report there, e.g. to upload it" },
107
108
  reporter: {
108
109
  type: 'string',
109
110
  describe:
110
111
  'Vitest reporter (https://vitest.dev/guide/reporters) to add to `default` and `json`, which are always on, ' +
111
112
  'e.g. github-actions. Repeat it for more',
112
- // A plain string, not `array: true`: yargs would fill an array option with the arguments after it too, so
113
- // `test --reporter github-actions test/platform` would take the test path as a reporter. A repeated flag
114
- // still comes as an array, a single one as a string, so both are normalized to an array.
113
+ // A plain string, not `array: true`: yargs would fill an array option with the arguments after it too. A
114
+ // repeated flag still comes as an array, a single one as a string, so both are normalized to an array.
115
115
  coerce: (value: string | string[] | undefined) => (value === undefined ? [] : [value].flat()),
116
116
  },
117
117
  } as const satisfies Record<string, Options>;
@@ -183,20 +183,12 @@ await yargs()
183
183
  },
184
184
  )
185
185
  .command(
186
- 'test [filters..]',
186
+ 'test',
187
187
  'Run the tests of the Actors listed on stdin (builds from build, or get-actor-configs for default builds), and print a report for notify',
188
- (y) =>
189
- y
190
- .positional('filters', {
191
- type: 'string',
192
- array: true,
193
- default: ['test/platform'],
194
- describe: 'Test files to run, as vitest filters',
195
- })
196
- .options(testOptions),
188
+ (y) => y.options(testOptions),
197
189
  async (args) => {
198
190
  const report = await runTests({
199
- filters: args.filters,
191
+ testFilesGlob: args.testFilesGlob,
200
192
  targets: parseTestTargets(await readStdin()),
201
193
  vitestOutputFile: args.vitestOutputFile,
202
194
  reporters: args.reporter,
@@ -229,7 +221,8 @@ await yargs()
229
221
  async (args) => {
230
222
  const input = parseBuildInput(await readStdin());
231
223
  const names = input.actors.map(({ actorFullName }) => actorFullName);
232
- // An empty --actors selects every Actor, but an empty list here means there is nothing to build
224
+ // An empty --actors selects every Actor, but an empty list here means there is nothing to build. The configs
225
+ // are picked by name, so an Actor listed twice (e.g. in two lists of get-affected-actors) is built once
233
226
  const actorConfigs = names.length > 0 ? await readConfigFile({ actors: names, ignore: [] }) : [];
234
227
  const builds = await buildActors({
235
228
  actorConfigs,
package/bin/run-tests.ts CHANGED
@@ -9,6 +9,7 @@ import { createInterface } from 'node:readline';
9
9
  import { withActorIds } from './actor-ids.js';
10
10
  import { logger } from './logger.js';
11
11
  import type { TestTarget } from './stdin-input.js';
12
+ import { resolveTestFilesGlob } from './test-files-glob.js';
12
13
  import { failedRunReport, logTestReport, type TestReport, toTestReport, type VitestJsonReport } from './test-report.js';
13
14
 
14
15
  /**
@@ -29,9 +30,32 @@ const resolveVitestBin = async (cwd: string): Promise<string> => {
29
30
  }
30
31
  };
31
32
 
33
+ /**
34
+ * Turns the glob into vitest filters. Vitest filters are parts of a file path, not globs, so the glob is resolved here.
35
+ * The filters are the folders that hold only matched files and the other matched files one by one, so the command
36
+ * line stays short. They go to vitest as absolute paths. Vitest also matches their path relative to the root anywhere
37
+ * in a test file's path, so a copy at `dist/test/platform/a.test.ts` would run too: vitest's own `exclude` keeps such
38
+ * copies out.
39
+ */
40
+ const resolveVitestFilters = async (testFilesGlob: string, cwd: string): Promise<string[]> => {
41
+ const { files, paths } = await resolveTestFilesGlob(testFilesGlob, cwd);
42
+ // Without filters vitest would run every test of the repo
43
+ if (files.length === 0) {
44
+ throw new Error(
45
+ `No files match --test-files-glob "${testFilesGlob}". The glob is relative to the repo root, e.g. "test/platform/**".`,
46
+ );
47
+ }
48
+ logger.info(`Running the ${files.length} file(s) that match --test-files-glob "${testFilesGlob}"`);
49
+ logger.debug(`Files: ${files.join(', ')}`);
50
+ // Vitest compares against real paths, and cwd can be a symlink (e.g. /tmp on macOS). join keeps a folder's
51
+ // trailing slash, which stops `test/platform/daily/` from also matching `test/platform/daily-without-pr/`.
52
+ const root = await fs.realpath(cwd);
53
+ return paths.map((filePath) => path.join(root, filePath));
54
+ };
55
+
32
56
  interface RunTestsOptions {
33
- /** Vitest file filters, e.g. `test/platform/core` */
34
- filters: string[];
57
+ /** Glob from the repo root (`cwd`) of the test files to run, e.g. `test/platform/**` */
58
+ testFilesGlob: string;
35
59
  /** The Actors to test, each with its build or, without one, its default build */
36
60
  targets: TestTarget[];
37
61
  /** Keeps vitest's own JSON report there, e.g. to upload it */
@@ -47,7 +71,7 @@ interface RunTestsOptions {
47
71
  * an error: the report is how the failure gets to `notify`.
48
72
  */
49
73
  export const runTests = async ({
50
- filters,
74
+ testFilesGlob,
51
75
  targets,
52
76
  vitestOutputFile,
53
77
  reporters = [],
@@ -58,6 +82,7 @@ export const runTests = async ({
58
82
  throw new Error('TESTER_APIFY_TOKEN is not set. The platform tests need it to run the Actors.');
59
83
  }
60
84
  const vitestBin = await resolveVitestBin(cwd);
85
+ const vitestFilters = await resolveVitestFilters(testFilesGlob, cwd);
61
86
  // A test only runs when the ID it names its Actor by is listed, so each Actor gets both its full name and raw ID
62
87
  const actorBuilds = await withActorIds(targets);
63
88
 
@@ -81,7 +106,7 @@ export const runTests = async ({
81
106
  const args = [
82
107
  vitestBin,
83
108
  'run',
84
- ...filters,
109
+ ...vitestFilters,
85
110
  '--reporter=default',
86
111
  '--reporter=json',
87
112
  ...reporters.map((reporter) => `--reporter=${reporter}`),