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
@@ -0,0 +1,221 @@
1
+ import type { MockInstance } from 'vitest';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { getAffectedActors } from '../../../bin/affected-actors.js';
5
+ import * as DiffChanges from '../../../bin/diff-changes.js';
6
+ import * as Git from '../../../bin/git.js';
7
+ import type { ActorConfig, Commit } from '../../../bin/types.js';
8
+ import * as LoadConfig from '../../../bin/utils/config/load-config.js';
9
+
10
+ // Everything that reads the configs is mocked, so only the identity and name of an Actor matter here
11
+ const actorA = { actorFullName: 'owner/a' } as ActorConfig;
12
+ const actorB = { actorFullName: 'owner/b' } as ActorConfig;
13
+
14
+ const headSha = 'f'.repeat(40);
15
+ const sinceSha = '5'.repeat(40);
16
+ const commit = (sha: string): Commit => ({ sha, author: 'Dev<dev@example.com>', date: 'Date', message: 'msg' });
17
+ const commits = [commit('1'.repeat(40)), commit('2'.repeat(40))];
18
+ const changedFiles = ['src/main.ts', 'CHANGELOG.md'];
19
+
20
+ const selection = { actors: [], ignore: [] };
21
+
22
+ describe('getAffectedActors', () => {
23
+ let changedActorsSpy: MockInstance;
24
+
25
+ beforeEach(() => {
26
+ vi.spyOn(LoadConfig, 'readConfigFile').mockResolvedValue([actorA, actorB]);
27
+ vi.spyOn(LoadConfig, 'readRepoSettings').mockResolvedValue({ platformTestsGlob: 'test/platform/**' });
28
+ changedActorsSpy = vi
29
+ .spyOn(DiffChanges, 'getChangedActors')
30
+ .mockResolvedValue({ functional: [actorA], cosmetic: [actorB], test: [] });
31
+ vi.spyOn(Git, 'resolveCommitSha').mockResolvedValue(headSha);
32
+ vi.spyOn(Git, 'getChangedFilesBetween').mockResolvedValue(changedFiles);
33
+ vi.spyOn(Git, 'getChangelogAdditions').mockResolvedValue('- Added foo');
34
+ });
35
+
36
+ it('requires --base or --since', async () => {
37
+ const options = { ...selection };
38
+ await expect(getAffectedActors(options)).rejects.toThrow('Pass --base, --since, or both');
39
+ // An empty --since (e.g. from an empty cache file) counts as not passed
40
+ await expect(getAffectedActors({ ...options, since: '' })).rejects.toThrow('Pass --base, --since, or both');
41
+ });
42
+
43
+ describe('with --base', () => {
44
+ const mergeBaseSha = 'b'.repeat(40);
45
+ let commitsBetweenSpy: MockInstance;
46
+
47
+ beforeEach(() => {
48
+ vi.spyOn(Git, 'getMergeBase').mockResolvedValue(mergeBaseSha);
49
+ commitsBetweenSpy = vi.spyOn(Git, 'getCommitsBetween').mockResolvedValue(commits);
50
+ });
51
+
52
+ it("inspects the branch's own changes, from where it forked from or last merged --base", async () => {
53
+ const findCommitSpy = vi.spyOn(Git, 'findCommitSha');
54
+
55
+ const result = await getAffectedActors({ base: 'origin/master', ...selection });
56
+
57
+ expect(result).toStrictEqual({
58
+ version: 1,
59
+ head: headSha,
60
+ commits,
61
+ changedFiles,
62
+ changelog: '- Added foo',
63
+ // Only the names, the config file stays the source of truth for the rest
64
+ functionallyChangedActors: [{ actorFullName: 'owner/a' }],
65
+ cosmeticallyChangedActors: [{ actorFullName: 'owner/b' }],
66
+ testChangedActors: [],
67
+ });
68
+ expect(Git.getMergeBase).toHaveBeenCalledWith('origin/master', 'HEAD');
69
+ expect(Git.getChangedFilesBetween).toHaveBeenCalledWith(mergeBaseSha, headSha);
70
+ expect(Git.getChangelogAdditions).toHaveBeenCalledWith(mergeBaseSha, headSha, changedFiles);
71
+ expect(commitsBetweenSpy).toHaveBeenCalledWith(mergeBaseSha, headSha);
72
+ expect(changedActorsSpy).toHaveBeenCalledWith({
73
+ filepathsChanged: changedFiles,
74
+ actorConfigs: [actorA, actorB],
75
+ range: { fromRef: mergeBaseSha, toRef: headSha },
76
+ platformTestsGlob: 'test/platform/**',
77
+ });
78
+ expect(findCommitSpy).not.toHaveBeenCalled();
79
+ });
80
+
81
+ describe('and --since', () => {
82
+ const sinceFiles = ['README.md'];
83
+
84
+ beforeEach(() => {
85
+ vi.spyOn(Git, 'findCommitSha').mockResolvedValue(sinceSha);
86
+ vi.spyOn(Git, 'getChangedFilesBetween').mockImplementation(async (fromRef) =>
87
+ fromRef === sinceSha ? sinceFiles : changedFiles,
88
+ );
89
+ });
90
+
91
+ const getNarrowed = async (
92
+ branch: DiffChanges.ChangedActors,
93
+ sinceValidated: DiffChanges.ChangedActors,
94
+ ) => {
95
+ changedActorsSpy.mockImplementation(async ({ range }) =>
96
+ range.fromRef === sinceSha ? sinceValidated : branch,
97
+ );
98
+ const result = await getAffectedActors({ base: 'origin/master', since: sinceSha, ...selection });
99
+ return {
100
+ functional: result.functionallyChangedActors.map(({ actorFullName }) => actorFullName),
101
+ cosmetic: result.cosmeticallyChangedActors.map(({ actorFullName }) => actorFullName),
102
+ test: result.testChangedActors.map(({ actorFullName }) => actorFullName),
103
+ };
104
+ };
105
+
106
+ it('classifies the changes since --since separately', async () => {
107
+ await getNarrowed(
108
+ { functional: [actorA], cosmetic: [], test: [] },
109
+ { functional: [actorA], cosmetic: [], test: [] },
110
+ );
111
+ expect(changedActorsSpy).toHaveBeenCalledWith({
112
+ filepathsChanged: sinceFiles,
113
+ actorConfigs: [actorA, actorB],
114
+ range: { fromRef: sinceSha, toRef: headSha },
115
+ platformTestsGlob: 'test/platform/**',
116
+ });
117
+ });
118
+
119
+ it('still describes the whole branch', async () => {
120
+ const result = await getAffectedActors({ base: 'origin/master', since: sinceSha, ...selection });
121
+ expect(result.commits).toStrictEqual(commits);
122
+ expect(result.changedFiles).toStrictEqual(changedFiles);
123
+ });
124
+
125
+ it('retests an Actor whose validated change meets a new functional change', async () => {
126
+ await expect(
127
+ getNarrowed(
128
+ { functional: [actorA], cosmetic: [], test: [] },
129
+ { functional: [actorA], cosmetic: [], test: [] },
130
+ ),
131
+ ).resolves.toStrictEqual({ functional: ['owner/a'], cosmetic: [], test: [] });
132
+ });
133
+
134
+ it('leaves out Actors that only the branch changed before --since or only --base changed', async () => {
135
+ await expect(
136
+ getNarrowed(
137
+ { functional: [actorA], cosmetic: [], test: [] },
138
+ { functional: [actorB], cosmetic: [], test: [] },
139
+ ),
140
+ ).resolves.toStrictEqual({ functional: [], cosmetic: [], test: [] });
141
+ });
142
+
143
+ it('takes the less impactful change when the two disagree', async () => {
144
+ // A validated functional change, then only cosmetic changes (e.g. a README merged in from base)
145
+ await expect(
146
+ getNarrowed(
147
+ { functional: [actorA], cosmetic: [], test: [] },
148
+ { functional: [], cosmetic: [actorA], test: [] },
149
+ ),
150
+ ).resolves.toStrictEqual({ functional: [], cosmetic: ['owner/a'], test: [] });
151
+ // A functional change merged in from base, while the branch only changes the README
152
+ await expect(
153
+ getNarrowed(
154
+ { functional: [], cosmetic: [actorA], test: [] },
155
+ { functional: [actorA], cosmetic: [], test: [] },
156
+ ),
157
+ ).resolves.toStrictEqual({ functional: [], cosmetic: ['owner/a'], test: [] });
158
+ });
159
+
160
+ it('runs the tests of an Actor whose tests the branch changed and changed again since --since', async () => {
161
+ await expect(
162
+ getNarrowed(
163
+ { functional: [actorA], cosmetic: [], test: [actorA, actorB] },
164
+ { functional: [], cosmetic: [], test: [actorA] },
165
+ ),
166
+ ).resolves.toStrictEqual({ functional: [], cosmetic: [], test: ['owner/a'] });
167
+ });
168
+
169
+ it('leaves out test changes that only --base or only the validated part of the branch has', async () => {
170
+ await expect(
171
+ getNarrowed(
172
+ { functional: [], cosmetic: [], test: [actorA] },
173
+ { functional: [], cosmetic: [], test: [actorB] },
174
+ ),
175
+ ).resolves.toStrictEqual({ functional: [], cosmetic: [], test: [] });
176
+ });
177
+
178
+ it.each([
179
+ ['is HEAD (a rerun)', headSha],
180
+ ['is not in the local history', undefined],
181
+ ])('checks the whole branch when --since %s', async (_, found) => {
182
+ vi.spyOn(Git, 'findCommitSha').mockResolvedValue(found);
183
+ const result = await getAffectedActors({ base: 'origin/master', since: sinceSha, ...selection });
184
+ expect(result.functionallyChangedActors).toStrictEqual([{ actorFullName: 'owner/a' }]);
185
+ // With no usable validated commit, the "since" diff falls back to the same range as the
186
+ // base diff, so getChangedActors runs twice for identical ranges instead of once.
187
+ expect(changedActorsSpy).toHaveBeenCalledTimes(2);
188
+ });
189
+ });
190
+ });
191
+
192
+ describe('with --since only', () => {
193
+ let resolveBaseSpy: MockInstance;
194
+ let commitsBetweenSpy: MockInstance;
195
+
196
+ beforeEach(() => {
197
+ resolveBaseSpy = vi.spyOn(Git, 'resolveReleaseBaseCommit').mockResolvedValue(sinceSha);
198
+ commitsBetweenSpy = vi.spyOn(Git, 'getCommitsBetween').mockResolvedValue(commits);
199
+ });
200
+
201
+ it('inspects everything after --since up to HEAD', async () => {
202
+ const result = await getAffectedActors({ since: 'HEAD~2', ...selection });
203
+
204
+ expect(result).toStrictEqual({
205
+ version: 1,
206
+ head: headSha,
207
+ commits,
208
+ changedFiles,
209
+ changelog: '- Added foo',
210
+ // Only the names, the config file stays the source of truth for the rest
211
+ functionallyChangedActors: [{ actorFullName: 'owner/a' }],
212
+ cosmeticallyChangedActors: [{ actorFullName: 'owner/b' }],
213
+ testChangedActors: [],
214
+ });
215
+ expect(resolveBaseSpy).toHaveBeenCalledWith('HEAD~2');
216
+ expect(commitsBetweenSpy).toHaveBeenCalledWith(sinceSha, headSha);
217
+ expect(Git.getChangedFilesBetween).toHaveBeenCalledWith(sinceSha, headSha);
218
+ expect(Git.getChangelogAdditions).toHaveBeenCalledWith(sinceSha, headSha, changedFiles);
219
+ });
220
+ });
221
+ });
@@ -0,0 +1,61 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import process from 'node:process';
3
+
4
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
5
+
6
+ import { compileCli } from './compiled-cli.js';
7
+
8
+ let cliPath: string;
9
+ let cleanup: (() => Promise<void>) | undefined;
10
+
11
+ const run = (command: string[], input: string, env: NodeJS.ProcessEnv = process.env) =>
12
+ spawnSync(cliPath, command, { encoding: 'utf8', input, stdio: ['pipe', 'pipe', 'pipe'], env });
13
+ const runBuild = (input: string, args: string[] = ['--version-number', '0.99']) => run(['build', ...args], input);
14
+
15
+ describe('build and notify CLI', () => {
16
+ beforeAll(async () => {
17
+ ({ cliPath, cleanup } = await compileCli());
18
+ });
19
+
20
+ afterAll(async () => {
21
+ await cleanup?.();
22
+ });
23
+
24
+ it('builds nothing for an empty list and prints an empty build list', () => {
25
+ const result = runBuild('{"actors":[]}');
26
+ expect(result.status, result.stderr).toBe(0);
27
+ expect(JSON.parse(result.stdout)).toStrictEqual([]);
28
+ });
29
+
30
+ it('fails on empty stdin instead of building nothing', () => {
31
+ const result = runBuild('');
32
+ expect(result.status).toBe(1);
33
+ expect(result.stderr).toContain('Expected the Actors to build as JSON on stdin, got nothing');
34
+ expect(result.stdout).toBe('');
35
+ });
36
+
37
+ it('rejects a malformed --version-number', () => {
38
+ const result = runBuild('{"actors":[]}', ['--version-number', 'latest']);
39
+ expect(result.status).toBe(1);
40
+ expect(result.stderr).toContain('--version-number must look like 0.1 or be "default"');
41
+ });
42
+
43
+ it('requires --version-number', () => {
44
+ const result = runBuild('{"actors":[]}', []);
45
+ expect(result.status).toBe(1);
46
+ expect(result.stderr).toContain('Missing required argument: version-number');
47
+ });
48
+
49
+ it('notify takes one channel per call', () => {
50
+ const result = run(['notify', '--slack-channel', '#a', '--slack-channel', '#b'], '{}');
51
+ expect(result.status).toBe(1);
52
+ expect(result.stderr).toContain('--slack-channel takes one channel, call notify once for each channel');
53
+ });
54
+
55
+ it('notify announces no release when there are no new commits', () => {
56
+ const document = { commits: [], changedFiles: [], changelog: null, functionallyChangedActors: [] };
57
+ const result = run(['notify', '--slack-channel', '#notif'], JSON.stringify(document));
58
+ expect(result.status, result.stderr).toBe(0);
59
+ expect(result.stderr).toContain('No new commits, so there is no release to announce');
60
+ });
61
+ });
@@ -289,5 +289,21 @@ describe('build-from-local helpers', () => {
289
289
  // Only the read-only lookup ran: builders for uploads and builds are created after the dry-run cutoff
290
290
  expect(fromActorConfig).toHaveBeenCalledTimes(1);
291
291
  });
292
+
293
+ it('fails before zipping anything when asked to require env vars in sync and they are not', async () => {
294
+ vi.spyOn(ApifyBuilder, 'fromActorConfig').mockReturnValue({
295
+ getActorInfo: async () => ({
296
+ defaultRunOptions: { build: 'latest' },
297
+ taggedBuilds: { latest: { buildNumber: '0.1.5' } },
298
+ versions: [{ versionNumber: '0.1', sourceType: 'GIT_REPO', envVars: [{ name: 'API_KEY' }] }],
299
+ }),
300
+ } as unknown as ApifyBuilder);
301
+ // Only the name matters, the check runs before the sources are read
302
+ const actorConfig = { actorFullName: 'owner/actor' } as unknown as ActorConfig;
303
+
304
+ await expect(
305
+ runBuildsFromLocal({ actorConfigs: [actorConfig], dryRun: true, requireEnvVarsInSync: true }),
306
+ ).rejects.toThrow('Env vars of version 0.98 are out of sync with default version 0.1 (missing API_KEY)');
307
+ });
292
308
  });
293
309
  });
@@ -7,11 +7,16 @@ import { ActorSourceType, ApifyClient } from 'apify-client';
7
7
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
8
8
 
9
9
  import {
10
+ ApifyBuilder,
11
+ assertEnvVarsMatchDefaultVersion,
10
12
  assertRepoUrlMatchesDefaultVersion,
13
+ buildActors,
11
14
  deleteOldBuilds,
12
15
  resolveDefaultVersion,
13
16
  runBuilds,
14
17
  } from '../../../bin/build.js';
18
+ import * as Git from '../../../bin/git.js';
19
+ import { logger } from '../../../bin/logger.js';
15
20
  import type { ActorConfig } from '../../../bin/types.js';
16
21
  import { ExistingDir, ExistingFile, RelativeDir } from '../../../bin/utils/path/repo-relative.js';
17
22
 
@@ -84,6 +89,119 @@ describe('assertRepoUrlMatchesDefaultVersion', () => {
84
89
  });
85
90
  });
86
91
 
92
+ describe('runBuilds version number', () => {
93
+ // Only the name and folder of the Actor matter here, the builder is stubbed
94
+ const actorConfig = { actorFullName: ACTOR, folder: { isRoot: () => true } } as unknown as ActorConfig;
95
+ // Restored after each test, so the stub never reaches the tests that run the real builder
96
+ afterEach(() => {
97
+ vi.mocked(ApifyBuilder.fromActorConfig).mockRestore();
98
+ });
99
+
100
+ const dryRunBuild = async (versionNumber: string, actorInfo = makeActorInfo()) => {
101
+ vi.spyOn(ApifyBuilder, 'fromActorConfig').mockReturnValue({
102
+ getActorInfo: async () => actorInfo,
103
+ } as unknown as ApifyBuilder);
104
+ const result = await runBuilds({
105
+ actorConfigs: [actorConfig],
106
+ versionNumber,
107
+ repoUrl: REPO_URL,
108
+ shouldVerifyRepoUrl: false,
109
+ branch: 'master',
110
+ dryRun: true,
111
+ });
112
+ if (Array.isArray(result)) throw new Error('A dry run must return a plan, not builds');
113
+ return result.builds[0].versionNumber;
114
+ };
115
+
116
+ it('builds the version it is given', async () => {
117
+ await expect(dryRunBuild('0.99')).resolves.toBe('0.99');
118
+ });
119
+
120
+ it("resolves default to the version the Actor's default build tag points to", async () => {
121
+ await expect(dryRunBuild('default')).resolves.toBe('0.1');
122
+ });
123
+
124
+ it('warns when the default version does not give its builds the default build tag', async () => {
125
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => undefined);
126
+ await dryRunBuild('default');
127
+ // The fixture's version 0.1 has no build tag of its own
128
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('users keep running the previous build'));
129
+ });
130
+
131
+ it('does not warn when the default version gives its builds the default build tag', async () => {
132
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => undefined);
133
+ const versions = [{ versionNumber: '0.1', buildTag: 'latest', sourceType: ActorSourceType.GitRepo }];
134
+ await dryRunBuild('default', makeActorInfo({ versions } as Partial<ApifyClientTypes.Actor>));
135
+ expect(warnSpy).not.toHaveBeenCalled();
136
+ });
137
+ });
138
+
139
+ describe('buildActors', () => {
140
+ it('builds nothing and needs no branch or repository when there are no Actors', async () => {
141
+ const branchSpy = vi.spyOn(Git, 'getCurrentBranch');
142
+ const repoSpy = vi.spyOn(Git, 'resolveRepoUrl');
143
+
144
+ await expect(buildActors({ actorConfigs: [], versionNumber: '0.99', dryRun: false })).resolves.toStrictEqual(
145
+ [],
146
+ );
147
+ // A dry run keeps the dry-run shape even with nothing to build
148
+ await expect(buildActors({ actorConfigs: [], versionNumber: '0.99', dryRun: true })).resolves.toStrictEqual({
149
+ dryRun: true,
150
+ builds: [],
151
+ });
152
+ expect(branchSpy).not.toHaveBeenCalled();
153
+ expect(repoSpy).not.toHaveBeenCalled();
154
+ });
155
+ });
156
+
157
+ describe('assertEnvVarsMatchDefaultVersion', () => {
158
+ const makeVersion = (versionNumber: string, names: string[]): ApifyClientTypes.ActorVersion => ({
159
+ versionNumber,
160
+ sourceType: ActorSourceType.GitRepo,
161
+ gitRepoUrl: REPO_URL,
162
+ envVars: names.map((name) => ({ name, isSecret: true })),
163
+ });
164
+ const makeActorInfoWithEnvVars = (defaultNames: string[], testNames?: string[]) =>
165
+ makeActorInfo({
166
+ versions: [makeVersion('0.1', defaultNames), ...(testNames ? [makeVersion('0.99', testNames)] : [])],
167
+ });
168
+ const check = (actorInfo: ApifyClientTypes.Actor, versionNumber = '0.99') =>
169
+ assertEnvVarsMatchDefaultVersion(
170
+ ACTOR,
171
+ actorInfo,
172
+ resolveDefaultVersion(ACTOR, actorInfo).defaultVersion,
173
+ versionNumber,
174
+ );
175
+
176
+ it('should pass when the names match in any order, whatever the values', () => {
177
+ expect(() => check(makeActorInfoWithEnvVars(['A', 'B'], ['B', 'A']))).not.toThrow();
178
+ });
179
+
180
+ it('should pass for the default version itself', () => {
181
+ expect(() => check(makeActorInfoWithEnvVars(['A']), '0.1')).not.toThrow();
182
+ });
183
+
184
+ it('should pass when neither version has env vars', () => {
185
+ expect(() => check(makeActorInfo())).not.toThrow();
186
+ });
187
+
188
+ it('should list the missing and extra names', () => {
189
+ expect(() => check(makeActorInfoWithEnvVars(['A', 'B'], ['B', 'C']))).toThrow(
190
+ 'Env vars of version 0.99 are out of sync with default version 0.1 (missing A; extra C)',
191
+ );
192
+ });
193
+
194
+ it('should ask to create the version when it does not exist yet', () => {
195
+ expect(() => check(makeActorInfoWithEnvVars(['A']))).toThrow("Version 0.99 doesn't exist yet");
196
+ });
197
+
198
+ it('should throw when the default version no longer exists', () => {
199
+ expect(() => assertEnvVarsMatchDefaultVersion(ACTOR, makeActorInfo(), undefined, '0.99')).toThrow(
200
+ 'no longer exists',
201
+ );
202
+ });
203
+ });
204
+
87
205
  describe('dry run', () => {
88
206
  const TOKEN_ENV_VAR = 'APIFY_TOKEN_OWNER';
89
207
  const originalCwd = process.cwd();
@@ -118,11 +236,14 @@ describe('dry run', () => {
118
236
  const createVersion = vi.fn();
119
237
  const buildClient = vi.fn(() => ({ delete: deleteBuild }));
120
238
 
239
+ let actorInfo: ApifyClientTypes.Actor;
240
+
121
241
  beforeEach(() => {
122
242
  vi.stubEnv(TOKEN_ENV_VAR, 'token');
243
+ actorInfo = makeActorInfo();
123
244
  const fakeClient = {
124
245
  actor: () => ({
125
- get: async () => makeActorInfo(),
246
+ get: async () => actorInfo,
126
247
  // Old enough to be deleted, and not the default build 0.1.5
127
248
  builds: () => ({
128
249
  list: async () => ({ items: [{ id: 'old-build', buildNumber: '0.0.1', startedAt: new Date(0) }] }),
@@ -143,14 +264,15 @@ describe('dry run', () => {
143
264
  vi.clearAllMocks();
144
265
  });
145
266
 
146
- const runDryBuilds = async () =>
267
+ const runDryBuilds = async (requireEnvVarsInSync = false) =>
147
268
  runBuilds({
148
269
  actorConfigs: [actorConfig],
149
270
  repoUrl: REPO_URL,
150
271
  shouldVerifyRepoUrl: true,
151
272
  branch: 'feature',
152
273
  dryRun: true,
153
- useDockerCache: false,
274
+ versionNumber: '0.99',
275
+ requireEnvVarsInSync,
154
276
  });
155
277
 
156
278
  it('should return a build plan without creating versions or builds', async () => {
@@ -161,7 +283,6 @@ describe('dry run', () => {
161
283
  actorFullName: ACTOR,
162
284
  versionNumber: '0.99',
163
285
  gitRepoUrl: `${REPO_URL}#feature:actors/a`,
164
- useDockerCache: false,
165
286
  },
166
287
  ],
167
288
  });
@@ -182,11 +303,34 @@ describe('dry run', () => {
182
303
  shouldVerifyRepoUrl: true,
183
304
  branch: 'feature',
184
305
  dryRun: true,
185
- useDockerCache: false,
306
+ versionNumber: '0.99',
186
307
  }),
187
308
  ).rejects.toThrow('Repository mismatch');
188
309
  });
189
310
 
311
+ describe('with the default version holding an env var the test version lacks', () => {
312
+ beforeEach(() => {
313
+ actorInfo = makeActorInfo({
314
+ versions: [
315
+ {
316
+ versionNumber: '0.1',
317
+ sourceType: ActorSourceType.GitRepo,
318
+ gitRepoUrl: REPO_URL,
319
+ envVars: [{ name: 'API_KEY', isSecret: true }],
320
+ },
321
+ ],
322
+ });
323
+ });
324
+
325
+ it('should still check the env vars when asked to', async () => {
326
+ await expect(runDryBuilds(true)).rejects.toThrow('missing API_KEY');
327
+ });
328
+
329
+ it('should not check the env vars unless asked to', async () => {
330
+ await expect(runDryBuilds()).resolves.toMatchObject({ dryRun: true });
331
+ });
332
+ });
333
+
190
334
  it('should not delete old builds', async () => {
191
335
  await deleteOldBuilds([actorConfig], { dryRun: true });
192
336
  expect(deleteBuild).not.toHaveBeenCalled();
@@ -0,0 +1,23 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { chmod, mkdtemp, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const projectRoot = fileURLToPath(new URL('../../../', import.meta.url));
8
+ const compilerPath = fileURLToPath(new URL('../../../node_modules/typescript/bin/tsc', import.meta.url));
9
+
10
+ /** Compiles the CLI from the sources into a temporary directory, so tests run it the way npm users do. */
11
+ export const compileCli = async () => {
12
+ const buildDir = await mkdtemp(path.join(projectRoot, '.cli-test-'));
13
+ const cleanup = async () => rm(buildDir, { recursive: true, force: true });
14
+ try {
15
+ execFileSync(process.execPath, [compilerPath, '--outDir', buildDir], { cwd: projectRoot });
16
+ const cliPath = path.join(buildDir, 'bin/main.js');
17
+ await chmod(cliPath, 0o755);
18
+ return { cliPath, cleanup };
19
+ } catch (error) {
20
+ await cleanup();
21
+ throw error;
22
+ }
23
+ };