@opengsd/gsd-core 1.20.0 → 1.21.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -20,6 +20,9 @@ const LINT_REASON = Object.freeze({
|
|
|
20
20
|
OK_FRAGMENT_PRESENT: 'ok_fragment_present',
|
|
21
21
|
OK_OPT_OUT_LABEL: 'ok_opt_out_label',
|
|
22
22
|
OK_NO_USER_FACING_CHANGES: 'ok_no_user_facing_changes',
|
|
23
|
+
// #5903: the release pipeline's own PRs consume every fragment into
|
|
24
|
+
// CHANGELOG.md, so they carry none by design.
|
|
25
|
+
OK_RELEASE_PIPELINE: 'ok_release_pipeline',
|
|
23
26
|
FAIL_MISSING_FRAGMENT: 'fail_missing_fragment',
|
|
24
27
|
FAIL_INVALID_FRAGMENT: 'fail_invalid_fragment',
|
|
25
28
|
FAIL_PR_FIELD_DRIFT: 'fail_pr_field_drift',
|
|
@@ -86,13 +89,18 @@ function findPrFieldDrift(prEntries, realPrNumber) {
|
|
|
86
89
|
return drift;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
function evaluateLint({
|
|
92
|
+
function evaluateLint({
|
|
93
|
+
changedFiles, labels, fragmentFailures = [], prFieldDrift = [], releasePipelineKind = null,
|
|
94
|
+
}) {
|
|
90
95
|
if (fragmentFailures.length > 0) {
|
|
91
96
|
return { ok: false, reason: LINT_REASON.FAIL_INVALID_FRAGMENT, failures: fragmentFailures };
|
|
92
97
|
}
|
|
93
98
|
if (prFieldDrift.length > 0) {
|
|
94
99
|
return { ok: false, reason: LINT_REASON.FAIL_PR_FIELD_DRIFT, drift: prFieldDrift };
|
|
95
100
|
}
|
|
101
|
+
if (releasePipelineKind) {
|
|
102
|
+
return { ok: true, reason: LINT_REASON.OK_RELEASE_PIPELINE, releasePipelineKind };
|
|
103
|
+
}
|
|
96
104
|
if (changedFiles.some(isFragment)) {
|
|
97
105
|
return { ok: true, reason: LINT_REASON.OK_FRAGMENT_PRESENT };
|
|
98
106
|
}
|
|
@@ -107,6 +115,7 @@ function evaluateLint({ changedFiles, labels, fragmentFailures = [], prFieldDrif
|
|
|
107
115
|
|
|
108
116
|
const { ExitError, runMain } = require('../lib/cli-exit.cjs');
|
|
109
117
|
const { parseFragment } = require('./parse.cjs');
|
|
118
|
+
const { releasePipelinePrKind } = require('../release-pipeline-pr.cjs');
|
|
110
119
|
|
|
111
120
|
function main() {
|
|
112
121
|
const fs = require('node:fs');
|
|
@@ -119,6 +128,12 @@ function main() {
|
|
|
119
128
|
// `pull_request` in the payload, or no payload at all) — the drift check
|
|
120
129
|
// below is a no-op in that case, it never fails a push run.
|
|
121
130
|
let realPrNumber = null;
|
|
131
|
+
// #5903: head/base refs and whether the head lives in this repository, for
|
|
132
|
+
// the release-pipeline exemption. sameRepo is false when anything is missing
|
|
133
|
+
// (a deleted fork has head.repo === null).
|
|
134
|
+
let headRef;
|
|
135
|
+
let baseRef;
|
|
136
|
+
let sameRepo = false;
|
|
122
137
|
if (eventPath && fs.existsSync(eventPath)) {
|
|
123
138
|
try {
|
|
124
139
|
const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
@@ -126,6 +141,12 @@ function main() {
|
|
|
126
141
|
if (event.pull_request && Number.isInteger(event.pull_request.number)) {
|
|
127
142
|
realPrNumber = event.pull_request.number;
|
|
128
143
|
}
|
|
144
|
+
headRef = event.pull_request?.head?.ref;
|
|
145
|
+
baseRef = event.pull_request?.base?.ref;
|
|
146
|
+
const headRepo = event.pull_request?.head?.repo?.full_name;
|
|
147
|
+
const thisRepo = process.env.GITHUB_REPOSITORY;
|
|
148
|
+
sameRepo = typeof headRepo === 'string' && typeof thisRepo === 'string'
|
|
149
|
+
&& headRepo !== '' && headRepo.toLowerCase() === thisRepo.toLowerCase();
|
|
129
150
|
} catch { /* fall through */ }
|
|
130
151
|
}
|
|
131
152
|
// #2988: local fallback must match the repo's integration branch (`next`),
|
|
@@ -176,11 +197,15 @@ function main() {
|
|
|
176
197
|
}
|
|
177
198
|
|
|
178
199
|
const prFieldDrift = findPrFieldDrift(prEntries, realPrNumber);
|
|
179
|
-
const
|
|
200
|
+
const releasePipelineKind = releasePipelinePrKind({ headRef, baseRef, sameRepo });
|
|
201
|
+
const verdict = evaluateLint({
|
|
202
|
+
changedFiles, labels, fragmentFailures, prFieldDrift, releasePipelineKind,
|
|
203
|
+
});
|
|
180
204
|
if (process.argv.includes('--json')) {
|
|
181
205
|
process.stdout.write(JSON.stringify({ ...verdict, changedFiles, labels }, null, 2) + '\n');
|
|
182
206
|
} else if (verdict.ok) {
|
|
183
|
-
|
|
207
|
+
const kind = verdict.releasePipelineKind ? ` (${verdict.releasePipelineKind})` : '';
|
|
208
|
+
process.stdout.write(`ok changeset-lint: ${verdict.reason}${kind}\n`);
|
|
184
209
|
} else if (verdict.reason === LINT_REASON.FAIL_INVALID_FRAGMENT) {
|
|
185
210
|
process.stderr.write(`\nERROR changeset-lint: ${verdict.reason}\n`);
|
|
186
211
|
process.stderr.write(`The following .changeset fragment(s) failed content validation:\n`);
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Body of a dated release rendered by `render --allow-empty` when no fragment
|
|
4
|
+
// exists (cli.cjs injectEmptyPlaceholder). Shared so consumers recognise it.
|
|
5
|
+
const EMPTY_RELEASE_PLACEHOLDER = '_No notable changes._';
|
|
6
|
+
|
|
7
|
+
// F3: matches linked headers: ## [1.42.1](url) - 2026-05-15. The
|
|
8
|
+
// (?:\([^)]*\))? group skips an optional (url) after the closing ] before the
|
|
9
|
+
// optional date suffix. Exported so line-slicing consumers (release-notes
|
|
10
|
+
// builder, #5922) locate headings with the exact regex parseChangelog uses.
|
|
11
|
+
const RELEASE_HEADING_RE = /^##\s+\[([^\]]+)\](?:\([^)]*\))?\s*(?:-\s*(\S+))?/;
|
|
12
|
+
|
|
3
13
|
/**
|
|
4
14
|
* Markdown serializer + parser for the changelog IR. The two are inverses
|
|
5
15
|
* over the well-formed subset; tests assert via round-trip (parse(serialize(ir)))
|
|
@@ -87,7 +97,7 @@ function parseChangelog(text) {
|
|
|
87
97
|
// before looking for the optional date suffix.
|
|
88
98
|
// F6: strip a leading `v` from the captured version so `## [v1.0.0]`
|
|
89
99
|
// parses as version "1.0.0" instead of "v1.0.0".
|
|
90
|
-
const releaseMatch = line.match(
|
|
100
|
+
const releaseMatch = line.match(RELEASE_HEADING_RE);
|
|
91
101
|
if (releaseMatch) {
|
|
92
102
|
flushBullet();
|
|
93
103
|
const rawVersion = releaseMatch[1];
|
|
@@ -131,4 +141,4 @@ function parseChangelog(text) {
|
|
|
131
141
|
return { releases };
|
|
132
142
|
}
|
|
133
143
|
|
|
134
|
-
module.exports = { serializeChangelog, parseChangelog };
|
|
144
|
+
module.exports = { serializeChangelog, parseChangelog, RELEASE_HEADING_RE, EMPTY_RELEASE_PLACEHOLDER };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compacts one CI shard's raw V8 coverage dumps into a single merged file.
|
|
6
|
+
*
|
|
7
|
+
* Why: the `coverage-gate` job used to download every shard's raw dumps
|
|
8
|
+
* (~1.68 GB across 3 shards, mostly node internals, tests/ and
|
|
9
|
+
* node_modules scripts that c8 discards at report time) and make
|
|
10
|
+
* `c8 report --merge-async` parse and merge all of it twice (lib report +
|
|
11
|
+
* scripts floor). Doing the same filter + merge on each shard, in parallel
|
|
12
|
+
* with the other shards, leaves the gate three small files to merge.
|
|
13
|
+
*
|
|
14
|
+
* Exactness: this applies the SAME steps c8's Report applies before it
|
|
15
|
+
* converts to istanbul — `_normalizeProcessCov` (node: urls, file:// urls,
|
|
16
|
+
* include/exclude filter), `_normalizeSourceMapCache` (file-url keys; a
|
|
17
|
+
* non-file key throws, as it does in c8) and then `@bcoe/v8-coverage`
|
|
18
|
+
* `mergeProcessCovs`. Merging is applied in batches of MERGE_BATCH_SIZE
|
|
19
|
+
* (`mergeProcessCovs([acc, ...batch])`) so peak memory does not grow with the
|
|
20
|
+
* number of dumps (#6140). Merge grouping preserves every per-offset execution
|
|
21
|
+
* count (what line/statement coverage reads), but @bcoe/v8-coverage
|
|
22
|
+
* normalization is not associative in range structure: a block range can be
|
|
23
|
+
* wrapped or joined differently than in a one-pass merge, so istanbul branch
|
|
24
|
+
* totals may differ by merge grouping, the same variance the shard -> gate
|
|
25
|
+
* merge already has (pinned by test). The merged output is a valid c8
|
|
26
|
+
* temp-directory dump, so the gate's unchanged
|
|
27
|
+
* `c8 report` commands read it as if it were one more raw dump. Retained urls
|
|
28
|
+
* are the UNION of every include glob the gate uses, so both
|
|
29
|
+
* `test:coverage:report` and `test:coverage:scripts-floor` see all of the
|
|
30
|
+
* data they would have seen before. The one deliberate deviation from raw c8
|
|
31
|
+
* is `normalizeBlockFlags`, which makes a never-called function's block flag
|
|
32
|
+
* independent of dump count (#5985).
|
|
33
|
+
*
|
|
34
|
+
* Guard: a shard that produced no V8 coverage dumps at all throws, restoring
|
|
35
|
+
* the `if-no-files-found: error` guarantee the raw-dump upload step gave.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
const { createRequire } = require('module');
|
|
41
|
+
const { fileURLToPath, pathToFileURL } = require('url');
|
|
42
|
+
|
|
43
|
+
// Single source of truth for what the gate may report on. Must stay equal to
|
|
44
|
+
// the union of the --include globs in package.json's test:coverage:report and
|
|
45
|
+
// test:coverage:scripts-floor (tests/ci-compact-v8-coverage.test.cjs pins it).
|
|
46
|
+
const INCLUDE_GLOBS = ['gsd-core/bin/lib/**/*.cjs', 'scripts/**/*.cjs'];
|
|
47
|
+
const EXCLUDE_GLOBS = ['tests/**'];
|
|
48
|
+
// Bounds how many filtered dumps are held before folding them into the accumulator (#6140).
|
|
49
|
+
const MERGE_BATCH_SIZE = 64;
|
|
50
|
+
|
|
51
|
+
function loadDeps(cwd) {
|
|
52
|
+
// Resolve through c8 so the versions are exactly the ones c8 itself uses.
|
|
53
|
+
const c8Require = createRequire(require.resolve('c8/package.json', { paths: [cwd, __dirname] }));
|
|
54
|
+
return {
|
|
55
|
+
mergeProcessCovs: c8Require('@bcoe/v8-coverage').mergeProcessCovs,
|
|
56
|
+
Exclude: c8Require('test-exclude'),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Normalized href for a retained script url, or null when c8 would discard it.
|
|
61
|
+
function retainedHref(shouldInstrument, url) {
|
|
62
|
+
if (typeof url !== 'string' || !/^file:\/\//.test(url)) return null; // node: and non-file urls
|
|
63
|
+
const filePath = fileURLToPath(url);
|
|
64
|
+
return shouldInstrument(filePath) ? pathToFileURL(filePath).href : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The same rule @bcoe/v8-coverage mergeFunctionCovs applies on a real merge
|
|
68
|
+
// (merge.js:158). Its single-input fast path skips it, so under c8's pairwise
|
|
69
|
+
// merge a never-called function adds a 0-count branch only when it appears in
|
|
70
|
+
// an odd number of dumps. Applying the rule unconditionally makes the gate's
|
|
71
|
+
// branch count independent of how dumps are split into shards (#5985).
|
|
72
|
+
function normalizeBlockFlags(result) {
|
|
73
|
+
for (const script of result) {
|
|
74
|
+
for (const fn of script.functions) {
|
|
75
|
+
if (fn.ranges.length === 1 && fn.ranges[0].count === 0) fn.isBlockCoverage = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// `mergeProcessCovs` is an injection point for tests; main() never passes it.
|
|
81
|
+
function compact({ tempDirectory, outFile, cwd = process.cwd(), batchSize = MERGE_BATCH_SIZE, mergeProcessCovs: mergeOverride }) {
|
|
82
|
+
if (!Number.isInteger(batchSize) || batchSize < 1) {
|
|
83
|
+
throw new RangeError(`ci-compact-v8-coverage: batchSize must be a positive integer, got ${batchSize}`);
|
|
84
|
+
}
|
|
85
|
+
const deps = loadDeps(cwd);
|
|
86
|
+
const mergeProcessCovs = mergeOverride || deps.mergeProcessCovs;
|
|
87
|
+
const { Exclude } = deps;
|
|
88
|
+
const exclude = new Exclude({
|
|
89
|
+
cwd,
|
|
90
|
+
include: INCLUDE_GLOBS,
|
|
91
|
+
// Exactly c8's set: an explicit --exclude REPLACES istanbul's default exclude
|
|
92
|
+
// (c8 lib/parse-args.js `exclude` default), so defaultExclude must not apply.
|
|
93
|
+
exclude: [...EXCLUDE_GLOBS],
|
|
94
|
+
relativePath: true,
|
|
95
|
+
excludeNodeModules: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Memoized per path, as c8 does (report.js shouldInstrumentCache): the same
|
|
99
|
+
// node_modules scripts repeat across every dump.
|
|
100
|
+
const instrumentCache = new Map();
|
|
101
|
+
const shouldInstrument = (filePath) => {
|
|
102
|
+
let decision = instrumentCache.get(filePath);
|
|
103
|
+
if (decision === undefined) {
|
|
104
|
+
decision = exclude.shouldInstrument(filePath);
|
|
105
|
+
instrumentCache.set(filePath, decision);
|
|
106
|
+
}
|
|
107
|
+
return decision;
|
|
108
|
+
};
|
|
109
|
+
const hrefCache = new Map();
|
|
110
|
+
const hrefFor = (url) => {
|
|
111
|
+
if (typeof url !== 'string') return null;
|
|
112
|
+
let href = hrefCache.get(url);
|
|
113
|
+
if (href === undefined) {
|
|
114
|
+
href = retainedHref(shouldInstrument, url);
|
|
115
|
+
hrefCache.set(url, href);
|
|
116
|
+
}
|
|
117
|
+
return href;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let merged = null;
|
|
121
|
+
let pending = [];
|
|
122
|
+
// Count equality across merge groupings relies on a V8 invariant: detailed
|
|
123
|
+
// (NODE_V8_COVERAGE) coverage reports isBlockCoverage:false only for a
|
|
124
|
+
// never-compiled function (one range, count 0). @bcoe/v8-coverage drops
|
|
125
|
+
// function-level entries when block-level ones exist, so a function-level
|
|
126
|
+
// entry with count > 0 would merge differently by grouping.
|
|
127
|
+
const flush = () => {
|
|
128
|
+
if (pending.length === 0) return;
|
|
129
|
+
merged = mergeProcessCovs(merged ? [merged, ...pending] : pending);
|
|
130
|
+
pending = [];
|
|
131
|
+
};
|
|
132
|
+
const sourceMapCache = {};
|
|
133
|
+
let inputFiles = 0;
|
|
134
|
+
for (const name of fs.readdirSync(tempDirectory).sort()) {
|
|
135
|
+
let dump;
|
|
136
|
+
try {
|
|
137
|
+
dump = JSON.parse(fs.readFileSync(path.join(tempDirectory, name), 'utf8'));
|
|
138
|
+
} catch {
|
|
139
|
+
continue; // c8 skips unparsable dumps too
|
|
140
|
+
}
|
|
141
|
+
if (!dump || !Array.isArray(dump.result)) continue;
|
|
142
|
+
inputFiles++;
|
|
143
|
+
const result = [];
|
|
144
|
+
for (const script of dump.result) {
|
|
145
|
+
const href = hrefFor(script.url);
|
|
146
|
+
if (href === null) continue;
|
|
147
|
+
script.url = href;
|
|
148
|
+
result.push(script);
|
|
149
|
+
}
|
|
150
|
+
const cache = dump['source-map-cache'];
|
|
151
|
+
if (cache) {
|
|
152
|
+
for (const key of Object.keys(cache)) {
|
|
153
|
+
// c8's _normalizeSourceMapCache calls fileURLToPath(key) unguarded: a
|
|
154
|
+
// non-file key throws there, so it throws here too.
|
|
155
|
+
const keyPath = fileURLToPath(key);
|
|
156
|
+
if (shouldInstrument(keyPath)) sourceMapCache[pathToFileURL(keyPath).href] = cache[key];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
pending.push({ result });
|
|
160
|
+
if (pending.length >= batchSize) flush();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (inputFiles === 0) {
|
|
164
|
+
throw new Error(`ci-compact-v8-coverage: no V8 coverage dumps in ${tempDirectory}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
flush();
|
|
168
|
+
const out = { result: merged ? merged.result : [], 'source-map-cache': sourceMapCache };
|
|
169
|
+
normalizeBlockFlags(out.result);
|
|
170
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
171
|
+
fs.writeFileSync(outFile, JSON.stringify(out));
|
|
172
|
+
return { inputFiles, scripts: out.result.length };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const USAGE = 'usage: ci-compact-v8-coverage.cjs --out <file> [--temp-directory <dir>]';
|
|
176
|
+
|
|
177
|
+
function parseArgs(argv) {
|
|
178
|
+
// undefined: flag absent; null: flag present without a usable value.
|
|
179
|
+
const value = (flag) => {
|
|
180
|
+
const i = argv.indexOf(flag);
|
|
181
|
+
if (i === -1) return undefined;
|
|
182
|
+
const v = argv[i + 1];
|
|
183
|
+
return v === undefined || v.startsWith('--') ? null : v;
|
|
184
|
+
};
|
|
185
|
+
const outFile = value('--out');
|
|
186
|
+
const tempDirectory = value('--temp-directory');
|
|
187
|
+
if (!outFile || tempDirectory === null) return { error: USAGE };
|
|
188
|
+
return { tempDirectory: tempDirectory === undefined ? path.join('coverage', 'tmp') : tempDirectory, outFile };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function main(argv) {
|
|
192
|
+
const parsed = parseArgs(argv);
|
|
193
|
+
if (parsed.error) {
|
|
194
|
+
console.error(parsed.error);
|
|
195
|
+
process.exitCode = 2;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const stats = compact({ tempDirectory: parsed.tempDirectory, outFile: parsed.outFile });
|
|
199
|
+
console.log(`compacted ${stats.inputFiles} dumps -> ${stats.scripts} scripts in ${parsed.outFile}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (require.main === module) main(process.argv.slice(2));
|
|
203
|
+
|
|
204
|
+
module.exports = { compact, parseArgs, main, INCLUDE_GLOBS, EXCLUDE_GLOBS, MERGE_BATCH_SIZE };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// ci-fix-next-rerun.cjs — #6076: re-run ONLY the `Base branch health` job of a
|
|
5
|
+
// PR's latest Tests run after the `fix-next` label was added or removed.
|
|
6
|
+
//
|
|
7
|
+
// Runs from .github/workflows/fix-next-rerun.yml (pull_request_target) against
|
|
8
|
+
// the BASE sha's copy of this file, never the PR's. The core is dependency
|
|
9
|
+
// injected (`gh`, `now`, `sleep`) so every branch is tested without a network.
|
|
10
|
+
|
|
11
|
+
const { execFile } = require('node:child_process');
|
|
12
|
+
const { runMain } = require('./lib/cli-exit.cjs');
|
|
13
|
+
|
|
14
|
+
const BASE_HEALTH_JOB_NAME = 'Base branch health';
|
|
15
|
+
const TESTS_WORKFLOW_FILE = 'test.yml';
|
|
16
|
+
const GH_TIMEOUT_MS = 30000;
|
|
17
|
+
const DEFAULT_DEADLINE_MS = 85 * 60 * 1000;
|
|
18
|
+
const DEFAULT_POLL_MS = 30 * 1000;
|
|
19
|
+
const PR_NUMBER_PATTERN = /^[1-9]\d*$/;
|
|
20
|
+
const HEAD_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {(args:string[]) => Promise<string>} opts.gh stdout of `gh <args>`; throws on failure
|
|
25
|
+
* @param {string} opts.repo owner/repo
|
|
26
|
+
* @param {string} opts.headSha head sha from the label event payload
|
|
27
|
+
* @param {string} opts.prNumber PR number
|
|
28
|
+
* @param {() => number} opts.now ms clock (read once at start, once per incomplete poll)
|
|
29
|
+
* @param {(ms:number) => Promise<void>} opts.sleep
|
|
30
|
+
* @param {number} opts.deadlineMs how long to wait for the run to complete
|
|
31
|
+
* @param {number} opts.pollMs
|
|
32
|
+
* @returns {Promise<{action:'rerun'|'skip'|'error', reason:string, runId?:string, jobId?:string}>}
|
|
33
|
+
*/
|
|
34
|
+
async function rerunBaseHealth({
|
|
35
|
+
gh, repo, headSha, prNumber, now, sleep, deadlineMs = DEFAULT_DEADLINE_MS, pollMs = DEFAULT_POLL_MS,
|
|
36
|
+
}) {
|
|
37
|
+
try {
|
|
38
|
+
const deadline = now() + deadlineMs;
|
|
39
|
+
let run;
|
|
40
|
+
for (;;) {
|
|
41
|
+
const listed = JSON.parse(await gh([
|
|
42
|
+
'run', 'list', '--workflow', TESTS_WORKFLOW_FILE, '--commit', headSha,
|
|
43
|
+
'--event', 'pull_request', '--limit', '1', '--json', 'databaseId,status,conclusion',
|
|
44
|
+
]));
|
|
45
|
+
if (!Array.isArray(listed) || listed.length === 0) return { action: 'skip', reason: 'no-run' };
|
|
46
|
+
[run] = listed;
|
|
47
|
+
if (run.status === 'completed') break;
|
|
48
|
+
if (now() >= deadline) return { action: 'error', reason: 'timeout', runId: String(run.databaseId) };
|
|
49
|
+
await sleep(pollMs);
|
|
50
|
+
}
|
|
51
|
+
const runId = String(run.databaseId);
|
|
52
|
+
|
|
53
|
+
// A cancelled run was superseded by a newer one, which reads the label from
|
|
54
|
+
// the API; re-running a job here would rejoin the Tests concurrency group
|
|
55
|
+
// and could cancel that newer run.
|
|
56
|
+
if (run.conclusion === 'cancelled') return { action: 'skip', reason: 'run-cancelled', runId };
|
|
57
|
+
|
|
58
|
+
// A run waiting for approval has not executed; there is nothing to re-run.
|
|
59
|
+
if (run.conclusion === 'action_required') return { action: 'skip', reason: 'run-awaiting-approval', runId };
|
|
60
|
+
|
|
61
|
+
const readHead = async () => (await gh(['api', `repos/${repo}/pulls/${prNumber}`, '--jq', '.head.sha'])).trim();
|
|
62
|
+
if ((await readHead()) !== headSha) return { action: 'skip', reason: 'head-moved', runId };
|
|
63
|
+
|
|
64
|
+
const view = JSON.parse(await gh(['run', 'view', runId, '--json', 'jobs']));
|
|
65
|
+
const jobs = view && Array.isArray(view.jobs) ? view.jobs : [];
|
|
66
|
+
const job = jobs.find((j) => j && j.name === BASE_HEALTH_JOB_NAME);
|
|
67
|
+
if (!job) return { action: 'error', reason: 'no-job', runId };
|
|
68
|
+
const jobId = String(job.databaseId);
|
|
69
|
+
|
|
70
|
+
// Second read immediately before the re-run, to shrink the stale-run window.
|
|
71
|
+
if ((await readHead()) !== headSha) return { action: 'skip', reason: 'head-moved', runId };
|
|
72
|
+
|
|
73
|
+
await gh(['run', 'rerun', runId, '--job', jobId]);
|
|
74
|
+
return { action: 'rerun', reason: 'rerun', runId, jobId };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return { action: 'error', reason: `gh-failed: ${err && err.message ? err.message : String(err)}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function realGh(args) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
execFile('gh', args, { timeout: GH_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, encoding: 'utf8' }, (err, stdout) => {
|
|
83
|
+
if (err) reject(err);
|
|
84
|
+
else resolve(stdout);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {object} [deps] test seams: env, gh, now, sleep, stdout, stderr, deadlineMs, pollMs
|
|
91
|
+
* @returns {Promise<number>} exit code
|
|
92
|
+
*/
|
|
93
|
+
async function main(deps = {}) {
|
|
94
|
+
const env = deps.env || process.env;
|
|
95
|
+
const out = deps.stdout || ((s) => process.stdout.write(s));
|
|
96
|
+
const errOut = deps.stderr || ((s) => process.stderr.write(s));
|
|
97
|
+
|
|
98
|
+
const prNumber = (env.PR_NUMBER || '').trim();
|
|
99
|
+
if (!PR_NUMBER_PATTERN.test(prNumber)) {
|
|
100
|
+
errOut(`::error::PR_NUMBER must be a positive integer, got ${JSON.stringify(prNumber)}\n`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const headSha = (env.HEAD_SHA || '').trim();
|
|
104
|
+
if (!HEAD_SHA_PATTERN.test(headSha)) {
|
|
105
|
+
errOut('::error::HEAD_SHA must be a 40-character hex sha\n');
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
const repo = (env.GH_REPO || '').trim();
|
|
109
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
110
|
+
errOut('::error::GH_REPO must be owner/repo\n');
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await rerunBaseHealth({
|
|
115
|
+
gh: deps.gh || realGh,
|
|
116
|
+
repo,
|
|
117
|
+
headSha,
|
|
118
|
+
prNumber,
|
|
119
|
+
now: deps.now || Date.now,
|
|
120
|
+
sleep: deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
121
|
+
deadlineMs: deps.deadlineMs,
|
|
122
|
+
pollMs: deps.pollMs,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (result.action === 'error') {
|
|
126
|
+
errOut(`::error::fix-next re-run failed (${result.reason})${result.runId ? ` on Tests run ${result.runId}` : ''}; re-run "${BASE_HEALTH_JOB_NAME}" by hand.\n`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
if (result.action === 'skip') {
|
|
130
|
+
out(`::notice::fix-next re-run skipped (${result.reason}); the newer or upcoming run reads the label from the API.\n`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
out(`Re-ran "${BASE_HEALTH_JOB_NAME}" (job ${result.jobId}) on Tests run ${result.runId}.\n`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (require.main === module) {
|
|
138
|
+
runMain(main);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
BASE_HEALTH_JOB_NAME,
|
|
143
|
+
TESTS_WORKFLOW_FILE,
|
|
144
|
+
DEFAULT_DEADLINE_MS,
|
|
145
|
+
DEFAULT_POLL_MS,
|
|
146
|
+
GH_TIMEOUT_MS,
|
|
147
|
+
rerunBaseHealth,
|
|
148
|
+
main,
|
|
149
|
+
};
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// with a non-success conclusion for the base branch's last push-triggered Tests
|
|
18
18
|
// run, that is treated as authoritative and blocks — with one explicit, visible,
|
|
19
19
|
// human-operated escape hatch (the `fix-next` label) for the PR that is itself
|
|
20
|
-
// the fix-forward.
|
|
20
|
+
// the fix-forward. On a pull request the label is read from the API (#6076), so
|
|
21
|
+
// after it is added the fix-next-rerun workflow re-runs this check and sees it.
|
|
21
22
|
//
|
|
22
23
|
// Every non-success conclusion (`failure`, `cancelled`, `timed_out`,
|
|
23
24
|
// `action_required`, ...) is treated as RED, not just `failure` — a cancelled or
|
|
@@ -205,11 +206,14 @@ async function evaluateQueueBypass({ headRef, baseRef, baseSha, headSha, repo, g
|
|
|
205
206
|
* (or throwing) — injected so tests never touch the network.
|
|
206
207
|
* `evaluateQueueBypass` (merge_group only) is an async function returning
|
|
207
208
|
* `{authorized, reason, actor}`; it is how a queued fix-forward is recognised.
|
|
209
|
+
* `fetchPrLabels` (pull_request only, RED base only) is an async function
|
|
210
|
+
* returning the PR's current label names (or throwing, which keeps the gate
|
|
211
|
+
* RED with reason `label-read-failed`); when absent, `labels` decides (#6076).
|
|
208
212
|
*
|
|
209
213
|
* @returns {Promise<{verdict:string, payload:*, reason:string}>}
|
|
210
214
|
*/
|
|
211
215
|
async function resolveNextHealth({
|
|
212
|
-
fetchLatestRun, eventName, baseRef, labels, evaluateQueueBypass: queueBypass,
|
|
216
|
+
fetchLatestRun, eventName, baseRef, labels, fetchPrLabels, evaluateQueueBypass: queueBypass,
|
|
213
217
|
} = {}) {
|
|
214
218
|
if (!APPLICABLE_EVENTS.has(eventName)) {
|
|
215
219
|
return { verdict: VERDICT.SKIPPED_NOT_APPLICABLE, payload: null, reason: 'not-applicable-event' };
|
|
@@ -235,10 +239,25 @@ async function resolveNextHealth({
|
|
|
235
239
|
return { verdict: assessed.verdict, payload: evidence, reason };
|
|
236
240
|
}
|
|
237
241
|
|
|
238
|
-
// Escape hatch for the PR that is itself the fix-forward. pull_request
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
+
// Escape hatch for the PR that is itself the fix-forward. A pull_request run
|
|
243
|
+
// with `fetchPrLabels` reads the PR's CURRENT labels from the API (#6076), so
|
|
244
|
+
// a re-run sees a label added or removed after the run started; a failed read
|
|
245
|
+
// keeps the gate RED. Without it, the event's `labels` decide (bootstrap).
|
|
246
|
+
// merge_group events carry no labels, so the queue run asks the API instead
|
|
247
|
+
// (evaluateQueueBypass).
|
|
248
|
+
let labelList = Array.isArray(labels) ? labels : [];
|
|
249
|
+
if (eventName === 'pull_request' && typeof fetchPrLabels === 'function') {
|
|
250
|
+
try {
|
|
251
|
+
const current = await fetchPrLabels();
|
|
252
|
+
labelList = Array.isArray(current) ? current : [];
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
verdict: VERDICT.RED,
|
|
256
|
+
payload: evidence,
|
|
257
|
+
reason: `${assessed.reason === 'resolved' ? 'red' : assessed.reason}; label-read-failed: ${err.message}`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
242
261
|
if (labelList.includes(BYPASS_LABEL)) {
|
|
243
262
|
return { verdict: VERDICT.BYPASSED, payload: evidence, reason: 'bypass-label' };
|
|
244
263
|
}
|
|
@@ -281,6 +300,9 @@ function usage() {
|
|
|
281
300
|
'newest settled run decides. Every other case (wrong event, no',
|
|
282
301
|
'resolvable base ref, an unreadable API read) fails open (exit 0).',
|
|
283
302
|
'',
|
|
303
|
+
'On pull_request with PR_NUMBER set, the label is read from the API, so a',
|
|
304
|
+
're-run sees the PR\'s current labels (#6076); a failed read keeps the gate RED.',
|
|
305
|
+
'',
|
|
284
306
|
'On merge_group the label is read back from the API: the queue ref must',
|
|
285
307
|
'name a PR, the group must hold exactly one commit, the PR must carry the',
|
|
286
308
|
'label, and whoever applied it must have write or admin permission.',
|
|
@@ -300,7 +322,15 @@ function usage() {
|
|
|
300
322
|
' PR_LABELS comma-separated PR label names (pull_request events;',
|
|
301
323
|
' the caller workflow must populate this from',
|
|
302
324
|
' github.event.pull_request.labels.*.name); checked',
|
|
303
|
-
' for the literal "fix-next" bypass label',
|
|
325
|
+
' for the literal "fix-next" bypass label; the fallback',
|
|
326
|
+
' when PR_NUMBER is unset or malformed',
|
|
327
|
+
' PR_NUMBER PR number (pull_request events; from',
|
|
328
|
+
' github.event.pull_request.number); trimmed, then it',
|
|
329
|
+
' must match /^[1-9]\\d*$/. When valid, the label is',
|
|
330
|
+
' read from the API so a re-run sees the current',
|
|
331
|
+
' label; a failed read keeps the gate RED. A',
|
|
332
|
+
' malformed value (0, 07, -1, abc, empty) falls back',
|
|
333
|
+
' to PR_LABELS',
|
|
304
334
|
' MERGE_GROUP_HEAD_REF merge-group queue branch',
|
|
305
335
|
' (github.event.merge_group.head_ref; falls back to',
|
|
306
336
|
' GITHUB_REF)',
|
|
@@ -437,8 +467,19 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
437
467
|
ghGet,
|
|
438
468
|
});
|
|
439
469
|
|
|
470
|
+
// #6076: with a valid PR number, read the PR's current labels from the API.
|
|
471
|
+
const prNumber = (process.env.PR_NUMBER || '').trim();
|
|
472
|
+
const fetchPrLabels = /^[1-9]\d*$/.test(prNumber)
|
|
473
|
+
? async () => {
|
|
474
|
+
const body = await ghGet(`/repos/${repo}/issues/${prNumber}/labels?per_page=100`);
|
|
475
|
+
return Array.isArray(body)
|
|
476
|
+
? body.map((l) => l && l.name).filter((name) => typeof name === 'string')
|
|
477
|
+
: [];
|
|
478
|
+
}
|
|
479
|
+
: undefined;
|
|
480
|
+
|
|
440
481
|
const result = await resolveNextHealth({
|
|
441
|
-
fetchLatestRun, eventName, baseRef, labels, evaluateQueueBypass: evaluateQueue,
|
|
482
|
+
fetchLatestRun, eventName, baseRef, labels, fetchPrLabels, evaluateQueueBypass: evaluateQueue,
|
|
442
483
|
});
|
|
443
484
|
|
|
444
485
|
writeOutput([`verdict=${result.verdict}`]);
|
|
@@ -450,7 +491,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
450
491
|
`::error::the base branch "${baseRef}"'s own last Tests run is red or still re-running after a failure: ${runUrl} (${result.reason}) — `
|
|
451
492
|
+ 'wait for a fix-forward merge, or if THIS pull request is the fix, ask a '
|
|
452
493
|
+ `maintainer with write access to apply the "${BYPASS_LABEL}" label and re-queue it `
|
|
453
|
-
+ '(the queue run reads the label from the API; it must be the only PR in its merge group)
|
|
494
|
+
+ '(the queue run reads the label from the API; it must be the only PR in its merge group). '
|
|
495
|
+
+ 'On a pull request the label is read from the API too, so after it is added the '
|
|
496
|
+
+ 'fix-next-rerun workflow re-runs this check.\n',
|
|
454
497
|
);
|
|
455
498
|
return 1;
|
|
456
499
|
}
|