apify-test-tools 0.10.2-beta.3 → 0.10.2-beta.5

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 (107) hide show
  1. package/.github/cli/apify-test-tools.mjs +117 -118
  2. package/.github/scripts/run-with-apify-tokens.mjs +1 -1
  3. package/.github/workflows/public_pr-build-test.yaml +25 -25
  4. package/.github/workflows/public_push-build-latest.yaml +20 -4
  5. package/CHANGELOG.md +6 -0
  6. package/CONTRIBUTING.md +4 -1
  7. package/README.md +104 -56
  8. package/bin/affected-actors.ts +135 -103
  9. package/bin/build.ts +53 -38
  10. package/bin/diff-changes.ts +7 -6
  11. package/bin/diff-json-schema.ts +5 -6
  12. package/bin/git.ts +24 -69
  13. package/bin/main.ts +46 -80
  14. package/bin/slack.ts +15 -4
  15. package/bin/stdin-input.ts +65 -0
  16. package/bin/types.ts +6 -0
  17. package/dist/bin/affected-actors.d.ts +23 -27
  18. package/dist/bin/affected-actors.d.ts.map +1 -1
  19. package/dist/bin/affected-actors.js +111 -65
  20. package/dist/bin/affected-actors.js.map +1 -1
  21. package/dist/bin/build.d.ts +11 -5
  22. package/dist/bin/build.d.ts.map +1 -1
  23. package/dist/bin/build.js +43 -23
  24. package/dist/bin/build.js.map +1 -1
  25. package/dist/bin/diff-changes.d.ts +4 -3
  26. package/dist/bin/diff-changes.d.ts.map +1 -1
  27. package/dist/bin/diff-changes.js +4 -4
  28. package/dist/bin/diff-changes.js.map +1 -1
  29. package/dist/bin/diff-json-schema.d.ts +2 -2
  30. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  31. package/dist/bin/diff-json-schema.js +1 -4
  32. package/dist/bin/diff-json-schema.js.map +1 -1
  33. package/dist/bin/git.d.ts +7 -15
  34. package/dist/bin/git.d.ts.map +1 -1
  35. package/dist/bin/git.js +23 -61
  36. package/dist/bin/git.js.map +1 -1
  37. package/dist/bin/main.js +39 -71
  38. package/dist/bin/main.js.map +1 -1
  39. package/dist/bin/slack.d.ts +6 -3
  40. package/dist/bin/slack.d.ts.map +1 -1
  41. package/dist/bin/slack.js +13 -1
  42. package/dist/bin/slack.js.map +1 -1
  43. package/dist/bin/stdin-input.d.ts +17 -0
  44. package/dist/bin/stdin-input.d.ts.map +1 -0
  45. package/dist/bin/stdin-input.js +54 -0
  46. package/dist/bin/stdin-input.js.map +1 -0
  47. package/dist/bin/types.d.ts +5 -0
  48. package/dist/bin/types.d.ts.map +1 -1
  49. package/dist/test/bundle/cli-bundle.test.js +32 -11
  50. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  51. package/dist/test/e2e/cli-bundle.e2e.test.js +23 -57
  52. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  53. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  54. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  55. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  56. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  57. package/dist/test/e2e/helpers.d.ts +37 -0
  58. package/dist/test/e2e/helpers.d.ts.map +1 -0
  59. package/dist/test/e2e/helpers.js +57 -0
  60. package/dist/test/e2e/helpers.js.map +1 -0
  61. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  62. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  63. package/dist/test/unit/bin/affected-actors-cli.test.js +161 -0
  64. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  65. package/dist/test/unit/bin/affected-actors.test.js +68 -87
  66. package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
  67. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  68. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  69. package/dist/test/unit/bin/build-cli.test.js +44 -0
  70. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  71. package/dist/test/unit/bin/build.test.js +61 -4
  72. package/dist/test/unit/bin/build.test.js.map +1 -1
  73. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  74. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  75. package/dist/test/unit/bin/compiled-cli.js +16 -0
  76. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  77. package/dist/test/unit/bin/diff-changes.test.js +55 -55
  78. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  79. package/dist/test/unit/bin/diff.test.js +25 -28
  80. package/dist/test/unit/bin/diff.test.js.map +1 -1
  81. package/dist/test/unit/bin/git.test.js +28 -108
  82. package/dist/test/unit/bin/git.test.js.map +1 -1
  83. package/dist/test/unit/bin/slack.test.js +19 -2
  84. package/dist/test/unit/bin/slack.test.js.map +1 -1
  85. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  86. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  87. package/dist/test/unit/bin/stdin-input.test.js +50 -0
  88. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  89. package/dist/test/unit/should-built-and-test.test.js +18 -21
  90. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  91. package/dist/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +1 -1
  93. package/test/bundle/cli-bundle.test.ts +47 -11
  94. package/test/e2e/cli-bundle.e2e.test.ts +47 -77
  95. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  96. package/test/e2e/helpers.ts +82 -0
  97. package/test/unit/bin/affected-actors-cli.test.ts +193 -0
  98. package/test/unit/bin/affected-actors.test.ts +86 -94
  99. package/test/unit/bin/build-cli.test.ts +55 -0
  100. package/test/unit/bin/build.test.ts +71 -3
  101. package/test/unit/bin/compiled-cli.ts +17 -0
  102. package/test/unit/bin/diff-changes.test.ts +55 -55
  103. package/test/unit/bin/diff.test.ts +25 -28
  104. package/test/unit/bin/git.test.ts +35 -132
  105. package/test/unit/bin/slack.test.ts +21 -2
  106. package/test/unit/bin/stdin-input.test.ts +61 -0
  107. package/test/unit/should-built-and-test.test.ts +19 -22
@@ -1,27 +1,31 @@
1
1
  import { type ChangedActors, getChangedActors } from './diff-changes.js';
2
2
  import {
3
- getBranchOnlyChangedFiles,
3
+ findCommitSha,
4
4
  getChangedFilesBetween,
5
5
  getChangelogAdditions,
6
- getCommits,
7
6
  getCommitsBetween,
8
- hasMergeFromTarget,
9
- resolveBranchName,
7
+ getMergeBase,
10
8
  resolveCommitSha,
11
9
  resolveReleaseBaseCommit,
12
10
  } from './git.js';
13
11
  import { logger } from './logger.js';
14
- import type { ActorConfig, Commit } from './types.js';
12
+ import type { ActorConfig, Commit, GitRange } from './types.js';
15
13
  import { readConfigFile } from './utils/config/load-config.js';
16
14
 
15
+ // The range always ends at the checked-out commit: check out what you want to inspect
16
+ const HEAD = 'HEAD';
17
+
17
18
  interface AffectedActorsOptions {
18
- /** End of the range. */
19
- head: string;
20
- /** Only the commits on head that are not on base count, i.e. the branch's own changes. */
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
+ */
21
24
  base?: string;
22
25
  /**
23
- * Only changes after this commit count. With `base` it's the last validated commit of the branch and is
24
- * ignored when it isn't on the branch. Without `base` it must be an ancestor of head (e.g. the last release).
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).
25
29
  */
26
30
  since?: string;
27
31
  /** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
@@ -30,83 +34,115 @@ interface AffectedActorsOptions {
30
34
  ignore: string[];
31
35
  }
32
36
 
37
+ /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
33
38
  interface AffectedActors {
34
- headSha: string;
35
- /** Branch head points to, which is what git builds are made from. Null for a SHA or a detached HEAD. */
36
- ref: string | null;
39
+ version: 1;
40
+ head: string;
41
+ /** The commits, changed files and changelog describe the whole range, `--since` with `--base` only narrows the Actors. */
37
42
  commits: Commit[];
38
43
  changedFiles: string[];
39
44
  changelog: string | null;
40
- /** Actors with at least one functional change. They need a new build and tests. */
41
- functionallyChangedActors: ActorConfig[];
45
+ /**
46
+ * Actors with at least one functional change. They need a new build and tests. Only the names: the config file
47
+ * stays the source of truth for the rest.
48
+ */
49
+ functionallyChangedActors: { actorFullName: string }[];
42
50
  /** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
43
- cosmeticallyChangedActors: ActorConfig[];
51
+ cosmeticallyChangedActors: { actorFullName: string }[];
44
52
  }
45
53
 
46
- interface CommitRange {
47
- commits: Commit[];
48
- fromRef: string;
49
- toRef: string;
50
- }
51
-
52
- /** The branch's own commits, after `since` when it is on the branch (see getCommits). */
53
- const getBranchRange = async (head: string, base: string, since: string | undefined): Promise<CommitRange> => {
54
- const commits = await getCommits({ sourceBranch: head, targetBranch: base, baseCommit: since });
55
- return { commits, fromRef: `${commits[0].sha}~`, toRef: commits[commits.length - 1].sha };
56
- };
54
+ const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
57
55
 
58
- /** Everything after `since` up to head. Unlike on a branch, `since` must be an ancestor of head. */
59
- const getRangeSince = async (head: string, headSha: string, since: string): Promise<CommitRange> => {
60
- const sinceSha = await resolveReleaseBaseCommit(since, head);
61
- const commits = sinceSha === headSha ? [] : await getCommitsBetween(sinceSha, headSha);
62
- return { commits, fromRef: sinceSha, toRef: headSha };
56
+ /** The branch's last validated commit, unless it can't narrow anything down. */
57
+ const findValidatedSha = async (since: string, headSha: string): Promise<string | undefined> => {
58
+ const sinceSha = await findCommitSha(since);
59
+ if (!sinceSha) {
60
+ logger.info(
61
+ `Last validated commit ${since} is not in the local history (force-push?), checking the whole branch`,
62
+ );
63
+ return undefined;
64
+ }
65
+ if (sinceSha === headSha) {
66
+ logger.info('Last validated commit is HEAD, so this is a rerun: checking the whole branch from scratch');
67
+ return undefined;
68
+ }
69
+ return sinceSha;
63
70
  };
64
71
 
65
72
  /**
66
- * This is an optimization for the common case where a branch only has cosmetic changes but had to merge in
67
- * functional changes from base (being up-to-date is a CI requirement). Base is already validated, and
68
- * since the branch has no functional changes of its own, there is nothing new to validate.
69
- * Exception: if the branch has any functional changes alongside the merge, we must re-test — even
70
- * individually validated changes can have novel interactions when combined.
71
- * Returns the Actors the branch's own commits changed, or undefined when it didn't merge base in.
73
+ * This is Juan's magical algorithm where we need just 2 git diffs to determine functional changes on Actors.
74
+ *
75
+ * Naively, we could just check branch HEAD changes vs base or parent of first commit and be done but
76
+ * we do this jiu-jitsu code to apply 2 optimizations:
77
+ * 1. On our PR branch, we track last validated commit (since) that was tested and so we only need to test new commits.
78
+ * 2. We also merge changes from the base branch (usually master) and we can consider those already validated as well.
79
+ *
80
+ * But there is annoying problem: even if both our branch and base are validated, they could have novel interactions and
81
+ * we need to retest those.
72
82
  *
73
- * TODO: Apply this per Actor instead of to the whole branch. Changes merged in from base only interact with the
74
- * branch's own changes in Actors whose context both touch, so an Actor that only base changed is already validated
75
- * and can be skipped even when the branch changes other Actors functionally. Today any functional change of the
76
- * branch's own keeps every Actor, and the log messages below describe the whole branch.
83
+ * So we look at:
84
+ * 1. The changes introduced by our branch since the last validated commit (`since..HEAD`).
85
+ * 2. The changes introduced by the branch compared to the base branch (`base...HEAD`). The important part is that this
86
+ * diff does check the whole branch, discarding the last validated commit (since) so it checks those novel interactions problem.
87
+ *
88
+ * The magic is that both need to contain changes for an Actor to be retested:
89
+ * 1. If we have changes only in since..HEAD (and not in base...HEAD), it means those changes came from merging the base alone
90
+ * which is already validated and there were actually no real changes introduced by our branch alone.
91
+ * 2. If we have changes only in base...HEAD (and not in since..HEAD), it means there are no new changes on our branch
92
+ * since it was last validated. base...HEAD has changes that we already validated.
93
+ *
94
+ * This enables to simply do those 2 diffs for each Actor's context separately and only Actors that have changes
95
+ * in both diffs need to be retested.
96
+ *
97
+ * THIS ASSUMES BRANCH IS UP TO DATE WITH BASE WHEN IT MERGES, which we currently require. Only then is HEAD what lands on
98
+ * base, and base's commits validated. Otherwise what lands is HEAD combined with base's newer commits, which nobody
99
+ * tested, however the Actors are picked.
100
+ *
101
+ * In each example the branch's functional change to Actor A was validated at `since`, then one more push follows:
102
+ *
103
+ * Example scenarios:
104
+ * - Our own branch only: a commit changes A's README and B's code.
105
+ * `since..HEAD`: A cosmetic, B functional. `base...HEAD`: A and B functional.
106
+ * → A cosmetic, B functional: only B is tested, A's code is still the validated one.
107
+ * - Cosmetic merge: the branch merges base, which only changed READMEs and the CHANGELOG.
108
+ * `since..HEAD`: every Actor cosmetic (CHANGELOG counts for all). `base...HEAD`: A functional, since the merged
109
+ * files now match base and drop out of it.
110
+ * → A cosmetic, the other Actors left out: nothing is tested.
111
+ * - Functional merge: the branch merges base, which changed code of A and of C.
112
+ * `since..HEAD`: A and C functional. `base...HEAD`: A functional.
113
+ * → A functional, C left out: A is tested, since base's change can interact with the branch's, while C only has
114
+ * base's change, which base already validated.
77
115
  */
78
- const getOwnChangesIfMergedFromBase = async (
79
- head: string,
80
- base: string,
81
- actorConfigs: ActorConfig[],
82
- testFilesGlob: string | undefined,
83
- ): Promise<ChangedActors | undefined> => {
84
- if (!(await hasMergeFromTarget(head, base))) return undefined;
116
+ const intersectChangedInBranchVsSince = (branch: ChangedActors, sinceValidated: ChangedActors): ChangedActors => {
117
+ const narrowed: ChangedActors = { functional: [], cosmetic: [] };
118
+ // All checks are done for each Actor separately, that's cleanest and most accurate
119
+ for (const actor of [...branch.functional, ...branch.cosmetic]) {
120
+ // base...HEAD
121
+ const changedFunctionallyOnBranch = branch.functional.includes(actor);
122
+ // since..HEAD
123
+ const changedFunctionallySince = sinceValidated.functional.includes(actor);
124
+
125
+ // changedSince is used for release. Release cares also about cosmetic.
126
+ const changedSince = changedFunctionallySince || sinceValidated.cosmetic.includes(actor);
85
127
 
86
- logger.info(
87
- '[MERGE-FROM-TARGET-OPTIMIZATION]: There is merge from target branch, checking if there are no functional changes in our own branch. If so, we can skip tests',
88
- );
89
- const own = await getChangedActors({
90
- filepathsChanged: await getBranchOnlyChangedFiles(head, base),
91
- actorConfigs,
92
- // Omit since to get full branch history. Validated functional commits can still interact with merged ones
93
- commits: await getCommits({ sourceBranch: head, targetBranch: base }),
94
- testFilesGlob,
95
- });
96
- logger.info(
97
- own.functional.length > 0
98
- ? `[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${own.functional.length} functional changes, cannot optimize, we continue with full check`
99
- : '[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests',
100
- );
101
- return own;
128
+ // Nothing changed since the last validation, so the branch's change to this Actor is already validated
129
+ if (!changedSince) continue;
130
+
131
+ // As we said, only intersection counts as functional, otherwise it's cosmetic
132
+ if (changedFunctionallyOnBranch && changedFunctionallySince) {
133
+ narrowed.functional.push(actor);
134
+ } else {
135
+ narrowed.cosmetic.push(actor);
136
+ }
137
+ }
138
+ return narrowed;
102
139
  };
103
140
 
104
141
  /**
105
- * Finds the Actors whose context changed in the given range. `base` selects the branch's own commits
106
- * (a PR), `since` the commits after a known one (a release, or the last validated commit of a PR).
142
+ * Finds the Actors whose context changed in the given range. `base` selects the branch's own changes
143
+ * (a PR), `since` the changes after a known commit (a release, or the last validated commit of a PR).
107
144
  */
108
145
  export const getAffectedActors = async ({
109
- head,
110
146
  base,
111
147
  since: sinceOption,
112
148
  testFilesGlob,
@@ -119,48 +155,44 @@ export const getAffectedActors = async ({
119
155
  throw new Error('Pass --base, --since, or both to select the commits to inspect.');
120
156
  }
121
157
 
122
- const headSha = await resolveCommitSha(head);
123
- const { commits, fromRef, toRef } = base
124
- ? await getBranchRange(head, base, since)
125
- : await getRangeSince(head, headSha, since!);
126
158
  const actorConfigs = await readConfigFile({ actors, ignore });
159
+ const getChangedInRange = async (range: GitRange) => {
160
+ const changedFiles = await getChangedFilesBetween(range.fromRef, range.toRef);
161
+ return {
162
+ changedFiles,
163
+ changedActors: await getChangedActors({
164
+ filepathsChanged: changedFiles,
165
+ actorConfigs,
166
+ range,
167
+ testFilesGlob,
168
+ }),
169
+ };
170
+ };
127
171
 
128
- // What the branch only merged in from base is already validated. Without functional changes of its own
129
- // the branch has nothing to test, and without any changes of its own nothing to release either.
130
- const own = base ? await getOwnChangesIfMergedFromBase(head, base, actorConfigs, testFilesGlob) : undefined;
131
- const changedFiles = await getChangedFilesBetween(fromRef, toRef);
132
- const changed = await getChangedActors({ filepathsChanged: changedFiles, actorConfigs, commits, testFilesGlob });
133
-
134
- return {
135
- headSha,
136
- ref: await resolveBranchName(head),
137
- commits,
138
- changedFiles,
139
- changelog: await getChangelogAdditions(fromRef, toRef, changedFiles),
140
- functionallyChangedActors: own && own.functional.length === 0 ? [] : changed.functional,
141
- cosmeticallyChangedActors: own && own.functional.length + own.cosmetic.length === 0 ? [] : changed.cosmetic,
172
+ const headSha = await resolveCommitSha(HEAD);
173
+ const rangeVsBase: GitRange = {
174
+ // merge base = PR, release base = push to master (release)
175
+ fromRef: base ? await getMergeBase(base, HEAD) : await resolveReleaseBaseCommit(since!),
176
+ toRef: headSha,
142
177
  };
143
- };
144
178
 
145
- /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
146
- export const toAffectedActorsDocument = ({
147
- headSha,
148
- ref,
149
- commits,
150
- changedFiles,
151
- changelog,
152
- functionallyChangedActors,
153
- cosmeticallyChangedActors,
154
- }: AffectedActors) => {
155
- const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
179
+ // Falls back to rangeVsBase.fromRef when there's no (usable) last validated commit — same as
180
+ // intersecting the branch's changes with themselves, which is a no-op (see this function's docs).
181
+ const validatedSha = base && since ? await findValidatedSha(since, headSha) : undefined;
182
+ const sinceFromRef = validatedSha ?? rangeVsBase.fromRef;
183
+
184
+ const { changedFiles, changedActors: changedVsBase } = await getChangedInRange(rangeVsBase);
185
+ const { changedActors: changedSinceActors } = await getChangedInRange({ fromRef: sinceFromRef, toRef: headSha });
186
+ // See this function's docs to understand the algorithm: only intersection counts as functional, otherwise it's cosmetic.
187
+ const changedActors = intersectChangedInBranchVsSince(changedVsBase, changedSinceActors);
188
+
156
189
  return {
157
190
  version: 1,
158
- ref,
159
191
  head: headSha,
160
- commits,
192
+ commits: await getCommitsBetween(rangeVsBase.fromRef, rangeVsBase.toRef),
161
193
  changedFiles,
162
- changelog,
163
- functionallyChangedActors: names(functionallyChangedActors),
164
- cosmeticallyChangedActors: names(cosmeticallyChangedActors),
194
+ changelog: await getChangelogAdditions(rangeVsBase.fromRef, rangeVsBase.toRef, changedFiles),
195
+ functionallyChangedActors: names(changedActors.functional),
196
+ cosmeticallyChangedActors: names(changedActors.cosmetic),
165
197
  };
166
198
  };
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 =
@@ -319,20 +318,18 @@ export const runAndSummarizeBuilds = async (
319
318
  type GitBuildPlan = {
320
319
  actorFullName: string;
321
320
  versionNumber: string;
322
- buildTag?: string;
323
321
  gitRepoUrl: string;
324
- useDockerCache: boolean;
325
322
  };
326
323
 
327
324
  type RunBuildsOptions = {
328
325
  actorConfigs: ActorConfig[];
329
- isLatest?: boolean;
326
+ // A version number like "0.99", or DEFAULT_VERSION_KEYWORD for each Actor's default version
327
+ versionNumber: string;
330
328
  repoUrl: string;
331
329
  // False when the repo URL was passed explicitly, see assertRepoUrlMatchesDefaultVersion
332
330
  shouldVerifyRepoUrl: boolean;
333
331
  branch: string;
334
332
  dryRun: boolean;
335
- useDockerCache: boolean;
336
333
  };
337
334
 
338
335
  export const runBuilds = async ({
@@ -340,9 +337,8 @@ export const runBuilds = async ({
340
337
  shouldVerifyRepoUrl,
341
338
  branch,
342
339
  actorConfigs,
343
- isLatest = false,
340
+ versionNumber,
344
341
  dryRun,
345
- useDockerCache,
346
342
  }: RunBuildsOptions): Promise<BuildData[] | DryRunPlan<GitBuildPlan>> => {
347
343
  // Fetched once per Actor and reused for the checks, the version numbers and the build itself
348
344
  const buildConfigs: BuildPrActorOptions[] = await Promise.all(
@@ -352,6 +348,15 @@ export const runBuilds = async ({
352
348
  actorConfig.actorFullName,
353
349
  actorInfo,
354
350
  );
351
+ // Builds keep the tags their version gives them, so a default version without the default build tag
352
+ // would build fine but leave users on the previous build
353
+ if (versionNumber === DEFAULT_VERSION_KEYWORD && defaultVersion?.buildTag !== defaultBuildTag) {
354
+ logger.warn(
355
+ `[${actorConfig.actorFullName}] Default version ${defaultVersionNumber} has build tag ` +
356
+ `"${defaultVersion?.buildTag ?? 'none'}", not "${defaultBuildTag}", so users keep running the previous build. ` +
357
+ 'Set the build tag of the version on the platform to change that.',
358
+ );
359
+ }
355
360
  if (shouldVerifyRepoUrl) {
356
361
  assertRepoUrlMatchesDefaultVersion(actorConfig.actorFullName, defaultVersion, repoUrl);
357
362
  }
@@ -365,9 +370,7 @@ export const runBuilds = async ({
365
370
  actorConfig,
366
371
  actorInfo,
367
372
  gitRepoUrl,
368
- versionNumber: isLatest ? defaultVersionNumber : DEFAULT_TEST_VERSION_NUMBER,
369
- buildTag: isLatest ? defaultBuildTag : undefined,
370
- useDockerCache,
373
+ versionNumber: versionNumber === DEFAULT_VERSION_KEYWORD ? defaultVersionNumber : versionNumber,
371
374
  };
372
375
  }),
373
376
  );
@@ -375,21 +378,15 @@ export const runBuilds = async ({
375
378
  // Everything above only reads, so a dry run has checked what the real run would, short of writing
376
379
  if (dryRun) {
377
380
  logger.info('[DRY RUN] Would build:');
378
- for (const { actorConfig, versionNumber, gitRepoUrl } of buildConfigs) {
379
- logger.info(` ${actorConfig.actorFullName} (${versionNumber}) from ${gitRepoUrl}`);
381
+ const builds = buildConfigs.map((buildConfig) => ({
382
+ actorFullName: buildConfig.actorConfig.actorFullName,
383
+ versionNumber: buildConfig.versionNumber,
384
+ gitRepoUrl: buildConfig.gitRepoUrl,
385
+ }));
386
+ for (const build of builds) {
387
+ logger.info(` ${build.actorFullName} (${build.versionNumber}) from ${build.gitRepoUrl}`);
380
388
  }
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
- };
389
+ return { dryRun: true, builds };
393
390
  }
394
391
 
395
392
  const buildConfigsByActorFullName = new Map(
@@ -397,20 +394,38 @@ export const runBuilds = async ({
397
394
  );
398
395
 
399
396
  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)!;
397
+ const buildConfig = buildConfigsByActorFullName.get(actorConfig.actorFullName)!;
398
+ // No buildTag: the version keeps the one it has, so its build gets the tag the version already gives it
407
399
  const actorVersion: ApifyClientTypes.ActorVersion = {
408
- buildTag,
409
- versionNumber,
410
- gitRepoUrl,
400
+ versionNumber: buildConfig.versionNumber,
401
+ gitRepoUrl: buildConfig.gitRepoUrl,
411
402
  sourceType: ActorSourceType.GitRepo,
412
403
  };
413
- return builder.createVersionAndBuild(versionNumber, actorVersion, useCache, actorInfo);
404
+ // Always with the Docker cache: CI always asked for it, and a clean build can still be started on the platform
405
+ return builder.createVersionAndBuild(buildConfig.versionNumber, actorVersion, true, buildConfig.actorInfo);
406
+ });
407
+ };
408
+
409
+ type BuildActorsOptions = Omit<RunBuildsOptions, 'repoUrl' | 'shouldVerifyRepoUrl' | 'branch'> & {
410
+ // Defaults to the `origin` remote, see resolveRepoUrl
411
+ repoUrl?: string;
412
+ };
413
+
414
+ /**
415
+ * Builds exactly the given Actors from the checked-out Git branch, as it is on the remote. Fails on a detached HEAD,
416
+ * where there is no branch to build. Used by `build` and the commands built on top of it.
417
+ */
418
+ export const buildActors = async ({ actorConfigs, repoUrl, ...options }: BuildActorsOptions) => {
419
+ if (actorConfigs.length === 0) {
420
+ logger.info('No Actors to build');
421
+ // Still the dry-run shape in a dry run, so it can't pass for a real (empty) result
422
+ return options.dryRun ? { dryRun: true as const, builds: [] } : [];
423
+ }
424
+ return runBuilds({
425
+ ...options,
426
+ ...(await resolveRepoUrl(repoUrl)),
427
+ actorConfigs,
428
+ branch: await getCurrentBranch(),
414
429
  });
415
430
  };
416
431
 
@@ -3,13 +3,14 @@ import picomatch from 'picomatch';
3
3
  import { isCosmeticOnlyJsonSchemaChange } from './diff-json-schema.js';
4
4
  import { type DockerIgnoreMatcher, loadDockerIgnore } from './dockerignore.js';
5
5
  import { logger } from './logger.js';
6
- import type { ActorConfig, Commit } from './types.js';
6
+ import type { ActorConfig, GitRange } from './types.js';
7
7
  import { type RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
8
8
 
9
9
  interface ShouldBuildAndTestOptions {
10
10
  filepathsChanged: string[];
11
11
  actorConfigs: ActorConfig[];
12
- commits: Commit[];
12
+ /** The range the files changed in, to compare `.actor` JSON files across. */
13
+ range: GitRange;
13
14
  // Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them.
14
15
  testFilesGlob?: string;
15
16
  }
@@ -52,7 +53,7 @@ type FileChangeForActor =
52
53
  const classifyFileChange = async (
53
54
  changedFile: RelativeFile,
54
55
  actorConfig: ActorConfig,
55
- commits: Commit[],
56
+ range: GitRange,
56
57
  dockerIgnoreMatcher: DockerIgnoreMatcher,
57
58
  isTestFile: (filePath: string) => boolean,
58
59
  ): Promise<FileChangeForActor> => {
@@ -91,7 +92,7 @@ const classifyFileChange = async (
91
92
  }
92
93
 
93
94
  if (lowercaseFilePath.endsWith('.json') && isUnderActorDotDir) {
94
- const isCosmetic = await isCosmeticOnlyJsonSchemaChange(commits, changedFile.path);
95
+ const isCosmetic = await isCosmeticOnlyJsonSchemaChange(range, changedFile.path);
95
96
  if (isCosmetic) return { impact: 'cosmetic', semanticallyVerified: true };
96
97
  }
97
98
 
@@ -175,7 +176,7 @@ export interface ChangedActors {
175
176
  export const getChangedActors = async ({
176
177
  filepathsChanged,
177
178
  actorConfigs,
178
- commits,
179
+ range,
179
180
  testFilesGlob,
180
181
  }: ShouldBuildAndTestOptions): Promise<ChangedActors> => {
181
182
  const actorsChangedMap = new Map<string, ActorChangeEntry>();
@@ -192,7 +193,7 @@ export const getChangedActors = async ({
192
193
  continue;
193
194
  }
194
195
 
195
- const change = await classifyFileChange(filePath, actorConfig, commits, dockerIgnoreMatcher, isTestFile);
196
+ const change = await classifyFileChange(filePath, actorConfig, range, dockerIgnoreMatcher, isTestFile);
196
197
  if (change.impact !== 'outside-context' && isTestFile(originalFilePath)) {
197
198
  testFilesInContext.add(originalFilePath);
198
199
  }
@@ -1,5 +1,5 @@
1
1
  import { logger } from './logger.js';
2
- import type { Commit } from './types.js';
2
+ import type { GitRange } from './types.js';
3
3
  import { runGitCommand } from './utils.js';
4
4
 
5
5
  const COSMETIC_JSON_FIELD_NAMES = new Set([
@@ -29,11 +29,10 @@ const isCosmeticObjectChange = (oldVal: unknown, newVal: unknown, currentKey?: s
29
29
  * Returns true if the two JSON strings differ only in cosmetic fields
30
30
  * (title, description, example, enumTitles, sectionCaption, sectionDescription).
31
31
  */
32
- export const isCosmeticOnlyJsonSchemaChange = async (commits: Commit[], changedFilepath: string): Promise<boolean> => {
33
- // TODO: validate this is the right commit range
34
- const oldRef = `${commits[0].sha}~`;
35
- const newRef = commits[commits.length - 1].sha;
36
-
32
+ export const isCosmeticOnlyJsonSchemaChange = async (
33
+ { fromRef: oldRef, toRef: newRef }: GitRange,
34
+ changedFilepath: string,
35
+ ): Promise<boolean> => {
37
36
  const [oldJson, newJson] = await Promise.all([
38
37
  runGitCommand(['show', `${oldRef}:${changedFilepath}`])
39
38
  .then(JSON.parse)