Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/commands/ship.mts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Env:
command-rewriting shell hooks (same caveat as SHIP_COMMIT_TIMEOUT).
Editing "coverage": false in guard.config.json does NOT work here — ship reads
that file from the committed tree, so a local-only edit is silently ignored.
GUARD_STRUCTURE_OK=1 Ship without structure lint, for THIS run only (alias: GUARD_NO_STRUCTURE=1).
Use only when the BASE branch already has structure violations your diff did
not cause. The gate logs a loud BYPASSED line, records telemetry, and keeps
every other deterministic gate active. Prefer exporting it on its own line.

Exits 0 on PR opened (or committed under SHIP_DRY_RUN), 1 on any preflight/git/gh error. A commit
that lands but fails to push KEEPS the branch (recovery line on stderr); a commit that never lands
Expand Down
4 changes: 4 additions & 0 deletions dist/cli/commands/ship.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Env:
command-rewriting shell hooks (same caveat as SHIP_COMMIT_TIMEOUT).
Editing "coverage": false in guard.config.json does NOT work here — ship reads
that file from the committed tree, so a local-only edit is silently ignored.
GUARD_STRUCTURE_OK=1 Ship without structure lint, for THIS run only (alias: GUARD_NO_STRUCTURE=1).
Use only when the BASE branch already has structure violations your diff did
not cause. The gate logs a loud BYPASSED line, records telemetry, and keeps
every other deterministic gate active. Prefer exporting it on its own line.

Exits 0 on PR opened (or committed under SHIP_DRY_RUN), 1 on any preflight/git/gh error. A commit
that lands but fails to push KEEPS the branch (recovery line on stderr); a commit that never lands
Expand Down
12 changes: 12 additions & 0 deletions dist/gate-engine/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ export function envFlag(name) {
export function coverageBypassed() {
return envFlag('COVERAGE_OK') || envFlag('NO_COVERAGE');
}
/**
* Is structure lint bypassed for THIS run? The orchestrator owns this predicate rather than
* guard-structure because Electron consumers supply their own arbitrary eslint command through
* `--structure`; putting the bypass inside guard-structure would leave those consumers wedged.
*
* `GUARD_STRUCTURE_OK` is the canonical operator assertion. `GUARD_NO_STRUCTURE` is the accepted
* guessable alias, matching coverageBypassed. guard-deterministic banners + telemeters the skip and
* salts its prefix-cache scope so this one-run assertion cannot authorise a later normal run.
*/
export function structureBypassed() {
return envFlag('STRUCTURE_OK') || envFlag('NO_STRUCTURE');
}
/**
* Does THIS run refuse to accept a deterministic gate's fail-open? Lives beside coverageBypassed for
* the same reason: guard-deterministic asks it twice — once to decide the verdict, once to salt the
Expand Down
46 changes: 37 additions & 9 deletions dist/gate-engine/deterministic/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, realpathSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { coverageBypassed, deterministicStrict } from "../config.mjs";
import { coverageBypassed, deterministicStrict, structureBypassed } from "../config.mjs";
import { emitGateEvent, finishGateTiming } from "../judge/gate-events.mjs";
import { prefixEntry, recordPrefix } from "../prefix-cache/prefix-cache.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -184,7 +184,15 @@ export function prefixCacheScope(scope, effectiveIds) {
// bypassed run cannot reuse a clean run's key either, so it re-runs the other gates. Same trade
// the review salt makes — correctness over a cache hit.) `?? 'devkit-guards'` materialises the
// default scope checkPrefix would otherwise supply internally, or the salt would be lost.
return coverageBypassed() ? `${strictBase ?? 'devkit-guards'}:coverage-bypassed` : strictBase;
const coverageBase = coverageBypassed()
? `${strictBase ?? 'devkit-guards'}:coverage-bypassed`
: strictBase;
// Structure commands are arbitrary (`guard-structure`, Electron's eslint invocation, devkit's
// own package script), so the bypass lives at this orchestrator layer. Keep its cache namespace
// apart from a normal run for the same anti-laundering reason as coverage above.
return structureBypassed()
? `${coverageBase ?? 'devkit-guards'}:structure-bypassed`
: coverageBase;
}
// Run one gate as a subprocess; return its exit code (0 on success). stdio inherited so the gate's
// own banner/output reaches the user exactly as it did when the hook invoked it directly.
Expand Down Expand Up @@ -254,23 +262,38 @@ export function runDeterministic(cwd = process.cwd(), opts = {}) {
const cacheScope = prefixCacheScope(opts.scope, effectiveIds);
// Deterministic-prefix cache (ship only — a no-op otherwise): a cached all-green staged tree skips
// every gate. checkPrefix returns true = skip, false = run.
const cachedPrefix = prefixEntry(cwd, { hookPath: opts.hookPath, scope: cacheScope });
const skip = Boolean(cachedPrefix);
const fails = [];
const cachedPrefix = prefixEntry(cwd, { hookPath: opts.hookPath, scope: cacheScope });
const skip = Boolean(cachedPrefix);
const bypassStructure = Boolean(opts.structure) && structureBypassed();
const fails = [];
// Gates that opted out (exit 2 where that IS an opt-out) and so proved nothing. Reported even on a
// green run — the whole defect this exists for is a skipped gate reading like a passed one.
const skipped = [];
if (!skip) {
const ids = new Set(effectiveIds);
const skipped = [];
if (!skip) {
if (bypassStructure) {
console.log('⚠️ Structure lint BYPASSED for this run (GUARD_STRUCTURE_OK=1).');
console.log(' Repository structure was NOT verified for this commit.');
emitGateEvent({
type: 'gate_result',
gate: 'structure-lint',
// The collector's gate_result schema accepts fail | could_not_run. Keep the deliberate
// bypass measurable as a non-run, and distinguish it from an infrastructure opt-out in
// detail instead of inventing a status that downstream readers would treat as clean.
status: 'could_not_run',
detail: 'structure-lint(bypassed:GUARD_STRUCTURE_OK)',
});
}
const ids = new Set(effectiveIds);
const gates = DETERMINISTIC.filter((g) => ids.has(g.id)).map((g) => ({
label: `guard-${g.id}`,
argv: ['node', path.resolve(HERE, g.module.replace(MJS_EXT_RE, SELF_EXT)), ...g.args],
failOpen2: true,
}));
for (const x of opts.extra ?? [])
gates.push(commandGate(x.label, x.cmd));
if (opts.structure)
if (opts.structure && !bypassStructure) {
gates.push(commandGate('structure-lint', opts.structure));
}
for (const gate of gates) {
if (!gate.argv) {
fails.push(`${gate.label}(unrunnable: empty command)`);
Expand Down Expand Up @@ -327,6 +350,11 @@ export function runDeterministic(cwd = process.cwd(), opts = {}) {
}
console.error(`✗ deterministic gates failed:${fails.map((f) => ` ${f}`).join('')}`);
console.error(' Every deterministic failure is listed above — fix them together, then commit once.');
if (fails.some((f) => f.startsWith('structure-lint'))) {
console.error(' Base branch structure debt that your diff did not cause? Re-run with the explicit');
console.error(' one-run assertion: export GUARD_STRUCTURE_OK=1');
console.error(' A structure violation introduced by your own change must be fixed instead.');
}
if (fails.some((f) => NOT_FOUND_RE.test(f))) {
console.error(' exit 127 = command not found: the gate ran, but its BINARY did not resolve — a');
console.error(' dependency problem, not a code finding. Under `devkit ship` the gates run in an');
Expand Down
13 changes: 13 additions & 0 deletions gate-engine/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,19 @@ export function coverageBypassed(): boolean {
return envFlag('COVERAGE_OK') || envFlag('NO_COVERAGE');
}

/**
* Is structure lint bypassed for THIS run? The orchestrator owns this predicate rather than
* guard-structure because Electron consumers supply their own arbitrary eslint command through
* `--structure`; putting the bypass inside guard-structure would leave those consumers wedged.
*
* `GUARD_STRUCTURE_OK` is the canonical operator assertion. `GUARD_NO_STRUCTURE` is the accepted
* guessable alias, matching coverageBypassed. guard-deterministic banners + telemeters the skip and
* salts its prefix-cache scope so this one-run assertion cannot authorise a later normal run.
*/
export function structureBypassed(): boolean {
return envFlag('STRUCTURE_OK') || envFlag('NO_STRUCTURE');
}

/**
* Does THIS run refuse to accept a deterministic gate's fail-open? Lives beside coverageBypassed for
* the same reason: guard-deterministic asks it twice — once to decide the verdict, once to salt the
Expand Down
105 changes: 105 additions & 0 deletions gate-engine/deterministic/__tests__/run.test.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execFileSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
Expand All @@ -15,6 +16,8 @@ afterEach(() => {
delete process.env.DEVKIT_SHIP;
delete process.env.GUARD_COVERAGE_OK;
delete process.env.GUARD_NO_COVERAGE;
delete process.env.GUARD_STRUCTURE_OK;
delete process.env.GUARD_NO_STRUCTURE;
// Both spellings: envVar() accepts the FRINK_ alias, and `devkit ship` exports strict envs that a
// pre-push vitest inherits — a leak that would silently flip every fail-open assertion below.
delete process.env.GUARD_DETERMINISTIC_STRICT;
Expand Down Expand Up @@ -140,6 +143,86 @@ describe('runDeterministic — --structure / --extra / --only', () => {
expect(exec).toHaveBeenCalledWith('bunx', ['eslint', 'src'], expect.anything());
});

it.each(['GUARD_STRUCTURE_OK', 'GUARD_NO_STRUCTURE'])(
'%s skips an arbitrary structure command but keeps the other deterministic gates active',
(key) => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
const d = repo(['size']);
const exec = mkExec({});
process.env[key] = '1';

expect(runDeterministic(d, { exec, structure: 'bunx eslint src' })).toBe(0);
expect(exec).toHaveBeenCalledTimes(1); // size still ran; only structure was skipped
expect(exec.mock.calls.some(([bin]) => bin === 'bunx')).toBe(false);
expect(log.mock.calls.flat().join('\n')).toContain('Structure lint BYPASSED');
},
);

it('records the structure bypass with the collector-supported non-run status', () => {
vi.spyOn(console, 'log').mockImplementation(() => {});
const d = repo(['size']);
const sink = join(d, 'events.jsonl');
process.env.GUARD_STRUCTURE_OK = '1';
process.env.DEVKIT_GATE_EVENTS = sink;

expect(runDeterministic(d, { exec: mkExec({}), structure: 'bunx eslint src' })).toBe(0);
const events = readFileSync(sink, 'utf8')
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line))
.filter((event) => event.type === 'gate_result');
expect(events).toContainEqual(
expect.objectContaining({
gate: 'structure-lint',
status: 'could_not_run',
detail: 'structure-lint(bypassed:GUARD_STRUCTURE_OK)',
}),
);
});

it('does not re-emit the structure bypass when a prefix-cache hit skips the retry', () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
const d = repo(['size']);
execFileSync('git', ['init', '-q'], { cwd: d });
execFileSync('git', ['add', '.'], { cwd: d });
const sink = join(d, 'events.jsonl');
const exec = mkExec({});
process.env.DEVKIT_SHIP = '1';
process.env.GUARD_STRUCTURE_OK = '1';
process.env.DEVKIT_GATE_EVENTS = sink;

expect(runDeterministic(d, { exec, structure: 'bunx eslint src' })).toBe(0);
expect(runDeterministic(d, { exec, structure: 'bunx eslint src' })).toBe(0);

expect(exec).toHaveBeenCalledTimes(1); // first run executes size; cached retry executes nothing
expect(
log.mock.calls.flat().filter((line) => String(line).includes('Structure lint BYPASSED')),
).toHaveLength(1);
const bypassEvents = readFileSync(sink, 'utf8')
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line))
.filter(
(event) =>
event.gate === 'structure-lint' &&
event.status === 'could_not_run' &&
event.detail === 'structure-lint(bypassed:GUARD_STRUCTURE_OK)',
);
expect(bypassEvents).toHaveLength(1);
});

it('a structure failure prints the explicit base-debt remedy', () => {
const err = vi.spyOn(console, 'error').mockImplementation(() => {});
const d = repo(['size']);
expect(
runDeterministic(d, {
exec: mkExec({ 'structure/run.mts': 1 }),
structure: 'guard-structure gate',
}),
).toBe(1);
expect(err.mock.calls.flat().join('\n')).toContain('export GUARD_STRUCTURE_OK=1');
});

it('--extra gates run under their own label and aggregate with the built-ins', () => {
const err = vi.spyOn(console, 'error').mockImplementation(() => {});
const d = repo(['size']);
Expand Down Expand Up @@ -367,6 +450,28 @@ describe('prefixCacheScope', () => {
expect(prefixCacheScope()).toBeUndefined();
expect(prefixCacheScope('custom')).toBe('custom');
});

it.each(['GUARD_STRUCTURE_OK', 'GUARD_NO_STRUCTURE'])(
'%s salts the scope away from a normal structure run',
(key) => {
const cleanDefault = prefixCacheScope();
const cleanCustom = prefixCacheScope('custom');
process.env[key] = '1';
expect(prefixCacheScope()).toBe('devkit-guards:structure-bypassed');
expect(prefixCacheScope('custom')).toBe('custom:structure-bypassed');
expect(prefixCacheScope()).not.toBe(cleanDefault);
expect(prefixCacheScope('custom')).not.toBe(cleanCustom);
},
);

it('the structure salt composes after strict and coverage salts', () => {
process.env.GUARD_DETERMINISTIC_STRICT = '1';
process.env.GUARD_COVERAGE_OK = '1';
process.env.GUARD_STRUCTURE_OK = '1';
expect(prefixCacheScope()).toBe(
'devkit-guards:deterministic-strict:coverage-bypassed:structure-bypassed',
);
});
});

// A gate that opts out proved nothing, but its own stderr scrolls past at the same weight as a gate
Expand Down
39 changes: 36 additions & 3 deletions gate-engine/deterministic/run.mts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, realpathSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { coverageBypassed, deterministicStrict } from '../config.mts';
import { coverageBypassed, deterministicStrict, structureBypassed } from '../config.mts';
import { emitGateEvent, finishGateTiming } from '../judge/gate-events.mts';
import { prefixEntry, recordPrefix } from '../prefix-cache/prefix-cache.mts';

Expand Down Expand Up @@ -216,7 +216,15 @@ export function prefixCacheScope(scope?: string, effectiveIds?: string[]): strin
// bypassed run cannot reuse a clean run's key either, so it re-runs the other gates. Same trade
// the review salt makes — correctness over a cache hit.) `?? 'devkit-guards'` materialises the
// default scope checkPrefix would otherwise supply internally, or the salt would be lost.
return coverageBypassed() ? `${strictBase ?? 'devkit-guards'}:coverage-bypassed` : strictBase;
const coverageBase = coverageBypassed()
? `${strictBase ?? 'devkit-guards'}:coverage-bypassed`
: strictBase;
// Structure commands are arbitrary (`guard-structure`, Electron's eslint invocation, devkit's
// own package script), so the bypass lives at this orchestrator layer. Keep its cache namespace
// apart from a normal run for the same anti-laundering reason as coverage above.
return structureBypassed()
? `${coverageBase ?? 'devkit-guards'}:structure-bypassed`
: coverageBase;
}

// Run one gate as a subprocess; return its exit code (0 on success). stdio inherited so the gate's
Expand Down Expand Up @@ -294,19 +302,35 @@ export function runDeterministic(cwd = process.cwd(), opts: RunDeterministicOpts
// every gate. checkPrefix returns true = skip, false = run.
const cachedPrefix = prefixEntry(cwd, { hookPath: opts.hookPath, scope: cacheScope });
const skip = Boolean(cachedPrefix);
const bypassStructure = Boolean(opts.structure) && structureBypassed();
const fails = [];
// Gates that opted out (exit 2 where that IS an opt-out) and so proved nothing. Reported even on a
// green run — the whole defect this exists for is a skipped gate reading like a passed one.
const skipped: string[] = [];
if (!skip) {
if (bypassStructure) {
console.log('⚠️ Structure lint BYPASSED for this run (GUARD_STRUCTURE_OK=1).');
console.log(' Repository structure was NOT verified for this commit.');
emitGateEvent({
type: 'gate_result',
gate: 'structure-lint',
// The collector's gate_result schema accepts fail | could_not_run. Keep the deliberate
// bypass measurable as a non-run, and distinguish it from an infrastructure opt-out in
// detail instead of inventing a status that downstream readers would treat as clean.
status: 'could_not_run',
detail: 'structure-lint(bypassed:GUARD_STRUCTURE_OK)',
});
}
const ids = new Set(effectiveIds);
const gates: Gate[] = DETERMINISTIC.filter((g) => ids.has(g.id)).map((g) => ({
label: `guard-${g.id}`,
argv: ['node', path.resolve(HERE, g.module.replace(MJS_EXT_RE, SELF_EXT)), ...g.args],
failOpen2: true,
}));
for (const x of opts.extra ?? []) gates.push(commandGate(x.label, x.cmd));
if (opts.structure) gates.push(commandGate('structure-lint', opts.structure));
if (opts.structure && !bypassStructure) {
gates.push(commandGate('structure-lint', opts.structure));
}
for (const gate of gates) {
if (!gate.argv) {
fails.push(`${gate.label}(unrunnable: empty command)`);
Expand Down Expand Up @@ -365,6 +389,15 @@ export function runDeterministic(cwd = process.cwd(), opts: RunDeterministicOpts
console.error(
' Every deterministic failure is listed above — fix them together, then commit once.',
);
if (fails.some((f) => f.startsWith('structure-lint'))) {
console.error(
' Base branch structure debt that your diff did not cause? Re-run with the explicit',
);
console.error(' one-run assertion: export GUARD_STRUCTURE_OK=1');
console.error(
' A structure violation introduced by your own change must be fixed instead.',
);
}
if (fails.some((f) => NOT_FOUND_RE.test(f))) {
console.error(
' exit 127 = command not found: the gate ran, but its BINARY did not resolve — a',
Expand Down
Loading