apify-test-tools 0.10.2-beta.1 → 0.10.2-beta.10
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/.gitattributes +2 -0
- package/.github/actions/checkout-restore-dependencies/action.yaml +64 -27
- package/.github/cli/apify-test-tools.mjs +360 -0
- package/.github/scripts/check-major-tag-refs.mjs +1 -1
- package/.github/scripts/mask-config-secrets.sh +17 -0
- package/.github/scripts/run-with-apify-tokens.mjs +37 -12
- package/.github/workflows/_check_code.yaml +25 -0
- package/.github/workflows/_move_major_tag.yaml +3 -81
- package/.github/workflows/_tests.yaml +34 -0
- package/.github/workflows/_update_release_metadata.yaml +0 -12
- package/.github/workflows/manual_move_major_tag.yaml +3 -3
- package/.github/workflows/manual_release_stable.yaml +3 -13
- package/.github/workflows/on_master.yaml +9 -5
- package/.github/workflows/on_pull_request.yaml +3 -48
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +13 -2
- package/.github/workflows/public_platform-tests.yaml +24 -1
- package/.github/workflows/public_pr-build-test.yaml +61 -25
- package/.github/workflows/public_push-build-latest.yaml +41 -8
- package/.husky/pre-commit +5 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +105 -21
- package/README.md +218 -600
- package/bin/affected-actors.ts +198 -0
- package/bin/build-from-local.ts +65 -13
- package/bin/build.ts +123 -41
- package/bin/diff-changes.ts +32 -23
- package/bin/diff-json-schema.ts +5 -6
- package/bin/git.ts +69 -99
- package/bin/main.ts +96 -135
- package/bin/slack.ts +43 -10
- package/bin/stdin-input.ts +65 -0
- package/bin/test-report.ts +6 -9
- package/bin/types.ts +15 -0
- package/dist/bin/affected-actors.d.ts +46 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +135 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build-from-local.d.ts +12 -3
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +34 -12
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts +30 -7
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +85 -26
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +11 -4
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +19 -19
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +2 -2
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +1 -4
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +23 -21
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +60 -83
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +82 -105
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +14 -4
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +39 -6
- 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/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +6 -8
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +13 -0
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/lib/extend-expect.d.ts.map +1 -1
- package/dist/lib/extend-expect.js +37 -218
- package/dist/lib/extend-expect.js.map +1 -1
- package/dist/lib/lib.d.ts +1 -2
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +15 -22
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/run-checks.d.ts +14 -0
- package/dist/lib/run-checks.d.ts.map +1 -0
- package/dist/lib/run-checks.js +82 -0
- package/dist/lib/run-checks.js.map +1 -0
- package/dist/lib/run-test-result.d.ts +3 -6
- package/dist/lib/run-test-result.d.ts.map +1 -1
- package/dist/lib/run-test-result.js +41 -54
- package/dist/lib/run-test-result.js.map +1 -1
- package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
- package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
- package/dist/test/bundle/cli-bundle.test.js +190 -0
- package/dist/test/bundle/cli-bundle.test.js.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
- package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js +138 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
- package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
- package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
- package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
- package/dist/test/e2e/helpers.d.ts +37 -0
- package/dist/test/e2e/helpers.d.ts.map +1 -0
- package/dist/test/e2e/helpers.js +57 -0
- package/dist/test/e2e/helpers.js.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js +167 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
- 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 +141 -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 +44 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build-from-local.test.js +65 -1
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/build.test.js +223 -3
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
- package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
- package/dist/test/unit/bin/compiled-cli.js +16 -0
- package/dist/test/unit/bin/compiled-cli.js.map +1 -0
- package/dist/test/unit/bin/diff-changes.test.js +130 -101
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +25 -28
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +69 -152
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/slack.test.d.ts +2 -0
- package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
- package/dist/test/unit/bin/slack.test.js +71 -0
- package/dist/test/unit/bin/slack.test.js.map +1 -0
- 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/github-scripts/mask-config-secrets.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
- package/dist/test/unit/run-checks.test.d.ts +2 -0
- package/dist/test/unit/run-checks.test.d.ts.map +1 -0
- package/dist/test/unit/run-checks.test.js +62 -0
- package/dist/test/unit/run-checks.test.js.map +1 -0
- package/dist/test/unit/run-test-result.test.d.ts +2 -0
- package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
- package/dist/test/unit/run-test-result.test.js +34 -0
- package/dist/test/unit/run-test-result.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +41 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/cli.md +373 -0
- package/docs/lib.md +271 -0
- package/docs/workflows.md +367 -0
- package/lib/extend-expect.ts +48 -258
- package/lib/lib.ts +24 -28
- package/lib/run-checks.ts +101 -0
- package/lib/run-test-result.ts +45 -55
- package/package.json +13 -3
- package/test/bundle/cli-bundle.test.ts +235 -0
- package/test/e2e/cli-bundle.e2e.test.ts +207 -0
- package/test/e2e/git-build-release.e2e.test.ts +113 -0
- package/test/e2e/helpers.ts +82 -0
- package/test/unit/bin/affected-actors-cli.test.ts +201 -0
- package/test/unit/bin/affected-actors.test.ts +182 -0
- package/test/unit/bin/build-cli.test.ts +55 -0
- package/test/unit/bin/build-from-local.test.ts +71 -0
- package/test/unit/bin/build.test.ts +279 -3
- package/test/unit/bin/compiled-cli.ts +17 -0
- package/test/unit/bin/diff-changes.test.ts +139 -101
- package/test/unit/bin/diff.test.ts +25 -28
- package/test/unit/bin/git.test.ts +85 -176
- package/test/unit/bin/slack.test.ts +84 -0
- package/test/unit/bin/stdin-input.test.ts +61 -0
- package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
- package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
- package/test/unit/run-checks.test.ts +76 -0
- package/test/unit/run-test-result.test.ts +44 -0
- package/test/unit/should-built-and-test.test.ts +46 -39
- package/.github/scripts/check-package-version-bump.mjs +0 -74
- package/.github/workflows-package-version +0 -1
package/bin/diff-changes.ts
CHANGED
|
@@ -3,14 +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,
|
|
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
|
-
|
|
13
|
-
|
|
12
|
+
/** The range the files changed in, to compare `.actor` JSON files across. */
|
|
13
|
+
range: GitRange;
|
|
14
14
|
// Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them.
|
|
15
15
|
testFilesGlob?: string;
|
|
16
16
|
}
|
|
@@ -53,7 +53,7 @@ type FileChangeForActor =
|
|
|
53
53
|
const classifyFileChange = async (
|
|
54
54
|
changedFile: RelativeFile,
|
|
55
55
|
actorConfig: ActorConfig,
|
|
56
|
-
|
|
56
|
+
range: GitRange,
|
|
57
57
|
dockerIgnoreMatcher: DockerIgnoreMatcher,
|
|
58
58
|
isTestFile: (filePath: string) => boolean,
|
|
59
59
|
): Promise<FileChangeForActor> => {
|
|
@@ -92,7 +92,7 @@ const classifyFileChange = async (
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
if (lowercaseFilePath.endsWith('.json') && isUnderActorDotDir) {
|
|
95
|
-
const isCosmetic = await isCosmeticOnlyJsonSchemaChange(
|
|
95
|
+
const isCosmetic = await isCosmeticOnlyJsonSchemaChange(range, changedFile.path);
|
|
96
96
|
if (isCosmetic) return { impact: 'cosmetic', semanticallyVerified: true };
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -112,6 +112,7 @@ const isExcludedBySibling = (filePath: RelativeFile, actor: ActorConfig, allActo
|
|
|
112
112
|
type ActorChangeEntry = {
|
|
113
113
|
actorConfig: ActorConfig;
|
|
114
114
|
files: string[];
|
|
115
|
+
hasFunctionalChange: boolean;
|
|
115
116
|
};
|
|
116
117
|
|
|
117
118
|
type ChangeGroup = { actors: string[]; files: string[] };
|
|
@@ -164,13 +165,20 @@ const logChangeGroups = (groups: ChangeGroup[]): void => {
|
|
|
164
165
|
}
|
|
165
166
|
};
|
|
166
167
|
|
|
168
|
+
export interface ChangedActors {
|
|
169
|
+
/** Actors with at least one functional change. They need a new build and tests. */
|
|
170
|
+
functional: ActorConfig[];
|
|
171
|
+
/** Actors whose changes are all cosmetic. They only need a new build when released. */
|
|
172
|
+
cosmetic: ActorConfig[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Finds the Actors the changed files belong to, split by the most impactful change each Actor has. */
|
|
167
176
|
export const getChangedActors = async ({
|
|
168
177
|
filepathsChanged,
|
|
169
178
|
actorConfigs,
|
|
170
|
-
|
|
171
|
-
commits,
|
|
179
|
+
range,
|
|
172
180
|
testFilesGlob,
|
|
173
|
-
}: ShouldBuildAndTestOptions): Promise<
|
|
181
|
+
}: ShouldBuildAndTestOptions): Promise<ChangedActors> => {
|
|
174
182
|
const actorsChangedMap = new Map<string, ActorChangeEntry>();
|
|
175
183
|
const testFilesInContext = new Set<string>();
|
|
176
184
|
const isTestFile = testFilesGlob ? picomatch(testFilesGlob, { nocase: true, dot: true }) : () => false;
|
|
@@ -185,27 +193,27 @@ export const getChangedActors = async ({
|
|
|
185
193
|
continue;
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
const change = await classifyFileChange(filePath, actorConfig,
|
|
196
|
+
const change = await classifyFileChange(filePath, actorConfig, range, dockerIgnoreMatcher, isTestFile);
|
|
189
197
|
if (change.impact !== 'outside-context' && isTestFile(originalFilePath)) {
|
|
190
198
|
testFilesInContext.add(originalFilePath);
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
if (change.impact === 'ignored' || change.impact === 'outside-context') continue;
|
|
194
|
-
if (change.impact === 'cosmetic' && !isLatest) continue;
|
|
195
202
|
|
|
196
|
-
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
203
|
+
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
204
|
+
actorConfig,
|
|
205
|
+
files: [],
|
|
206
|
+
hasFunctionalChange: false,
|
|
207
|
+
};
|
|
197
208
|
entry.files.push(originalFilePath);
|
|
209
|
+
entry.hasFunctionalChange ||= change.impact === 'functional';
|
|
198
210
|
actorsChangedMap.set(actorConfig.folder.path, entry);
|
|
199
211
|
}
|
|
200
212
|
}
|
|
201
213
|
|
|
202
|
-
const actorsChanged = Array.from(actorsChangedMap.values()).map((entry) => entry.actorConfig);
|
|
203
|
-
|
|
204
214
|
// Log changes grouped by actor set, so changes shared across actors are logged once
|
|
205
215
|
// instead of being repeated per actor.
|
|
206
|
-
|
|
207
|
-
const groups = groupFilesByActorSet(fileToActors);
|
|
208
|
-
logChangeGroups(groups);
|
|
216
|
+
logChangeGroups(groupFilesByActorSet(buildFileToActorsMap(actorsChangedMap)));
|
|
209
217
|
|
|
210
218
|
for (const testFile of filepathsChanged.filter(
|
|
211
219
|
(filePath) => isTestFile(filePath) && !testFilesInContext.has(filePath),
|
|
@@ -216,12 +224,13 @@ export const getChangedActors = async ({
|
|
|
216
224
|
);
|
|
217
225
|
}
|
|
218
226
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
227
|
+
const entries = Array.from(actorsChangedMap.values());
|
|
228
|
+
const functional = entries.filter((entry) => entry.hasFunctionalChange).map((entry) => entry.actorConfig);
|
|
229
|
+
const cosmetic = entries.filter((entry) => !entry.hasFunctionalChange).map((entry) => entry.actorConfig);
|
|
230
|
+
|
|
231
|
+
const names = (configs: ActorConfig[]) => configs.map((config) => config.actorFullName).join(', ') || 'none';
|
|
232
|
+
logger.info(`[DIFF]: Actors with functional changes: ${names(functional)}`);
|
|
233
|
+
logger.info(`[DIFF]: Actors with only cosmetic changes: ${names(cosmetic)}`);
|
|
225
234
|
|
|
226
|
-
return
|
|
235
|
+
return { functional, cosmetic };
|
|
227
236
|
};
|
package/bin/diff-json-schema.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
|
-
import type {
|
|
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 (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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)
|
package/bin/git.ts
CHANGED
|
@@ -5,80 +5,56 @@ import { GitCommandError, runGitCommand } from './utils.js';
|
|
|
5
5
|
export const GIT_FORMAT_SEPARATOR = '»¦«';
|
|
6
6
|
const GIT_LOG_FORMAT = ['%H', '%aN<%aE>', '%aD', '%s'].join(GIT_FORMAT_SEPARATOR);
|
|
7
7
|
|
|
8
|
+
// Lists the changed paths. With rename detection (on by default) a moved file is listed only under its new path,
|
|
9
|
+
// so the Actor whose context it left wouldn't count as changed. --no-renames lists it as deleted and added instead.
|
|
10
|
+
const LIST_CHANGED_PATHS = ['--name-only', '--no-renames'];
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
|
-
* Gets the list of changed files between the given commits (inclusive).
|
|
13
|
+
* Gets the list of changed files between the given commits (inclusive), so it diffs from the parent of the oldest.
|
|
10
14
|
*/
|
|
11
15
|
export const getChangedFiles = async (commits: Commit[]) => {
|
|
12
|
-
// getCommits never returns an empty list (
|
|
13
|
-
//
|
|
16
|
+
// getCommits never returns an empty list (it throws on an empty range, and the rerun check returns all
|
|
17
|
+
// commits when the base commit is the branch HEAD), so this signals a programmer error
|
|
14
18
|
if (commits.length === 0) {
|
|
15
19
|
throw new Error('Cannot get changed files: the commit list is empty. This should never happen.');
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
['diff', '--name-only', `${commits[0].sha}~..${commits[commits.length - 1].sha}`],
|
|
20
|
-
{ operation: 'get the list of changed files between commits' },
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const changedFiles = changedFilesString.split('\n').filter(Boolean);
|
|
24
|
-
logger.info(`Changed files (up to 50): ${changedFiles.slice(0, 50).join(', ')}`);
|
|
25
|
-
return changedFiles;
|
|
22
|
+
return getChangedFilesBetween(`${commits[0].sha}~`, commits[commits.length - 1].sha);
|
|
26
23
|
};
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Files that differ between two commits, i.e. changed by the commits in the range (fromRef, toRef]. fromRef's own
|
|
27
|
+
* changes are not included: to include them, pass its parent, `${sha}~` (as getChangedFiles does).
|
|
28
|
+
* Diffing the endpoints, rather than walking a commit list, is what keeps merges right: the oldest commit of a
|
|
29
|
+
* merged branch can predate fromRef, and its changes are already in fromRef.
|
|
32
30
|
*/
|
|
33
|
-
export const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
38
|
-
)
|
|
39
|
-
.split('\n')
|
|
40
|
-
.filter(Boolean);
|
|
31
|
+
export const getChangedFilesBetween = async (fromRef: string, toRef: string) => {
|
|
32
|
+
const changedFilesString = await runGitCommand(['diff', ...LIST_CHANGED_PATHS, `${fromRef}..${toRef}`], {
|
|
33
|
+
operation: 'get the list of changed files between commits',
|
|
34
|
+
});
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
operation: 'get parents of a merge commit',
|
|
46
|
-
})
|
|
47
|
-
).split(' ');
|
|
48
|
-
for (const parent of parents) {
|
|
49
|
-
// git merge-base A B outputs the common ancestor.
|
|
50
|
-
// If that equals A, then A is an ancestor of B (i.e. parent is reachable from targetBranch).
|
|
51
|
-
let mergeBase: string;
|
|
52
|
-
try {
|
|
53
|
-
mergeBase = await runGitCommand(['merge-base', parent, targetBranch], {
|
|
54
|
-
operation: 'check whether a merge parent is reachable from the target branch',
|
|
55
|
-
});
|
|
56
|
-
} catch (error) {
|
|
57
|
-
// Exit code 1 on merge-base means the histories have no common ancestor.
|
|
58
|
-
if (error instanceof GitCommandError && error.exitCode === 1) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
if (mergeBase === parent) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
36
|
+
const changedFiles = changedFilesString.split('\n').filter(Boolean);
|
|
37
|
+
logger.info(`Changed files (up to 50): ${changedFiles.slice(0, 50).join(', ')}`);
|
|
38
|
+
return changedFiles;
|
|
69
39
|
};
|
|
70
40
|
|
|
71
41
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
42
|
+
* The commit `git diff base...head` compares against: where `head` forked from `base`, or the last commit of `base`
|
|
43
|
+
* that `head` merged in. The diff from it holds only what `head` itself changed, like GitHub's "Files changed" tab.
|
|
44
|
+
* With criss-cross merges there are several such commits and git picks one.
|
|
74
45
|
*/
|
|
75
|
-
export const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
46
|
+
export const getMergeBase = async (base: string, head: string): Promise<string> =>
|
|
47
|
+
runGitCommand(['merge-base', base, head], { operation: `find where ${head} forked from ${base}` });
|
|
48
|
+
|
|
49
|
+
/** Resolves a ref to a commit, or undefined when it isn't in the local history (e.g. it was force-pushed away). */
|
|
50
|
+
export const findCommitSha = async (ref: string): Promise<string | undefined> =>
|
|
51
|
+
runGitCommand(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
|
|
52
|
+
operation: `look up ${ref} in local Git history`,
|
|
53
|
+
}).catch((error: unknown) => {
|
|
54
|
+
// --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1
|
|
55
|
+
if (error instanceof GitCommandError && error.exitCode === 1) return undefined;
|
|
56
|
+
throw error;
|
|
57
|
+
});
|
|
82
58
|
|
|
83
59
|
const SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
84
60
|
|
|
@@ -103,13 +79,16 @@ export const parseBaseCommit = (shaOrCommit: string | undefined): string | undef
|
|
|
103
79
|
return sha;
|
|
104
80
|
};
|
|
105
81
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Commits reachable from toRef but not from fromRef, oldest first: the range (fromRef, toRef], so fromRef itself is
|
|
84
|
+
* not included. To include it, pass its parent, `${sha}~`.
|
|
85
|
+
*/
|
|
86
|
+
export const getCommitsBetween = async (fromRef: string, toRef: string): Promise<Commit[]> => {
|
|
87
|
+
const output = await runGitCommand(['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${fromRef}..${toRef}`], {
|
|
88
|
+
operation: 'list commits reachable from the source ref but absent from the target ref',
|
|
89
|
+
});
|
|
90
|
+
// An empty range (e.g. `x..x`) prints nothing
|
|
91
|
+
const commits = output.split('\n').filter(Boolean).map(parseCommit);
|
|
113
92
|
commits.reverse();
|
|
114
93
|
return commits;
|
|
115
94
|
};
|
|
@@ -124,7 +103,10 @@ export const getCommits = async ({
|
|
|
124
103
|
baseCommit,
|
|
125
104
|
}: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Promise<Commit[]> => {
|
|
126
105
|
const baseCommitSha = parseBaseCommit(baseCommit);
|
|
127
|
-
const commits = await
|
|
106
|
+
const commits = await getCommitsBetween(targetBranch, sourceBranch);
|
|
107
|
+
if (commits.length === 0) {
|
|
108
|
+
throw new Error(`${sourceBranch} has no commits that are not on ${targetBranch}.`);
|
|
109
|
+
}
|
|
128
110
|
|
|
129
111
|
// The last validated (base) commit being the branch HEAD means nothing new was pushed since the last
|
|
130
112
|
// validation — the dev reran the workflow (or force-pushed to the same state) to trigger a clean test
|
|
@@ -169,21 +151,25 @@ export const parseCommit = (commitString: string): Commit => {
|
|
|
169
151
|
};
|
|
170
152
|
|
|
171
153
|
/**
|
|
172
|
-
* Returns the currently checked-out branch.
|
|
154
|
+
* Returns the currently checked-out branch. Builds point the Actor version at this branch,
|
|
173
155
|
* so a detached HEAD (no branch to point at) is an error rather than a guess.
|
|
174
156
|
*/
|
|
175
157
|
export const getCurrentBranch = async (): Promise<string> => {
|
|
176
158
|
const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
177
|
-
operation: 'determine the
|
|
159
|
+
operation: 'determine the checked-out branch',
|
|
178
160
|
});
|
|
179
161
|
if (branch === 'HEAD') {
|
|
180
162
|
throw new Error(
|
|
181
|
-
'Cannot determine the branch to
|
|
163
|
+
'Cannot determine the branch to build from: HEAD is detached. Check out the branch you want to build.',
|
|
182
164
|
);
|
|
183
165
|
}
|
|
184
166
|
return branch;
|
|
185
167
|
};
|
|
186
168
|
|
|
169
|
+
/** Resolves a ref (branch, remote branch, SHA, HEAD, ...) to the full SHA of the commit it points to. */
|
|
170
|
+
export const resolveCommitSha = async (ref: string): Promise<string> =>
|
|
171
|
+
runGitCommand(['rev-parse', '--verify', `${ref}^{commit}`], { operation: `resolve ${ref} to a commit` });
|
|
172
|
+
|
|
187
173
|
/**
|
|
188
174
|
* Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
|
|
189
175
|
* uses for Git repo sources, e.g. git@github.com:apify-store/google-maps
|
|
@@ -236,15 +222,15 @@ const ZERO_SHA_REGEX = /^0{40}$/;
|
|
|
236
222
|
* Unlike the PR path (getCommits), there is no lenient fallback. Falling back to "everything"
|
|
237
223
|
* would rebuild the latest build of every Actor and post it to Slack, so every problem is an error.
|
|
238
224
|
*/
|
|
239
|
-
export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<string> => {
|
|
225
|
+
export const resolveReleaseBaseCommit = async (baseCommit: string, head = 'HEAD'): Promise<string> => {
|
|
240
226
|
const sha = parseBaseCommit(baseCommit);
|
|
241
227
|
if (!sha) {
|
|
242
|
-
throw new Error('
|
|
228
|
+
throw new Error('A base commit is required for release. See the "Releasing Actors" section of docs/cli.md.');
|
|
243
229
|
}
|
|
244
230
|
if (ZERO_SHA_REGEX.test(sha)) {
|
|
245
231
|
throw new Error(
|
|
246
232
|
`Base commit is ${sha}, which means the branch was just created and there is no previous release to diff against. ` +
|
|
247
|
-
`Rerun with
|
|
233
|
+
`Rerun with the base commit set to the last commit before the changes you want to release.`,
|
|
248
234
|
);
|
|
249
235
|
}
|
|
250
236
|
// --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1.
|
|
@@ -261,16 +247,16 @@ export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<stri
|
|
|
261
247
|
);
|
|
262
248
|
}
|
|
263
249
|
// 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:
|
|
250
|
+
const mergeBase = await runGitCommand(['merge-base', verifiedSha, head], {
|
|
251
|
+
operation: `check whether the release base commit is an ancestor of ${head}`,
|
|
266
252
|
}).catch((error: unknown) => {
|
|
267
253
|
if (error instanceof GitCommandError && error.exitCode === 1) return '';
|
|
268
254
|
throw error;
|
|
269
255
|
});
|
|
270
256
|
if (mergeBase !== sha) {
|
|
271
257
|
throw new Error(
|
|
272
|
-
`Base commit ${sha} is not an ancestor of
|
|
273
|
-
`The changed files cannot be determined reliably. Rerun with
|
|
258
|
+
`Base commit ${sha} is not an ancestor of ${head}, most likely because the branch was force-pushed. ` +
|
|
259
|
+
`The changed files cannot be determined reliably. Rerun with the base commit set to an ancestor of ${head} ` +
|
|
274
260
|
`from before the changes you want to release.`,
|
|
275
261
|
);
|
|
276
262
|
}
|
|
@@ -279,12 +265,16 @@ export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<stri
|
|
|
279
265
|
|
|
280
266
|
const CHANGELOG_PATH = 'CHANGELOG.md';
|
|
281
267
|
|
|
282
|
-
/** Returns the lines added to the root CHANGELOG.md between
|
|
283
|
-
const getChangelogAdditions = async (
|
|
268
|
+
/** Returns the lines added to the root CHANGELOG.md between fromRef and toRef, or null if it didn't change. */
|
|
269
|
+
export const getChangelogAdditions = async (
|
|
270
|
+
fromRef: string,
|
|
271
|
+
toRef: string,
|
|
272
|
+
changedFiles: string[],
|
|
273
|
+
): Promise<string | null> => {
|
|
284
274
|
if (!changedFiles.includes(CHANGELOG_PATH)) {
|
|
285
275
|
return null;
|
|
286
276
|
}
|
|
287
|
-
const diff = await runGitCommand(['diff',
|
|
277
|
+
const diff = await runGitCommand(['diff', fromRef, toRef, '--', CHANGELOG_PATH], {
|
|
288
278
|
operation: 'read changelog changes since the last release',
|
|
289
279
|
});
|
|
290
280
|
|
|
@@ -307,23 +297,3 @@ const getChangelogAdditions = async (baseSha: string, changedFiles: string[]): P
|
|
|
307
297
|
}
|
|
308
298
|
return added.join('\n').trim();
|
|
309
299
|
};
|
|
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
|
-
};
|