apify-test-tools 0.10.2-beta.2 → 0.10.2-beta.4
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.
- package/.github/cli/apify-test-tools.mjs +140 -140
- package/.github/scripts/run-with-apify-tokens.mjs +1 -1
- package/.github/workflows/public_pr-build-test.yaml +25 -25
- package/.github/workflows/public_push-build-latest.yaml +20 -4
- package/CHANGELOG.md +2 -0
- package/README.md +119 -14
- package/bin/affected-actors.ts +157 -0
- package/bin/build.ts +55 -38
- package/bin/diff-changes.ts +25 -17
- package/bin/git.ts +44 -44
- package/bin/main.ts +132 -106
- package/bin/slack.ts +24 -0
- package/bin/stdin-input.ts +65 -0
- package/dist/bin/affected-actors.d.ts +45 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +87 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build.d.ts +12 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +45 -23
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +8 -2
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +16 -16
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/git.d.ts +19 -14
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +37 -35
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +110 -86
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +8 -0
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +18 -0
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +17 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +54 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/test/bundle/cli-bundle.test.js +36 -6
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/e2e/cli-bundle.e2e.test.js +16 -9
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
- package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +154 -0
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
- package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/build-cli.test.js +52 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build.test.js +61 -4
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +78 -49
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +39 -54
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/slack.test.js +39 -1
- package/dist/test/unit/bin/slack.test.js.map +1 -1
- package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
- package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.js +50 -0
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +23 -17
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/test/bundle/cli-bundle.test.ts +59 -6
- package/test/e2e/cli-bundle.e2e.test.ts +26 -11
- package/test/unit/bin/affected-actors.test.ts +184 -0
- package/test/unit/bin/build-cli.test.ts +61 -0
- package/test/unit/bin/build.test.ts +71 -3
- package/test/unit/bin/diff-changes.test.ts +87 -49
- package/test/unit/bin/git.test.ts +44 -55
- package/test/unit/bin/slack.test.ts +44 -1
- package/test/unit/bin/stdin-input.test.ts +61 -0
- package/test/unit/should-built-and-test.test.ts +27 -17
package/bin/build.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
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
|
-
|
|
17
|
+
// Version that build-for-test builds, so tests never touch the version users run
|
|
18
|
+
export const TEST_VERSION_NUMBER = '0.99';
|
|
19
|
+
// Stands for each Actor's default version: the one its default build tag points to
|
|
20
|
+
export const DEFAULT_VERSION_KEYWORD = 'default';
|
|
20
21
|
|
|
21
22
|
// Shown whenever an Actor isn't set up the way CI builds need it
|
|
22
23
|
const ACTOR_SETUP_REQUIREMENT =
|
|
@@ -319,20 +320,18 @@ export const runAndSummarizeBuilds = async (
|
|
|
319
320
|
type GitBuildPlan = {
|
|
320
321
|
actorFullName: string;
|
|
321
322
|
versionNumber: string;
|
|
322
|
-
buildTag?: string;
|
|
323
323
|
gitRepoUrl: string;
|
|
324
|
-
useDockerCache: boolean;
|
|
325
324
|
};
|
|
326
325
|
|
|
327
326
|
type RunBuildsOptions = {
|
|
328
327
|
actorConfigs: ActorConfig[];
|
|
329
|
-
|
|
328
|
+
// A version number like "0.99", or DEFAULT_VERSION_KEYWORD for each Actor's default version
|
|
329
|
+
versionNumber: string;
|
|
330
330
|
repoUrl: string;
|
|
331
331
|
// False when the repo URL was passed explicitly, see assertRepoUrlMatchesDefaultVersion
|
|
332
332
|
shouldVerifyRepoUrl: boolean;
|
|
333
333
|
branch: string;
|
|
334
334
|
dryRun: boolean;
|
|
335
|
-
useDockerCache: boolean;
|
|
336
335
|
};
|
|
337
336
|
|
|
338
337
|
export const runBuilds = async ({
|
|
@@ -340,9 +339,8 @@ export const runBuilds = async ({
|
|
|
340
339
|
shouldVerifyRepoUrl,
|
|
341
340
|
branch,
|
|
342
341
|
actorConfigs,
|
|
343
|
-
|
|
342
|
+
versionNumber,
|
|
344
343
|
dryRun,
|
|
345
|
-
useDockerCache,
|
|
346
344
|
}: RunBuildsOptions): Promise<BuildData[] | DryRunPlan<GitBuildPlan>> => {
|
|
347
345
|
// Fetched once per Actor and reused for the checks, the version numbers and the build itself
|
|
348
346
|
const buildConfigs: BuildPrActorOptions[] = await Promise.all(
|
|
@@ -352,6 +350,15 @@ export const runBuilds = async ({
|
|
|
352
350
|
actorConfig.actorFullName,
|
|
353
351
|
actorInfo,
|
|
354
352
|
);
|
|
353
|
+
// Builds keep the tags their version gives them, so a default version without the default build tag
|
|
354
|
+
// would build fine but leave users on the previous build
|
|
355
|
+
if (versionNumber === DEFAULT_VERSION_KEYWORD && defaultVersion?.buildTag !== defaultBuildTag) {
|
|
356
|
+
logger.warn(
|
|
357
|
+
`[${actorConfig.actorFullName}] Default version ${defaultVersionNumber} has build tag ` +
|
|
358
|
+
`"${defaultVersion?.buildTag ?? 'none'}", not "${defaultBuildTag}", so users keep running the previous build. ` +
|
|
359
|
+
'Set the build tag of the version on the platform to change that.',
|
|
360
|
+
);
|
|
361
|
+
}
|
|
355
362
|
if (shouldVerifyRepoUrl) {
|
|
356
363
|
assertRepoUrlMatchesDefaultVersion(actorConfig.actorFullName, defaultVersion, repoUrl);
|
|
357
364
|
}
|
|
@@ -365,9 +372,7 @@ export const runBuilds = async ({
|
|
|
365
372
|
actorConfig,
|
|
366
373
|
actorInfo,
|
|
367
374
|
gitRepoUrl,
|
|
368
|
-
versionNumber:
|
|
369
|
-
buildTag: isLatest ? defaultBuildTag : undefined,
|
|
370
|
-
useDockerCache,
|
|
375
|
+
versionNumber: versionNumber === DEFAULT_VERSION_KEYWORD ? defaultVersionNumber : versionNumber,
|
|
371
376
|
};
|
|
372
377
|
}),
|
|
373
378
|
);
|
|
@@ -375,21 +380,15 @@ export const runBuilds = async ({
|
|
|
375
380
|
// Everything above only reads, so a dry run has checked what the real run would, short of writing
|
|
376
381
|
if (dryRun) {
|
|
377
382
|
logger.info('[DRY RUN] Would build:');
|
|
378
|
-
|
|
379
|
-
|
|
383
|
+
const builds = buildConfigs.map((buildConfig) => ({
|
|
384
|
+
actorFullName: buildConfig.actorConfig.actorFullName,
|
|
385
|
+
versionNumber: buildConfig.versionNumber,
|
|
386
|
+
gitRepoUrl: buildConfig.gitRepoUrl,
|
|
387
|
+
}));
|
|
388
|
+
for (const build of builds) {
|
|
389
|
+
logger.info(` ${build.actorFullName} (${build.versionNumber}) from ${build.gitRepoUrl}`);
|
|
380
390
|
}
|
|
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
|
-
};
|
|
391
|
+
return { dryRun: true, builds };
|
|
393
392
|
}
|
|
394
393
|
|
|
395
394
|
const buildConfigsByActorFullName = new Map(
|
|
@@ -397,20 +396,38 @@ export const runBuilds = async ({
|
|
|
397
396
|
);
|
|
398
397
|
|
|
399
398
|
return runAndSummarizeBuilds(actorConfigs, 'BUILDS', async (actorConfig, builder) => {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
buildTag,
|
|
403
|
-
versionNumber,
|
|
404
|
-
gitRepoUrl,
|
|
405
|
-
useDockerCache: useCache,
|
|
406
|
-
} = buildConfigsByActorFullName.get(actorConfig.actorFullName)!;
|
|
399
|
+
const buildConfig = buildConfigsByActorFullName.get(actorConfig.actorFullName)!;
|
|
400
|
+
// No buildTag: the version keeps the one it has, so its build gets the tag the version already gives it
|
|
407
401
|
const actorVersion: ApifyClientTypes.ActorVersion = {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
gitRepoUrl,
|
|
402
|
+
versionNumber: buildConfig.versionNumber,
|
|
403
|
+
gitRepoUrl: buildConfig.gitRepoUrl,
|
|
411
404
|
sourceType: ActorSourceType.GitRepo,
|
|
412
405
|
};
|
|
413
|
-
|
|
406
|
+
// Always with the Docker cache: CI always asked for it, and a clean build can still be started on the platform
|
|
407
|
+
return builder.createVersionAndBuild(buildConfig.versionNumber, actorVersion, true, buildConfig.actorInfo);
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
type BuildActorsOptions = Omit<RunBuildsOptions, 'repoUrl' | 'shouldVerifyRepoUrl' | 'branch'> & {
|
|
412
|
+
// Defaults to the `origin` remote, see resolveRepoUrl
|
|
413
|
+
repoUrl?: string;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Builds exactly the given Actors from the checked-out Git branch, as it is on the remote. Fails on a detached HEAD,
|
|
418
|
+
* where there is no branch to build. Used by `build` and the commands built on top of it.
|
|
419
|
+
*/
|
|
420
|
+
export const buildActors = async ({ actorConfigs, repoUrl, ...options }: BuildActorsOptions) => {
|
|
421
|
+
if (actorConfigs.length === 0) {
|
|
422
|
+
logger.info('No Actors to build');
|
|
423
|
+
// Still the dry-run shape in a dry run, so it can't pass for a real (empty) result
|
|
424
|
+
return options.dryRun ? { dryRun: true as const, builds: [] } : [];
|
|
425
|
+
}
|
|
426
|
+
return runBuilds({
|
|
427
|
+
...options,
|
|
428
|
+
...(await resolveRepoUrl(repoUrl)),
|
|
429
|
+
actorConfigs,
|
|
430
|
+
branch: await getCurrentBranch(),
|
|
414
431
|
});
|
|
415
432
|
};
|
|
416
433
|
|
package/bin/diff-changes.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { type RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
|
|
|
9
9
|
interface ShouldBuildAndTestOptions {
|
|
10
10
|
filepathsChanged: string[];
|
|
11
11
|
actorConfigs: ActorConfig[];
|
|
12
|
-
isLatest?: boolean;
|
|
13
12
|
commits: Commit[];
|
|
14
13
|
// Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them.
|
|
15
14
|
testFilesGlob?: string;
|
|
@@ -112,6 +111,7 @@ const isExcludedBySibling = (filePath: RelativeFile, actor: ActorConfig, allActo
|
|
|
112
111
|
type ActorChangeEntry = {
|
|
113
112
|
actorConfig: ActorConfig;
|
|
114
113
|
files: string[];
|
|
114
|
+
hasFunctionalChange: boolean;
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
type ChangeGroup = { actors: string[]; files: string[] };
|
|
@@ -164,13 +164,20 @@ const logChangeGroups = (groups: ChangeGroup[]): void => {
|
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
export interface ChangedActors {
|
|
168
|
+
/** Actors with at least one functional change. They need a new build and tests. */
|
|
169
|
+
functional: ActorConfig[];
|
|
170
|
+
/** Actors whose changes are all cosmetic. They only need a new build when released. */
|
|
171
|
+
cosmetic: ActorConfig[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Finds the Actors the changed files belong to, split by the most impactful change each Actor has. */
|
|
167
175
|
export const getChangedActors = async ({
|
|
168
176
|
filepathsChanged,
|
|
169
177
|
actorConfigs,
|
|
170
|
-
isLatest = false,
|
|
171
178
|
commits,
|
|
172
179
|
testFilesGlob,
|
|
173
|
-
}: ShouldBuildAndTestOptions): Promise<
|
|
180
|
+
}: ShouldBuildAndTestOptions): Promise<ChangedActors> => {
|
|
174
181
|
const actorsChangedMap = new Map<string, ActorChangeEntry>();
|
|
175
182
|
const testFilesInContext = new Set<string>();
|
|
176
183
|
const isTestFile = testFilesGlob ? picomatch(testFilesGlob, { nocase: true, dot: true }) : () => false;
|
|
@@ -191,21 +198,21 @@ export const getChangedActors = async ({
|
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
if (change.impact === 'ignored' || change.impact === 'outside-context') continue;
|
|
194
|
-
if (change.impact === 'cosmetic' && !isLatest) continue;
|
|
195
201
|
|
|
196
|
-
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
202
|
+
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
203
|
+
actorConfig,
|
|
204
|
+
files: [],
|
|
205
|
+
hasFunctionalChange: false,
|
|
206
|
+
};
|
|
197
207
|
entry.files.push(originalFilePath);
|
|
208
|
+
entry.hasFunctionalChange ||= change.impact === 'functional';
|
|
198
209
|
actorsChangedMap.set(actorConfig.folder.path, entry);
|
|
199
210
|
}
|
|
200
211
|
}
|
|
201
212
|
|
|
202
|
-
const actorsChanged = Array.from(actorsChangedMap.values()).map((entry) => entry.actorConfig);
|
|
203
|
-
|
|
204
213
|
// Log changes grouped by actor set, so changes shared across actors are logged once
|
|
205
214
|
// instead of being repeated per actor.
|
|
206
|
-
|
|
207
|
-
const groups = groupFilesByActorSet(fileToActors);
|
|
208
|
-
logChangeGroups(groups);
|
|
215
|
+
logChangeGroups(groupFilesByActorSet(buildFileToActorsMap(actorsChangedMap)));
|
|
209
216
|
|
|
210
217
|
for (const testFile of filepathsChanged.filter(
|
|
211
218
|
(filePath) => isTestFile(filePath) && !testFilesInContext.has(filePath),
|
|
@@ -216,12 +223,13 @@ export const getChangedActors = async ({
|
|
|
216
223
|
);
|
|
217
224
|
}
|
|
218
225
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
226
|
+
const entries = Array.from(actorsChangedMap.values());
|
|
227
|
+
const functional = entries.filter((entry) => entry.hasFunctionalChange).map((entry) => entry.actorConfig);
|
|
228
|
+
const cosmetic = entries.filter((entry) => !entry.hasFunctionalChange).map((entry) => entry.actorConfig);
|
|
229
|
+
|
|
230
|
+
const names = (configs: ActorConfig[]) => configs.map((config) => config.actorFullName).join(', ') || 'none';
|
|
231
|
+
logger.info(`[DIFF]: Actors with functional changes: ${names(functional)}`);
|
|
232
|
+
logger.info(`[DIFF]: Actors with only cosmetic changes: ${names(cosmetic)}`);
|
|
225
233
|
|
|
226
|
-
return
|
|
234
|
+
return { functional, cosmetic };
|
|
227
235
|
};
|
package/bin/git.ts
CHANGED
|
@@ -6,7 +6,7 @@ export const GIT_FORMAT_SEPARATOR = '»¦«';
|
|
|
6
6
|
const GIT_LOG_FORMAT = ['%H', '%aN<%aE>', '%aD', '%s'].join(GIT_FORMAT_SEPARATOR);
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Gets the list of changed files between the given commits (inclusive).
|
|
9
|
+
* Gets the list of changed files between the given commits (inclusive), so it diffs from the parent of the oldest.
|
|
10
10
|
*/
|
|
11
11
|
export const getChangedFiles = async (commits: Commit[]) => {
|
|
12
12
|
// getCommits never returns an empty list (the rerun check returns all commits when the base commit
|
|
@@ -15,10 +15,19 @@ export const getChangedFiles = async (commits: Commit[]) => {
|
|
|
15
15
|
throw new Error('Cannot get changed files: the commit list is empty. This should never happen.');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
return getChangedFilesBetween(`${commits[0].sha}~`, commits[commits.length - 1].sha);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Files that differ between two commits, i.e. changed by the commits in the range (fromRef, toRef]. fromRef's own
|
|
23
|
+
* changes are not included: to include them, pass its parent, `${sha}~` (as getChangedFiles does).
|
|
24
|
+
* Diffing the endpoints, rather than walking a commit list, is what keeps merges right: the oldest commit of a
|
|
25
|
+
* merged branch can predate fromRef, and its changes are already in fromRef.
|
|
26
|
+
*/
|
|
27
|
+
export const getChangedFilesBetween = async (fromRef: string, toRef: string) => {
|
|
28
|
+
const changedFilesString = await runGitCommand(['diff', '--name-only', `${fromRef}..${toRef}`], {
|
|
29
|
+
operation: 'get the list of changed files between commits',
|
|
30
|
+
});
|
|
22
31
|
|
|
23
32
|
const changedFiles = changedFilesString.split('\n').filter(Boolean);
|
|
24
33
|
logger.info(`Changed files (up to 50): ${changedFiles.slice(0, 50).join(', ')}`);
|
|
@@ -103,11 +112,14 @@ export const parseBaseCommit = (shaOrCommit: string | undefined): string | undef
|
|
|
103
112
|
return sha;
|
|
104
113
|
};
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Commits reachable from toRef but not from fromRef, oldest first: the range (fromRef, toRef], so fromRef itself is
|
|
117
|
+
* not included. To include it, pass its parent, `${sha}~`.
|
|
118
|
+
*/
|
|
119
|
+
export const getCommitsBetween = async (fromRef: string, toRef: string): Promise<Commit[]> => {
|
|
120
|
+
const output = await runGitCommand(['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${fromRef}..${toRef}`], {
|
|
121
|
+
operation: 'list commits reachable from the source ref but absent from the target ref',
|
|
122
|
+
});
|
|
111
123
|
const commitsStrings = output.split('\n');
|
|
112
124
|
const commits = commitsStrings.map((commitString) => parseCommit(commitString));
|
|
113
125
|
commits.reverse();
|
|
@@ -124,7 +136,7 @@ export const getCommits = async ({
|
|
|
124
136
|
baseCommit,
|
|
125
137
|
}: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Promise<Commit[]> => {
|
|
126
138
|
const baseCommitSha = parseBaseCommit(baseCommit);
|
|
127
|
-
const commits = await
|
|
139
|
+
const commits = await getCommitsBetween(targetBranch, sourceBranch);
|
|
128
140
|
|
|
129
141
|
// The last validated (base) commit being the branch HEAD means nothing new was pushed since the last
|
|
130
142
|
// validation — the dev reran the workflow (or force-pushed to the same state) to trigger a clean test
|
|
@@ -169,21 +181,25 @@ export const parseCommit = (commitString: string): Commit => {
|
|
|
169
181
|
};
|
|
170
182
|
|
|
171
183
|
/**
|
|
172
|
-
* Returns the currently checked-out branch.
|
|
184
|
+
* Returns the currently checked-out branch. Builds point the Actor version at this branch,
|
|
173
185
|
* so a detached HEAD (no branch to point at) is an error rather than a guess.
|
|
174
186
|
*/
|
|
175
187
|
export const getCurrentBranch = async (): Promise<string> => {
|
|
176
188
|
const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
177
|
-
operation: 'determine the
|
|
189
|
+
operation: 'determine the checked-out branch',
|
|
178
190
|
});
|
|
179
191
|
if (branch === 'HEAD') {
|
|
180
192
|
throw new Error(
|
|
181
|
-
'Cannot determine the branch to
|
|
193
|
+
'Cannot determine the branch to build from: HEAD is detached. Check out the branch you want to build.',
|
|
182
194
|
);
|
|
183
195
|
}
|
|
184
196
|
return branch;
|
|
185
197
|
};
|
|
186
198
|
|
|
199
|
+
/** Resolves a ref (branch, remote branch, SHA, HEAD, ...) to the full SHA of the commit it points to. */
|
|
200
|
+
export const resolveCommitSha = async (ref: string): Promise<string> =>
|
|
201
|
+
runGitCommand(['rev-parse', '--verify', `${ref}^{commit}`], { operation: `resolve ${ref} to a commit` });
|
|
202
|
+
|
|
187
203
|
/**
|
|
188
204
|
* Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
|
|
189
205
|
* uses for Git repo sources, e.g. git@github.com:apify-store/google-maps
|
|
@@ -236,15 +252,15 @@ const ZERO_SHA_REGEX = /^0{40}$/;
|
|
|
236
252
|
* Unlike the PR path (getCommits), there is no lenient fallback. Falling back to "everything"
|
|
237
253
|
* would rebuild the latest build of every Actor and post it to Slack, so every problem is an error.
|
|
238
254
|
*/
|
|
239
|
-
export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<string> => {
|
|
255
|
+
export const resolveReleaseBaseCommit = async (baseCommit: string, head = 'HEAD'): Promise<string> => {
|
|
240
256
|
const sha = parseBaseCommit(baseCommit);
|
|
241
257
|
if (!sha) {
|
|
242
|
-
throw new Error('
|
|
258
|
+
throw new Error('A base commit is required for release. See the README section "Releasing Actors".');
|
|
243
259
|
}
|
|
244
260
|
if (ZERO_SHA_REGEX.test(sha)) {
|
|
245
261
|
throw new Error(
|
|
246
262
|
`Base commit is ${sha}, which means the branch was just created and there is no previous release to diff against. ` +
|
|
247
|
-
`Rerun with
|
|
263
|
+
`Rerun with the base commit set to the last commit before the changes you want to release.`,
|
|
248
264
|
);
|
|
249
265
|
}
|
|
250
266
|
// --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1.
|
|
@@ -261,16 +277,16 @@ export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<stri
|
|
|
261
277
|
);
|
|
262
278
|
}
|
|
263
279
|
// git merge-base A B outputs the common ancestor. If that equals A, then A is an ancestor of B.
|
|
264
|
-
const mergeBase = await runGitCommand(['merge-base', verifiedSha,
|
|
265
|
-
operation:
|
|
280
|
+
const mergeBase = await runGitCommand(['merge-base', verifiedSha, head], {
|
|
281
|
+
operation: `check whether the release base commit is an ancestor of ${head}`,
|
|
266
282
|
}).catch((error: unknown) => {
|
|
267
283
|
if (error instanceof GitCommandError && error.exitCode === 1) return '';
|
|
268
284
|
throw error;
|
|
269
285
|
});
|
|
270
286
|
if (mergeBase !== sha) {
|
|
271
287
|
throw new Error(
|
|
272
|
-
`Base commit ${sha} is not an ancestor of
|
|
273
|
-
`The changed files cannot be determined reliably. Rerun with
|
|
288
|
+
`Base commit ${sha} is not an ancestor of ${head}, most likely because the branch was force-pushed. ` +
|
|
289
|
+
`The changed files cannot be determined reliably. Rerun with the base commit set to an ancestor of ${head} ` +
|
|
274
290
|
`from before the changes you want to release.`,
|
|
275
291
|
);
|
|
276
292
|
}
|
|
@@ -279,12 +295,16 @@ export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<stri
|
|
|
279
295
|
|
|
280
296
|
const CHANGELOG_PATH = 'CHANGELOG.md';
|
|
281
297
|
|
|
282
|
-
/** Returns the lines added to the root CHANGELOG.md between
|
|
283
|
-
const getChangelogAdditions = async (
|
|
298
|
+
/** Returns the lines added to the root CHANGELOG.md between fromRef and toRef, or null if it didn't change. */
|
|
299
|
+
export const getChangelogAdditions = async (
|
|
300
|
+
fromRef: string,
|
|
301
|
+
toRef: string,
|
|
302
|
+
changedFiles: string[],
|
|
303
|
+
): Promise<string | null> => {
|
|
284
304
|
if (!changedFiles.includes(CHANGELOG_PATH)) {
|
|
285
305
|
return null;
|
|
286
306
|
}
|
|
287
|
-
const diff = await runGitCommand(['diff',
|
|
307
|
+
const diff = await runGitCommand(['diff', fromRef, toRef, '--', CHANGELOG_PATH], {
|
|
288
308
|
operation: 'read changelog changes since the last release',
|
|
289
309
|
});
|
|
290
310
|
|
|
@@ -307,23 +327,3 @@ const getChangelogAdditions = async (baseSha: string, changedFiles: string[]): P
|
|
|
307
327
|
}
|
|
308
328
|
return added.join('\n').trim();
|
|
309
329
|
};
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Everything that changed since the last release (baseSha, exclusive) up to HEAD, or null when there
|
|
313
|
-
* are no new commits. Changed files come from diffing the range directly instead of from the commit
|
|
314
|
-
* list: with a merge commit, the oldest commit of the merged branch can be older than baseSha, and
|
|
315
|
-
* diffing from its parent would pull in already-released changes.
|
|
316
|
-
*/
|
|
317
|
-
export const getReleaseChanges = async (baseSha: string) => {
|
|
318
|
-
if (
|
|
319
|
-
(await runGitCommand(['rev-parse', 'HEAD'], { operation: 'identify the current release commit' })) === baseSha
|
|
320
|
-
) {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
const commits = await fetchAllBranchCommits('HEAD', baseSha);
|
|
324
|
-
const changedFiles = await runGitCommand(['diff', '--name-only', baseSha, 'HEAD'], {
|
|
325
|
-
operation: 'list files changed since the last release',
|
|
326
|
-
}).then((output) => output.split('\n').filter(Boolean));
|
|
327
|
-
const changelog = await getChangelogAdditions(baseSha, changedFiles);
|
|
328
|
-
return { commits, changedFiles, changelog };
|
|
329
|
-
};
|