apify-test-tools 0.10.2-beta.2 → 0.10.2-beta.21

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 +23 -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 +182 -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 +149 -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 +165 -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 +473 -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 +207 -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
@@ -0,0 +1,113 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { ApifyClient } from 'apify-client';
7
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
8
+
9
+ import type { ActorBuild } from './helpers.js';
10
+ import { gitEnv, runCli as runCliIn, runCliWithInput as runCliWithInputIn, token } from './helpers.js';
11
+
12
+ // Real git builds of a real repository: the shipped CLI bundle builds an Actor from a clone of
13
+ // apify-store/testing-repo-for-github-actions, and the Apify builder clones the same repository
14
+ // (over SSH, with the sandbox org's deploy key installed on it).
15
+ //
16
+ // The repo's `e2e/base` and `e2e/feature` branches are fixtures and never change: `e2e/feature`
17
+ // adds one commit touching only dummy-for-testing-1. Like the reusable workflows, the commands run
18
+ // with `e2e/feature` checked out and compare it with `origin/e2e/base`. The release pipeline rebuilds
19
+ // dummy-for-testing-1 under `latest` on every run, which is fine for a sandbox Actor nobody else uses.
20
+ //
21
+ // Runs with `npm run test:e2e`, kept out of `npm test`. Skipped without the token, e.g. on fork PRs.
22
+ const FIXTURE_REPO = 'https://github.com/apify-store/testing-repo-for-github-actions';
23
+ const BASE_BRANCH = 'e2e/base';
24
+ const FEATURE_BRANCH = 'e2e/feature';
25
+ const CHANGED_ACTOR = 'test-tools-testing-sandbox/dummy-for-testing-1';
26
+ const CHANGED_ACTOR_FOLDER = 'actors/dummy-for-testing-1';
27
+ // The CLI builds from the SSH form of the origin URL, which is what lets a deploy key work.
28
+ const BUILD_SOURCE = `git@github.com:apify-store/testing-repo-for-github-actions#${FEATURE_BRANCH}:${CHANGED_ACTOR_FOLDER}`;
29
+
30
+ let repoDir: string;
31
+
32
+ const git = (...args: string[]) => execFileSync('git', args, { cwd: repoDir, env: gitEnv, encoding: 'utf8' }).trim();
33
+ const runCli = async (...args: string[]) => runCliIn(repoDir, ...args);
34
+ const runCliWithInput = async (input: string, ...args: string[]) => runCliWithInputIn(repoDir, input, ...args);
35
+
36
+ const getAffectedActors = async () => {
37
+ const { status, stdout, stderr } = await runCli('get-affected-actors', '--base', `origin/${BASE_BRANCH}`);
38
+ expect(status, stderr).toBe(0);
39
+ return JSON.parse(stdout);
40
+ };
41
+
42
+ describe.runIf(token)('CLI bundle building a real repository', { timeout: 2 * 60_000 }, () => {
43
+ const client = new ApifyClient({ token });
44
+
45
+ const versionSource = async (versionNumber: string) => {
46
+ const actor = await client.actor(CHANGED_ACTOR).get();
47
+ const version = actor?.versions.find(({ versionNumber: number }) => number === versionNumber);
48
+ return version && 'gitRepoUrl' in version ? version.gitRepoUrl : undefined;
49
+ };
50
+
51
+ // A full clone checked out on the feature branch, like actions/checkout with fetch-depth: 0.
52
+ beforeAll(async () => {
53
+ repoDir = await mkdtemp(path.join(os.tmpdir(), 'apify-cli-e2e-git-'));
54
+ execFileSync('git', ['clone', '-q', FIXTURE_REPO, repoDir], { env: gitEnv });
55
+ git('checkout', '-q', '-b', FEATURE_BRANCH, `origin/${FEATURE_BRANCH}`);
56
+ }, 5 * 60_000);
57
+
58
+ afterAll(async () => {
59
+ if (repoDir) await rm(repoDir, { recursive: true, force: true });
60
+ });
61
+
62
+ it('get-affected-actors finds only the Actor the feature branch changed', async () => {
63
+ expect(await getAffectedActors()).toMatchObject({
64
+ functionallyChangedActors: [{ actorFullName: CHANGED_ACTOR }],
65
+ cosmeticallyChangedActors: [],
66
+ });
67
+ });
68
+
69
+ it('build builds the changed Actor from the feature branch', { timeout: 10 * 60_000 }, async () => {
70
+ // What the PR workflow does: the functionally changed Actors, piped into a test build
71
+ const { functionallyChangedActors } = await getAffectedActors();
72
+ const { status, stdout, stderr } = await runCliWithInput(
73
+ JSON.stringify({ actors: functionallyChangedActors }),
74
+ 'build',
75
+ '--version-number',
76
+ '0.99',
77
+ );
78
+ expect(status, stderr).toBe(0);
79
+ const builds: ActorBuild[] = JSON.parse(stdout);
80
+ expect(builds).toMatchObject([
81
+ { actorFullName: CHANGED_ACTOR, buildNumber: expect.stringMatching(/^0\.99\.\d+$/) },
82
+ ]);
83
+
84
+ // The version builds the checked-out branch, as it is on the remote.
85
+ expect(await versionSource('0.99')).toBe(BUILD_SOURCE);
86
+ expect((await client.build(builds[0].buildId).get())?.status).toBe('SUCCEEDED');
87
+ });
88
+
89
+ it('the release pipeline rebuilds the default version of the changed Actor', { timeout: 10 * 60_000 }, async () => {
90
+ // What the release workflow does: every Actor changed since the last release, built into its default version
91
+ const baseSha = git('rev-parse', `origin/${BASE_BRANCH}`);
92
+ const affected = await runCli('get-affected-actors', '--since', baseSha);
93
+ expect(affected.status, affected.stderr).toBe(0);
94
+ const { functionallyChangedActors, cosmeticallyChangedActors } = JSON.parse(affected.stdout);
95
+
96
+ const { status, stderr } = await runCliWithInput(
97
+ JSON.stringify({ actors: [...functionallyChangedActors, ...cosmeticallyChangedActors] }),
98
+ 'build',
99
+ '--version-number',
100
+ 'default',
101
+ );
102
+ expect(status, stderr).toBe(0);
103
+ expect(stderr).toMatch(new RegExp(`\\[${CHANGED_ACTOR}\\]: 0\\.0\\.\\d+`));
104
+
105
+ // Releasing builds the default version from the branch being released.
106
+ expect(await versionSource('0.0')).toBe(BUILD_SOURCE);
107
+
108
+ // Without a channel the announcement is skipped, so it needs no Slack token
109
+ const notify = await runCliWithInput(affected.stdout, 'notify');
110
+ expect(notify.status, notify.stderr).toBe(0);
111
+ expect(notify.stderr).toContain('Skipping notification');
112
+ });
113
+ });
@@ -0,0 +1,82 @@
1
+ import { execFile, type ExecFileException } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { promisify } from 'node:util';
7
+
8
+ // Shared by the e2e suites. They run the shipped CLI bundle against Actors in the isolated
9
+ // `test-tools-testing-sandbox` org, whose token can reach nothing else.
10
+ export const TOKEN_ENV_VAR = 'E2E_TEST_APIFY_TOKEN_DBYPODOIHUZABNQTR';
11
+ export const token = process.env[TOKEN_ENV_VAR];
12
+
13
+ export const projectRoot = fileURLToPath(new URL('../../', import.meta.url));
14
+ export const bundlePath = path.join(projectRoot, '.github/cli/apify-test-tools.mjs');
15
+
16
+ // Proxy and CA settings pass through so the API is reachable behind a proxy; no other variable does.
17
+ const networkEnv = Object.fromEntries(
18
+ ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'NO_PROXY', 'no_proxy', 'NODE_EXTRA_CA_CERTS']
19
+ .filter((name) => process.env[name] !== undefined)
20
+ .map((name) => [name, process.env[name]!]),
21
+ );
22
+ const baseEnv = { PATH: process.env.PATH ?? '', HOME: os.tmpdir() };
23
+ // The CLI gets only the sandbox token, under the name the fixture configs point at.
24
+ export const cliEnv = { ...baseEnv, ...networkEnv, [TOKEN_ENV_VAR]: token ?? '' };
25
+ export const gitEnv = {
26
+ ...baseEnv,
27
+ ...networkEnv,
28
+ GIT_AUTHOR_NAME: 'Tester',
29
+ GIT_AUTHOR_EMAIL: 'tester@example.com',
30
+ GIT_COMMITTER_NAME: 'Tester',
31
+ GIT_COMMITTER_EMAIL: 'tester@example.com',
32
+ };
33
+
34
+ // Processes are spawned asynchronously: a blocking spawn would stall the test worker for the length
35
+ // of a build, long enough for vitest's own worker RPC to time out. The kill timeout still applies on
36
+ // top of vitest's, since apify-client retries an unreachable API for minutes.
37
+ export const PROCESS_TIMEOUT_MS = 8 * 60_000;
38
+ const execFileAsync = promisify(execFile);
39
+
40
+ export interface ProcessResult {
41
+ status: number | null;
42
+ stdout: string;
43
+ stderr: string;
44
+ }
45
+
46
+ export const runProcess = async (
47
+ command: string,
48
+ args: string[],
49
+ { input = '', ...options }: { cwd: string; env: Record<string, string>; input?: string },
50
+ ): Promise<ProcessResult> => {
51
+ try {
52
+ const promise = execFileAsync(command, args, {
53
+ ...options,
54
+ encoding: 'utf8',
55
+ timeout: PROCESS_TIMEOUT_MS,
56
+ maxBuffer: 100 * 1024 * 1024,
57
+ });
58
+ // Closing stdin hands a command that reads it (e.g. `build`) the input, and one that doesn't never waits
59
+ promise.child.stdin?.end(input);
60
+ const { stdout, stderr } = await promise;
61
+ return { status: 0, stdout, stderr };
62
+ } catch (error) {
63
+ // A non-zero exit rejects too, with the output on the error.
64
+ const { code, stdout = '', stderr = '', message } = error as ExecFileException;
65
+ // Without an exit code (killed by the timeout, or never started) the message says why.
66
+ return typeof code === 'number'
67
+ ? { status: code, stdout, stderr }
68
+ : { status: null, stdout, stderr: `${stderr}\n${message}` };
69
+ }
70
+ };
71
+
72
+ export const runCliWithInput = async (cwd: string, input: string, ...args: string[]) =>
73
+ runProcess(process.execPath, [bundlePath, ...args], { cwd, env: cliEnv, input });
74
+
75
+ export const runCli = async (cwd: string, ...args: string[]) => runCliWithInput(cwd, '', ...args);
76
+
77
+ export interface ActorBuild {
78
+ buildId: string;
79
+ actorRawId: string;
80
+ buildNumber: string;
81
+ actorFullName: string;
82
+ }
@@ -36,6 +36,14 @@ describe('selectActors', () => {
36
36
  ]);
37
37
  });
38
38
 
39
+ it('selects an actor listed twice once, in config order', () => {
40
+ // build relies on this to build an Actor in both functionallyChangedActors and testChangedActors once
41
+ expect(names(selectActors({ actors: ['owner/c', 'owner/a', 'owner/c'], ignore: [] }, configs))).toStrictEqual([
42
+ 'owner/a',
43
+ 'owner/c',
44
+ ]);
45
+ });
46
+
39
47
  it('drops the actors listed in --ignore', () => {
40
48
  expect(names(selectActors({ actors: [], ignore: ['owner/b'] }, configs))).toStrictEqual(['owner/a', 'owner/c']);
41
49
  });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { withActorIds } from '../../../bin/actor-ids.js';
4
+
5
+ type Client = NonNullable<Parameters<typeof withActorIds>[1]>;
6
+
7
+ /** A client that knows one Actor, by its full name and by its raw ID, as the API does */
8
+ const fakeClient = () => {
9
+ const known = { id: 'rawA', username: 'owner', name: 'a' };
10
+ const actor = vi.fn((actorId: string) => ({
11
+ get: async () => (actorId === 'owner/a' || actorId === 'rawA' ? known : undefined),
12
+ }));
13
+ return { actor } as unknown as Client & { actor: typeof actor };
14
+ };
15
+
16
+ describe('withActorIds', () => {
17
+ it('adds the raw ID to an Actor listed by full name, as get-actor-configs lists them', async () => {
18
+ expect(await withActorIds([{ actorFullName: 'owner/a' }], fakeClient())).toEqual([
19
+ { actorFullName: 'owner/a', actorRawId: 'rawA' },
20
+ ]);
21
+ });
22
+
23
+ it('adds the full name to an Actor listed by raw ID', async () => {
24
+ const build = { actorRawId: 'rawA', buildId: 'b1', buildNumber: '0.99.1' };
25
+ expect(await withActorIds([build], fakeClient())).toEqual([{ ...build, actorFullName: 'owner/a' }]);
26
+ });
27
+
28
+ it('keeps the IDs of a build and does not look them up', async () => {
29
+ const client = fakeClient();
30
+ const build = { actorFullName: 'owner/a', buildId: 'b1', actorRawId: 'raw1', buildNumber: '0.99.1' };
31
+ expect(await withActorIds([build], client)).toEqual([build]);
32
+ expect(client.actor).not.toHaveBeenCalled();
33
+ });
34
+
35
+ it('keeps an Actor the token cannot see as listed, instead of failing the run', async () => {
36
+ const targets = [
37
+ { actorFullName: 'owner/missing' },
38
+ { actorRawId: 'rawMissing' },
39
+ { actorFullName: 'owner/a' },
40
+ ];
41
+ expect(await withActorIds(targets, fakeClient())).toEqual([
42
+ { actorFullName: 'owner/missing' },
43
+ { actorRawId: 'rawMissing' },
44
+ { actorFullName: 'owner/a', actorRawId: 'rawA' },
45
+ ]);
46
+ });
47
+ });
@@ -0,0 +1,271 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
8
+
9
+ import { compileCli } from './compiled-cli.js';
10
+
11
+ // `get-affected-actors --base` on real git histories. Actors a and c share `src/`, b is standalone.
12
+ // `main` is the base branch and `feature` the PR branch, checked out like in the PR workflow.
13
+ const CONFIG = {
14
+ actors: [
15
+ {
16
+ folder: 'actors/a',
17
+ actorFullName: 'owner/actor-a',
18
+ tokenEnvVar: 'T',
19
+ overrideActorContext: ['actors/a', 'src'],
20
+ },
21
+ {
22
+ folder: 'actors/c',
23
+ actorFullName: 'owner/actor-c',
24
+ tokenEnvVar: 'T',
25
+ overrideActorContext: ['actors/c', 'src'],
26
+ },
27
+ { folder: 'actors/b', actorFullName: 'owner/actor-b', tokenEnvVar: 'T' },
28
+ ],
29
+ };
30
+
31
+ // Only what node and git need, so nothing from the developer's git config leaks in
32
+ const env = { PATH: process.env.PATH ?? '', HOME: os.tmpdir() };
33
+ const gitEnv = {
34
+ ...env,
35
+ GIT_AUTHOR_NAME: 'Tester',
36
+ GIT_AUTHOR_EMAIL: 'tester@example.com',
37
+ GIT_COMMITTER_NAME: 'Tester',
38
+ GIT_COMMITTER_EMAIL: 'tester@example.com',
39
+ };
40
+
41
+ let cliPath: string;
42
+ let cleanup: (() => Promise<void>) | undefined;
43
+ let repoDir: string;
44
+
45
+ const git = (...args: string[]) => execFileSync('git', args, { cwd: repoDir, env: gitEnv, encoding: 'utf8' }).trim();
46
+
47
+ const writeFiles = async (content: string, files: string[]) => {
48
+ for (const file of files) {
49
+ await mkdir(path.dirname(path.join(repoDir, file)), { recursive: true });
50
+ await writeFile(path.join(repoDir, file), `${content}\n`);
51
+ }
52
+ };
53
+
54
+ /** Commits a change to each file and returns the commit. */
55
+ const commit = async (message: string, ...files: string[]) => {
56
+ await writeFiles(message, files);
57
+ git('add', '-A');
58
+ git('commit', '-q', '-m', message);
59
+ return git('rev-parse', 'HEAD');
60
+ };
61
+
62
+ /** Commits on `main`, then checks `feature` out again. */
63
+ const commitOnMain = async (message: string, ...files: string[]) => {
64
+ git('checkout', '-q', 'main');
65
+ await commit(message, ...files);
66
+ git('checkout', '-q', 'feature');
67
+ };
68
+
69
+ const runGetAffectedActors = (...args: string[]) => {
70
+ const result = spawnSync(cliPath, ['get-affected-actors', ...args], { cwd: repoDir, env, encoding: 'utf8' });
71
+ expect(result.status, result.stderr).toBe(0);
72
+ return JSON.parse(result.stdout);
73
+ };
74
+
75
+ const names = (actors: { actorFullName: string }[]) =>
76
+ actors.map(({ actorFullName }) => actorFullName.replace('owner/actor-', '')).sort();
77
+
78
+ /** All three lists, for the tests about test changes */
79
+ const getAllChangedActors = (...args: string[]) => {
80
+ const document = runGetAffectedActors(...args);
81
+ return {
82
+ functional: names(document.functionallyChangedActors),
83
+ cosmetic: names(document.cosmeticallyChangedActors),
84
+ test: names(document.testChangedActors),
85
+ };
86
+ };
87
+
88
+ /** Writes the config with actor-a's context also covering `extraContext`. It is committed with the next commit. */
89
+ const writeConfigWithTestsInContext = async (extraContext: string, settings: Record<string, unknown> = {}) => {
90
+ const actors = CONFIG.actors.map((actor) =>
91
+ actor.folder === 'actors/a'
92
+ ? { ...actor, overrideActorContext: [...actor.overrideActorContext!, extraContext] }
93
+ : actor,
94
+ );
95
+ await writeFile(path.join(repoDir, 'apify-test-tools.config.json'), JSON.stringify({ ...settings, actors }));
96
+ };
97
+
98
+ const getAffectedActors = (...args: string[]) => {
99
+ const document = runGetAffectedActors(...args);
100
+ return {
101
+ functional: names(document.functionallyChangedActors),
102
+ cosmetic: names(document.cosmeticallyChangedActors),
103
+ };
104
+ };
105
+
106
+ describe('get-affected-actors CLI on real git histories', () => {
107
+ beforeAll(async () => {
108
+ ({ cliPath, cleanup } = await compileCli());
109
+ });
110
+
111
+ afterAll(async () => {
112
+ await cleanup?.();
113
+ });
114
+
115
+ beforeEach(async () => {
116
+ repoDir = await mkdtemp(path.join(os.tmpdir(), 'apify-affected-actors-'));
117
+ git('init', '-q', '-b', 'main');
118
+ await writeFile(path.join(repoDir, 'apify-test-tools.config.json'), JSON.stringify(CONFIG));
119
+ for (const { folder } of CONFIG.actors) {
120
+ await mkdir(path.join(repoDir, folder, '.actor'), { recursive: true });
121
+ await writeFile(
122
+ path.join(repoDir, folder, '.actor/actor.json'),
123
+ JSON.stringify({ actorSpecification: 1, name: `actor-${path.basename(folder)}`, version: '0.1' }),
124
+ );
125
+ }
126
+ await commit('chore: init', 'src/shared.ts', 'actors/a/main.ts', 'actors/b/main.ts', 'actors/c/main.ts');
127
+ git('checkout', '-q', '-b', 'feature');
128
+ });
129
+
130
+ afterEach(async () => {
131
+ if (repoDir) await rm(repoDir, { recursive: true, force: true });
132
+ });
133
+
134
+ it("tests only what changed since the last validated commit, on the branch's own commits", async () => {
135
+ const validated = await commit('feat(a): validated change', 'actors/a/main.ts');
136
+ await commit('feat(c): new change, docs for a', 'actors/c/main.ts', 'actors/a/README.md');
137
+
138
+ // A is still the validated code, only its README changed since
139
+ expect(getAffectedActors('--base', 'main', '--since', validated)).toStrictEqual({
140
+ functional: ['c'],
141
+ cosmetic: ['a'],
142
+ });
143
+ });
144
+
145
+ it('tests nothing after merging cosmetic changes from base', async () => {
146
+ const validated = await commit('feat(a): validated change', 'actors/a/main.ts');
147
+ await commitOnMain('docs: readme and changelog', 'actors/a/README.md', 'CHANGELOG.md');
148
+ git('merge', '-q', '--no-edit', 'main');
149
+
150
+ expect(getAffectedActors('--base', 'main', '--since', validated)).toStrictEqual({
151
+ functional: [],
152
+ cosmetic: ['a'],
153
+ });
154
+ });
155
+
156
+ it("tests the Actors where base's functional change meets the branch's after a merge", async () => {
157
+ const validated = await commit('feat(a): validated change', 'actors/a/main.ts');
158
+ await commitOnMain('feat: shared change', 'src/shared.ts');
159
+ git('merge', '-q', '--no-edit', 'main');
160
+
161
+ // Base changed A and C. A can interact with the branch's change, C only has base's, which base validated
162
+ expect(getAffectedActors('--base', 'main', '--since', validated)).toStrictEqual({
163
+ functional: ['a'],
164
+ cosmetic: [],
165
+ });
166
+ });
167
+
168
+ it('leaves out Actors that only base changed', async () => {
169
+ await commit('feat: shared change', 'src/shared.ts');
170
+ await commitOnMain('feat(b): base change', 'actors/b/main.ts');
171
+ git('merge', '-q', '--no-edit', 'main');
172
+
173
+ expect(getAffectedActors('--base', 'main')).toStrictEqual({ functional: ['a', 'c'], cosmetic: [] });
174
+ });
175
+
176
+ it('counts code changed in the merge commit itself', async () => {
177
+ await commit('docs(a): readme', 'actors/a/README.md');
178
+ await commitOnMain('feat(b): base change', 'actors/b/main.ts');
179
+ git('merge', '-q', '--no-commit', '--no-ff', 'main');
180
+ await writeFiles('changed while merging', ['actors/c/main.ts']);
181
+ git('add', '-A');
182
+ git('commit', '-q', '--no-edit');
183
+
184
+ expect(getAffectedActors('--base', 'main')).toStrictEqual({ functional: ['c'], cosmetic: ['a'] });
185
+ });
186
+
187
+ it('tests nothing after a rebase onto base changes to other Actors', async () => {
188
+ const validated = await commit('feat(a): validated change', 'actors/a/main.ts');
189
+ await commitOnMain('feat(b): base change', 'actors/b/main.ts');
190
+ git('rebase', '-q', 'main');
191
+
192
+ expect(getAffectedActors('--base', 'main', '--since', validated)).toStrictEqual({
193
+ functional: [],
194
+ cosmetic: [],
195
+ });
196
+ });
197
+
198
+ it('counts a moved file for both the Actor it left and the one it joined', async () => {
199
+ git('mv', 'actors/a/main.ts', 'actors/c/moved.ts');
200
+ git('commit', '-q', '-m', 'refactor: move a file from a to c');
201
+
202
+ // With rename detection, git would list only the new path, and A would look unchanged
203
+ expect(getAffectedActors('--base', 'main')).toStrictEqual({ functional: ['a', 'c'], cosmetic: [] });
204
+ });
205
+
206
+ it('checks the whole branch on a rerun or when --since is not in the history', async () => {
207
+ await commit('feat(a): validated change', 'actors/a/main.ts');
208
+ const head = await commit('docs(a): readme', 'actors/a/README.md');
209
+
210
+ const wholeBranch = { functional: ['a'], cosmetic: [] };
211
+ expect(getAffectedActors('--base', 'main', '--since', head)).toStrictEqual(wholeBranch);
212
+ expect(getAffectedActors('--base', 'main', '--since', 'e'.repeat(40))).toStrictEqual(wholeBranch);
213
+ });
214
+
215
+ it('lists Actors whose platform tests changed, and a test change alone is not functional', async () => {
216
+ await writeConfigWithTestsInContext('test');
217
+ await writeFiles('test/', ['.dockerignore']);
218
+ // A test and its helper, both under the default platformTestsGlob test/platform/**
219
+ await commit('test(a): change a test and its helper', 'test/platform/a.test.ts', 'test/platform/utils.ts');
220
+
221
+ const testOnly = { functional: [], cosmetic: [], test: ['a'] };
222
+ // A PR runs the tests
223
+ expect(getAllChangedActors('--base', 'main')).toStrictEqual(testOnly);
224
+ // A release builds only the functional and cosmetic lists, so it builds nothing
225
+ expect(getAllChangedActors('--since', git('rev-parse', 'main'))).toStrictEqual(testOnly);
226
+ });
227
+
228
+ it('lists an Actor in both lists when its source and its tests changed', async () => {
229
+ await writeConfigWithTestsInContext('test');
230
+ await commit('feat(a): change and test it', 'actors/a/main.ts', 'test/platform/a.test.ts');
231
+
232
+ expect(getAllChangedActors('--base', 'main')).toStrictEqual({ functional: ['a'], cosmetic: [], test: ['a'] });
233
+ });
234
+
235
+ it('lists only the tests changed since the last validated commit', async () => {
236
+ await writeConfigWithTestsInContext('test');
237
+ const validated = await commit('feat(a): validated change', 'actors/a/main.ts');
238
+ await commit('test(a): new test', 'test/platform/a.test.ts');
239
+
240
+ // The validated build of A has the branch's code, only the tests changed since
241
+ expect(getAllChangedActors('--base', 'main', '--since', validated)).toStrictEqual({
242
+ functional: [],
243
+ cosmetic: [],
244
+ test: ['a'],
245
+ });
246
+ });
247
+
248
+ it('reads which files are platform tests from platformTestsGlob in the config', async () => {
249
+ await writeConfigWithTestsInContext('e2e', { platformTestsGlob: 'e2e/platform/**' });
250
+ await commit('test(a): new test', 'e2e/platform/a.test.ts');
251
+ expect(getAllChangedActors('--base', 'main')).toStrictEqual({ functional: [], cosmetic: [], test: ['a'] });
252
+
253
+ // Outside the glob, a file in the context is a source change like any other
254
+ await commit('feat(a): other file', 'e2e/fixtures/data.json');
255
+ expect(getAllChangedActors('--base', 'main')).toStrictEqual({ functional: ['a'], cosmetic: [], test: ['a'] });
256
+ });
257
+
258
+ it('reports nothing for an empty range', async () => {
259
+ const nothing = {
260
+ commits: [],
261
+ changedFiles: [],
262
+ functionallyChangedActors: [],
263
+ cosmeticallyChangedActors: [],
264
+ testChangedActors: [],
265
+ };
266
+ // The branch has no commits of its own yet
267
+ expect(runGetAffectedActors('--base', 'main')).toMatchObject(nothing);
268
+ // A release with nothing new since the last one
269
+ expect(runGetAffectedActors('--since', git('rev-parse', 'HEAD'))).toMatchObject(nothing);
270
+ });
271
+ });