文件
hyb-oa/scripts/qa/tests/release-gate-enforcement.test.mjs
T

212 行
12 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync, writeFileSync, symlinkSync, rmSync, existsSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { spawnSync } from 'node:child_process';
import { fixture } from './fixtures/release-gate-fixture.mjs';
const entries=['ci-release','all','deploy','bootstrap','existing','raw'];
const syntheticEnv={
DEPLOY_SERVER:'fixture-target',DEPLOY_REFRESH_SERVER:'fixture-refresh',
REMOTE_DIR:'/fixture/app',POSTGRES_DATA_DIR:'/fixture/db',STORAGE_HOST_DIR:'/fixture/storage',
COMPOSE_PROJECT_NAME:'fixture-project',DEPLOY_DATA_MODE:'existing',
EXPECTED_PRODUCTION_ORIGIN:'https://fixture.invalid',EXPECTED_PRODUCTION_DB_NAME_SHA256:'a'.repeat(64),
EXPECTED_PENDING_MIGRATIONS:'',EXPECTED_DEPLOY_SERVER:'fixture-target',EXPECTED_REMOTE_DIR:'/fixture/app',
EXPECTED_COMPOSE_PROJECT_NAME:'fixture-project',
};
const output=r=>r.stdout+r.stderr;
const named=(r,name,suffix)=>r.commands.filter(c=>c.name===name&&(!suffix||c.args[0]?.endsWith(suffix)));
const writes=r=>r.commands.filter(c=>c.name==='rsync'||c.name==='ssh'&&(/mkdir -p|BUILD_SHA=|^refresh$/.test(c.args.at(-1))));
function blockedBeforeAccess(r) {
assert.notEqual(r.status,0,output(r));
assert.equal(writes(r).length,0);
assert.equal(named(r,'ssh').length,0);
assert.equal(named(r,'config-read').length,0);
assert.equal(named(r,'node','check-source-provenance.mjs').length,0);
}
function complete(f) {
const r=f.run();assert.equal(r.status,0,output(r));
f.env.RELEASE_GATE_RECEIPT_DIR=f.env.RELEASE_GATE_ARTIFACT_DIR;
return r;
}
function replace(f,relative,from,to) {
const file=path.join(f.root,relative),source=readFileSync(file,'utf8');
assert.equal(source.split(from).length,2,'mutation must hit exactly once');
writeFileSync(file,source.replace(from,to));f.commit();
}
for(const entry of entries)test(`${entry}: explicit bad receipts cannot reach config, provenance or remote actions`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const file=path.join(f.env.RELEASE_GATE_RECEIPT_DIR,'release-result.json'),original=readFileSync(file,'utf8');
const mutations=[
record=>{record.profile='focused';},
record=>{record.sourceBefore.commit='f'.repeat(40);},
record=>{record.executionExit=130;record.status='PASS';record.deploymentEligible=true;},
record=>{record.phases.stages.pop();},
record=>{record.policy.toolchain.node='stale';},
];
for(const mutate of mutations) {
const record=JSON.parse(original);mutate(record);writeFileSync(file,JSON.stringify(record));
blockedBeforeAccess(f.invoke(entry));
}
writeFileSync(file,original);
blockedBeforeAccess(f.invoke(entry,{RELEASE_GATE_RECEIPT_DIR:''}));
blockedBeforeAccess(f.invoke(entry,{RELEASE_GATE_RECEIPT_DIR:path.join(f.root,'missing')}));
writeFileSync(file,'{');blockedBeforeAccess(f.invoke(entry));writeFileSync(file,original);
writeFileSync(path.join(f.root,'client/package.json'),'{"changed":true}');
blockedBeforeAccess(f.invoke(entry));writeFileSync(path.join(f.root,'client/package.json'),'{}');
for(const value of ['yes','01','']) {
blockedBeforeAccess(f.invoke(entry,{PRODUCTION_READ_ONLY_PREFLIGHT:value}));
blockedBeforeAccess(f.invoke(entry,{PRODUCTION_IDENTITY_ONLY:value,PRODUCTION_READ_ONLY_PREFLIGHT:'1'}));
}
});
for(const entry of ['deploy','bootstrap','existing','raw'])test(`${entry}: missing receipt refuses without starting tests`,context=>{
const f=fixture(context,syntheticEnv),r=f.invoke(entry);
blockedBeforeAccess(r);assert.equal(named(r,'npm').length,0);
});
for(const entry of entries)test(`${entry}: valid explicit receipt reaches exactly one synthetic deployment`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke(entry);assert.equal(r.status,0,output(r));
assert.equal(named(r,'rsync').length,1);
assert.equal(named(r,'npm').length,0);
assert.equal(named(r,'bash','run-web-gates.sh').filter(c=>c.args[1]==='full-local').length,0);
assert.equal(r.commands.filter(c=>c.name==='ssh'&&c.args.at(-1).includes('BUILD_SHA=')).length,1);
const firstWrite=r.commands.findIndex(c=>writes({commands:[c]}).length);
const checks=r.commands.slice(0,firstWrite).filter(c=>c.name==='node'&&c.args[0]?.endsWith('release-gate-result.mjs')&&c.args[1]==='require');
assert.ok(checks.length>=3,'raw deployment must recheck receipt before every write boundary');
});
for(const entry of ['ci-release','all'])test(`${entry}: no explicit receipt runs static/full-local once with no recursive deployment`,context=>{
const f=fixture(context,syntheticEnv),r=f.invoke(entry);assert.equal(r.status,0,output(r));
assert.equal(named(r,'bash','run-release-gate.sh').length,1);
assert.equal(named(r,'bash','run-web-gates.sh').filter(c=>c.args[1]==='full-local').length,1);
assert.equal(named(r,'npm').filter(c=>c.args[0]==='test').length,1);
assert.equal(named(r,'node','production-identity-bootstrap.mjs').length,1);
assert.equal(named(r,'rsync').length,1);
const web=named(r,'bash','run-web-gates.sh').map(c=>c.args[1]);
assert.deepEqual(web,entry==='all'?['smoke-local','full-local']:['full-local']);
});
for(const entry of ['ci-release','all'])test(`${entry}: failed verification never enters production chain`,context=>{
const f=fixture(context,{...syntheticEnv,FAIL_STATIC:'1'}),r=f.invoke(entry);
blockedBeforeAccess(r);assert.equal(named(r,'node','production-identity-bootstrap.mjs').length,0);
assert.equal(named(r,'bash','run-release-gate.sh').length,1);
});
for(const [entry,flag] of [['bootstrap','PRODUCTION_IDENTITY_ONLY'],['bootstrap','PRODUCTION_READ_ONLY_PREFLIGHT'],['existing','PRODUCTION_READ_ONLY_PREFLIGHT']])test(`${entry}/${flag}: synthetic read-only route terminates without receipt or writes`,context=>{
const f=fixture(context,syntheticEnv),r=f.invoke(entry,{[flag]:'1'});
assert.equal(r.status,0,output(r));assert.equal(writes(r).length,0);
assert.equal(named(r,'bash','deploy.sh').length,0);
if(flag==='PRODUCTION_IDENTITY_ONLY') {
assert.equal(named(r,'bash','deploy-existing.sh').length,0);
assert.ok(!named(r,'ssh').some(c=>c.args.at(-1).includes("IDENTITY_BOOTSTRAP_MODE='database'")));
}
});
for(const entry of ['ci-release','all','deploy','raw'])test(`${entry}: read-only flags cannot masquerade as successful publication`,context=>{
const f=fixture(context,syntheticEnv);
for(const flag of ['PRODUCTION_IDENTITY_ONLY','PRODUCTION_READ_ONLY_PREFLIGHT'])blockedBeforeAccess(f.invoke(entry,{[flag]:'1'}));
});
for(const [entry,point] of [['bootstrap','origin'],['existing','preflight'],['raw','remote-preflight']])test(`${entry}: source or receipt changed after initial check cannot write`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke(entry,{HYB_MUTATE_AT:point});
assert.notEqual(r.status,0,output(r));assert.equal(writes(r).length,0);
assert.equal(named(r,'bash','deploy.sh').length,0);
});
for(const kind of ['head','index','receipt'])test(`raw: ${kind} drift during remote preflight fails final recheck`,context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke('raw',{HYB_MUTATE_AT:'remote-preflight',HYB_MUTATE_KIND:kind});
assert.notEqual(r.status,0,output(r));assert.equal(writes(r).length,0);
});
test('raw: provenance commit must equal receipt/HEAD before archive or environment reads',context=>{
const f=fixture(context,syntheticEnv);complete(f);
const r=f.invoke('raw',{HYB_WRONG_SOURCE:'1'});
assert.notEqual(r.status,0,output(r));assert.equal(writes(r).length,0);
assert.equal(named(r,'node','validate-env-contract.js').length,0);
});
test('caller expected commit is an additional constraint, never an override',context=>{
const f=fixture(context,syntheticEnv);complete(f);
blockedBeforeAccess(f.invoke('bootstrap',{RELEASE_GATE_EXPECTED_COMMIT:'f'.repeat(40)}));
blockedBeforeAccess(f.invoke('raw',{RELEASE_GATE_EXPECTED_COMMIT:''}));
});
test('skip/internal flags never authorize a lower-level write',context=>{
const f=fixture(context,syntheticEnv);complete(f);
for(const entry of entries)blockedBeforeAccess(f.invoke(entry,{PROJECT_WIDE_SKIP_VISUAL:'1',PROJECT_WIDE_SKIP_REASON:'synthetic',PROJECT_WIDE_SKIP_OPERATOR:'fixture',PROJECT_WIDE_SKIP_APPROVAL_REF:'fixture'}));
delete f.env.RELEASE_GATE_RECEIPT_DIR;
for(const entry of ['bootstrap','existing','raw'])blockedBeforeAccess(f.invoke(entry,{GATE_PASSED:'1',SKIP_RELEASE_GATE:'1',DEPLOY_DEFER_FINAL_SUCCESS:'1'}));
});
test('receipt directory or record symlinks and outside paths are refused',context=>{
const f=fixture(context,syntheticEnv);complete(f);
const link=path.join(f.root,'artifacts/release-gate/linked');
symlinkSync(f.env.RELEASE_GATE_RECEIPT_DIR,link);
blockedBeforeAccess(f.invoke('raw',{RELEASE_GATE_RECEIPT_DIR:link}));
blockedBeforeAccess(f.invoke('raw',{RELEASE_GATE_RECEIPT_DIR:f.root}));
const file=path.join(f.env.RELEASE_GATE_RECEIPT_DIR,'release-result.json');
rmSync(file);symlinkSync('/synthetic-missing-target',file);
blockedBeforeAccess(f.invoke('raw'));
});
test('removing bootstrap first check is caught by config-access assertion',context=>{
const f=fixture(context,syntheticEnv);blockedBeforeAccess(f.invoke('bootstrap'));
replace(f,'deploy/trialrun/production-identity-bootstrap.mjs',
'const release = identityOnly === "1" || preflightOnly ? null : requireReleaseReceipt();',
'const release = null; // isolated mutation');
assert.throws(()=>blockedBeforeAccess(f.invoke('bootstrap')),assert.AssertionError);
});
test('removing final raw check makes the no-write counterexample fail',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'deploy/trialrun/deploy.sh',
'node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$SOURCE_COMMIT" >/dev/null\n\necho ">>> 3.1',
': # isolated mutation\n\necho ">>> 3.1');
complete(f);const r=f.invoke('raw',{HYB_MUTATE_AT:'remote-preflight'});
assert.throws(()=>assert.equal(writes(r).length,0),assert.AssertionError);
});
test('removing existing-wrapper recheck is caught before lower-level invocation',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'deploy/trialrun/deploy-existing.sh',
'node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$source_commit" >/dev/null',
': # isolated mutation');
complete(f);const r=f.invoke('existing',{HYB_MUTATE_AT:'preflight'});
assert.equal(writes(r).length,0); // Raw entry still independently protects writes.
assert.throws(()=>assert.equal(named(r,'bash','deploy.sh').length,0),assert.AssertionError);
});
test('relaxing expected commit binding is caught before environment/archive',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'scripts/qa/release-gate-result.mjs',
"expected!==undefined && (!/^[a-f0-9]{40}$/.test(expected) || expected!==current.source.commit)",
'false');
complete(f);const r=f.invoke('raw',{HYB_WRONG_SOURCE:'1'});
assert.throws(()=>assert.equal(named(r,'node','validate-env-contract.js').length,0),assert.AssertionError);
});
test('removing bootstrap second check is caught before wrapper invocation',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'deploy/trialrun/production-identity-bootstrap.mjs',
'if (release) requireReleaseReceipt(release.directory, release.commit);',
'// isolated mutation');
complete(f);const r=f.invoke('bootstrap',{HYB_MUTATE_AT:'origin'});
assert.equal(writes(r).length,0);
assert.throws(()=>assert.equal(named(r,'bash','deploy-existing.sh').length,0),assert.AssertionError);
});
test('relaxing clean-candidate validation makes the dirty receipt counterexample fail',context=>{
const f=fixture(context,syntheticEnv);
replace(f,'scripts/qa/release-gate-result.mjs','source.dirty===false','true');
writeFileSync(path.join(f.root,'client/package.json'),'{"dirty":true}');
const r=f.run();
// Deliberate verifier mutation admits a consistently dirty fixture. The
// ordinary shell suite independently requires PARTIAL/2 for this same case.
assert.throws(()=>assert.equal(r.status,2,output(r)),assert.AssertionError);
assert.equal(r.status,0,output(r));
});
test('private fixture rejects unknown tools and never falls back to real SSH/Docker',context=>{
const f=fixture(context,syntheticEnv);
assert.equal(existsSync(path.join(f.root,'bin/docker')),true);
assert.equal(f.env.PATH,path.join(f.root,'bin'));
assert.equal(f.env.HOME,path.join(f.root,'bin/home'));
for(const command of ['ssh','docker','git']) {
const r=spawnSync(path.join(f.root,'bin',command),['unexpected'],{env:f.env,cwd:f.root,encoding:'utf8'});
assert.equal(r.status,97,r.stdout+r.stderr);
}
});