mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
screen #3613: merge 9e1593ded9 onto main 319ef500ca
This commit is contained in:
commit
fd11b56eef
49 files changed
+3074
-88
No files matched your search
@@ -39,9 +39,10 @@ const prVersionEnv = (process.env.PR_VERSION ?? "").trim();
|
||||
// head is unstamped and the lane stamps at its slot (docs/ship-policy.md#run-a-merge-train).
|
||||
// The workflow passes BASE_REF and HEAD_SHA with the PR head checked out as cwd.
|
||||
if (process.env.BASE_REF && process.env.HEAD_SHA) {
|
||||
const train = classifyTrainHead({ repoRoot: process.cwd(), baseRef: process.env.BASE_REF, head: process.env.HEAD_SHA });
|
||||
const train = classifyTrainHead({ repoRoot: process.cwd(), baseRef: process.env.BASE_REF, head: process.env.HEAD_SHA, author: process.env.PR_AUTHOR });
|
||||
if (train.kind === "unstamped") {
|
||||
console.log(`✓ ${tag} is unstamped under the base's train policy: VERSION and package.json match its merge base, any changed release output is what the release tool generates at the merge base's VERSION, and the CHANGELOG change is confined to [Unreleased]. The lane stamps at its slot.`);
|
||||
const entry = train.entry === "fragment" ? `its entry is ${train.fragments} CHANGELOG fragment(s)` : train.entry === "none-bot" ? "it is a bot PR with no entry (the merge step writes one from its title)" : "the CHANGELOG change is confined to [Unreleased]";
|
||||
console.log(`✓ ${tag} is unstamped under the base's ship policy: VERSION and package.json match its merge base, any changed release output is what the release tool generates at the merge base's VERSION, and ${entry}. The merge step stamps it.`);
|
||||
process.exit(0);
|
||||
}
|
||||
if (train.kind === "invalid") {
|
||||
@@ -49,9 +50,13 @@ if (process.env.BASE_REF && process.env.HEAD_SHA) {
|
||||
process.exit(1);
|
||||
}
|
||||
if (train.kind === "stamped" && prVersionEnv !== train.expected) {
|
||||
console.log(`::error::${tag} claims v${prVersionEnv}, but under the base's train policy a stamped head must carry v${train.expected} (base + bump, what gstack-restamp --next allocates at its slot). Leave VERSION, package.json and the release outputs at the base's values until the slot is called.`);
|
||||
console.log(`::error::${tag} claims v${prVersionEnv}, but under the base's ship policy a stamped head must carry v${train.expected} (base + the effective bump, what gstack-restamp --next allocates at the merge step). Leave VERSION, package.json and the release outputs at the base's values until the merge step stamps them.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (train.kind === "stamped" && train.fragments) {
|
||||
console.log(`✓ ${tag} carries v${train.expected}, what the merge step allocates under changelog_fragments; the land lock serializes allocation, so open claims do not apply.`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.offline === true) {
|
||||
|
||||
@@ -51,6 +51,7 @@ const WINDOWS_FRAGILE_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
|
||||
// when possible; this list is for environment-/runtime-specific tests where
|
||||
// the failure mode is structural rather than detectable via source-file scan.
|
||||
export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }> = [
|
||||
{ file: 'test/ship-queue-land.test.ts', reason: 'its gh shim is an extensionless #!/usr/bin/env node script on a colon-joined PATH, which windows-latest cannot launch, so land reaches the real gh (no GH_TOKEN) and every case fails (windows-free-shard 6, run 40824965988); land is a controller command run on Linux and macOS' },
|
||||
// Like test/paid-rerun.test.ts (excluded by pattern), it replays a gate capture through the paid lane's --report and --rerun-plan CLI.
|
||||
{ file: 'test/shared-libs-index-flags-readonly.test.ts', reason: 'its assume-unchanged replay seeds the review through test/helpers/shared-libs-path-fixture.ts, which launches bin/gstack-review-log (a bash script) directly; windows-latest refuses it as not found in $PATH (windows-free-shard 5, run 40727825859)' },
|
||||
{ file: 'test/credential-rejection.test.ts', reason: 'replays run 39696775546 through the --report and --rerun-plan CLI of the paid lane, which runs only on Linux CI; on windows-latest that CLI exits 1 with EINVAL in open() (windows-free-shard run 39982914852)' },
|
||||
@@ -290,6 +291,10 @@ export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }>
|
||||
reason: 'spawns bin/gstack-ship-queue (shebang) and a #!/bin/sh gh shim through test/helpers/restamp-fixture.ts and test/helpers/ship-queue-fixture.ts',
|
||||
},
|
||||
// GSTA-49 T1 (merge trains): the train lib logic runs on Windows in test/ship-queue-train-logic.test.ts.
|
||||
{
|
||||
file: 'test/ship-queue-train-land.test.ts',
|
||||
reason: 'drives train land against test/helpers/ship-queue-train-fixture.ts plus an extensionless node gh wrapper on a colon-joined PATH; on windows-latest spawnSync(\'gh\') resolves the runner\'s gh.exe instead, as for test/ship-queue-train.test.ts and test/ship-queue-land.test.ts (windows-free-shard 6, run 40824965988)',
|
||||
},
|
||||
{
|
||||
file: 'test/ship-queue-train.test.ts',
|
||||
reason: 'drives bin/gstack-ship-queue against the stateful #!/bin/sh gh shim in test/helpers/ship-queue-train-fixture.ts; on windows-latest spawnSync(\'gh\') resolves the runner\'s gh.exe instead of the shim (windows-free-shard run 39152702056)',
|
||||
|
||||
@@ -53,8 +53,11 @@ function noteRecords<T extends { kind: string; repo_key?: string }>(git: Git, ki
|
||||
});
|
||||
}
|
||||
|
||||
/** The newest green-train record whose prefix trees include this digest, with its run; null when none. */
|
||||
export function coveringRecord(gh: GhRunner, repo: string, train: TrainPolicy, digest: string, lines: string[]): { run: Run; record: TrainRecord; index: number } | null {
|
||||
/**
|
||||
* The newest green-train record with a prefix covering main, with its run; null when none. A plain prefix T_i
|
||||
* covers main's tree modulo stamps (its digest); a stamped prefix S_i covers only main's exact tree.
|
||||
*/
|
||||
export function coveringRecord(gh: GhRunner, repo: string, train: TrainPolicy, main: { digest: string | null; tree: string }, lines: string[]): { run: Run; record: TrainRecord; index: number; exact: boolean } | null {
|
||||
const runs = json<{ workflow_runs?: Run[] }>(gh(['api', '-X', 'GET', `repos/${repo}/actions/workflows/${train.gate_workflow}/runs`, '-f', 'event=workflow_dispatch', '-f', 'status=success', '-f', 'per_page=50'], GH_MS));
|
||||
if (!runs) { lines.push('cannot list the train gate runs; the backstop gates main'); return null; }
|
||||
const trains = (runs.workflow_runs ?? []).map(run => ({ run, nonce: /\[eval-bg ([A-Za-z0-9._-]+)\]/.exec(run.display_title ?? '')?.[1] ?? '' }))
|
||||
@@ -69,7 +72,7 @@ export function coveringRecord(gh: GhRunner, repo: string, train: TrainPolicy, d
|
||||
try { record = file ? JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')) as TrainRecord : null; } catch { record = null; }
|
||||
if (!record || record.kind !== 'train-gate' || record.verdict !== 'GREEN' || record.census?.complete !== true || record.nonce !== nonce
|
||||
|| record.head_sha !== run.head_sha || !Array.isArray(record.prefixes)) continue;
|
||||
const hit = record.prefixes.find(p => p.stamp_free_tree === digest);
|
||||
const hit = record.prefixes.find(p => p.exact ? p.tree === main.tree : main.digest !== null && p.stamp_free_tree === main.digest);
|
||||
if (!hit) continue;
|
||||
const checks = json<{ check_runs?: Array<{ name?: string; conclusion?: string | null }> }>(
|
||||
gh(['api', `repos/${repo}/commits/${run.head_sha}/check-runs?check_name=${encodeURIComponent(train.full_gate_check)}`], GH_MS));
|
||||
@@ -77,7 +80,7 @@ export function coveringRecord(gh: GhRunner, repo: string, train: TrainPolicy, d
|
||||
lines.push(`train ${nonce} (run ${run.id}) lists this tree but its ${train.full_gate_check} check run is not a success`);
|
||||
continue;
|
||||
}
|
||||
return { run, record, index: hit.index };
|
||||
return { run, record, index: hit.index, exact: !!hit.exact };
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
@@ -150,9 +153,10 @@ export function runBackstop(o: BackstopOptions): { action: BackstopAction; lines
|
||||
}
|
||||
const digest = stampFreeTree(git, o.sha, o.repoRoot, loaded.policy);
|
||||
if (typeof digest !== 'string') { lines.push(`BACKSTOP: no tree modulo stamps (${digest.error}); gating main`); }
|
||||
const covered = typeof digest === 'string' ? coveringRecord(gh, o.repo, train, digest, lines) : null;
|
||||
const tree = git(['rev-parse', `${o.sha}^{tree}`]).out;
|
||||
const covered = coveringRecord(gh, o.repo, train, { digest: typeof digest === 'string' ? digest : null, tree }, lines);
|
||||
if (covered) {
|
||||
lines.push(`BACKSTOP: covered: main ${o.sha.slice(0, 12)} is T${covered.index} of green train ${covered.record.nonce} (run ${covered.run.id}, tree modulo stamps ${String(digest).slice(0, 12)}); no gate`);
|
||||
lines.push(`BACKSTOP: covered: main ${o.sha.slice(0, 12)} is ${covered.exact ? `S${covered.index}` : `T${covered.index}`} of green train ${covered.record.nonce} (run ${covered.run.id}, ${covered.exact ? `exact tree ${tree.slice(0, 12)}` : `tree modulo stamps ${String(digest).slice(0, 12)}`}); no gate`);
|
||||
return done('covered');
|
||||
}
|
||||
const owned = noteRecords<FreezeRecord>(git, 'freeze', repoKey).find(f => !f.cleared_at && f.main.sha === o.sha && f.backstop);
|
||||
|
||||
@@ -49,7 +49,7 @@ export const MAX_BISECT_CASES = 40;
|
||||
/** A full gate's recorded cost (MEASUREMENT.md: $90.93 to $94.70; lib/gate-cost.ts uses the same scale). */
|
||||
export const FULL_GATE_USD = 93.14;
|
||||
/** T1's prefix commits: `train T<i>: merge #<pr> at <sha12>` (lib/ship-queue-train.ts buildChain). */
|
||||
const PREFIX_SUBJECT = /^train T(\d+): merge #(\d+) at [0-9a-f]{12}$/;
|
||||
const PREFIX_SUBJECT = /^train (?:T(\d+): merge #(\d+) at [0-9a-f]{12}|S(\d+): merge #(\d+) at [0-9a-f]{12} v[0-9.]+)$/;
|
||||
|
||||
export type Git = (args: string[]) => { ok: boolean; out: string };
|
||||
export const gitIn = (cwd: string): Git => (args) => {
|
||||
@@ -185,9 +185,10 @@ export function stampFreeTree(git: Git, commit: string, repoRoot: string, policy
|
||||
return createHash('sha256').update(lines.sort().join('\n')).digest('hex');
|
||||
}
|
||||
|
||||
export interface TrainPrefixTree { index: number; commit: string; tree: string; stamp_free_tree: string }
|
||||
/** `exact`: a stamped prefix S_i (changelog_fragments trains), which covers only its exact tree. */
|
||||
export interface TrainPrefixTree { index: number; commit: string; tree: string; stamp_free_tree: string; exact?: true }
|
||||
|
||||
/** T1..Tk from the train head, walking first parents over T1's prefix commits. */
|
||||
/** T1..Tk (or stamped S1..Sk, never mixed) from the train head, walking first parents over the prefix commits. */
|
||||
export function trainPrefixes(git: Git, head: string, repoRoot: string, policy: ShipPolicy): TrainPrefixTree[] | { error: string } {
|
||||
const log = git(['log', '--first-parent', `--max-count=${MAX_PREFIXES}`, '--format=%H%x00%P%x00%s', head]);
|
||||
if (!log.ok) return { error: `cannot walk ${head.slice(0, 12)}'s first parents (${log.out.split('\n')[0]})` };
|
||||
@@ -196,11 +197,13 @@ export function trainPrefixes(git: Git, head: string, repoRoot: string, policy:
|
||||
const [commit, parents, subject] = line.split('\0') as [string, string, string];
|
||||
const m = PREFIX_SUBJECT.exec(subject ?? '');
|
||||
if (!m || parents.split(' ').length !== 2) break;
|
||||
const index = Number(m[1]);
|
||||
const index = Number(m[1] ?? m[3]);
|
||||
const exact = m[3] !== undefined;
|
||||
if (found.length && exact !== !!found[0]!.exact) return { error: `${commit.slice(0, 12)} mixes stamped and plain prefixes` };
|
||||
if (found.length && index !== found.at(-1)!.index - 1) return { error: `prefix T${index} follows T${found.at(-1)!.index} at ${commit.slice(0, 12)}` };
|
||||
const digest = stampFreeTree(git, commit, repoRoot, policy);
|
||||
if (typeof digest !== 'string') return digest;
|
||||
found.push({ index, commit, tree: git(['rev-parse', `${commit}^{tree}`]).out, stamp_free_tree: digest });
|
||||
found.push({ index, commit, tree: git(['rev-parse', `${commit}^{tree}`]).out, stamp_free_tree: digest, ...(exact ? { exact: true as const } : {}) });
|
||||
}
|
||||
if (!found.length || found.at(-1)!.index !== 1) return { error: `${head.slice(0, 12)} is not a train head (no T1..Tk prefix chain)` };
|
||||
return found.reverse();
|
||||
|
||||
Reference in new issue
Block a user