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,41 @@
1
+ import { ApifyClient } from 'apify-client';
2
+
3
+ import { logger } from './logger.js';
4
+ import type { TestTarget } from './stdin-input.js';
5
+
6
+ type ActorGetter = Pick<ApifyClient, 'actor'>;
7
+
8
+ /**
9
+ * Adds the ID each target is missing: the raw ID to those listed by full name, e.g. by `get-actor-configs`, and the
10
+ * full name to those listed by raw ID. The lib matches a test to its Actor by the ID the test names, so a target
11
+ * without either ID would skip the tests that name its Actor by the other one.
12
+ *
13
+ * An Actor the token can't see keeps only the ID it was listed with, with a warning, rather than failing the run:
14
+ * `get-actor-configs` lists every Actor in the repository, also ones no platform test uses, such as Actors the tester
15
+ * account was never given access to. A test that names it by the listed ID still runs and fails on its own.
16
+ */
17
+ export const withActorIds = async (
18
+ targets: TestTarget[],
19
+ client: ActorGetter = new ApifyClient({ token: process.env.TESTER_APIFY_TOKEN }),
20
+ ): Promise<TestTarget[]> =>
21
+ Promise.all(
22
+ targets.map(async (target) => {
23
+ const { actorFullName, actorRawId } = target;
24
+ if (actorFullName && actorRawId) return target;
25
+ // The parser lets no target through without either ID
26
+ const actorId = (actorFullName ?? actorRawId)!;
27
+ const actor = await client.actor(actorId).get();
28
+ if (!actor) {
29
+ logger.warn(
30
+ `Actor ${actorId} not found, or TESTER_APIFY_TOKEN can't access it. Tests that name it by ` +
31
+ `${actorFullName ? 'its raw ID' : 'its full name'} will be skipped.`,
32
+ );
33
+ return target;
34
+ }
35
+ return {
36
+ ...target,
37
+ actorFullName: actorFullName ?? `${actor.username}/${actor.name}`,
38
+ actorRawId: actorRawId ?? actor.id,
39
+ };
40
+ }),
41
+ );
@@ -0,0 +1,211 @@
1
+ import { type ChangedActors, getChangedActors } from './diff-changes.js';
2
+ import {
3
+ findCommitSha,
4
+ getChangedFilesBetween,
5
+ getChangelogAdditions,
6
+ getCommitsBetween,
7
+ getMergeBase,
8
+ resolveCommitSha,
9
+ resolveReleaseBaseCommit,
10
+ } from './git.js';
11
+ import { logger } from './logger.js';
12
+ import type { ActorConfig, Commit, GitRange } from './types.js';
13
+ import { readConfigFile, readRepoSettings } from './utils/config/load-config.js';
14
+
15
+ // The range always ends at the checked-out commit: check out what you want to inspect
16
+ const HEAD = 'HEAD';
17
+
18
+ interface AffectedActorsOptions {
19
+ /**
20
+ * Base usually points to the master branch
21
+ * Only what HEAD changes compared to base counts, like `git diff base...HEAD` or GitHub's "Files changed" tab:
22
+ * the branch's own changes, not what it merged in from base.
23
+ */
24
+ base?: string;
25
+ /**
26
+ * With `base` it's the last validated commit of the branch: Actors that didn't change after it are left out.
27
+ * It's ignored when it is HEAD (a rerun) or isn't in the local history. Without `base` everything after it
28
+ * counts, and it must be an ancestor of HEAD (e.g. the last release).
29
+ */
30
+ since?: string;
31
+ actors: string[];
32
+ ignore: string[];
33
+ }
34
+
35
+ /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
36
+ interface AffectedActors {
37
+ version: 1;
38
+ head: string;
39
+ /** The commits, changed files and changelog describe the whole range, `--since` with `--base` only narrows the Actors. */
40
+ commits: Commit[];
41
+ changedFiles: string[];
42
+ changelog: string | null;
43
+ /**
44
+ * Actors with at least one functional change. They need a new build and tests. Only the names: the config file
45
+ * stays the source of truth for the rest.
46
+ */
47
+ functionallyChangedActors: { actorFullName: string }[];
48
+ /**
49
+ * Actors with cosmetic changes (README, CHANGELOG, cosmetic .actor JSON) and no functional one. They only need a
50
+ * release build.
51
+ */
52
+ cosmeticallyChangedActors: { actorFullName: string }[];
53
+ /**
54
+ * Actors whose platform tests (`platformTestsGlob` in the config) changed in their context, whatever else
55
+ * changed. Their tests need to run, but a test change alone needs no release. An Actor can also be in one of the
56
+ * lists above.
57
+ */
58
+ testChangedActors: { actorFullName: string }[];
59
+ }
60
+
61
+ const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
62
+
63
+ /** The branch's last validated commit, unless it can't narrow anything down. */
64
+ const findValidatedSha = async (since: string, headSha: string): Promise<string | undefined> => {
65
+ const sinceSha = await findCommitSha(since);
66
+ if (!sinceSha) {
67
+ logger.info(
68
+ `Last validated commit ${since} is not in the local history (force-push?), checking the whole branch`,
69
+ );
70
+ return undefined;
71
+ }
72
+ if (sinceSha === headSha) {
73
+ logger.info('Last validated commit is HEAD, so this is a rerun: checking the whole branch from scratch');
74
+ return undefined;
75
+ }
76
+ return sinceSha;
77
+ };
78
+
79
+ /**
80
+ * This is Juan's magical algorithm where we need just 2 git diffs to determine functional changes on Actors.
81
+ *
82
+ * Naively, we could just check branch HEAD changes vs base or parent of first commit and be done but
83
+ * we do this jiu-jitsu code to apply 2 optimizations:
84
+ * 1. On our PR branch, we track last validated commit (since) that was tested and so we only need to test new commits.
85
+ * 2. We also merge changes from the base branch (usually master) and we can consider those already validated as well.
86
+ *
87
+ * But there is annoying problem: even if both our branch and base are validated, they could have novel interactions and
88
+ * we need to retest those.
89
+ *
90
+ * So we look at:
91
+ * 1. The changes introduced by our branch since the last validated commit (`since..HEAD`).
92
+ * 2. The changes introduced by the branch compared to the base branch (`base...HEAD`). The important part is that this
93
+ * diff does check the whole branch, discarding the last validated commit (since) so it checks those novel interactions problem.
94
+ *
95
+ * The magic is that both need to contain changes for an Actor to be retested:
96
+ * 1. If we have changes only in since..HEAD (and not in base...HEAD), it means those changes came from merging the base alone
97
+ * which is already validated and there were actually no real changes introduced by our branch alone.
98
+ * 2. If we have changes only in base...HEAD (and not in since..HEAD), it means there are no new changes on our branch
99
+ * since it was last validated. base...HEAD has changes that we already validated.
100
+ *
101
+ * This enables to simply do those 2 diffs for each Actor's context separately and only Actors that have changes
102
+ * in both diffs need to be retested.
103
+ *
104
+ * THIS ASSUMES BRANCH IS UP TO DATE WITH BASE WHEN IT MERGES, which we currently require. Only then is HEAD what lands on
105
+ * base, and base's commits validated. Otherwise what lands is HEAD combined with base's newer commits, which nobody
106
+ * tested, however the Actors are picked.
107
+ *
108
+ * In each example the branch's functional change to Actor A was validated at `since`, then one more push follows:
109
+ *
110
+ * Example scenarios:
111
+ * - Our own branch only: a commit changes A's README and B's code.
112
+ * `since..HEAD`: A cosmetic, B functional. `base...HEAD`: A and B functional.
113
+ * → A cosmetic, B functional: only B is tested, A's code is still the validated one.
114
+ * - Cosmetic merge: the branch merges base, which only changed READMEs and the CHANGELOG.
115
+ * `since..HEAD`: every Actor cosmetic (CHANGELOG counts for all). `base...HEAD`: A functional, since the merged
116
+ * files now match base and drop out of it.
117
+ * → A cosmetic, the other Actors left out: nothing is tested.
118
+ * - Functional merge: the branch merges base, which changed code of A and of C.
119
+ * `since..HEAD`: A and C functional. `base...HEAD`: A functional.
120
+ * → A functional, C left out: A is tested, since base's change can interact with the branch's, while C only has
121
+ * base's change, which base already validated.
122
+ */
123
+ const intersectChangedInBranchVsSince = (branch: ChangedActors, sinceValidated: ChangedActors): ChangedActors => {
124
+ const narrowed: ChangedActors = {
125
+ functional: [],
126
+ cosmetic: [],
127
+ // The same rule for tests: the branch changed them, and they changed again since they were last validated
128
+ test: branch.test.filter((actor) => sinceValidated.test.includes(actor)),
129
+ };
130
+ // All checks are done for each Actor separately, that's cleanest and most accurate
131
+ for (const actor of [...branch.functional, ...branch.cosmetic]) {
132
+ // base...HEAD
133
+ const changedFunctionallyOnBranch = branch.functional.includes(actor);
134
+ // since..HEAD
135
+ const changedFunctionallySince = sinceValidated.functional.includes(actor);
136
+
137
+ // changedSince is used for release. Release cares also about cosmetic.
138
+ const changedSince = changedFunctionallySince || sinceValidated.cosmetic.includes(actor);
139
+
140
+ // Nothing changed since the last validation, so the branch's change to this Actor is already validated
141
+ if (!changedSince) continue;
142
+
143
+ // As we said, only intersection counts as functional, otherwise it's cosmetic
144
+ if (changedFunctionallyOnBranch && changedFunctionallySince) {
145
+ narrowed.functional.push(actor);
146
+ } else {
147
+ narrowed.cosmetic.push(actor);
148
+ }
149
+ }
150
+ return narrowed;
151
+ };
152
+
153
+ /**
154
+ * Finds the Actors whose context changed in the given range. `base` selects the branch's own changes
155
+ * (a PR), `since` the changes after a known commit (a release, or the last validated commit of a PR).
156
+ */
157
+ export const getAffectedActors = async ({
158
+ base,
159
+ since: sinceOption,
160
+ actors,
161
+ ignore,
162
+ }: AffectedActorsOptions): Promise<AffectedActors> => {
163
+ // An empty --since (e.g. an empty cache file) means the same as not passing it
164
+ const since = sinceOption || undefined;
165
+ if (!base && !since) {
166
+ throw new Error('Pass --base, --since, or both to select the commits to inspect.');
167
+ }
168
+
169
+ const actorConfigs = await readConfigFile({ actors, ignore });
170
+ const { platformTestsGlob } = await readRepoSettings();
171
+ const getChangedInRange = async (range: GitRange) => {
172
+ const changedFiles = await getChangedFilesBetween(range.fromRef, range.toRef);
173
+ return {
174
+ changedFiles,
175
+ changedActors: await getChangedActors({
176
+ filepathsChanged: changedFiles,
177
+ actorConfigs,
178
+ range,
179
+ platformTestsGlob,
180
+ }),
181
+ };
182
+ };
183
+
184
+ const headSha = await resolveCommitSha(HEAD);
185
+ const rangeVsBase: GitRange = {
186
+ // merge base = PR, release base = push to master (release)
187
+ fromRef: base ? await getMergeBase(base, HEAD) : await resolveReleaseBaseCommit(since!),
188
+ toRef: headSha,
189
+ };
190
+
191
+ // Falls back to rangeVsBase.fromRef when there's no (usable) last validated commit — same as
192
+ // intersecting the branch's changes with themselves, which is a no-op (see this function's docs).
193
+ const validatedSha = base && since ? await findValidatedSha(since, headSha) : undefined;
194
+ const sinceFromRef = validatedSha ?? rangeVsBase.fromRef;
195
+
196
+ const { changedFiles, changedActors: changedVsBase } = await getChangedInRange(rangeVsBase);
197
+ const { changedActors: changedSinceActors } = await getChangedInRange({ fromRef: sinceFromRef, toRef: headSha });
198
+ // See this function's docs to understand the algorithm: only intersection counts as functional, otherwise it's cosmetic.
199
+ const changedActors = intersectChangedInBranchVsSince(changedVsBase, changedSinceActors);
200
+
201
+ return {
202
+ version: 1,
203
+ head: headSha,
204
+ commits: await getCommitsBetween(rangeVsBase.fromRef, rangeVsBase.toRef),
205
+ changedFiles,
206
+ changelog: await getChangelogAdditions(rangeVsBase.fromRef, rangeVsBase.toRef, changedFiles),
207
+ functionallyChangedActors: names(changedActors.functional),
208
+ cosmeticallyChangedActors: names(changedActors.cosmetic),
209
+ testChangedActors: names(changedActors.test),
210
+ };
211
+ };
@@ -6,7 +6,13 @@ import AdmZip from 'adm-zip';
6
6
  import type * as ApifyClientTypes from 'apify-client';
7
7
  import { ActorSourceType } from 'apify-client';
8
8
 
9
- import { ApifyBuilder, LOCAL_SOURCE_VERSION_NUMBER, runAndSummarizeBuilds } from './build.js';
9
+ import {
10
+ ApifyBuilder,
11
+ assertEnvVarsMatchDefaultVersion,
12
+ LOCAL_SOURCE_VERSION_NUMBER,
13
+ resolveDefaultVersion,
14
+ runAndSummarizeBuilds,
15
+ } from './build.js';
10
16
  import { loadDockerIgnore } from './dockerignore.js';
11
17
  import { logger } from './logger.js';
12
18
  import { isPathWithinScope } from './path-utils.js';
@@ -190,15 +196,27 @@ type LocalBuildPlan = {
190
196
  export const runBuildsFromLocal = async ({
191
197
  actorConfigs,
192
198
  dryRun,
199
+ requireEnvVarsInSync = false,
193
200
  }: {
194
201
  actorConfigs: ActorConfig[];
195
202
  dryRun: boolean;
203
+ // See assertEnvVarsMatchDefaultVersion
204
+ requireEnvVarsInSync?: boolean;
196
205
  }): Promise<BuildData[] | DryRunPlan<LocalBuildPlan>> => {
197
206
  // Read-only preparation shared with the real build, so a dry run fails on a missing token, an unknown
198
207
  // Actor or unreadable sources just like the build would
199
208
  const preparedBuilds = await Promise.all(
200
209
  actorConfigs.map(async (actorConfig) => {
201
210
  const actorInfo = await ApifyBuilder.fromActorConfig(actorConfig).getActorInfo();
211
+ if (requireEnvVarsInSync) {
212
+ const { defaultVersion } = resolveDefaultVersion(actorConfig.actorFullName, actorInfo);
213
+ assertEnvVarsMatchDefaultVersion(
214
+ actorConfig.actorFullName,
215
+ actorInfo,
216
+ defaultVersion,
217
+ LOCAL_SOURCE_VERSION_NUMBER,
218
+ );
219
+ }
202
220
  // Zipped and uploaded like `apify push` does, since inline SOURCE_FILES are capped at ~9 MB.
203
221
  const zip = new AdmZip();
204
222
  const sourceFiles = await collectSourceFiles(actorConfig);
package/bin/build.ts CHANGED
@@ -1,22 +1,21 @@
1
1
  import type * as ApifyClientTypes from 'apify-client';
2
2
  import { ActorSourceType, ApifyClient } from 'apify-client';
3
3
 
4
- import { normalizeRepoUrl } from './git.js';
4
+ import { getCurrentBranch, normalizeRepoUrl, resolveRepoUrl } from './git.js';
5
5
  import { logger } from './logger.js';
6
6
  import type { ActorConfig, BuildData, DryRunPlan } from './types.js';
7
7
 
8
8
  type BuildPrActorOptions = {
9
- buildTag?: string;
10
9
  versionNumber: string;
11
10
  gitRepoUrl: string;
12
11
  actorConfig: ActorConfig;
13
12
  actorInfo: ApifyClientTypes.Actor;
14
- useDockerCache: boolean;
15
13
  };
16
14
 
17
15
  // Fixed version number used to build from local source files, since there is no real version to track.
18
16
  export const LOCAL_SOURCE_VERSION_NUMBER = '0.98';
19
- const DEFAULT_TEST_VERSION_NUMBER = '0.99';
17
+ // Stands for each Actor's default version: the one its default build tag points to
18
+ export const DEFAULT_VERSION_KEYWORD = 'default';
20
19
 
21
20
  // Shown whenever an Actor isn't set up the way CI builds need it
22
21
  const ACTOR_SETUP_REQUIREMENT =
@@ -83,6 +82,44 @@ export const assertRepoUrlMatchesDefaultVersion = (
83
82
  }
84
83
  };
85
84
 
85
+ const getEnvVarNames = (version: ApifyClientTypes.ActorVersion | undefined): string[] =>
86
+ (version?.envVars ?? []).map(({ name }) => name).filter((name): name is string => !!name);
87
+
88
+ /**
89
+ * Guards against testing an Actor with different env vars than users run it with, e.g. a variable added to
90
+ * the default version but not to the test version, which would make the tests pass or fail for the wrong
91
+ * reason. Only names are compared because the API doesn't return secret values.
92
+ */
93
+ export const assertEnvVarsMatchDefaultVersion = (
94
+ actorFullName: string,
95
+ actorInfo: ApifyClientTypes.Actor,
96
+ defaultVersion: ApifyClientTypes.ActorVersion | undefined,
97
+ versionNumber: string,
98
+ ) => {
99
+ if (!defaultVersion) {
100
+ throw new Error(
101
+ `[${actorFullName}] The version of the default build no longer exists. ${ACTOR_SETUP_REQUIREMENT}`,
102
+ );
103
+ }
104
+ const version = actorInfo.versions.find((v) => v.versionNumber === versionNumber);
105
+ const defaultNames = getEnvVarNames(defaultVersion);
106
+ const names = getEnvVarNames(version);
107
+ const missing = defaultNames.filter((name) => !names.includes(name));
108
+ const extra = names.filter((name) => !defaultNames.includes(name));
109
+ if (missing.length === 0 && extra.length === 0) return;
110
+
111
+ const differences = [
112
+ missing.length > 0 ? `missing ${missing.join(', ')}` : undefined,
113
+ extra.length > 0 ? `extra ${extra.join(', ')}` : undefined,
114
+ ].filter(Boolean);
115
+ throw new Error(
116
+ `[${actorFullName}] Env vars of version ${versionNumber} are out of sync with default version ` +
117
+ `${defaultVersion.versionNumber} (${differences.join('; ')}). ` +
118
+ `${version ? '' : `Version ${versionNumber} doesn't exist yet, so create it on the platform first. `}` +
119
+ `Add or remove the env vars in version ${versionNumber} so their names match the default version's.`,
120
+ );
121
+ };
122
+
86
123
  export class ApifyBuilder {
87
124
  private constructor(
88
125
  private readonly apifyClient: ApifyClient,
@@ -206,7 +243,7 @@ export class ApifyBuilder {
206
243
 
207
244
  const actorInfo = await this.getActorInfo();
208
245
 
209
- // 'devel' used to be hardcoded for testing version 0.99, once we get rid of this tag everywhere, we can remove this code
246
+ // A build tagged 'devel' is kept for DEFAULT_DAYS_BACK_DEVEL only. Remove this once no Actor has the tag
210
247
  const taggedDevelBuildNumber: string | undefined = actorInfo.taggedBuilds!.devel?.buildNumber;
211
248
 
212
249
  const allTags = Object.keys(actorInfo.taggedBuilds ?? {});
@@ -319,20 +356,20 @@ export const runAndSummarizeBuilds = async (
319
356
  type GitBuildPlan = {
320
357
  actorFullName: string;
321
358
  versionNumber: string;
322
- buildTag?: string;
323
359
  gitRepoUrl: string;
324
- useDockerCache: boolean;
325
360
  };
326
361
 
327
362
  type RunBuildsOptions = {
328
363
  actorConfigs: ActorConfig[];
329
- isLatest?: boolean;
364
+ // A version number like "0.99", or DEFAULT_VERSION_KEYWORD for each Actor's default version
365
+ versionNumber: string;
330
366
  repoUrl: string;
331
367
  // False when the repo URL was passed explicitly, see assertRepoUrlMatchesDefaultVersion
332
368
  shouldVerifyRepoUrl: boolean;
333
369
  branch: string;
334
370
  dryRun: boolean;
335
- useDockerCache: boolean;
371
+ // See assertEnvVarsMatchDefaultVersion
372
+ requireEnvVarsInSync?: boolean;
336
373
  };
337
374
 
338
375
  export const runBuilds = async ({
@@ -340,9 +377,9 @@ export const runBuilds = async ({
340
377
  shouldVerifyRepoUrl,
341
378
  branch,
342
379
  actorConfigs,
343
- isLatest = false,
380
+ versionNumber,
344
381
  dryRun,
345
- useDockerCache,
382
+ requireEnvVarsInSync = false,
346
383
  }: RunBuildsOptions): Promise<BuildData[] | DryRunPlan<GitBuildPlan>> => {
347
384
  // Fetched once per Actor and reused for the checks, the version numbers and the build itself
348
385
  const buildConfigs: BuildPrActorOptions[] = await Promise.all(
@@ -352,9 +389,28 @@ export const runBuilds = async ({
352
389
  actorConfig.actorFullName,
353
390
  actorInfo,
354
391
  );
392
+ // Builds keep the tags their version gives them, so a default version without the default build tag
393
+ // would build fine but leave users on the previous build
394
+ if (versionNumber === DEFAULT_VERSION_KEYWORD && defaultVersion?.buildTag !== defaultBuildTag) {
395
+ logger.warn(
396
+ `[${actorConfig.actorFullName}] Default version ${defaultVersionNumber} has build tag ` +
397
+ `"${defaultVersion?.buildTag ?? 'none'}", not "${defaultBuildTag}", so users keep running the previous build. ` +
398
+ 'Set the build tag of the version on the platform to change that.',
399
+ );
400
+ }
355
401
  if (shouldVerifyRepoUrl) {
356
402
  assertRepoUrlMatchesDefaultVersion(actorConfig.actorFullName, defaultVersion, repoUrl);
357
403
  }
404
+ const targetVersionNumber =
405
+ versionNumber === DEFAULT_VERSION_KEYWORD ? defaultVersionNumber : versionNumber;
406
+ if (requireEnvVarsInSync) {
407
+ assertEnvVarsMatchDefaultVersion(
408
+ actorConfig.actorFullName,
409
+ actorInfo,
410
+ defaultVersion,
411
+ targetVersionNumber,
412
+ );
413
+ }
358
414
 
359
415
  // Depending on if these are miniactors or standaloneActors
360
416
  let gitRepoUrl = `${repoUrl}#${branch}`;
@@ -365,9 +421,7 @@ export const runBuilds = async ({
365
421
  actorConfig,
366
422
  actorInfo,
367
423
  gitRepoUrl,
368
- versionNumber: isLatest ? defaultVersionNumber : DEFAULT_TEST_VERSION_NUMBER,
369
- buildTag: isLatest ? defaultBuildTag : undefined,
370
- useDockerCache,
424
+ versionNumber: targetVersionNumber,
371
425
  };
372
426
  }),
373
427
  );
@@ -375,21 +429,15 @@ export const runBuilds = async ({
375
429
  // Everything above only reads, so a dry run has checked what the real run would, short of writing
376
430
  if (dryRun) {
377
431
  logger.info('[DRY RUN] Would build:');
378
- for (const { actorConfig, versionNumber, gitRepoUrl } of buildConfigs) {
379
- logger.info(` ${actorConfig.actorFullName} (${versionNumber}) from ${gitRepoUrl}`);
432
+ const builds = buildConfigs.map((buildConfig) => ({
433
+ actorFullName: buildConfig.actorConfig.actorFullName,
434
+ versionNumber: buildConfig.versionNumber,
435
+ gitRepoUrl: buildConfig.gitRepoUrl,
436
+ }));
437
+ for (const build of builds) {
438
+ logger.info(` ${build.actorFullName} (${build.versionNumber}) from ${build.gitRepoUrl}`);
380
439
  }
381
- return {
382
- dryRun: true,
383
- builds: buildConfigs.map(
384
- ({ actorConfig, versionNumber, buildTag, gitRepoUrl, useDockerCache: useCache }) => ({
385
- actorFullName: actorConfig.actorFullName,
386
- versionNumber,
387
- buildTag,
388
- gitRepoUrl,
389
- useDockerCache: useCache,
390
- }),
391
- ),
392
- };
440
+ return { dryRun: true, builds };
393
441
  }
394
442
 
395
443
  const buildConfigsByActorFullName = new Map(
@@ -397,20 +445,38 @@ export const runBuilds = async ({
397
445
  );
398
446
 
399
447
  return runAndSummarizeBuilds(actorConfigs, 'BUILDS', async (actorConfig, builder) => {
400
- const {
401
- actorInfo,
402
- buildTag,
403
- versionNumber,
404
- gitRepoUrl,
405
- useDockerCache: useCache,
406
- } = buildConfigsByActorFullName.get(actorConfig.actorFullName)!;
448
+ const buildConfig = buildConfigsByActorFullName.get(actorConfig.actorFullName)!;
449
+ // No buildTag: the version keeps the one it has, so its build gets the tag the version already gives it
407
450
  const actorVersion: ApifyClientTypes.ActorVersion = {
408
- buildTag,
409
- versionNumber,
410
- gitRepoUrl,
451
+ versionNumber: buildConfig.versionNumber,
452
+ gitRepoUrl: buildConfig.gitRepoUrl,
411
453
  sourceType: ActorSourceType.GitRepo,
412
454
  };
413
- return builder.createVersionAndBuild(versionNumber, actorVersion, useCache, actorInfo);
455
+ // Always with the Docker cache: CI always asked for it, and a clean build can still be started on the platform
456
+ return builder.createVersionAndBuild(buildConfig.versionNumber, actorVersion, true, buildConfig.actorInfo);
457
+ });
458
+ };
459
+
460
+ type BuildActorsOptions = Omit<RunBuildsOptions, 'repoUrl' | 'shouldVerifyRepoUrl' | 'branch'> & {
461
+ // Defaults to the `origin` remote, see resolveRepoUrl
462
+ repoUrl?: string;
463
+ };
464
+
465
+ /**
466
+ * Builds exactly the given Actors from the checked-out Git branch, as it is on the remote. Fails on a detached HEAD,
467
+ * where there is no branch to build. Used by `build` and the commands built on top of it.
468
+ */
469
+ export const buildActors = async ({ actorConfigs, repoUrl, ...options }: BuildActorsOptions) => {
470
+ if (actorConfigs.length === 0) {
471
+ logger.info('No Actors to build');
472
+ // Still the dry-run shape in a dry run, so it can't pass for a real (empty) result
473
+ return options.dryRun ? { dryRun: true as const, builds: [] } : [];
474
+ }
475
+ return runBuilds({
476
+ ...options,
477
+ ...(await resolveRepoUrl(repoUrl)),
478
+ actorConfigs,
479
+ branch: await getCurrentBranch(),
414
480
  });
415
481
  };
416
482