Merge commit '0c84f4ee14c32734313e68b700417e0748ad1d55' into capy/gst620-sealed-identifiers

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

# Conflicts:
#	test/helpers/free-fixtures-data.ts
This commit is contained in:
garrytan committed 2026-11-02 08:24:48 +00:00
commit c0d1482b05
568 files changed
+36850 -8414

No files matched your search

+12 -30
View File
@@ -4,7 +4,7 @@
*
* gstack-pregate [run] [--stage preflight|tests|all] [--only <check,..>] [--base <branch>]
* [--allow-repo-commands] [--policy-from base|head] [--out <pregate.json>]
* [--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--explain] [--json]
* [--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--from-run <id|url>] [--explain] [--json]
* gstack-pregate verify [--out <pregate.json>] [--json] publication check: current tree, no fail/incomplete, obligations cleared
* gstack-pregate clear --remote <workflow.yml/job> --run-url <url> | --run <id|url> [--run …] [--allow-repo-commands] | --bundle-lane <label> | --receipt <file> [--out <pregate.json>]
* gstack-pregate lanes [--explain] the lane list and platform tiers read from .github/workflows
@@ -27,17 +27,15 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT, exitTableHelp, formatResult, type PregateFile } from '../lib/headless-artifacts';
import { PREGATE_CONFIG_PATH, explainConfig, isConfigUnavailable, loadConfigFromBase, loadConfigFromHead } from '../lib/pregate/config';
import { PREGATE_CONFIG_PATH, explainConfig, loadConfigFromHead } from '../lib/pregate/config';
import { describeDraft, draftConfig } from '../lib/pregate/init-draft';
import { buildRunCheck } from '../lib/pregate/run-check';
import { buildContext, defaultUserConfigPath, describeRefusal, loadedDotenvFiles } from '../lib/pregate/context';
import { CHECKS, CHECK_IDS, PREGATE_OUT_REL, PREGATE_TIER2, clearDecision, clearObligation, readPregateFile, renderTable, runPregate, verifyPregate, writePregateFile, type RunCheck } from '../lib/pregate/index';
import { ghRunDeps, runInventory } from '../lib/run-evidence';
import { wtreeFingerprint } from '../lib/script-invocation';
import { CHECKS, CHECK_IDS, PREGATE_OUT_REL, PREGATE_TIER2, clearDecision, clearObligation, readPregateFile, renderTable, runPregate, verifyPregate, writePregateFile } from '../lib/pregate/index';
import { describeLane, discoverLanes } from '../lib/pregate/workflows';
import { REMOTE_GATE_LANE } from '../lib/pregate/checks/lanes';
import { describeSubstitution } from '../lib/pregate/remote-receipt';
import { ensureTmpExcluded } from '../lib/restamp';
import { normalizeRemoteUrl } from '../lib/ship-queue-train-record';
import { RESULT_CODES, resultCodeSuffix, resultLine } from '../lib/result-codes';
import { DEFAULT_STALL_TIMEOUT_S } from '../lib/pregate/context';
import { cancelActive } from '../lib/pregate/supervise';
@@ -90,6 +88,9 @@ const HELP = `gstack-pregate — cheap checks before anything paid or remote (do
spent, reserved and unknown
--run <id> the run label recorded in pregate.json and the name of this run's scratch directory (a taken name gets a numbered sibling, <id>-2 … -9)
(<state root>/projects/<slug>/pregate/runs/<id>: lane and guard logs, scan files; default a unique id)
--from-run <id|url> a Free Tests run on this exact tree, verified against the base branch's run profile for this OS
whose workflow holds a local lane (docs/pregate.md#from-run): the selected test files its tree.json lists count as passed and
are recorded with its receipt; the rest still run. A refused run prints REUSE: refused and runs everything
--explain print each selected test's provenance and each uncertain mapping
--json one JSON object on stdout; a refusal before any check runs (unavailable base, invalid pin
file, WTREE_UNAVAILABLE) is {ok:false,status:'refused',code,detail,fix,docs} with exit 3,
@@ -108,7 +109,7 @@ function flag(args: string[], name: string): string | undefined { const i = args
/** Flags each verb accepts: `[flags that take a value, bare flags]`. Anything else is usage (exit 2), never a silent default run. */
const VERB_FLAGS: Record<string, [string[], string[]]> = {
run: [['--stage', '--only', '--base', '--policy-from', '--out', '--timeout', '--stall-timeout', '--spend-cap', '--run'], ['--allow-repo-commands', '--explain', '--json', '--remote']],
run: [['--stage', '--only', '--base', '--policy-from', '--out', '--timeout', '--stall-timeout', '--spend-cap', '--run', '--from-run'], ['--allow-repo-commands', '--explain', '--json', '--remote']],
verify: [['--out'], ['--json']],
clear: [['--remote', '--run-url', '--run', '--bundle-lane', '--receipt', '--out'], ['--allow-repo-commands']],
lanes: [[], ['--explain', '--json']],
@@ -141,6 +142,8 @@ function outPath(root: string, rest: string[]): string { const o = flag(rest, '-
async function cmdRun(root: string, rest: string[], json: boolean): Promise<number> {
const stage = (flag(rest, '--stage') ?? 'all') as 'preflight' | 'tests' | 'all';
if (!['preflight', 'tests', 'all'].includes(stage)) { process.stderr.write('gstack-pregate: --stage needs preflight, tests or all\n'); return EXIT.usage; }
const fromRun = flag(rest, '--from-run');
if (fromRun !== undefined && stage === 'preflight') { process.stderr.write('gstack-pregate: --from-run applies to the tests stage (--stage tests or all)\n'); return EXIT.usage; }
const only = flag(rest, '--only')?.split(',').map(s => s.trim()).filter(Boolean);
const unknown = (only ?? []).filter(id => !CHECK_IDS.includes(id));
if (unknown.length) { process.stderr.write(`gstack-pregate: unknown check(s) ${unknown.join(', ')}${unknown.some(u => (PREGATE_TIER2 as readonly string[]).includes(u)) ? ' (tier 2, not in this gstack)' : ''}; known: ${CHECK_IDS.join(', ')}\n`); return EXIT.usage; }
@@ -172,6 +175,7 @@ async function cmdRun(root: string, rest: string[], json: boolean): Promise<numb
return EXIT.refused;
}
const ctx = loaded.ctx;
if (fromRun !== undefined) ctx.fromRun = { ref: fromRun, check: buildRunCheck(root, null, allow, base) };
ensureTmpExcluded(root);
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.once(signal, () => {
@@ -228,7 +232,7 @@ function cmdClear(root: string, rest: string[]): number {
const runs = rest.flatMap((a, i) => a === '--run' ? [rest[i + 1]!] : []);
const runUrl = flag(rest, '--run-url');
const bundleLane = flag(rest, '--bundle-lane');
const checked = runs.length || runUrl || bundleLane ? runCheck(root, lane, rest.includes('--allow-repo-commands')) : undefined;
const checked = runs.length || runUrl || bundleLane ? buildRunCheck(root, lane, rest.includes('--allow-repo-commands')) : undefined;
if (checked && 'error' in checked && (runs.length || checked.profiled)) { process.stderr.write(`gstack-pregate clear: ${checked.error}${checked.profiled ? `; ${lane} may have a run profile on the base branch, so it clears only with a verified --run` : ''}\n`); return EXIT.fail; }
const evidence = checked && !('error' in checked) ? checked : undefined;
const decision = clearDecision(read.file, lane, { runUrl, runs, bundleLane, receiptPath: flag(rest, '--receipt'), evidence }, root);
@@ -248,28 +252,6 @@ function cmdClear(root: string, rest: string[]): number {
return EXIT.ok;
}
/** The run-evidence inputs for `clear --run`: the base branch's run profiles, this repository on GitHub, and the gate's inventory command (a pin-file command, so only with --allow-repo-commands). */
function runCheck(root: string, lane: string, allowRepoCommands: boolean): RunCheck | { error: string; profiled?: boolean } {
const base = resolveBaseBranch(root);
const loaded = loadConfigFromBase(root, base);
// Unknown profiles are treated as present: a lane is cleared unverified only once the base shows it has no profile.
if (isConfigUnavailable(loaded)) return { error: `the base branch's ${PREGATE_CONFIG_PATH} is unreadable (${loaded.detail}); run profiles come only from the base`, profiled: true };
if (loaded.errors.length) return { error: `the base branch's ${PREGATE_CONFIG_PATH} is invalid: ${loaded.errors.map(e => `${e.path}: ${e.message}`).join('; ')}`, profiled: true };
const repo = /^https:\/\/github\.com\/(.+)$/.exec(normalizeRemoteUrl(gitOut(root, ['config', '--get', 'remote.origin.url']) ?? ''))?.[1];
if (!repo) return { error: 'origin is not a github.com repository; run evidence is read from GitHub Actions', profiled: lane === REMOTE_GATE_LANE ? !!loaded.config.remote_gate_runs : !!loaded.config.run_profiles[lane] };
return {
repo, deps: ghRunDeps(repo, root), profiles: loaded.config.run_profiles, gateRuns: loaded.config.remote_gate_runs, now: new Date(),
inventory: (command, tree) => {
if (!allowRepoCommands) return { error: 'the inventory command is a pin-file command; pass --allow-repo-commands' };
const at = () => { const fp = wtreeFingerprint({ cwd: root }); return 'tree' in fp ? fp.tree : null; };
if (at() !== tree) return { error: `the working tree is not pregate.json's tree ${tree.slice(0, 12)}; the inventory must list that tree's gate files` };
const listed = runInventory(command, root);
if (at() !== tree) return { error: `the working tree changed while ${command} ran` };
return listed;
},
};
}
function gitOut(root: string, args: string[]): string | null {
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', timeout: 30_000 });
return r.status === 0 ? r.stdout.trim() : null;