apify-test-tools 0.10.2-beta.2 → 0.10.2-beta.21
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 +1 -0
- package/.github/actionlint.yaml +11 -0
- package/.github/actions/checkout-restore-dependencies/action.yaml +58 -6
- package/.github/cli/apify-test-tools.mjs +135 -142
- package/.github/scripts/mask-config-secrets.sh +17 -0
- package/.github/scripts/run-with-apify-tokens.mjs +63 -103
- package/.github/workflows/_check_code.yaml +5 -21
- package/.github/workflows/_move_major_tag.yaml +2 -2
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +89 -8
- package/.github/workflows/public_platform-tests.yaml +68 -30
- package/.github/workflows/public_pr-build-test.yaml +77 -38
- package/.github/workflows/public_push-build-latest.yaml +49 -11
- package/.github/workflows/public_review.yaml +5 -4
- package/.github/workflows/public_stale-issues.yaml +51 -0
- package/.husky/pre-commit +4 -4
- package/.prettierignore +2 -1
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +117 -17
- package/README.md +221 -595
- package/bin/actor-ids.ts +41 -0
- package/bin/affected-actors.ts +211 -0
- package/bin/build-from-local.ts +19 -1
- package/bin/build.ts +105 -39
- package/bin/diff-changes.ts +69 -36
- package/bin/diff-json-schema.ts +5 -6
- package/bin/git.ts +73 -175
- package/bin/main.ts +142 -189
- package/bin/notify.ts +198 -0
- package/bin/run-tests.ts +182 -0
- package/bin/slack.ts +16 -76
- package/bin/stdin-input.ts +156 -0
- package/bin/test-files-glob.ts +126 -0
- package/bin/test-report.ts +141 -140
- package/bin/types.ts +4 -7
- package/bin/utils/config/load-config.ts +20 -2
- package/bin/utils/config/parser.ts +40 -8
- package/bin/utils/config/structures/grouped.ts +2 -2
- package/bin/utils/config/structures/legacy.ts +1 -1
- package/dist/bin/actor-ids.d.ts +15 -0
- package/dist/bin/actor-ids.d.ts.map +1 -0
- package/dist/bin/actor-ids.js +30 -0
- package/dist/bin/actor-ids.js.map +1 -0
- package/dist/bin/affected-actors.d.ts +55 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +142 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build-from-local.d.ts +2 -1
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +6 -2
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts +18 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +74 -24
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +21 -5
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +41 -28
- 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 +26 -34
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +60 -143
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +126 -140
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/notify.d.ts +35 -0
- package/dist/bin/notify.d.ts.map +1 -0
- package/dist/bin/notify.js +131 -0
- package/dist/bin/notify.js.map +1 -0
- package/dist/bin/run-tests.d.ts +21 -0
- package/dist/bin/run-tests.d.ts.map +1 -0
- package/dist/bin/run-tests.js +149 -0
- package/dist/bin/run-tests.js.map +1 -0
- package/dist/bin/slack.d.ts +2 -14
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +17 -46
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +79 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +124 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/bin/test-files-glob.d.ts +18 -0
- package/dist/bin/test-files-glob.d.ts.map +1 -0
- package/dist/bin/test-files-glob.js +108 -0
- package/dist/bin/test-files-glob.js.map +1 -0
- package/dist/bin/test-report.d.ts +69 -8
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +76 -67
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +4 -7
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.d.ts +8 -0
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +14 -1
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/config/parser.d.ts +15 -2
- package/dist/bin/utils/config/parser.d.ts.map +1 -1
- package/dist/bin/utils/config/parser.js +29 -8
- package/dist/bin/utils/config/parser.js.map +1 -1
- package/dist/bin/utils/config/structures/grouped.js +2 -2
- package/dist/bin/utils/config/structures/grouped.js.map +1 -1
- package/dist/bin/utils/config/structures/legacy.js +1 -1
- package/dist/bin/utils/config/structures/legacy.js.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 -5
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +19 -31
- 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.js +76 -56
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/bundle/token-wrapper.test.d.ts +2 -0
- package/dist/test/bundle/token-wrapper.test.d.ts.map +1 -0
- package/dist/test/bundle/token-wrapper.test.js +80 -0
- package/dist/test/bundle/token-wrapper.test.js.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js +33 -68
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
- 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/actor-filtering.test.js +7 -0
- package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
- package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
- package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
- package/dist/test/unit/bin/actor-ids.test.js +40 -0
- package/dist/test/unit/bin/actor-ids.test.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 +224 -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 +153 -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 +49 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build-from-local.test.js +12 -0
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/build.test.js +116 -6
- 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 +23 -0
- package/dist/test/unit/bin/compiled-cli.js.map +1 -0
- package/dist/test/unit/bin/diff-changes.test.js +168 -114
- 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 +67 -288
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/notify.test.d.ts +2 -0
- package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
- package/dist/test/unit/bin/notify.test.js +222 -0
- package/dist/test/unit/bin/notify.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 +92 -0
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
- package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-cli.test.js +165 -0
- package/dist/test/unit/bin/test-cli.test.js.map +1 -0
- package/dist/test/unit/bin/test-files-glob.test.d.ts +2 -0
- package/dist/test/unit/bin/test-files-glob.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-files-glob.test.js +102 -0
- package/dist/test/unit/bin/test-files-glob.test.js.map +1 -0
- package/dist/test/unit/bin/test-report.test.d.ts +2 -0
- package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-report.test.js +100 -0
- package/dist/test/unit/bin/test-report.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +19 -1
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/parser.test.js +36 -4
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/structures/grouped.test.js +6 -0
- package/dist/test/unit/bin/utils/config/structures/grouped.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/structures/legacy.test.js +1 -0
- package/dist/test/unit/bin/utils/config/structures/legacy.test.js.map +1 -1
- 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/dist/workflow-scripts-src/run-with-apify-tokens.d.ts +3 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts.map +1 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js +100 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js.map +1 -0
- package/docs/cli.md +473 -0
- package/docs/lib.md +276 -0
- package/docs/workflows.md +455 -0
- package/eslint.config.mjs +10 -1
- package/lib/extend-expect.ts +48 -258
- package/lib/lib.ts +25 -34
- package/lib/run-checks.ts +101 -0
- package/lib/run-test-result.ts +45 -55
- package/package.json +4 -2
- package/platform-tests-investigate-and-fix/skills/assess-impact/SKILL.md +42 -0
- package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +4 -2
- package/test/bundle/cli-bundle.test.ts +97 -67
- package/test/bundle/token-wrapper.test.ts +110 -0
- package/test/e2e/cli-bundle.e2e.test.ts +60 -93
- package/test/e2e/git-build-release.e2e.test.ts +113 -0
- package/test/e2e/helpers.ts +82 -0
- package/test/unit/bin/actor-filtering.test.ts +8 -0
- package/test/unit/bin/actor-ids.test.ts +47 -0
- package/test/unit/bin/affected-actors-cli.test.ts +271 -0
- package/test/unit/bin/affected-actors.test.ts +221 -0
- package/test/unit/bin/build-cli.test.ts +61 -0
- package/test/unit/bin/build-from-local.test.ts +16 -0
- package/test/unit/bin/build.test.ts +149 -5
- package/test/unit/bin/compiled-cli.ts +23 -0
- package/test/unit/bin/diff-changes.test.ts +181 -114
- package/test/unit/bin/diff.test.ts +25 -28
- package/test/unit/bin/git.test.ts +76 -361
- package/test/unit/bin/notify.test.ts +299 -0
- package/test/unit/bin/stdin-input.test.ts +120 -0
- package/test/unit/bin/test-cli.test.ts +207 -0
- package/test/unit/bin/test-files-glob.test.ts +118 -0
- package/test/unit/bin/test-report.test.ts +118 -0
- package/test/unit/bin/utils/config/load-config.test.ts +26 -1
- package/test/unit/bin/utils/config/parser.test.ts +46 -5
- package/test/unit/bin/utils/config/structures/grouped.test.ts +12 -0
- package/test/unit/bin/utils/config/structures/legacy.test.ts +1 -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/tsconfig.json +1 -1
- package/vitest.config.ts +9 -0
- package/workflow-scripts-src/run-with-apify-tokens.ts +112 -0
- package/.github/scripts/check-major-tag-refs.mjs +0 -72
- package/dist/test/unit/bin/logging.test.d.ts +0 -2
- package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
- package/dist/test/unit/bin/logging.test.js +0 -43
- package/dist/test/unit/bin/logging.test.js.map +0 -1
- package/dist/test/unit/bin/slack.test.d.ts +0 -2
- package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
- package/dist/test/unit/bin/slack.test.js +0 -54
- package/dist/test/unit/bin/slack.test.js.map +0 -1
- package/test/unit/bin/logging.test.ts +0 -52
- package/test/unit/bin/slack.test.ts +0 -65
package/bin/diff-changes.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import picomatch from 'picomatch';
|
|
2
|
-
|
|
3
1
|
import { isCosmeticOnlyJsonSchemaChange } from './diff-json-schema.js';
|
|
4
2
|
import { type DockerIgnoreMatcher, loadDockerIgnore } from './dockerignore.js';
|
|
5
3
|
import { logger } from './logger.js';
|
|
6
|
-
import
|
|
4
|
+
import { createTestFilesMatcher } from './test-files-glob.js';
|
|
5
|
+
import type { ActorConfig, GitRange } from './types.js';
|
|
7
6
|
import { type RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
|
|
8
7
|
|
|
9
8
|
interface ShouldBuildAndTestOptions {
|
|
10
9
|
filepathsChanged: string[];
|
|
11
10
|
actorConfigs: ActorConfig[];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
/** The range the files changed in, to compare `.actor` JSON files across. */
|
|
12
|
+
range: GitRange;
|
|
13
|
+
/**
|
|
14
|
+
* Glob of the platform test files, from the repo root (`platformTestsGlob` in the config). A change to a matching
|
|
15
|
+
* file is a test change, even when .dockerignore lists it.
|
|
16
|
+
*/
|
|
17
|
+
platformTestsGlob?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const IGNORED_TOP_LEVEL_FILES = [
|
|
@@ -34,6 +36,7 @@ type FileChangeForActor =
|
|
|
34
36
|
| { impact: 'ignored' }
|
|
35
37
|
| { impact: 'outside-context' }
|
|
36
38
|
| { impact: 'cosmetic'; semanticallyVerified: boolean }
|
|
39
|
+
| { impact: 'test' }
|
|
37
40
|
| { impact: 'functional' };
|
|
38
41
|
|
|
39
42
|
/**
|
|
@@ -44,16 +47,18 @@ type FileChangeForActor =
|
|
|
44
47
|
* not one per actor, so it applies to every actor regardless of context/folder.
|
|
45
48
|
* 2. Context matching (actorConfig.contextPaths) → outside-context if no match
|
|
46
49
|
* 3. Hardcoded ignore list, checked against the path hoisted relative to the matched context entry → ignored
|
|
47
|
-
* 4.
|
|
48
|
-
*
|
|
49
|
-
* 5.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* 4. Platform test files (`platformTestsGlob`) → test, even when .dockerignore lists them. A test change needs the
|
|
51
|
+
* tests to run, but no release.
|
|
52
|
+
* 5. .dockerignore filtering (patterns relative to dockerContextDir), skipped for the actor's own `.actor/`
|
|
53
|
+
* dir → ignored if matched
|
|
54
|
+
* 6. README.md by filename → cosmetic if inside the actor's own folder, otherwise ignored
|
|
55
|
+
* 7. .json inside the actor's own `.actor/` dir with only cosmetic schema diffs → cosmetic (semantically verified)
|
|
56
|
+
* 8. Everything else → functional
|
|
52
57
|
*/
|
|
53
58
|
const classifyFileChange = async (
|
|
54
59
|
changedFile: RelativeFile,
|
|
55
60
|
actorConfig: ActorConfig,
|
|
56
|
-
|
|
61
|
+
range: GitRange,
|
|
57
62
|
dockerIgnoreMatcher: DockerIgnoreMatcher,
|
|
58
63
|
isTestFile: (filePath: string) => boolean,
|
|
59
64
|
): Promise<FileChangeForActor> => {
|
|
@@ -75,13 +80,17 @@ const classifyFileChange = async (
|
|
|
75
80
|
return { impact: 'ignored' };
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
// Test files are often in .dockerignore, but a change to them must still run the tests
|
|
84
|
+
if (isTestFile(changedFile.path)) {
|
|
85
|
+
return { impact: 'test' };
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
const isUnderActorDotDir = actorConfig.actorJson.file.parent.contains(changedFile);
|
|
79
89
|
|
|
80
90
|
// .actor/ can legitimately be listed in .dockerignore (the Apify platform evaluates it before
|
|
81
91
|
// the Docker build, so excluding it from the build context is a valid caching optimization) —
|
|
82
92
|
// that shouldn't cause changes to .actor/ itself to be ignored here.
|
|
83
|
-
|
|
84
|
-
if (!isUnderActorDotDir && !isTestFile(changedFile.path) && dockerIgnoreMatcher(changedFile.path)) {
|
|
93
|
+
if (!isUnderActorDotDir && dockerIgnoreMatcher(changedFile.path)) {
|
|
85
94
|
return { impact: 'ignored' };
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -92,7 +101,7 @@ const classifyFileChange = async (
|
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
if (lowercaseFilePath.endsWith('.json') && isUnderActorDotDir) {
|
|
95
|
-
const isCosmetic = await isCosmeticOnlyJsonSchemaChange(
|
|
104
|
+
const isCosmetic = await isCosmeticOnlyJsonSchemaChange(range, changedFile.path);
|
|
96
105
|
if (isCosmetic) return { impact: 'cosmetic', semanticallyVerified: true };
|
|
97
106
|
}
|
|
98
107
|
|
|
@@ -112,6 +121,9 @@ const isExcludedBySibling = (filePath: RelativeFile, actor: ActorConfig, allActo
|
|
|
112
121
|
type ActorChangeEntry = {
|
|
113
122
|
actorConfig: ActorConfig;
|
|
114
123
|
files: string[];
|
|
124
|
+
hasFunctionalChange: boolean;
|
|
125
|
+
hasCosmeticChange: boolean;
|
|
126
|
+
hasTestChange: boolean;
|
|
115
127
|
};
|
|
116
128
|
|
|
117
129
|
type ChangeGroup = { actors: string[]; files: string[] };
|
|
@@ -164,16 +176,28 @@ const logChangeGroups = (groups: ChangeGroup[]): void => {
|
|
|
164
176
|
}
|
|
165
177
|
};
|
|
166
178
|
|
|
179
|
+
export interface ChangedActors {
|
|
180
|
+
/** Actors with at least one functional change. They need a new build and tests. */
|
|
181
|
+
functional: ActorConfig[];
|
|
182
|
+
/** Actors with cosmetic changes and no functional one. They only need a new build when released. */
|
|
183
|
+
cosmetic: ActorConfig[];
|
|
184
|
+
/**
|
|
185
|
+
* Actors with at least one platform test change, whatever else changed. Their tests need to run, but test changes
|
|
186
|
+
* alone need no release. An Actor can be in this list and in one of the others.
|
|
187
|
+
*/
|
|
188
|
+
test: ActorConfig[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Finds the Actors the changed files belong to, split by the most impactful change each Actor has. */
|
|
167
192
|
export const getChangedActors = async ({
|
|
168
193
|
filepathsChanged,
|
|
169
194
|
actorConfigs,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}: ShouldBuildAndTestOptions): Promise<ActorConfig[]> => {
|
|
195
|
+
range,
|
|
196
|
+
platformTestsGlob,
|
|
197
|
+
}: ShouldBuildAndTestOptions): Promise<ChangedActors> => {
|
|
174
198
|
const actorsChangedMap = new Map<string, ActorChangeEntry>();
|
|
175
199
|
const testFilesInContext = new Set<string>();
|
|
176
|
-
const isTestFile =
|
|
200
|
+
const isTestFile = createTestFilesMatcher(platformTestsGlob);
|
|
177
201
|
|
|
178
202
|
for (const actorConfig of actorConfigs) {
|
|
179
203
|
const dockerIgnoreMatcher = loadDockerIgnore(actorConfig.dockerContextDir);
|
|
@@ -185,27 +209,31 @@ export const getChangedActors = async ({
|
|
|
185
209
|
continue;
|
|
186
210
|
}
|
|
187
211
|
|
|
188
|
-
const change = await classifyFileChange(filePath, actorConfig,
|
|
212
|
+
const change = await classifyFileChange(filePath, actorConfig, range, dockerIgnoreMatcher, isTestFile);
|
|
189
213
|
if (change.impact !== 'outside-context' && isTestFile(originalFilePath)) {
|
|
190
214
|
testFilesInContext.add(originalFilePath);
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
if (change.impact === 'ignored' || change.impact === 'outside-context') continue;
|
|
194
|
-
if (change.impact === 'cosmetic' && !isLatest) continue;
|
|
195
218
|
|
|
196
|
-
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
219
|
+
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
220
|
+
actorConfig,
|
|
221
|
+
files: [],
|
|
222
|
+
hasFunctionalChange: false,
|
|
223
|
+
hasCosmeticChange: false,
|
|
224
|
+
hasTestChange: false,
|
|
225
|
+
};
|
|
197
226
|
entry.files.push(originalFilePath);
|
|
227
|
+
entry.hasFunctionalChange ||= change.impact === 'functional';
|
|
228
|
+
entry.hasCosmeticChange ||= change.impact === 'cosmetic';
|
|
229
|
+
entry.hasTestChange ||= change.impact === 'test';
|
|
198
230
|
actorsChangedMap.set(actorConfig.folder.path, entry);
|
|
199
231
|
}
|
|
200
232
|
}
|
|
201
233
|
|
|
202
|
-
const actorsChanged = Array.from(actorsChangedMap.values()).map((entry) => entry.actorConfig);
|
|
203
|
-
|
|
204
234
|
// Log changes grouped by actor set, so changes shared across actors are logged once
|
|
205
235
|
// instead of being repeated per actor.
|
|
206
|
-
|
|
207
|
-
const groups = groupFilesByActorSet(fileToActors);
|
|
208
|
-
logChangeGroups(groups);
|
|
236
|
+
logChangeGroups(groupFilesByActorSet(buildFileToActorsMap(actorsChangedMap)));
|
|
209
237
|
|
|
210
238
|
for (const testFile of filepathsChanged.filter(
|
|
211
239
|
(filePath) => isTestFile(filePath) && !testFilesInContext.has(filePath),
|
|
@@ -216,12 +244,17 @@ export const getChangedActors = async ({
|
|
|
216
244
|
);
|
|
217
245
|
}
|
|
218
246
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
const entries = Array.from(actorsChangedMap.values());
|
|
248
|
+
const functional = entries.filter((entry) => entry.hasFunctionalChange).map((entry) => entry.actorConfig);
|
|
249
|
+
const cosmetic = entries
|
|
250
|
+
.filter((entry) => !entry.hasFunctionalChange && entry.hasCosmeticChange)
|
|
251
|
+
.map((entry) => entry.actorConfig);
|
|
252
|
+
const test = entries.filter((entry) => entry.hasTestChange).map((entry) => entry.actorConfig);
|
|
253
|
+
|
|
254
|
+
const names = (configs: ActorConfig[]) => configs.map((config) => config.actorFullName).join(', ') || 'none';
|
|
255
|
+
logger.info(`[DIFF]: Actors with functional changes: ${names(functional)}`);
|
|
256
|
+
logger.info(`[DIFF]: Actors with only cosmetic changes: ${names(cosmetic)}`);
|
|
257
|
+
logger.info(`[DIFF]: Actors with platform test changes: ${names(test)}`);
|
|
225
258
|
|
|
226
|
-
return
|
|
259
|
+
return { functional, cosmetic, test };
|
|
227
260
|
};
|
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
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
|
-
import type { Commit
|
|
2
|
+
import type { Commit } from './types.js';
|
|
3
3
|
import { GitCommandError, runGitCommand } from './utils.js';
|
|
4
4
|
|
|
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
|
-
*
|
|
13
|
+
* Files that differ between two commits, i.e. changed by the commits in the range (fromRef, toRef]. fromRef's own
|
|
14
|
+
* changes are not included: to include them, pass its parent, `${sha}~` (as getChangedFiles does).
|
|
15
|
+
* Diffing the endpoints, rather than walking a commit list, is what keeps merges right: the oldest commit of a
|
|
16
|
+
* merged branch can predate fromRef, and its changes are already in fromRef.
|
|
10
17
|
*/
|
|
11
|
-
export const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw new Error('Cannot get changed files: the commit list is empty. This should never happen.');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const changedFilesString = await runGitCommand(
|
|
19
|
-
['diff', '--name-only', `${commits[0].sha}~..${commits[commits.length - 1].sha}`],
|
|
20
|
-
{ operation: 'get the list of changed files between commits' },
|
|
21
|
-
);
|
|
18
|
+
export const getChangedFilesBetween = async (fromRef: string, toRef: string) => {
|
|
19
|
+
const changedFilesString = await runGitCommand(['diff', ...LIST_CHANGED_PATHS, `${fromRef}..${toRef}`], {
|
|
20
|
+
operation: 'get the list of changed files between commits',
|
|
21
|
+
});
|
|
22
22
|
|
|
23
23
|
const changedFiles = changedFilesString.split('\n').filter(Boolean);
|
|
24
24
|
logger.info(`Changed files (up to 50): ${changedFiles.slice(0, 50).join(', ')}`);
|
|
@@ -26,134 +26,37 @@ export const getChangedFiles = async (commits: Commit[]) => {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* The commit `git diff base...head` compares against: where `head` forked from `base`, or the last commit of `base`
|
|
30
|
+
* that `head` merged in. The diff from it holds only what `head` itself changed, like GitHub's "Files changed" tab.
|
|
31
|
+
* With criss-cross merges there are several such commits and git picks one.
|
|
32
32
|
*/
|
|
33
|
-
export const
|
|
34
|
-
|
|
35
|
-
await runGitCommand(['log', '--merges', '--pretty=format:%H', `${targetBranch}..${sourceBranch}`], {
|
|
36
|
-
operation: 'find merge commits on the source branch that are absent from the target branch',
|
|
37
|
-
})
|
|
38
|
-
)
|
|
39
|
-
.split('\n')
|
|
40
|
-
.filter(Boolean);
|
|
33
|
+
export const getMergeBase = async (base: string, head: string): Promise<string> =>
|
|
34
|
+
runGitCommand(['merge-base', base, head], { operation: `find where ${head} forked from ${base}` });
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Returns all files touched by non-merge commits on the branch (full history, ignoring baseCommit).
|
|
73
|
-
* Used to check whether the branch itself has any functional changes, independent of what master merged in.
|
|
74
|
-
*/
|
|
75
|
-
export const getBranchOnlyChangedFiles = async (sourceBranch: string, targetBranch: string): Promise<string[]> => {
|
|
76
|
-
const output = await runGitCommand(
|
|
77
|
-
['log', '--no-merges', '--name-only', '--pretty=format:', `${targetBranch}..${sourceBranch}`],
|
|
78
|
-
{ operation: 'list files changed by non-merge commits on the source branch' },
|
|
79
|
-
);
|
|
80
|
-
return output.split('\n').filter(Boolean);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
36
|
+
/** Resolves a ref to a commit, or undefined when it isn't in the local history (e.g. it was force-pushed away). */
|
|
37
|
+
export const findCommitSha = async (ref: string): Promise<string | undefined> =>
|
|
38
|
+
runGitCommand(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
|
|
39
|
+
operation: `look up ${ref} in local Git history`,
|
|
40
|
+
}).catch((error: unknown) => {
|
|
41
|
+
// --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1
|
|
42
|
+
if (error instanceof GitCommandError && error.exitCode === 1) return undefined;
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
84
45
|
|
|
85
46
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* @returns The SHA string if valid, otherwise throws an error.
|
|
47
|
+
* Commits reachable from toRef but not from fromRef, oldest first: the range (fromRef, toRef], so fromRef itself is
|
|
48
|
+
* not included. To include it, pass its parent, `${sha}~`.
|
|
89
49
|
*/
|
|
90
|
-
export const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
sha = shaOrCommit;
|
|
97
|
-
}
|
|
98
|
-
if (!SHA_REGEX.test(sha)) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Invalid base commit SHA: "${sha}". It should be a 40-character hexadecimal string, instead got input: "${shaOrCommit}".`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return sha;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const fetchAllBranchCommits = async (sourceBranch: string, targetBranch: string): Promise<Commit[]> => {
|
|
107
|
-
const output = await runGitCommand(
|
|
108
|
-
['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${targetBranch}..${sourceBranch}`],
|
|
109
|
-
{ operation: 'list commits reachable from the source ref but absent from the target ref' },
|
|
110
|
-
);
|
|
111
|
-
const commitsStrings = output.split('\n');
|
|
112
|
-
const commits = commitsStrings.map((commitString) => parseCommit(commitString));
|
|
50
|
+
export const getCommitsBetween = async (fromRef: string, toRef: string): Promise<Commit[]> => {
|
|
51
|
+
const output = await runGitCommand(['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${fromRef}..${toRef}`], {
|
|
52
|
+
operation: 'list commits reachable from the source ref but absent from the target ref',
|
|
53
|
+
});
|
|
54
|
+
// An empty range (e.g. `x..x`) prints nothing
|
|
55
|
+
const commits = output.split('\n').filter(Boolean).map(parseCommit);
|
|
113
56
|
commits.reverse();
|
|
114
57
|
return commits;
|
|
115
58
|
};
|
|
116
59
|
|
|
117
|
-
/**
|
|
118
|
-
* Gets the commits between sourceBranch and targetBranch (exclusive).
|
|
119
|
-
* - If baseCommit is provided, only returns commits after the baseCommit.
|
|
120
|
-
*/
|
|
121
|
-
export const getCommits = async ({
|
|
122
|
-
sourceBranch,
|
|
123
|
-
targetBranch,
|
|
124
|
-
baseCommit,
|
|
125
|
-
}: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Promise<Commit[]> => {
|
|
126
|
-
const baseCommitSha = parseBaseCommit(baseCommit);
|
|
127
|
-
const commits = await fetchAllBranchCommits(sourceBranch, targetBranch);
|
|
128
|
-
|
|
129
|
-
// The last validated (base) commit being the branch HEAD means nothing new was pushed since the last
|
|
130
|
-
// validation — the dev reran the workflow (or force-pushed to the same state) to trigger a clean test
|
|
131
|
-
const headSha = commits[commits.length - 1]?.sha;
|
|
132
|
-
if (baseCommitSha !== undefined && baseCommitSha === headSha) {
|
|
133
|
-
logger.info(
|
|
134
|
-
`Detected rerun with the same commit that we already validated. This usually means the user wants to rerun the Action from scratch, ignoring last validated commit ${baseCommitSha} and returning all commits`,
|
|
135
|
-
);
|
|
136
|
-
logger.info(`Commits being returned: ${commits.map((c) => c.sha).join(', ')}`);
|
|
137
|
-
return commits;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const baseCommitIndex = commits.findIndex((commit) => commit.sha === baseCommitSha);
|
|
141
|
-
|
|
142
|
-
const hasBaseCommit = baseCommitIndex !== -1;
|
|
143
|
-
if (hasBaseCommit) {
|
|
144
|
-
const commitsUpToBaseCommit = commits.slice(baseCommitIndex + 1);
|
|
145
|
-
logger.info(
|
|
146
|
-
`Found base commit ${baseCommitSha} at index ${baseCommitIndex}, returning ${commitsUpToBaseCommit.length} commits after it`,
|
|
147
|
-
);
|
|
148
|
-
logger.info(`Commits being returned: ${commitsUpToBaseCommit.map((c) => c.sha).join(', ')}`);
|
|
149
|
-
return commitsUpToBaseCommit;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
logger.info(`Base commit ${baseCommitSha} not found in the commit range, returning all ${commits.length} commits`);
|
|
153
|
-
logger.info(`Commits being returned: ${commits.map((c) => c.sha).join(', ')}`);
|
|
154
|
-
return commits;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
60
|
export const parseCommit = (commitString: string): Commit => {
|
|
158
61
|
const splits = commitString.split(GIT_FORMAT_SEPARATOR);
|
|
159
62
|
if (splits.length !== 4) {
|
|
@@ -169,21 +72,25 @@ export const parseCommit = (commitString: string): Commit => {
|
|
|
169
72
|
};
|
|
170
73
|
|
|
171
74
|
/**
|
|
172
|
-
* Returns the currently checked-out branch.
|
|
75
|
+
* Returns the currently checked-out branch. Builds point the Actor version at this branch,
|
|
173
76
|
* so a detached HEAD (no branch to point at) is an error rather than a guess.
|
|
174
77
|
*/
|
|
175
78
|
export const getCurrentBranch = async (): Promise<string> => {
|
|
176
79
|
const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
177
|
-
operation: 'determine the
|
|
80
|
+
operation: 'determine the checked-out branch',
|
|
178
81
|
});
|
|
179
82
|
if (branch === 'HEAD') {
|
|
180
83
|
throw new Error(
|
|
181
|
-
'Cannot determine the branch to
|
|
84
|
+
'Cannot determine the branch to build from: HEAD is detached. Check out the branch you want to build.',
|
|
182
85
|
);
|
|
183
86
|
}
|
|
184
87
|
return branch;
|
|
185
88
|
};
|
|
186
89
|
|
|
90
|
+
/** Resolves a ref (branch, remote branch, SHA, HEAD, ...) to the full SHA of the commit it points to. */
|
|
91
|
+
export const resolveCommitSha = async (ref: string): Promise<string> =>
|
|
92
|
+
runGitCommand(['rev-parse', '--verify', `${ref}^{commit}`], { operation: `resolve ${ref} to a commit` });
|
|
93
|
+
|
|
187
94
|
/**
|
|
188
95
|
* Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
|
|
189
96
|
* uses for Git repo sources, e.g. git@github.com:apify-store/google-maps
|
|
@@ -223,11 +130,22 @@ export const normalizeRepoUrl = (repoUrl: string): string => {
|
|
|
223
130
|
.toLowerCase();
|
|
224
131
|
};
|
|
225
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Web link to a commit, e.g. https://github.com/apify-store/google-maps/commit/<sha>. Null for hosts other than
|
|
135
|
+
* GitHub, whose commit pages live under other paths.
|
|
136
|
+
*/
|
|
137
|
+
export const getCommitUrl = (repoUrl: string, sha: string): string | null => {
|
|
138
|
+
const repository = normalizeRepoUrl(repoUrl);
|
|
139
|
+
return repository.startsWith('github.com/') ? `https://${repository}/commit/${sha}` : null;
|
|
140
|
+
};
|
|
141
|
+
|
|
226
142
|
/** Repository name from its URL, e.g. `google-maps` for git@github.com:apify-store/google-maps.git */
|
|
227
143
|
export const getRepoName = (repoUrl: string): string => {
|
|
228
144
|
return normalizeRepoUrl(repoUrl).split('/').pop()!;
|
|
229
145
|
};
|
|
230
146
|
|
|
147
|
+
const SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
148
|
+
|
|
231
149
|
// Sent by e.g. GitHub as the "before" commit of a push that created the branch
|
|
232
150
|
const ZERO_SHA_REGEX = /^0{40}$/;
|
|
233
151
|
|
|
@@ -236,41 +154,37 @@ const ZERO_SHA_REGEX = /^0{40}$/;
|
|
|
236
154
|
* Unlike the PR path (getCommits), there is no lenient fallback. Falling back to "everything"
|
|
237
155
|
* would rebuild the latest build of every Actor and post it to Slack, so every problem is an error.
|
|
238
156
|
*/
|
|
239
|
-
export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<string> => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
157
|
+
export const resolveReleaseBaseCommit = async (baseCommit: string, head = 'HEAD'): Promise<string> => {
|
|
158
|
+
if (!SHA_REGEX.test(baseCommit)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Invalid base commit SHA: "${baseCommit}". It should be a 40-character hexadecimal string. ` +
|
|
161
|
+
'See the "Releasing Actors" section of docs/cli.md.',
|
|
162
|
+
);
|
|
243
163
|
}
|
|
164
|
+
const sha = baseCommit;
|
|
244
165
|
if (ZERO_SHA_REGEX.test(sha)) {
|
|
245
166
|
throw new Error(
|
|
246
167
|
`Base commit is ${sha}, which means the branch was just created and there is no previous release to diff against. ` +
|
|
247
|
-
`Rerun with
|
|
168
|
+
`Rerun with the base commit set to the last commit before the changes you want to release.`,
|
|
248
169
|
);
|
|
249
170
|
}
|
|
250
|
-
|
|
251
|
-
const verifiedSha = await runGitCommand(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`], {
|
|
252
|
-
operation: 'verify the release base commit in local Git history',
|
|
253
|
-
}).catch((error: unknown) => {
|
|
254
|
-
if (error instanceof GitCommandError && error.exitCode === 1) return '';
|
|
255
|
-
throw error;
|
|
256
|
-
});
|
|
257
|
-
if (!verifiedSha) {
|
|
171
|
+
if (!(await findCommitSha(sha))) {
|
|
258
172
|
throw new Error(
|
|
259
173
|
`Base commit ${sha} is not in the local git history. Either the checkout is shallow ` +
|
|
260
174
|
`(fetch the full history, e.g. fetch-depth: 0 in actions/checkout) or the branch was force-pushed.`,
|
|
261
175
|
);
|
|
262
176
|
}
|
|
263
177
|
// 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',
|
|
265
|
-
operation:
|
|
178
|
+
const mergeBase = await runGitCommand(['merge-base', sha, head], {
|
|
179
|
+
operation: `check whether the release base commit is an ancestor of ${head}`,
|
|
266
180
|
}).catch((error: unknown) => {
|
|
267
181
|
if (error instanceof GitCommandError && error.exitCode === 1) return '';
|
|
268
182
|
throw error;
|
|
269
183
|
});
|
|
270
184
|
if (mergeBase !== sha) {
|
|
271
185
|
throw new Error(
|
|
272
|
-
`Base commit ${sha} is not an ancestor of
|
|
273
|
-
`The changed files cannot be determined reliably. Rerun with
|
|
186
|
+
`Base commit ${sha} is not an ancestor of ${head}, most likely because the branch was force-pushed. ` +
|
|
187
|
+
`The changed files cannot be determined reliably. Rerun with the base commit set to an ancestor of ${head} ` +
|
|
274
188
|
`from before the changes you want to release.`,
|
|
275
189
|
);
|
|
276
190
|
}
|
|
@@ -279,12 +193,16 @@ export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<stri
|
|
|
279
193
|
|
|
280
194
|
const CHANGELOG_PATH = 'CHANGELOG.md';
|
|
281
195
|
|
|
282
|
-
/** Returns the lines added to the root CHANGELOG.md between
|
|
283
|
-
const getChangelogAdditions = async (
|
|
196
|
+
/** Returns the lines added to the root CHANGELOG.md between fromRef and toRef, or null if it didn't change. */
|
|
197
|
+
export const getChangelogAdditions = async (
|
|
198
|
+
fromRef: string,
|
|
199
|
+
toRef: string,
|
|
200
|
+
changedFiles: string[],
|
|
201
|
+
): Promise<string | null> => {
|
|
284
202
|
if (!changedFiles.includes(CHANGELOG_PATH)) {
|
|
285
203
|
return null;
|
|
286
204
|
}
|
|
287
|
-
const diff = await runGitCommand(['diff',
|
|
205
|
+
const diff = await runGitCommand(['diff', fromRef, toRef, '--', CHANGELOG_PATH], {
|
|
288
206
|
operation: 'read changelog changes since the last release',
|
|
289
207
|
});
|
|
290
208
|
|
|
@@ -307,23 +225,3 @@ const getChangelogAdditions = async (baseSha: string, changedFiles: string[]): P
|
|
|
307
225
|
}
|
|
308
226
|
return added.join('\n').trim();
|
|
309
227
|
};
|
|
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
|
-
};
|