apify-test-tools 0.10.2-beta.2 → 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 (275) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/actionlint.yaml +11 -0
  3. package/.github/actions/checkout-restore-dependencies/action.yaml +58 -6
  4. package/.github/cli/apify-test-tools.mjs +135 -142
  5. package/.github/scripts/mask-config-secrets.sh +17 -0
  6. package/.github/scripts/run-with-apify-tokens.mjs +63 -103
  7. package/.github/workflows/_check_code.yaml +5 -21
  8. package/.github/workflows/_move_major_tag.yaml +2 -2
  9. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +89 -8
  10. package/.github/workflows/public_platform-tests.yaml +68 -30
  11. package/.github/workflows/public_pr-build-test.yaml +77 -38
  12. package/.github/workflows/public_push-build-latest.yaml +49 -11
  13. package/.github/workflows/public_review.yaml +5 -4
  14. package/.github/workflows/public_stale-issues.yaml +51 -0
  15. package/.husky/pre-commit +4 -4
  16. package/.prettierignore +2 -1
  17. package/CHANGELOG.md +22 -0
  18. package/CONTRIBUTING.md +117 -17
  19. package/README.md +221 -595
  20. package/bin/actor-ids.ts +41 -0
  21. package/bin/affected-actors.ts +211 -0
  22. package/bin/build-from-local.ts +19 -1
  23. package/bin/build.ts +105 -39
  24. package/bin/diff-changes.ts +69 -36
  25. package/bin/diff-json-schema.ts +5 -6
  26. package/bin/git.ts +73 -175
  27. package/bin/main.ts +142 -189
  28. package/bin/notify.ts +198 -0
  29. package/bin/run-tests.ts +167 -0
  30. package/bin/slack.ts +16 -76
  31. package/bin/stdin-input.ts +156 -0
  32. package/bin/test-files-glob.ts +126 -0
  33. package/bin/test-report.ts +141 -140
  34. package/bin/types.ts +4 -7
  35. package/bin/utils/config/load-config.ts +20 -2
  36. package/bin/utils/config/parser.ts +40 -8
  37. package/bin/utils/config/structures/grouped.ts +2 -2
  38. package/bin/utils/config/structures/legacy.ts +1 -1
  39. package/dist/bin/actor-ids.d.ts +15 -0
  40. package/dist/bin/actor-ids.d.ts.map +1 -0
  41. package/dist/bin/actor-ids.js +30 -0
  42. package/dist/bin/actor-ids.js.map +1 -0
  43. package/dist/bin/affected-actors.d.ts +55 -0
  44. package/dist/bin/affected-actors.d.ts.map +1 -0
  45. package/dist/bin/affected-actors.js +142 -0
  46. package/dist/bin/affected-actors.js.map +1 -0
  47. package/dist/bin/build-from-local.d.ts +2 -1
  48. package/dist/bin/build-from-local.d.ts.map +1 -1
  49. package/dist/bin/build-from-local.js +6 -2
  50. package/dist/bin/build-from-local.js.map +1 -1
  51. package/dist/bin/build.d.ts +18 -5
  52. package/dist/bin/build.d.ts.map +1 -1
  53. package/dist/bin/build.js +74 -24
  54. package/dist/bin/build.js.map +1 -1
  55. package/dist/bin/diff-changes.d.ts +21 -5
  56. package/dist/bin/diff-changes.d.ts.map +1 -1
  57. package/dist/bin/diff-changes.js +41 -28
  58. package/dist/bin/diff-changes.js.map +1 -1
  59. package/dist/bin/diff-json-schema.d.ts +2 -2
  60. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  61. package/dist/bin/diff-json-schema.js +1 -4
  62. package/dist/bin/diff-json-schema.js.map +1 -1
  63. package/dist/bin/git.d.ts +26 -34
  64. package/dist/bin/git.d.ts.map +1 -1
  65. package/dist/bin/git.js +60 -143
  66. package/dist/bin/git.js.map +1 -1
  67. package/dist/bin/main.js +126 -140
  68. package/dist/bin/main.js.map +1 -1
  69. package/dist/bin/notify.d.ts +35 -0
  70. package/dist/bin/notify.d.ts.map +1 -0
  71. package/dist/bin/notify.js +131 -0
  72. package/dist/bin/notify.js.map +1 -0
  73. package/dist/bin/run-tests.d.ts +21 -0
  74. package/dist/bin/run-tests.d.ts.map +1 -0
  75. package/dist/bin/run-tests.js +138 -0
  76. package/dist/bin/run-tests.js.map +1 -0
  77. package/dist/bin/slack.d.ts +2 -14
  78. package/dist/bin/slack.d.ts.map +1 -1
  79. package/dist/bin/slack.js +17 -46
  80. package/dist/bin/slack.js.map +1 -1
  81. package/dist/bin/stdin-input.d.ts +79 -0
  82. package/dist/bin/stdin-input.d.ts.map +1 -0
  83. package/dist/bin/stdin-input.js +124 -0
  84. package/dist/bin/stdin-input.js.map +1 -0
  85. package/dist/bin/test-files-glob.d.ts +18 -0
  86. package/dist/bin/test-files-glob.d.ts.map +1 -0
  87. package/dist/bin/test-files-glob.js +108 -0
  88. package/dist/bin/test-files-glob.js.map +1 -0
  89. package/dist/bin/test-report.d.ts +69 -8
  90. package/dist/bin/test-report.d.ts.map +1 -1
  91. package/dist/bin/test-report.js +76 -67
  92. package/dist/bin/test-report.js.map +1 -1
  93. package/dist/bin/types.d.ts +4 -7
  94. package/dist/bin/types.d.ts.map +1 -1
  95. package/dist/bin/utils/config/load-config.d.ts +8 -0
  96. package/dist/bin/utils/config/load-config.d.ts.map +1 -1
  97. package/dist/bin/utils/config/load-config.js +14 -1
  98. package/dist/bin/utils/config/load-config.js.map +1 -1
  99. package/dist/bin/utils/config/parser.d.ts +15 -2
  100. package/dist/bin/utils/config/parser.d.ts.map +1 -1
  101. package/dist/bin/utils/config/parser.js +29 -8
  102. package/dist/bin/utils/config/parser.js.map +1 -1
  103. package/dist/bin/utils/config/structures/grouped.js +2 -2
  104. package/dist/bin/utils/config/structures/grouped.js.map +1 -1
  105. package/dist/bin/utils/config/structures/legacy.js +1 -1
  106. package/dist/bin/utils/config/structures/legacy.js.map +1 -1
  107. package/dist/lib/extend-expect.d.ts.map +1 -1
  108. package/dist/lib/extend-expect.js +37 -218
  109. package/dist/lib/extend-expect.js.map +1 -1
  110. package/dist/lib/lib.d.ts +1 -5
  111. package/dist/lib/lib.d.ts.map +1 -1
  112. package/dist/lib/lib.js +19 -31
  113. package/dist/lib/lib.js.map +1 -1
  114. package/dist/lib/run-checks.d.ts +14 -0
  115. package/dist/lib/run-checks.d.ts.map +1 -0
  116. package/dist/lib/run-checks.js +82 -0
  117. package/dist/lib/run-checks.js.map +1 -0
  118. package/dist/lib/run-test-result.d.ts +3 -6
  119. package/dist/lib/run-test-result.d.ts.map +1 -1
  120. package/dist/lib/run-test-result.js +41 -54
  121. package/dist/lib/run-test-result.js.map +1 -1
  122. package/dist/test/bundle/cli-bundle.test.js +76 -56
  123. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  124. package/dist/test/bundle/token-wrapper.test.d.ts +2 -0
  125. package/dist/test/bundle/token-wrapper.test.d.ts.map +1 -0
  126. package/dist/test/bundle/token-wrapper.test.js +80 -0
  127. package/dist/test/bundle/token-wrapper.test.js.map +1 -0
  128. package/dist/test/e2e/cli-bundle.e2e.test.js +33 -68
  129. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  130. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  131. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  132. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  133. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  134. package/dist/test/e2e/helpers.d.ts +37 -0
  135. package/dist/test/e2e/helpers.d.ts.map +1 -0
  136. package/dist/test/e2e/helpers.js +57 -0
  137. package/dist/test/e2e/helpers.js.map +1 -0
  138. package/dist/test/unit/bin/actor-filtering.test.js +7 -0
  139. package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
  140. package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
  141. package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
  142. package/dist/test/unit/bin/actor-ids.test.js +40 -0
  143. package/dist/test/unit/bin/actor-ids.test.js.map +1 -0
  144. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  145. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  146. package/dist/test/unit/bin/affected-actors-cli.test.js +224 -0
  147. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  148. package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
  149. package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
  150. package/dist/test/unit/bin/affected-actors.test.js +153 -0
  151. package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
  152. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  153. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  154. package/dist/test/unit/bin/build-cli.test.js +49 -0
  155. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  156. package/dist/test/unit/bin/build-from-local.test.js +12 -0
  157. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  158. package/dist/test/unit/bin/build.test.js +116 -6
  159. package/dist/test/unit/bin/build.test.js.map +1 -1
  160. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  161. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  162. package/dist/test/unit/bin/compiled-cli.js +23 -0
  163. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  164. package/dist/test/unit/bin/diff-changes.test.js +168 -114
  165. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  166. package/dist/test/unit/bin/diff.test.js +25 -28
  167. package/dist/test/unit/bin/diff.test.js.map +1 -1
  168. package/dist/test/unit/bin/git.test.js +67 -288
  169. package/dist/test/unit/bin/git.test.js.map +1 -1
  170. package/dist/test/unit/bin/notify.test.d.ts +2 -0
  171. package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
  172. package/dist/test/unit/bin/notify.test.js +222 -0
  173. package/dist/test/unit/bin/notify.test.js.map +1 -0
  174. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  175. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  176. package/dist/test/unit/bin/stdin-input.test.js +92 -0
  177. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  178. package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
  179. package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
  180. package/dist/test/unit/bin/test-cli.test.js +146 -0
  181. package/dist/test/unit/bin/test-cli.test.js.map +1 -0
  182. package/dist/test/unit/bin/test-files-glob.test.d.ts +2 -0
  183. package/dist/test/unit/bin/test-files-glob.test.d.ts.map +1 -0
  184. package/dist/test/unit/bin/test-files-glob.test.js +102 -0
  185. package/dist/test/unit/bin/test-files-glob.test.js.map +1 -0
  186. package/dist/test/unit/bin/test-report.test.d.ts +2 -0
  187. package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
  188. package/dist/test/unit/bin/test-report.test.js +100 -0
  189. package/dist/test/unit/bin/test-report.test.js.map +1 -0
  190. package/dist/test/unit/bin/utils/config/load-config.test.js +19 -1
  191. package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
  192. package/dist/test/unit/bin/utils/config/parser.test.js +36 -4
  193. package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
  194. package/dist/test/unit/bin/utils/config/structures/grouped.test.js +6 -0
  195. package/dist/test/unit/bin/utils/config/structures/grouped.test.js.map +1 -1
  196. package/dist/test/unit/bin/utils/config/structures/legacy.test.js +1 -0
  197. package/dist/test/unit/bin/utils/config/structures/legacy.test.js.map +1 -1
  198. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
  199. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
  200. package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
  201. package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
  202. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
  203. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
  204. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
  205. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
  206. package/dist/test/unit/run-checks.test.d.ts +2 -0
  207. package/dist/test/unit/run-checks.test.d.ts.map +1 -0
  208. package/dist/test/unit/run-checks.test.js +62 -0
  209. package/dist/test/unit/run-checks.test.js.map +1 -0
  210. package/dist/test/unit/run-test-result.test.d.ts +2 -0
  211. package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
  212. package/dist/test/unit/run-test-result.test.js +34 -0
  213. package/dist/test/unit/run-test-result.test.js.map +1 -0
  214. package/dist/test/unit/should-built-and-test.test.js +41 -38
  215. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  216. package/dist/tsconfig.tsbuildinfo +1 -1
  217. package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts +3 -0
  218. package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts.map +1 -0
  219. package/dist/workflow-scripts-src/run-with-apify-tokens.js +100 -0
  220. package/dist/workflow-scripts-src/run-with-apify-tokens.js.map +1 -0
  221. package/docs/cli.md +469 -0
  222. package/docs/lib.md +276 -0
  223. package/docs/workflows.md +455 -0
  224. package/eslint.config.mjs +10 -1
  225. package/lib/extend-expect.ts +48 -258
  226. package/lib/lib.ts +25 -34
  227. package/lib/run-checks.ts +101 -0
  228. package/lib/run-test-result.ts +45 -55
  229. package/package.json +4 -2
  230. package/platform-tests-investigate-and-fix/skills/assess-impact/SKILL.md +42 -0
  231. package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +4 -2
  232. package/test/bundle/cli-bundle.test.ts +97 -67
  233. package/test/bundle/token-wrapper.test.ts +110 -0
  234. package/test/e2e/cli-bundle.e2e.test.ts +60 -93
  235. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  236. package/test/e2e/helpers.ts +82 -0
  237. package/test/unit/bin/actor-filtering.test.ts +8 -0
  238. package/test/unit/bin/actor-ids.test.ts +47 -0
  239. package/test/unit/bin/affected-actors-cli.test.ts +271 -0
  240. package/test/unit/bin/affected-actors.test.ts +221 -0
  241. package/test/unit/bin/build-cli.test.ts +61 -0
  242. package/test/unit/bin/build-from-local.test.ts +16 -0
  243. package/test/unit/bin/build.test.ts +149 -5
  244. package/test/unit/bin/compiled-cli.ts +23 -0
  245. package/test/unit/bin/diff-changes.test.ts +181 -114
  246. package/test/unit/bin/diff.test.ts +25 -28
  247. package/test/unit/bin/git.test.ts +76 -361
  248. package/test/unit/bin/notify.test.ts +299 -0
  249. package/test/unit/bin/stdin-input.test.ts +120 -0
  250. package/test/unit/bin/test-cli.test.ts +175 -0
  251. package/test/unit/bin/test-files-glob.test.ts +118 -0
  252. package/test/unit/bin/test-report.test.ts +118 -0
  253. package/test/unit/bin/utils/config/load-config.test.ts +26 -1
  254. package/test/unit/bin/utils/config/parser.test.ts +46 -5
  255. package/test/unit/bin/utils/config/structures/grouped.test.ts +12 -0
  256. package/test/unit/bin/utils/config/structures/legacy.test.ts +1 -0
  257. package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
  258. package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
  259. package/test/unit/run-checks.test.ts +76 -0
  260. package/test/unit/run-test-result.test.ts +44 -0
  261. package/test/unit/should-built-and-test.test.ts +46 -39
  262. package/tsconfig.json +1 -1
  263. package/vitest.config.ts +9 -0
  264. package/workflow-scripts-src/run-with-apify-tokens.ts +112 -0
  265. package/.github/scripts/check-major-tag-refs.mjs +0 -72
  266. package/dist/test/unit/bin/logging.test.d.ts +0 -2
  267. package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
  268. package/dist/test/unit/bin/logging.test.js +0 -43
  269. package/dist/test/unit/bin/logging.test.js.map +0 -1
  270. package/dist/test/unit/bin/slack.test.d.ts +0 -2
  271. package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
  272. package/dist/test/unit/bin/slack.test.js +0 -54
  273. package/dist/test/unit/bin/slack.test.js.map +0 -1
  274. package/test/unit/bin/logging.test.ts +0 -52
  275. package/test/unit/bin/slack.test.ts +0 -65
package/README.md CHANGED
@@ -1,8 +1,71 @@
1
1
  # Apify Test Tools
2
2
 
3
- [Contributing link](CONTRIBUTING.md)
3
+ Build, test and release Apify Actors from a Git repository or local source code. The package has three parts:
4
4
 
5
- ## Getting Started
5
+ 1. **A vitest library** for writing platform tests that run your Actors on Apify and check the results:
6
+
7
+ ```ts
8
+ testActor('myteam/web-scraper', 'scrapes the start URL', async ({ expect, run }) => {
9
+ const runResult = await run({ input: { startUrls: [{ url: 'https://apify.com' }] } });
10
+ await expect(runResult).toFinishWith({ datasetItemCount: { min: 1, max: 10 } });
11
+ });
12
+ ```
13
+
14
+ 2. **A CLI** that finds the Actors a change affects, builds them, and reports the results. It runs anywhere, so you
15
+ can build your own pipeline on it in any CI:
16
+
17
+ ```bash
18
+ npx apify-test-tools get-affected-actors --base origin/master \
19
+ | jq '{ actors: (.functionallyChangedActors + .testChangedActors) }' \
20
+ | npx apify-test-tools build --version-number 0.99
21
+ ```
22
+
23
+ 3. **Ready-made GitHub Actions workflows** built on the CLI and the library, so a GitHub repo needs only a few lines
24
+ per workflow:
25
+
26
+ ```yaml
27
+ jobs:
28
+ buildDevelAndTest:
29
+ uses: apify/apify-test-tools/.github/workflows/public_pr-build-test.yaml@workflows-v0
30
+ secrets:
31
+ TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
32
+ # One entry per secret apify-test-tools.config.json names, today each `tokenEnvVar`
33
+ CONFIG_SECRETS: |
34
+ { "APIFY_TOKEN_MYTEAM": "${{ secrets.APIFY_TOKEN_MYTEAM }}" }
35
+ ```
36
+
37
+ Most repos only need the library and the workflows: the workflows call the CLI for you. Use the CLI directly only to
38
+ build your own pipeline, e.g. on a CI platform other than GitHub, or to build and test locally.
39
+
40
+ ## Only what changed is retested
41
+
42
+ Builds and platform tests cost time and money, so the tool skips everything a change can't affect:
43
+
44
+ - **Only affected Actors.** A changed file only counts for the Actors whose build context it falls in, minus their
45
+ `.dockerignore`. In a monorepo, a change to one Actor doesn't build or test the others.
46
+ - **Cosmetic changes skip tests.** README, CHANGELOG and formatting-only schema changes don't trigger tests on a PR;
47
+ they're only rebuilt when released.
48
+ - **Only new changes on a PR.** Each push to a PR retests only the Actors that changed since its last green run, not
49
+ the whole PR again.
50
+ - **Only their tests.** Only the tests of the Actors that were built run.
51
+
52
+ See [Change detection](docs/cli.md#change-detection) for the exact rules.
53
+
54
+ This README covers what you need to get a repo running. The full reference lives in `docs/`:
55
+
56
+ - [Vitest library API](docs/lib.md) — `testActor`, `testStandbyActor`, `RunTestResult`, matchers, running tests locally
57
+ - [CLI API](docs/cli.md) — every command, change detection, releasing, building locally
58
+ - [Workflow API](docs/workflows.md) — every GitHub workflow, its inputs and secrets, versioning
59
+
60
+ [Contributing](CONTRIBUTING.md)
61
+
62
+ ## Getting started
63
+
64
+ Your Actors can be written in any programming language, e.g. Python: anything the platform builds from a Dockerfile
65
+ works. Only the tests and this tooling run on Node.js, so all a repo needs is a `package.json` at the root with
66
+ `apify-test-tools` and `vitest` in its `devDependencies`, and tests written in TypeScript or JavaScript. If you use the
67
+ [PR workflow](docs/workflows.md#public_pr-build-testyaml), also define the `build-check`, `lint` and `test` scripts
68
+ its static checks run, e.g. with your language's own linter and unit tests.
6
69
 
7
70
  ### 1. Install the package
8
71
 
@@ -10,118 +73,87 @@
10
73
  npm i -D apify-test-tools
11
74
  ```
12
75
 
13
- - Requires `vitest` version `3.2.0` or later (uses [annotate](https://vitest.dev/guide/test-context.html#annotate))
76
+ - Requires `vitest` version `3.2.4` or later (uses [annotate](https://vitest.dev/guide/test-context.html#annotate))
14
77
  - Make sure `target` and `module` in your `tsconfig.json`'s `compilerOptions` are set to `ES2022`
15
78
  - Every Actor must already have at least one build under its default build tag (usually `latest`), built
16
79
  from a Git repository version whose URL points to this repository. Build it once manually on the
17
- platform before CI takes over. See [Repository check](#repository-check).
80
+ platform before CI takes over. See [Repository check](docs/cli.md#repository-check).
18
81
 
19
82
  ### 2. Create the config file
20
83
 
21
- Every repo that uses `apify-test-tools` must have an `apify-test-tools.config.json` file at the root. It lists the actors in the repo, their folders, their full names on the platform, and which env var holds the token to use for each. A top-level `mode` field picks the file's layout; leave it out to use the default `"legacy"` layout.
84
+ Every repo must have an `apify-test-tools.config.json` file at the root that lists its Actors. See
85
+ [Config file](#config-file) below.
22
86
 
23
- See [Config file](#config-file) for the available modes and every field.
87
+ ### 3. Set up Actor folders
24
88
 
25
- ### 3. Set up actor folders
89
+ Each Actor in the config must have a `.actor/actor.json` file. The `dockerContextDir` field in `actor.json` defines
90
+ the build context boundary — this is what the tool uses to decide which files can affect the Actor's build (see
91
+ [Change detection](docs/cli.md#change-detection)).
26
92
 
27
- Each actor in the config must have a `.actor/actor.json` file. The `dockerContextDir` field in `actor.json` defines the build context boundary — this is what the tool uses to determine which files can affect the actor's build.
93
+ This example has two Actors that share the code at the repo root, and one standalone Actor with its own code:
28
94
 
29
95
  ```
30
96
  my-repo
31
97
  ├── apify-test-tools.config.json
98
+ ├── Dockerfile <- shared by web-scraper and email-sender
99
+ ├── package.json
100
+ ├── src/ <- shared by web-scraper and email-sender
32
101
  ├── actors
33
102
  │ ├── web-scraper
34
- │ │ ├── .actor
35
- │ │ │ └── actor.json
36
- │ │ └── src/
103
+ │ │ └── .actor
104
+ │ │ └── actor.json <- points to the repo root
37
105
  │ └── email-sender
106
+ │ └── .actor
107
+ │ └── actor.json <- points to the repo root
108
+ ├── standalone-actors
109
+ │ └── orchestrator
38
110
  │ ├── .actor
39
- │ │ └── actor.json
111
+ │ │ └── actor.json <- default context: its own folder
112
+ │ ├── Dockerfile
113
+ │ ├── package.json
40
114
  │ └── src/
41
115
  └── test
42
- ├── unit
43
116
  └── platform
44
- ├── core <- Core (hourly) tests
45
- │ └── core.test.ts
46
- ├── some.test.ts <- Daily tests can be anywhere inside platform/
47
- └── some-other.test.ts
117
+ ├── web-scraper.test.ts
118
+ ├── email-sender.test.ts
119
+ └── orchestrator
120
+ └── orchestrator.test.ts
48
121
  ```
49
122
 
50
- For a single-actor repo, set `"folder": "."` in the config and place `.actor/actor.json` at the repo root.
51
-
52
- ### Change detection
53
-
54
- When a PR is opened or code is pushed, the tool determines which actors need to be built and tested based on the changed files. For each changed file, for each actor:
55
-
56
- 1. **Sibling exclusion** — files inside another actor's `folder` are excluded first. This prevents an actor with broad context from being triggered by changes that belong to a sibling actor.
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
- 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
- 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.
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
- 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
- 8. **Functional** — everything else triggers both build and tests.
123
+ The Actors that share code point their `dockerfile` and `dockerContextDir` at the repo root. Both paths are relative
124
+ to the `.actor` folder:
64
125
 
65
- ### 4. Create test directories
66
-
67
- ```bash
68
- mkdir -p test/platform/core
126
+ ```json
127
+ {
128
+ "actorSpecification": 1,
129
+ "name": "web-scraper",
130
+ "version": "0.1",
131
+ "dockerfile": "../../../Dockerfile",
132
+ "dockerContextDir": "../../.."
133
+ }
69
134
  ```
70
135
 
71
- - Core (hourly) tests go in `test/platform/core`
72
- - Daily tests go anywhere in `test/platform`
73
-
74
- ### 5. Set up GitHub workflows
75
-
76
- See the [GitHub workflows](#github-worklows) section below.
77
-
78
- ## Config file
79
-
80
- `apify-test-tools.config.json` lives at the repo root and tells the tool which actors live in the repo, how to identify them, and which token to use.
136
+ A change to the shared `src/` then builds and tests both of them, while a change in `standalone-actors/orchestrator`
137
+ counts only for the orchestrator, even though it lies inside the others' context. The orchestrator leaves
138
+ `dockerContextDir` out, so its context is its own folder.
81
139
 
82
- ### Actor fields
140
+ For a single-Actor repo, set `"folder": "."` in the config and place `.actor/actor.json` at the repo root.
83
141
 
84
- Every actor in the config is described by these fields, whatever the mode:
142
+ ### 4. Write tests
85
143
 
86
- | Field | Required | Description |
87
- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
- | `folder` | yes | Relative path from repo root to the actor's own project directory — the folder that directly contains `.actor/actor.json` (i.e. `<folder>/.actor/actor.json`), the actor's README/CHANGELOG, and its source. Use `"."` for a single-actor repo where `.actor/` is at the root. |
89
- | `actorFullName` | yes | Full actor identifier in `owner/name` format (e.g. `"apify/web-scraper"`). This is the source of truth for the actor name — the `name` field in `actor.json` is not used. |
90
- | `tokenEnvVar` | yes | Name of the environment variable holding the Apify API token for this actor. In [grouped mode](#grouped-mode) it's inherited from the group unless the actor sets its own. No fallback — if the env var is not set at build time, the build fails. |
91
- | `overrideActorContext` | no | Array of paths (relative to repo root) that define which files are relevant to this actor. When set, replaces the `dockerContextDir` from `actor.json` for change detection. Useful when an actor depends on shared packages outside its Docker build context. Entries must not be prefixes of one another (e.g. `["", "code"]` or `["actors", "actors/foo"]` are rejected). The actor's own `folder` is always part of its context — if none of the listed entries reach it, it's added automatically. |
144
+ Put platform tests in `test/platform`. How you organize them inside is up to you. See
145
+ [Writing tests](#writing-tests) below.
92
146
 
93
- ### Modes
147
+ ### 5. Set up CI
94
148
 
95
- The file's layout is picked by the top-level `mode` field. Each mode is a different way of writing the same thing: every mode resolves to a list of actors with the fields above, and the same rules apply to all of them — no two actors may share a `folder`, and no two entries may point at the same `actorFullName`. If `mode` is left out, [`"legacy"`](#legacy-mode) is used, so existing config files keep working unchanged.
149
+ On GitHub, add the [GitHub workflows](#github-workflows) below. On another CI platform, call the
150
+ [CLI](docs/cli.md) from your own pipeline; the workflows are a good template for the steps.
96
151
 
97
- ### Legacy mode
98
-
99
- The default. A flat `actors` array where every entry carries all of its settings. Good for repos with a few actors, or where every actor has different settings.
100
-
101
- ```json
102
- {
103
- "mode": "legacy",
104
- "actors": [
105
- {
106
- "folder": "actors/web-scraper",
107
- "actorFullName": "myteam/web-scraper",
108
- "tokenEnvVar": "APIFY_TOKEN_MYTEAM"
109
- },
110
- {
111
- "folder": "actors/email-sender",
112
- "actorFullName": "myteam/email-sender",
113
- "tokenEnvVar": "APIFY_TOKEN_MYTEAM",
114
- "overrideActorContext": ["actors/email-sender", "packages/shared"]
115
- }
116
- ]
117
- }
118
- ```
119
-
120
- `actors` must hold at least one entry. Every entry takes all the [actor fields](#actor-fields) directly, and `tokenEnvVar` is required on each one.
121
-
122
- ### Grouped mode
152
+ ## Config file
123
153
 
124
- Actors are organized into `groups`, and each group sets `tokenEnvVar` and `overrideActorContext` once for all of its actors. Good for repos where many actors share the same token or context.
154
+ `apify-test-tools.config.json` lives at the repo root and tells the tool which Actors live in the repo, where they
155
+ are, and which token builds them. Actors are organized into groups, and each group sets the token once for all of its
156
+ Actors:
125
157
 
126
158
  ```json
127
159
  {
@@ -129,72 +161,58 @@ Actors are organized into `groups`, and each group sets `tokenEnvVar` and `overr
129
161
  "groups": {
130
162
  "myteam": {
131
163
  "tokenEnvVar": "APIFY_TOKEN_MYTEAM",
132
- "overrideActorContext": ["packages/shared"],
133
164
  "actors": [
134
165
  { "folder": "actors/web-scraper", "actorFullName": "myteam/web-scraper" },
166
+ { "folder": "actors/email-sender", "actorFullName": "myteam/email-sender" },
135
167
  {
136
- "folder": "actors/email-sender",
137
- "actorFullName": "myteam/email-sender",
138
- "overrideActorContext": ["actors/email-sender", "packages/mailer"]
168
+ "folder": "standalone-actors/orchestrator",
169
+ "actorFullName": "myteam/orchestrator",
170
+ "overrideActorContext": ["standalone-actors/orchestrator", "test/platform/orchestrator"]
139
171
  }
140
172
  ]
141
- },
142
- "partner": {
143
- "tokenEnvVar": "APIFY_TOKEN_PARTNER",
144
- "actors": [{ "folder": "actors/partner-crawler", "actorFullName": "partner/crawler" }]
145
173
  }
146
174
  }
147
175
  }
148
176
  ```
149
177
 
150
- `groups` is an object with at least one group. The group key (`myteam`, `partner` above) is only a label for readability; it has no effect on the result, and it does not need to match any folder or username. Each group needs a `tokenEnvVar` and at least one actor, and can optionally set `overrideActorContext`.
151
-
152
- An actor inside a group can set its own `tokenEnvVar` or `overrideActorContext`, and that value replaces the group's for that actor only. Values are replaced, not merged: in the example above, `myteam/email-sender` gets `["actors/email-sender", "packages/mailer"]`, not `packages/shared` as well.
153
-
154
- ## Github worklows
155
-
156
- The reusable workflows live in this repo, alongside the package they call. They are the
157
- `public_`-prefixed files in `.github/workflows`; everything else there is this repo's own CI.
158
- Reference them at the `@workflows-v0` major tag, never at `@master` — see
159
- [Versioning and releases](#versioning-and-releases).
178
+ The group key (`myteam` above) is only a label for readability; it doesn't need to match any folder or username. Each
179
+ group needs a `tokenEnvVar` and at least one Actor. Add another group for Actors that need a different token, e.g.
180
+ Actors owned by another account.
160
181
 
161
- There should be 4 GH workflow files in `.github/workflows`, plus an optional fifth for Claude reviews.
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 test it. The other two already have the whole repo, tests included, as
184
+ their context.
162
185
 
163
- ### `platform-tests-core.yaml`
186
+ | Field | Where | Required | Description |
187
+ | ---------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
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
+ | `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
+ | `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). |
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. |
164
193
 
165
- ```yaml
166
- name: Platform tests - Core
194
+ A value set on an Actor replaces the group's for that Actor only. Values are replaced, not merged: if the `myteam`
195
+ group also set `overrideActorContext`, the orchestrator would still get only the two paths it lists.
167
196
 
168
- on:
169
- schedule:
170
- # Runs at the start of every hour
171
- - cron: '0 * * * *'
172
- workflow_dispatch:
197
+ No two Actors may share a `folder`, and no two entries may point at the same `actorFullName`.
173
198
 
174
- jobs:
175
- platformTestsCore:
176
- uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
177
- with:
178
- subtest: core
179
- secrets: inherit
180
- ```
199
+ ## GitHub workflows
181
200
 
182
- ### `platform-tests-daily.yaml`
201
+ The workflows take every change straight to production, with tests in between:
183
202
 
184
- ```yaml
185
- name: Platform tests - Daily
203
+ - **Every push to a PR** builds the Actors the PR changes into a test version (`0.99`) that users never run, and runs
204
+ their platform tests against those builds. Only the Actors whose files changed are built and tested, and a later
205
+ push only re-tests what changed since the last green run.
206
+ - **Every merge to `master`** builds the changed Actors into their default version (`latest`), which is what users
207
+ run, and announces the release on Slack. There is no separate release step.
208
+ - **Scheduled runs** (optional) run the tests against the released builds to catch what breaks without a code change,
209
+ like a website changing its layout.
186
210
 
187
- on:
188
- schedule:
189
- # Runs at 00:00 UTC every day
190
- - cron: '0 0 * * *'
191
- workflow_dispatch:
211
+ These are GitHub Actions workflows. On another CI platform, build the same flow with the [CLI](docs/cli.md).
192
212
 
193
- jobs:
194
- platformTestsDaily:
195
- uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
196
- secrets: inherit
197
- ```
213
+ Add these files to `.github/workflows` in your repo. They call the reusable workflows in this repo, pinned at the
214
+ `@workflows-v0` major tag — never at `@master`. Every input, the optional Claude workflows, stale issue closer, and how the tag moves are
215
+ in the [Workflow API](docs/workflows.md).
198
216
 
199
217
  ### `pr-build-devel-test.yaml`
200
218
 
@@ -208,7 +226,14 @@ on:
208
226
  jobs:
209
227
  buildDevelAndTest:
210
228
  uses: apify/apify-test-tools/.github/workflows/public_pr-build-test.yaml@workflows-v0
211
- secrets: inherit
229
+ secrets:
230
+ TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
231
+ # One entry per secret apify-test-tools.config.json names, today each `tokenEnvVar`
232
+ CONFIG_SECRETS: |
233
+ {
234
+ "APIFY_TOKEN_MYTEAM": "${{ secrets.APIFY_TOKEN_MYTEAM }}",
235
+ "APIFY_TOKEN_PARTNER": "${{ secrets.APIFY_TOKEN_PARTNER }}"
236
+ }
212
237
  ```
213
238
 
214
239
  ### `release-latest.yaml`
@@ -223,504 +248,105 @@ on:
223
248
  jobs:
224
249
  buildLatest:
225
250
  uses: apify/apify-test-tools/.github/workflows/public_push-build-latest.yaml@workflows-v0
226
- secrets: inherit
251
+ secrets:
252
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
253
+ # One entry per secret apify-test-tools.config.json names, today each `tokenEnvVar`
254
+ CONFIG_SECRETS: |
255
+ {
256
+ "APIFY_TOKEN_MYTEAM": "${{ secrets.APIFY_TOKEN_MYTEAM }}",
257
+ "APIFY_TOKEN_PARTNER": "${{ secrets.APIFY_TOKEN_PARTNER }}"
258
+ }
227
259
  ```
228
260
 
229
- This workflow runs `apify-test-tools release --base-commit ${{ github.event.before }}`. See
230
- [Releasing Actors](#releasing-actors-release) for how the released range is determined and what to
231
- pass if you call the CLI from somewhere else.
232
-
233
- ### `claude-review.yaml`
261
+ ### Scheduled tests (optional)
234
262
 
235
- Optional. Reviews a PR against the shared guidelines when you add the trigger label, and again on
236
- every push while that label is on.
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:
237
265
 
238
266
  ```yaml
239
- name: Claude review
267
+ name: Platform tests - Daily
240
268
 
241
269
  on:
242
- pull_request:
243
- types: [labeled, synchronize]
270
+ schedule:
271
+ # Runs at 00:00 UTC every day
272
+ - cron: '0 0 * * *'
273
+ workflow_dispatch:
244
274
 
245
275
  jobs:
246
- review:
247
- uses: apify/apify-test-tools/.github/workflows/public_review.yaml@workflows-v0
248
- secrets: inherit
276
+ platformTestsDaily:
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}/**'
280
+ secrets:
281
+ TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
282
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
249
283
  ```
250
284
 
251
- The review instructions live in `.github/review-prompt.md` in this repo and are fetched at run time,
252
- because a reusable workflow doesn't get its own repo checked out. `prompt-ref` selects which ref to
253
- fetch them from and defaults to `workflows-v0`, so the instructions match the workflow you're calling — point
254
- it at a branch only to test a prompt change.
255
-
256
- ### Secrets
257
-
258
- Callers pass `secrets: inherit`. The workflows do not turn every inherited secret into job-wide
259
- environment variables, so a secret is only visible to the step that needs it:
260
-
261
- | Secret | Reaches |
262
- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
263
- | `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
- | `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
-
268
- The Actor tokens are the one set that cannot be listed in the workflow, because each Actor names its
269
- own token via `tokenEnvVar` in `apify-test-tools.config.json`. Those steps pass
270
- `${{ toJSON(secrets) }}` as `ALL_SECRETS` and run the command through
271
- `.github/scripts/run-with-apify-tokens.mjs`, which reads that same config file to decide which
272
- secrets to pass on:
285
+ and a few critical tests every hour:
273
286
 
274
287
  ```yaml
275
- - name: Build
276
- env:
277
- ALL_SECRETS: ${{ toJSON(secrets) }}
278
- run: |
279
- node "${{ steps.setup.outputs.scripts-path }}/run-with-apify-tokens.mjs" \
280
- npx apify-test-tools build --target-branch ...
281
- ```
282
-
283
- The wrapper passes only the tokens the config declares and drops `ALL_SECRETS`, so neither `npx` nor
284
- anything under `node_modules` sees the blob. Nothing is written to `$GITHUB_ENV`, so the tokens stay
285
- inside that one command rather than leaking into later steps. Reading the same file
286
- `apify-test-tools` reads means the two can't drift, and a secret that merely looks like an Actor
287
- token is not passed just because of its name.
288
-
289
- A token the config declares but the repo hasn't set is a warning, not a failure: a repo can carry an
290
- Actor whose token isn't configured and still build fine as long as that Actor never changes, and
291
- `apify-test-tools` raises a precise error naming the Actor at the point it actually needs the token.
292
-
293
- `scripts-path` comes from the setup action (give the step `id: setup`) and points at this repo's
294
- `.github/scripts/` directory inside the runner's action checkout, so workflows can run these helpers
295
- without checking this repo out again. The caller's workspace holds the caller's repo, not this one.
296
-
297
- Two tidier-looking alternatives don't work, so don't reach for them:
298
-
299
- - **Exporting to `$GITHUB_ENV`** would let the steps call `npx` directly with no wrapper, but
300
- `$GITHUB_ENV` applies to every later step in the job. In `pr-build-test` the vitest step runs after
301
- the build, so it would inherit Actor tokens it has no use for.
302
- - **Returning the tokens as a step output** would be scoped correctly, but the runner refuses to set
303
- an output whose value contains a registered secret. It logs `Skip output <name> since it may
304
- contain secret` and leaves the output empty, so anything reading it downstream gets nothing.
305
-
306
- The `unitTest` job runs static checks and needs `NPM_TOKEN` only. No job runs `npm ci` with Apify or
307
- Slack credentials in scope, so a postinstall script in the dependency tree cannot read them.
308
-
309
- ## Versioning and releases
310
-
311
- The workflows and the npm package live in one repo but ship on their own schedules:
312
-
313
- | What | Consumers get it through | Ships when |
314
- | -------------------------------------------- | ------------------------------------------ | ----------------------- |
315
- | the workflows and the CLI they call | the `@workflows-v0` tag in `uses:` | every master push |
316
- | the lib (`testActor`, `describe`, matchers…) | `apify-test-tools` in their `package.json` | a stable release on npm |
317
-
318
- The CLI is bundled with its dependencies into `.github/cli/apify-test-tools.mjs` (`npm run bundle`,
319
- run by the pre-commit hook and checked in CI), and the setup action runs it from there. Nothing is
320
- installed for it, so a tag is a complete statement: these workflows and this CLI, with the exact
321
- dependency versions from `package-lock.json` at that commit. The consumer's own `node_modules` is
322
- left alone, so their tests keep the lib version they locked.
323
-
324
- - **Workflow or CLI change** — merge it. `workflows-v0` moves once checks and tests pass, and it goes
325
- live. No release needed, even when a workflow starts using a new CLI feature: both are in the
326
- same commit.
327
- - **Lib change** — merge it, then cut a release when you want it out. Consumers pick it up when
328
- they update `apify-test-tools`.
329
-
330
- The CLI and the lib in one job can be different versions now, so the formats between them are a
331
- contract: the `ACTOR_BUILDS` JSON that `build` writes and the lib reads, and the vitest JSON report
332
- `report-tests` reads. Add fields freely; renaming or removing one needs a new major tag.
333
-
334
- `workflows-v0` moving on every master push means `@workflows-v0` is as live as `@master` was —
335
- there's no staging step. What the tag buys you is a `workflows-v1` for breaking workflow changes,
336
- so repos migrate one at a time instead of all at once, and a way to roll back by pointing the tag at
337
- an earlier commit (**Move major version tag** workflow). Bump `MAJOR_TAG` in
338
- `.github/workflows/_move_major_tag.yaml` to cut the next major; the old tag then freezes where it is
339
- and keeps working, CLI included, since the bundle is part of the frozen commit.
340
-
341
- The tag tracks the **workflows'** contract, not the npm package version. They move independently on
342
- purpose, so `@workflows-v0` is expected to stay `@workflows-v0` after the package reaches 1.0 —
343
- bump it when a workflow breaks its callers, not when the package does. Because `uses:` cannot take an expression, every ref
344
- into this repo repeats the tag literally; `check-major-tag-refs.mjs` fails the build if `MAJOR_TAG`
345
- and those refs disagree, which is the mistake that would otherwise ship silently during a bump.
346
-
347
- ### Testing workflow changes
348
-
349
- - Point [testing-repo-for-github-actions](https://github.com/apify-store/testing-repo-for-github-actions)
350
- at your branch (`uses: ...@your-branch`). It has real attached Actors and tests. The branch carries
351
- its own CLI bundle, so one branch tests the workflows and the CLI together.
352
- - To change the composite action itself, repoint the `uses:` refs inside the reusable workflows at
353
- your branch as well, and change them back before merging.
354
- - Make sure the shell code actually works on your laptop first.
355
- - After merging, watch the workflow on a real project before moving on.
288
+ name: Platform tests - Hourly
356
289
 
357
- ## Writing tests
358
-
359
- ### Test structure
360
-
361
- `testActor` runs the actor and provides extended `expect` and `run` inside the callback.
362
-
363
- ```ts
364
- import { describe, testActor } from 'apify-test-tools';
365
-
366
- describe('test', () => {
367
- testActor(actorId, 'actor test 1', async ({ expect, run }) => {
368
- const runResult = await run({ input });
369
-
370
- // your checks
371
- });
372
-
373
- testActor(actorId, 'actor test 2', async ({ expect, run }) => {
374
- const runResult = await run({ input });
375
-
376
- // your checks
377
- });
378
- });
379
- ```
380
-
381
- ---
382
-
383
- ### Validating basic run attributes
384
-
385
- `toFinishWith` validates common run properties in a single call:
386
-
387
- ```ts
388
- await expect(runResult).toFinishWith({
389
- datasetItemCount: 100,
390
- });
391
- ```
392
-
393
- You can also specify a range:
394
-
395
- ```ts
396
- await expect(runResult).toFinishWith({
397
- datasetItemCount: { min: 80, max: 120 },
398
- });
399
- ```
400
-
401
- Here is full example of what you can validate with `toFinishWith`
402
-
403
- ```ts
404
- await expect(runResult).toFinishWith({
405
- // These are default
406
- status: 'SUCCEEDED',
407
- duration: {
408
- min: 600, // 0.6 sec
409
- max: 600_000, // 10 min
410
- },
411
- failedRequests: 0,
412
- requestsRetries: { max: 3 },
413
- forbiddenLogs: ['ReferenceError', 'TypeError'],
414
-
415
- // only datasetItemCount is required
416
- datasetItemCount: { min: 80, max: 120 },
417
-
418
- // optional
419
- chargedEventCounts: {
420
- 'actor-start': 1,
421
- 'place-scraped': 9,
422
- },
423
- });
424
- ```
425
-
426
- ---
427
-
428
- ### Custom validations
429
-
430
- ```ts
431
- expect(place.title, `London Eye's title`).toEqual('lastminute.com London Eye');
432
- ```
433
-
434
- ---
435
-
436
- ### Custom validation functions
437
-
438
- You can create your own functions wrapping a common validation logic in e.g. `test/platform/utils.ts` and import it in test files.
439
-
440
- ```ts
441
- import { ExpectStatic } from 'apify-test-tools'
442
-
443
- export const validateItem = (expect: ExpectStatic, item: any) {
444
- expect(item.title, 'Item title').toBeString();
445
- }
446
- ```
447
-
448
- ---
449
-
450
- ### Test options
451
-
452
- You can pass options as the fourth argument to `testActor`:
453
-
454
- ```ts
455
- testActor(
456
- actorId,
457
- 'slow actor test',
458
- async ({ expect, run }) => {
459
- const runResult = await run({ input });
460
- await expect(runResult).toFinishWith({ datasetItemCount: 100 });
461
- },
462
- {
463
- timeout: 2 * 60 * 60 * 1000, // 2 hours (default is 1 hour)
464
- retry: 3, // retry up to 3 times (default is 1)
465
- },
466
- );
467
- ```
468
-
469
- ---
470
-
471
- ### Using prefilled input
472
-
473
- If the actor has a prefilled input on the platform, you can merge it with your test input:
290
+ on:
291
+ schedule:
292
+ # Runs at the start of every hour
293
+ - cron: '0 * * * *'
294
+ workflow_dispatch:
474
295
 
475
- ```ts
476
- testActor(actorId, 'with prefilled input', async ({ expect, run }) => {
477
- const runResult = await run({
478
- prefilledInput: true,
479
- input: { maxItems: 10 }, // merged on top of the prefilled input
480
- });
481
- await expect(runResult).toFinishWith({ datasetItemCount: 10 });
482
- });
296
+ jobs:
297
+ platformTestsHourly:
298
+ uses: apify/apify-test-tools/.github/workflows/public_platform-tests.yaml@workflows-v0
299
+ with:
300
+ test-files-glob: 'test/platform/{hourly,hourly-without-pr}/**'
301
+ secrets:
302
+ TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
303
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
483
304
  ```
484
305
 
485
- ---
486
-
487
- ### Testing an existing run
306
+ ### Secrets
488
307
 
489
- You can skip starting a new run and validate an existing one by passing `runId`:
308
+ Set these as repository or organization secrets and pass them in each job's `secrets:` block, as in the examples
309
+ above. The Actor tokens go packed in one JSON object, `CONFIG_SECRETS`, because their names differ per repo:
490
310
 
491
- ```ts
492
- testActor(actorId, 'validate existing run', async ({ expect, run }) => {
493
- const runResult = await run({ runId: 'some-run-id' });
494
- await expect(runResult).toFinishWith({ datasetItemCount: 100 });
495
- });
496
- ```
311
+ | Secret | Needed for |
312
+ | ------------------------------------------------------------------------- | ----------------------------------------------------------------- |
313
+ | the Actor tokens named by `tokenEnvVar` in `apify-test-tools.config.json` | building Actors and deleting their old builds |
314
+ | `TESTER_APIFY_TOKEN` | running the platform tests |
315
+ | `SLACK_BOT_TOKEN` | announcing releases and reporting scheduled test results to Slack |
316
+ | `NPM_TOKEN` | optional, installing private npm packages. Use a read-only token |
497
317
 
498
- ---
318
+ Repos in the `apify` org can pass `secrets: inherit` instead. Each secret only reaches the step that needs it. See
319
+ [Secrets](docs/workflows.md#secrets) for both, and for passing `CONFIG_SECRETS` without listing the tokens.
499
320
 
500
- ### Accessing run data
321
+ ## Writing tests
501
322
 
502
- `RunTestResult` provides methods to access the run's data:
323
+ Platform tests use `describe` and `testActor` from `apify-test-tools` instead of the ones from `vitest`. `testActor`
324
+ registers a test that only runs when that Actor was built for it (or when you ask for all tests), and gives the
325
+ callback an extended `expect` and a `run` function that starts the Actor.
503
326
 
504
327
  ```ts
505
- testActor(actorId, 'check dataset items', async ({ expect, run }) => {
506
- const runResult = await run({ input });
507
-
508
- // Access dataset items
509
- const { items } = await runResult.getDataset();
510
- expect(items[0].title).toBeNonEmptyString();
511
-
512
- // Access run log
513
- const log = await runResult.getLog();
514
- expect(log).toContain('Crawl finished');
515
-
516
- // Access crawler statistics
517
- const stats = await runResult.getStatistics();
518
- expect(stats?.requestsFinished).toBeGreaterThan(0);
519
-
520
- // Access key-value store
521
- const kvs = runResult.getKeyValueStoreClient();
522
- const record = await kvs.getRecord('OUTPUT');
523
-
524
- // Access run info (refreshed from API)
525
- const runInfo = await runResult.getRunInfo();
526
- });
527
- ```
528
-
529
- ---
328
+ import { describe, testActor } from 'apify-test-tools';
530
329
 
531
- ### Testing standby actors
330
+ describe('web-scraper', () => {
331
+ testActor('myteam/web-scraper', 'scrapes the start URL', async ({ expect, run }) => {
332
+ const runResult = await run({ input: { startUrls: [{ url: 'https://apify.com' }] } });
532
333
 
533
- Use `testStandbyActor` for actors that support standby mode:
334
+ // Status SUCCEEDED, a sane duration, no failed requests, no errors in the log, and the item count
335
+ await expect(runResult).toFinishWith({ datasetItemCount: { min: 1, max: 10 } });
534
336
 
535
- ```ts
536
- import { describe, testStandbyActor } from 'apify-test-tools';
537
-
538
- describe('standby tests', () => {
539
- testStandbyActor(actorId, 'standby request', async ({ expect, callStandby }) => {
540
- const { data, status } = await callStandby({
541
- input: { query: 'test' },
542
- path: '/search',
543
- headers: { 'Content-Type': 'application/json' },
544
- });
545
-
546
- expect(status).toBe(200);
547
- expect(data.results).toBeNonEmptyArray();
337
+ const { items } = await runResult.getDataset();
338
+ expect(items[0].title, 'Page title').toBeNonEmptyString();
548
339
  });
549
340
  });
550
341
  ```
551
342
 
552
- ---
553
-
554
- ### Custom matchers
555
-
556
- `testActor` extends `expect` with the following custom matchers:
557
-
558
- - `toBeArray()` / `toBeEmptyArray()` / `toBeNonEmptyArray()`
559
- - `toBeString()` / `toBeNonEmptyString()` / `toStartWith(prefix)`
560
- - `toBeNumber()` / `toBeWholeNumber()` / `toBeWithinRange(min, max)`
561
- - `toBeBoolean()` / `toBeTrue()` / `toBeFalse()`
562
- - `toBeObject()` / `toBeNonEmptyObject()`
563
- - `toFinishWith(options)` - validates run status, duration, dataset, logs, etc.
564
-
565
- ## CLI (`apify-test-tools` bin)
566
-
567
- The package includes a CLI binary used by CI workflows to build Actors, detect changes, and report test results. You can also run it locally.
568
-
569
- ### Dry runs (`--dry-run`)
343
+ Platform tests are skipped unless `TESTER_APIFY_TOKEN` is set, so your regular unit test run stays unaffected.
570
344
 
571
- Every command that changes something (`build`, `build-from-local`, `release`, `delete-old-builds`, `report-tests`) takes `--dry-run`. A dry run:
572
-
573
- - **Changes nothing.** No versions or builds are created, nothing is uploaded or deleted, and no Slack message is posted.
574
- - **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
- - **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
-
578
- ### Running locally
579
-
580
- Running the testing library locally is useful when you only want to update the testing code in /test because you can iterate on it without pushing new code to the remote.
581
-
582
- If you don't need to change any source files and only iterate on /test code, you can skip steps 1-4. But if you want to test vs changed /src, you have to push that GitHub branch since it needs to build the Actors with that code.
583
-
584
- The main local flow is:
585
-
586
- 1. Switch to a dummy branch that you will push and can later delete
587
- 2. `npm i apify-test-tools@latest -D`
588
- 3. Push your code (changes you want to test)
589
- 4. Build Actors on Apify (with your new code)
590
- 5. Run tests against those builds. You can change tests and run on the same builds.
591
-
592
- Run every command from the root of the actor repository you want to work with.
593
-
594
- #### 4. Build affected Actors
595
-
596
- If you want to test vs existing src code, you can skip this and instead construct the output JSON manually from existing builds only for the Actors you need to test.
597
-
598
- 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`).
599
-
600
- ```bash
601
- APIFY_TOKEN_JOHN_DOE=<token> \
602
- npx apify-test-tools build \
603
- --target-branch origin/master \
604
- --source-branch origin/my-dummy-branch \
605
- --dry-run
606
- ```
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:
609
-
610
- ```json
611
- [{ "buildId": "...", "actorRawId": "...", "buildNumber": "...", "actorFullName": "john.doe/my-actor" }]
612
- ```
613
-
614
- #### Build from local source (no push needed)
615
-
616
- If you don't want to push a dummy branch just to test a change and wait for all the tests to finish, `build-from-local` builds Actors directly from your local files (zipped into the Actor's `actor-<id>-source` key-value store and built as `TARBALL`, like `apify push`), skipping steps 1-4 above.
617
-
618
- ```bash
619
- APIFY_TOKEN_JOHN_DOE=<token> \
620
- npx apify-test-tools build-from-local --actors john.doe/my-actor
621
- ```
622
-
623
- Pass a hardcoded actor name via `--actors` to build only that Actor (comma-separate multiple names). Omit `--actors` to build all Actors in the repo, or add `--dry-run` to check the token, the Actor and the zipped sources without uploading or building. It outputs the same JSON build array as `build`, so you run tests against it the same way as in step 5 below:
624
-
625
- ```bash
626
- # Build from local source and capture output
627
- BUILDS=$(APIFY_TOKEN_JOHN_DOE=apify_api_xxx \
628
- npx apify-test-tools build-from-local --actors apify/my-actor)
629
- ```
630
-
631
- Since you already scoped the build to just the Actor(s) you care about, point vitest at a specific test file (or a `-t` name filter) instead of the whole `test/platform` directory — you get feedback on that one test without waiting for the full suite to run.
632
-
633
- #### 5. Run tests against the builds
634
-
635
- Pass the build output as `ACTOR_BUILDS` and provide `TESTER_APIFY_TOKEN`. The token can point to your own account (if you have enough memory) or you can use the testing account (xRGg9iAfJSymqartk). Platform test suites are skipped unless `TESTER_APIFY_TOKEN` is set, so regular unit test runs stay unaffected.
636
-
637
- If you want to run only certain tests, change the `test/platform` to be more specific.
638
-
639
- ```bash
640
- ACTOR_BUILDS='<JSON output from build command>' \
641
- TESTER_APIFY_TOKEN=<token> \
642
- npx vitest --run --maxConcurrency 20 --fileParallelism=true --maxWorkers 100 test/platform
643
- ```
644
-
645
- #### Full example
646
-
647
- 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.
345
+ To run them locally against the Actors' current default builds:
648
346
 
649
347
  ```bash
650
- # Build and capture output
651
- BUILDS=$(APIFY_TOKEN_JOHN_DOE=apify_api_xxx \
652
- npx apify-test-tools build \
653
- --target-branch origin/master \
654
- --source-branch origin/my-dummy-branch \
655
- --test-files-glob 'test/platform/**')
656
-
657
- # Run tests with the builds
658
- ACTOR_BUILDS="$BUILDS" \
659
- TESTER_APIFY_TOKEN=apify_api_yyy \
660
- npx vitest --run --maxConcurrency 20 --fileParallelism=true --maxWorkers 100 test/platform
348
+ npx apify-test-tools get-actor-configs | TESTER_APIFY_TOKEN=<token> npx apify-test-tools test
661
349
  ```
662
350
 
663
- ### Releasing Actors (`release`)
664
-
665
- `release` builds the `latest` (default) version of every Actor changed since the last release and
666
- posts release notes to Slack. It reads everything else from git: the branch from the checked-out
667
- `HEAD`, the commits and changed files from the range `<base-commit>..HEAD`, and the repository from
668
- the `origin` remote.
669
-
670
- ```bash
671
- npx apify-test-tools release --base-commit <sha> --release-slack-channel "#releases" --dry-run
672
- ```
673
-
674
- #### `--base-commit`
675
-
676
- The last commit that is **already released**. Everything after it, up to `HEAD`, is released. It is
677
- required: git alone can't tell which commits a push brought, and guessing wrong silently leaves Actors
678
- on old code.
679
-
680
- The changed files come from diffing `<base-commit>` and `HEAD` directly, so this works for every
681
- merge strategy as long as the base commit is the branch tip from **before** the push:
682
-
683
- | Merge strategy | What lands on the branch | What to pass |
684
- | ------------------------------ | --------------------------------- | ---------------------------------------------------------------------- |
685
- | Squash and merge | 1 new commit | the push's "before" commit (`HEAD~1` also works) |
686
- | Merge commit | the PR's commits + 1 merge commit | the push's "before" commit (`HEAD^1`, the first parent, also works) |
687
- | Rebase and merge / direct push | N new commits | the push's "before" commit (`HEAD~1` would miss the first N-1 commits) |
688
-
689
- On GitHub, the push's "before" commit is `${{ github.event.before }}`, which the reusable workflow
690
- passes for you. On other CIs use the equivalent, e.g. `$CI_COMMIT_BEFORE_SHA` on GitLab. The checkout
691
- needs the full history (`fetch-depth: 0` in `actions/checkout`) so the base commit is available locally.
692
-
693
- `release` fails instead of guessing when:
694
-
695
- - the base commit is all zeros (GitHub sends this for a push that created the branch)
696
- - the base commit isn't in the local history (shallow checkout) or isn't an ancestor of `HEAD`
697
- (the branch was force-pushed)
698
-
699
- In those cases, rerun `release` with a `--base-commit` you pick yourself: an ancestor of `HEAD` from
700
- before the changes you want to release (for a shallow checkout, fetch the full history instead).
701
- `--actors` only narrows which of the changed Actors get released; it never forces a build, so on its
702
- own it doesn't get past these errors.
703
- If `HEAD` is the base commit, there is nothing to release and the command exits successfully.
704
-
705
- A failed release isn't retried by the next push: that push's base commit is the failed one's head, so
706
- its changes are skipped. Re-run the failed workflow run, or release the Actors manually.
707
-
708
- #### Repository check
709
-
710
- `release` and `build` point each Actor's version at the `origin` remote. Because a build overwrites the
711
- version's Git URL, building from a fork or a mirror would repoint the Actor. So, before building, the
712
- remote is compared with the Git URL of the Actor's default version (the one its default build tag
713
- points to), and the command fails if they differ or if the default version isn't built from a Git
714
- repository. The URL form doesn't matter (`https://github.com/org/repo` equals
715
- `git@github.com:org/repo.git`).
716
-
717
- To build from a different repository on purpose, e.g. when moving an Actor, pass `--repo-url`. That
718
- skips the check.
719
-
720
- #### Dev mode
721
-
722
- For development on `apify-test-tools` itself, use `tsx` directly:
723
-
724
- ```bash
725
- cd local-clone && tsx ../bin/main.ts get-actor-configs
726
- ```
351
+ The [Vitest library API](docs/lib.md) covers everything `toFinishWith` checks, the other matchers, standby Actors,
352
+ reading run data, and running tests locally against builds of your own changes.