接入发布候选回执强制校验(09-06-release-gate-integrity)

这个提交包含在:
2026-09-19 14:25:33 +08:00
父节点 1f8d5b658a
当前提交 16b9cad98c
共修改 15 个文件,包含 722 行新增和 91 行删除
+51 -4
查看文件
@@ -11,6 +11,7 @@ import { verifyLocalCandidateResults } from './verify-local-candidate-results.mj
import { FULL_LOCAL_STAGES, newStageRecord, updateStageRecord, validateStages, validTime } from './qa-stage-record.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const DEPLOYMENT_ENTRIES = ['deploy/trialrun/production-identity-bootstrap.mjs','deploy/trialrun/deploy-existing.sh','deploy/trialrun/deploy.sh'];
const hash = value => createHash('sha256').update(value).digest('hex');
const read = file => JSON.parse(readFileSync(file,'utf8'));
const PHASES = ['static','full-local'];
@@ -37,7 +38,7 @@ export function captureReleaseSource(root=ROOT) {
return {...source,untrackedDigest:hash(JSON.stringify(files))};
}
export function currentPolicy(root=ROOT) {
return {digest:hash(JSON.stringify(POLICY_FILES.map(file=>[file,hash(readFileSync(path.join(root,file)))]))), environmentDigest:environmentPolicyDigest(), runnerDigest:hash(readFileSync(path.join(root,'scripts/qa/full-local-environment.mjs'))), probeDigest:hash(readFileSync(path.join(root,'scripts/qa/browser-environment-probe.mjs'))), toolchain:{node:process.version,playwright:read(path.join(root,'client/node_modules/@playwright/test/package.json')).version,browserRegistry:hash(readFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'))),browserVersions:Object.fromEntries(read(path.join(root,'client/node_modules/playwright-core/browsers.json')).browsers.filter(browser=>['chromium','webkit'].includes(browser.name)).map(browser=>[browser.name,browser.browserVersion])),platform:process.platform,arch:process.arch,ci:Boolean(process.env.CI),workers:process.env.CI?2:4}};
return {digest:hash(JSON.stringify([...POLICY_FILES,...DEPLOYMENT_ENTRIES].map(file=>[file,hash(readFileSync(path.join(root,file)))]))), environmentDigest:environmentPolicyDigest(), runnerDigest:hash(readFileSync(path.join(root,'scripts/qa/full-local-environment.mjs'))), probeDigest:hash(readFileSync(path.join(root,'scripts/qa/browser-environment-probe.mjs'))), toolchain:{node:process.version,playwright:read(path.join(root,'client/node_modules/@playwright/test/package.json')).version,browserRegistry:hash(readFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'))),browserVersions:Object.fromEntries(read(path.join(root,'client/node_modules/playwright-core/browsers.json')).browsers.filter(browser=>['chromium','webkit'].includes(browser.name)).map(browser=>[browser.name,browser.browserVersion])),platform:process.platform,arch:process.arch,ci:Boolean(process.env.CI),workers:process.env.CI?2:4}};
}
function saneSource(source) {
return /^[a-f0-9]{40}$/.test(source?.commit ?? '') && /^[a-f0-9]{40}$/.test(source?.tree ?? '') && /^[a-f0-9]{64}$/.test(source?.digest ?? '') && source?.repository?.trackedCoverageComplete === true && typeof source.repository.porcelainV1Z === 'string' && source.dirty === Boolean(source.repository.porcelainV1Z.length);
@@ -122,7 +123,7 @@ function readBundle(record,seal=false) {
}
function facts() {return {root:ROOT,source:captureReleaseSource(),policy:currentPolicy(),contract:read(path.join(ROOT,'scripts/qa/full-local-coverage-contract.json')),manifest:read(path.join(ROOT,'scripts/qa/route-manifest.json'))};}
function ownedDirectory(input,create=false) {
const relative=path.relative(ROOT,path.resolve(input)).split(path.sep).join('/');
const relative=path.relative(ROOT,path.resolve(ROOT,input)).split(path.sep).join('/');
if (!relative.startsWith('artifacts/release-gate/')) throw Error('release_report_outside_artifacts');
const dir=confinedPath(ROOT,relative);
if (create && existsSync(dir) && readdirSync(dir).length) throw Error('release_report_not_empty');
@@ -130,10 +131,52 @@ function ownedDirectory(input,create=false) {
if (realpathSync(dir)!==dir) throw Error('release_report_symlink');
return dir;
}
export class ReleaseGateError extends Error {
constructor(code) { super(code); this.code=code; }
}
export function deploymentReadOnlyMode(env=process.env, allowReadOnly=false) {
const flags=['PRODUCTION_IDENTITY_ONLY','PRODUCTION_READ_ONLY_PREFLIGHT'].map(key=>env[key]??'0');
if (flags.some(value=>!['0','1'].includes(value))) throw new ReleaseGateError('read_only_flag_invalid');
if (!allowReadOnly && flags.includes('1')) throw new ReleaseGateError('read_only_cannot_deploy');
return {identityOnly:flags[0]==='1',preflightOnly:flags[1]==='1'};
}
// Read-only assertion: never generates evidence or invokes a test/deploy runner.
// A returned commit is pinned by each caller across its subsequent preflights.
export function requireReleaseReceipt(input=process.env.RELEASE_GATE_RECEIPT_DIR, expectedCommit) {
try {
deploymentReadOnlyMode();
validateSkipRequest('deploy',process.env);
if (typeof input!=='string' || !input.trim()) throw new ReleaseGateError('receipt_required');
const dir=ownedDirectory(input);
const file=confinedPath(dir,'release-result.json');
if (!lstatSync(file).isFile()) throw new ReleaseGateError('receipt_invalid');
const record=read(file), current=facts();
for (const expected of [expectedCommit,process.env.RELEASE_GATE_EXPECTED_COMMIT]) {
if (expected!==undefined && (!/^[a-f0-9]{40}$/.test(expected) || expected!==current.source.commit)) {
throw new ReleaseGateError('candidate_commit_mismatch');
}
}
const result=verifyReleaseEvidence(record,readBundle(record),current);
if (!result.deploymentEligible) throw new ReleaseGateError('receipt_not_eligible');
return {commit:current.source.commit,directory:dir};
} catch (error) {
if (error instanceof ReleaseGateError) throw error;
throw new ReleaseGateError('receipt_invalid');
}
}
if (process.argv[1] && import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href) {
try {
const [action,input,arg,rawExit]=process.argv.slice(2);
if (action==='skip') {
if (action==='require') {
console.log(requireReleaseReceipt(input,arg).commit);
} else if (action==='write-mode') {
deploymentReadOnlyMode();
validateSkipRequest('deploy',process.env);
} else if (action==='skip') {
const result=validateSkipRequest(arg,process.env);
if (result) { const dir=ownedDirectory(input,true);atomic(confinedPath(dir,'skip-receipt.json'),result);console.log('[release-gate] approved diagnostic skips: PARTIAL'); }
} else {
@@ -162,5 +205,9 @@ if (process.argv[1] && import.meta.url===pathToFileURL(path.resolve(process.argv
} else throw Error('action_invalid');
}
}
} catch {console.error('[release-receipt] invalid_or_incomplete');process.exitCode=1;}
} catch (error) {
const code=error instanceof ReleaseGateError?error.code:'invalid_or_incomplete';
console.error(`[release-receipt] ${code}; run verify-candidate on the final clean candidate before deployment`);
process.exitCode=1;
}
}
+35 -10
查看文件
@@ -2,6 +2,7 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
ARTIFACT_ROOT="${RELEASE_GATE_ARTIFACT_DIR:-${ROOT_DIR}/artifacts/release-gate/$(date +%Y%m%d-%H%M%S)}"
MODE="${1:-all}"
CURRENT_STAGE=""
@@ -16,13 +17,14 @@ Usage: bash scripts/qa/run-release-gate.sh <verify-candidate|all|static|smoke-lo
smoke-local 运行本地网页 smoke 门禁
full-local 运行本地全量门禁(server regression + client full E2E)
project-wide 项目级门禁:static + 覆盖扫描 + UI/移动契约 + 迁移漂移诊断 + 业务流 + 全页面视觉 + live readiness
deploy 调用 deploy/trialrun/deploy.sh,内部会串联 /ready、external deps、runtime smoke 与 live smoke
all 依次执行 static -> smoke-local -> full-local -> deploy
ci-release 依次执行 static -> full-local -> deploy
deploy 只消费显式回执,经生产身份引导器发布;不隐式运行测试
all smoke-local -> 单次 verify-candidate(或重验显式回执)-> 生产发布链
ci-release 单次 verify-candidate(或重验显式回执)-> 生产发布链
r66-trialrun R66 小团队试点门禁:static -> server regression-all -> smoke-local
可选环境变量:
RELEASE_GATE_ARTIFACT_DIR 失败产物输出目录
RELEASE_GATE_RECEIPT_DIR 显式消费的完整候选回执目录;不自动选择历史回执
QA_* 透传给 scripts/qa/run-web-gates.sh
LIVE_SMOKE_* 透传给 deploy.sh 与 live smoke
PROJECT_WIDE_SKIP_BUSINESS_FLOW=1 跳过业务流 E2E
@@ -45,7 +47,11 @@ collect_failure_artifacts() {
handle_error() {
local exit_code="$?"
echo "[release-gate][FAIL] 阶段 ${CURRENT_STAGE:-unknown} 失败(exit=${exit_code})" >&2
collect_failure_artifacts
# Rejected publication must not trigger diagnostic reads of real configuration.
case "$MODE" in
ci-release|all|deploy) ;;
*) collect_failure_artifacts ;;
esac
exit "$exit_code"
}
trap handle_error ERR
@@ -129,10 +135,21 @@ stage_project_wide_live_readiness() {
}
stage_deploy() {
bash "$ROOT_DIR/deploy/trialrun/deploy.sh"
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
node "$ROOT_DIR/deploy/trialrun/production-identity-bootstrap.mjs"
}
# This entry does not install enforcement in any existing deployment path.
prepare_release_receipt() {
if [[ -z "${RELEASE_GATE_RECEIPT_DIR+x}" ]]; then
# This verification-only child cannot call back into the deployment chain.
RELEASE_GATE_ARTIFACT_DIR="$ARTIFACT_ROOT" bash "$ROOT_DIR/scripts/qa/run-release-gate.sh" verify-candidate
RELEASE_GATE_RECEIPT_DIR="$ARTIFACT_ROOT"
export RELEASE_GATE_RECEIPT_DIR
fi
}
# The verification-only mode records its own original exit and never deploys.
finish_candidate() {
local original_status=$? receipt_status=0
trap - EXIT ERR INT TERM
@@ -145,6 +162,16 @@ if [[ "$MODE" != "help" && "$MODE" != "-h" && "$MODE" != "--help" ]]; then
# Validate before static, remote readiness or deployment side effects.
trap - ERR
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" skip "$ARTIFACT_ROOT" "$MODE"
case "$MODE" in
ci-release|all|deploy)
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" write-mode
# Invalid explicit receipts fail before smoke or other side effects.
if [[ -n "${RELEASE_GATE_RECEIPT_DIR+x}" || "$MODE" == "deploy" ]]; then
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
fi
;;
esac
if [[ "$MODE" != "verify-candidate" ]]; then trap handle_error ERR; fi
fi
@@ -186,8 +213,7 @@ case "$MODE" in
run_stage deploy stage_deploy
;;
ci-release)
run_stage static stage_static
run_stage full-local stage_full_local
prepare_release_receipt
run_stage deploy stage_deploy
;;
r66-trialrun)
@@ -196,9 +222,8 @@ case "$MODE" in
run_stage smoke-local stage_smoke_local
;;
all)
run_stage static stage_static
run_stage smoke-local stage_smoke_local
run_stage full-local stage_full_local
prepare_release_receipt
run_stage deploy stage_deploy
;;
*)
+89
查看文件
@@ -0,0 +1,89 @@
// Private subprocess fixture. There is deliberately no real-network fallback.
const fs = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const root = process.env.HYB_RELEASE_FIXTURE_ROOT;
if (!root || !path.basename(root).startsWith('hyb-release-shell-')) process.exit(97);
const args = process.argv.slice(2), name = path.basename(process.argv[1]);
const trace = path.join(root, 'artifacts/commands.jsonl');
fs.mkdirSync(path.dirname(trace), { recursive: true });
fs.appendFileSync(trace, JSON.stringify({ name, args }) + '\n');
function run(command, values) {
const result = spawnSync(command, values, { cwd: root, env: process.env, stdio: 'inherit' });
process.exit(result.status ?? 97);
}
function output(value) { process.stdout.write(value + '\n'); process.exit(0); }
function git(values) {
const result = spawnSync('/usr/bin/git', values, { cwd: root, env: process.env, encoding: 'utf8' });
if (result.status !== 0) process.exit(97);
return result.stdout.trim();
}
function mutate(point) {
if (process.env.HYB_MUTATE_AT !== point || fs.existsSync(path.join(root, 'artifacts/mutated'))) return;
fs.writeFileSync(path.join(root, 'artifacts/mutated'), point);
const kind = process.env.HYB_MUTATE_KIND || 'tracked';
if (kind === 'receipt') {
const file = path.join(process.env.RELEASE_GATE_RECEIPT_DIR, 'release-result.json');
const record = JSON.parse(fs.readFileSync(file)); record.executionExit = 23;
fs.writeFileSync(file, JSON.stringify(record));
} else if (kind === 'head') {
git(['-c', 'user.name=Fixture', '-c', '[email protected]', 'commit', '--allow-empty', '-qm', 'changed']);
} else {
fs.appendFileSync(path.join(root, 'client/package.json'), ' ');
if (kind === 'index') git(['add', 'client/package.json']);
}
}
const fullCommit = () => git(['rev-parse', 'HEAD']);
const contract = 'REMOTE_DIR=/fixture/app REMOTE_ENV_FILE=/fixture/app/deploy/trialrun/.env COMPOSE_FILE=deploy/trialrun/docker-compose.yml COMPOSE_PROJECT_NAME=fixture-project POSTGRES_DATA_DIR=/fixture/db STORAGE_HOST_DIR=/fixture/storage';
if (name === 'git') {
if (args[0] === 'show' && args[1] === '34181e3:deploy/trialrun/deploy.sh') output('DEPLOY_SERVER="${DEPLOY_SERVER:-fixture-target}"');
if (args[0] === 'show' && args[1] === '5e2d038938265c926e06d3cc6d0841f6dcc06148:deploy/trialrun/deploy.sh') output('COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-fixture-project}"');
const values = args[0] === '-C' && args[1] === root ? args.slice(2) : args;
if (['ls-files', 'status', 'rev-parse', 'archive'].includes(values[0])) run('/usr/bin/git', values);
process.exit(97);
}
if (name === 'node') {
if (args[0] === '-e') run(process.execPath, args);
const file = path.basename(args[0] || '');
if (file === 'release-gate-result.mjs' || file === 'production-identity-bootstrap.mjs') {
run(process.execPath, ['--require', path.join(root, 'bin/fs-guard.cjs'), ...args]);
}
if (file === 'check-source-provenance.mjs') {
mutate('provenance');
const commit = process.env.HYB_WRONG_SOURCE ? 'f'.repeat(40) : fullCommit();
output(JSON.stringify({ commit, branch: 'fixture', published: true, remoteVerified: true, overrideUsed: false, buildEnv: { BUILD_SHA: commit, VITE_BUILD_SHA: commit.slice(0, 12) } }));
}
if (file === 'production-preflight.mjs') { mutate('preflight'); process.exit(0); }
if (file === 'validate-env-contract.js' && process.env.FAIL_STATIC) process.exit(23);
if (['validate-env-contract.js', 'check-public-bundle-copy.mjs', 'check-ui-design-rules.mjs', 'check-error-reason-contract.mjs', 'check-mobile-route-contract.mjs', 'check-flow-coverage.mjs', 'check-live-smoke-env.mjs'].includes(file)) process.exit(0);
process.exit(97);
}
if (name === 'bash') {
const file = path.basename(args[0] || '');
if (['run-release-gate.sh', 'deploy-existing.sh', 'deploy.sh'].includes(file)) run('/bin/bash', args);
if (file === 'run-web-gates.sh' && args[1] === 'full-local') {
if (process.env.OMIT_EVIDENCE) process.exit(0);
run(process.execPath, [path.join(root, 'bin/evidence.mjs')]);
}
if (['run-web-gates.sh', 'run-business-flow-gates.sh', 'run-live-smoke-readiness.sh', 'check-local-migration-drift.sh', 'collect-failure-artifacts.sh', 'check-existing-deployment.sh'].includes(file)) process.exit(0);
process.exit(97);
}
if (name === 'ssh') {
const last = args.at(-1) || '';
if (last === 'probe') output('[deployment-contract-refresh][PASS] probe');
if (last === 'refresh') output('[deployment-contract-refresh][PASS] refreshed');
if (last.startsWith('systemctl show ')) output(contract);
if (last.includes('bash -s')) {
fs.readFileSync(0); // Drain only the synthetic script; never execute it.
if (last.includes("IDENTITY_BOOTSTRAP_MODE='origin'")) { mutate('origin'); output('[production-identity-bootstrap][PASS] origin_identity'); }
if (last.includes("IDENTITY_BOOTSTRAP_MODE='database'")) output('[production-identity-bootstrap][DB_SHA256] ' + 'a'.repeat(64));
if (!last.includes('BUILD_SHA=')) mutate('remote-preflight');
process.exit(0);
}
if (last === "mkdir -p '/fixture/app'") process.exit(0);
process.exit(97);
}
if (name === 'rsync') process.exit(0);
if (name === 'curl') output(JSON.stringify({ data: { buildSha: fullCommit().slice(0, 12) } }));
if (name === 'npm' || name === 'python3') process.exit(0);
process.exit(97);
+15
查看文件
@@ -0,0 +1,15 @@
// Loaded only in private fixture children; audit config access without real HOME.
const fs = require('node:fs');
const path = require('node:path');
const { syncBuiltinESMExports } = require('node:module');
const root = process.env.HYB_RELEASE_FIXTURE_ROOT;
const original = fs.readFileSync;
fs.readFileSync = function(file, ...args) {
const text = String(file);
if (text.endsWith('/.ssh/config') || /\/\.env(?:$|\.)/.test(text)) {
if (text !== path.join(root, 'bin/home/.ssh/config')) throw Error('fixture_config_read_forbidden');
fs.appendFileSync(path.join(root, 'artifacts/commands.jsonl'), JSON.stringify({ name: 'config-read', args: ['synthetic-ssh'] }) + '\n');
}
return original.call(this, file, ...args);
};
syncBuiltinESMExports();
+85
查看文件
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
const projectRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../../..');
const receiptFiles={environment:'environment/result.json',phases:'results/full-local-stages.json',discovery:'results/discovery.json',plan:'results/coverage-plan.json',execution:'results/playwright-results.json',selection:'results/execution-selection.json'};
export function fixture(context,overrides={}) {
const root=mkdtempSync(path.join(os.tmpdir(),'hyb-release-shell-'));
context.after(()=>rmSync(root,{recursive:true,force:true}));
for(const dir of ['scripts/qa','deploy/trialrun','client/tests/e2e','server','bin','client/node_modules/@playwright/test','client/node_modules/playwright-core'])mkdirSync(path.join(root,dir),{recursive:true});
for(const file of readdirSync(path.join(projectRoot,'scripts/qa')).filter(file=>file.endsWith('.mjs')||file.endsWith('.sh')||file.endsWith('.py')))copyFileSync(path.join(projectRoot,'scripts/qa',file),path.join(root,'scripts/qa',file));
for(const file of readdirSync(path.join(projectRoot,'deploy/trialrun')).filter(file=>/\.(mjs|sh|yml)$/.test(file)))copyFileSync(path.join(projectRoot,'deploy/trialrun',file),path.join(root,'deploy/trialrun',file));
for(const dir of ['docs/trialrun','server/src/scripts','bin/home/.ssh'])mkdirSync(path.join(root,dir),{recursive:true});
writeFileSync(path.join(root,'bin/home/.ssh/config'),'Host fixture-refresh\n');
writeFileSync(path.join(root,'docs/trialrun/OPS7运维交接Runbook.md'),'curl -fsS https://fixture.invalid/ready\n');
for(const file of ['smoke-trialrun-runtime.js','smoke-external-deps.js'])writeFileSync(path.join(root,'server/src/scripts',file),'// synthetic');
const rows=Array.from({length:10},(_,i)=>({file:`client/tests/e2e/flow-${i}.spec.ts`,id:`flow-${i}`,title:`flow-${i} @local-candidate:flow-${i}`,line:1}));
rows.push({file:'client/tests/e2e/ordinary.spec.ts',id:'ordinary',title:'ordinary',line:1});
for(const row of rows)writeFileSync(path.join(root,row.file),'synthetic');
const manifest={localCandidateFlows:rows.slice(0,10).map((row,i)=>({bundle:`flow-${i}`,witnessTag:`@local-candidate:flow-${i}`,witnessSpec:row.file})),acceptanceSpecs:rows.map(row=>row.file)};
const contract={schemaVersion:1,approvalReference:'synthetic',exceptions:[]};
writeFileSync(path.join(root,'scripts/qa/route-manifest.json'),JSON.stringify(manifest));
writeFileSync(path.join(root,'scripts/qa/full-local-coverage-contract.json'),JSON.stringify(contract));
for(const file of ['client/package.json','client/package-lock.json','server/package.json','server/package-lock.json','client/node_modules/playwright-core/browsers.json'])writeFileSync(path.join(root,file),'{}');
writeFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'),JSON.stringify({browsers:['chromium','webkit'].map(name=>({name,browserVersion:'synthetic'}))}));
writeFileSync(path.join(root,'client/node_modules/@playwright/test/package.json'),JSON.stringify({version:'1.58.2'}));
writeFileSync(path.join(root,'client/playwright.config.ts'),'// synthetic policy');
writeFileSync(path.join(root,'deploy/trialrun/validate-env-contract.js'),'// fake tooling owns this command');
writeFileSync(path.join(root,'.gitignore'),'artifacts/\nbin/\nnode_modules/\n');
// Private child environment: no real HOME/config, credentials or fallback PATH.
const env={PATH:path.join(root,'bin'),HOME:path.join(root,'bin/home'),GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_CONFIG_SYSTEM:'/dev/null',LANG:'C.UTF-8',HYB_RELEASE_FIXTURE_ROOT:root,RELEASE_GATE_ARTIFACT_DIR:path.join(root,'artifacts/release-gate/run'),...overrides};
const fake=`#!${process.execPath}\nrequire('./deploy-fake.cjs');\n`;
writeFileSync(path.join(root,'bin/fake'),fake,{mode:0o755});
for(const name of ['node','npm','bash','python3','git','ssh','rsync','curl','docker'])symlinkSync('fake',path.join(root,'bin',name));
for(const name of ['dirname','date','mkdir','mktemp','tar','rm'])symlinkSync('/usr/bin/'+name,path.join(root,'bin',name));
copyFileSync(path.join(projectRoot,'scripts/qa/tests/fixtures/release-deploy-fake.cjs'),path.join(root,'bin/deploy-fake.cjs'));
copyFileSync(path.join(projectRoot,'scripts/qa/tests/fixtures/release-fs-guard.cjs'),path.join(root,'bin/fs-guard.cjs'));
writeFileSync(path.join(root,'bin/evidence.mjs'),`
import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';
import path from 'node:path';
import {captureReleaseSource,currentPolicy} from '../scripts/qa/release-gate-result.mjs';
import {FULL_LOCAL_STAGES,newStageRecord,updateStageRecord} from '../scripts/qa/qa-stage-record.mjs';
import {coveragePlan,localSpecArgs} from '../scripts/qa/full-local-coverage.mjs';
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,base=process.env.QA_FULL_REPORT_DIR,now=()=>new Date().toISOString();
const source=captureReleaseSource(root),policy=currentPolicy(root),startedAt=now();
const phases=newStageRecord(FULL_LOCAL_STAGES,'full-local-stages');
const rows=${JSON.stringify(rows)},contract=${JSON.stringify(contract)};
const report=executed=>({errors:[],suites:rows.map(row=>({file:row.file,specs:[{...row,tests:[{projectName:'',repeatEachIndex:0,...(executed?{expectedStatus:'passed',status:'expected',results:[{status:'passed',retry:0}]}:{})}]}]}))});
const discovery=report(false),execution=report(true);
execution.config={forbidOnly:true,projects:[{name:'',retries:0,repeatEach:1,timeout:180000,testIgnore:['**/manual/**']}],shard:null,maxFailures:0,workers:policy.toolchain.workers,version:policy.toolchain.playwright};
for(const stage of FULL_LOCAL_STAGES){updateStageRecord(phases,'start',stage);if(stage==='browser')execution.stats={startTime:now(),duration:0,expected:rows.length,unexpected:0,flaky:0,skipped:0};updateStageRecord(phases,'end',stage,0);}updateStageRecord(phases,'finish',null,0);
const environment={schemaVersion:1,profile:'isolated-online-v1',mode:'full-local',policyDigest:policy.environmentDigest,platform:process.platform,status:'PASS',exitCode:0,childExit:0,childSignal:null,interrupted:null,startedAt,endedAt:now(),sourceBefore:source,sourceAfter:source,sourceUnchanged:true,repositoryUnchanged:true,candidateIntegrityStatus:'UNCHANGED',cleanup:{containerOwnerVerified:true,containerRemoved:true,scratchRemoved:true},environment:{schemaVersion:1,profile:'isolated-online-v1',policyDigest:policy.environmentDigest,platform:process.platform,runnerDigest:policy.runnerDigest,probeDigest:policy.probeDigest,supervisorObservation:{source:'live-supervisor-unix-ipc',sampledAt:now(),namespace:'parent'},namespace:{inheritedRunner:'parent',ownedChild:'child',runnerCreated:true,currentMatchesOwnedChild:true,topologyMatched:true},nativeOnlineProbe:{valid:true,online:true,browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,engines:['chromium','webkit'].map(name=>({name,version:'synthetic',online:true,valid:true,noNavigatorOverride:true,browserNamespace:'child',browserMatchesOwnedChild:true}))},bootstrapObservation:{valid:true,observationPoint:'guest-entry-black-box',freshContext:true,initialSessionHint:false,listenerBeforeNavigation:true,noRouteMocks:true,loginFormVisible:true,refreshRequestCount:0,loginRequestCount:0,nativeOnlineSamples:{beforeNavigation:true,documentStart:true,afterForm:true},browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,documentStatus:200,stayedOnLogin:true}}};
const bundle={environment,phases,discovery,execution,plan:coveragePlan(root,discovery,contract,source)};
bundle.selection={schemaVersion:1,profile:'full-local-selection',specArgs:localSpecArgs(bundle.plan),additionalArgs:[],retries:0,forbidOnly:true,workers:policy.toolchain.workers,mode:'local'};
if(process.env.BAD_PROFILE)environment.mode='retained-local';
if(process.env.BAD_SOURCE)environment.sourceBefore={...source,digest:'0'.repeat(64)};
if(process.env.BAD_COVERAGE)execution.suites.pop();
if(process.env.BAD_CLEANUP)environment.cleanup.containerRemoved=false;
if(process.env.BAD_STAGE)phases.stages.splice(2,1);
for(const [key,relative] of Object.entries(${JSON.stringify(receiptFiles)})){const file=path.join(base,relative);mkdirSync(path.dirname(file),{recursive:true});writeFileSync(file,JSON.stringify(bundle[key]));}
if(process.env.FAIL_FULL)process.exit(17);
`);
for(const args of [['init','-q'],['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic fixture']])assert.equal(spawnSync('/usr/bin/git',args,{cwd:root,env}).status,0);
const commands=()=>existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').filter(Boolean).map(JSON.parse):[];
return {root,env,commands,commit() {
for(const args of [['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic mutation']])assert.equal(spawnSync('/usr/bin/git',args,{cwd:root,env}).status,0);
},invoke(entry,overrides={}) {
rmSync(path.join(root,'artifacts/commands.jsonl'),{force:true});
const script=entry==='bootstrap'?'deploy/trialrun/production-identity-bootstrap.mjs':entry==='existing'?'deploy/trialrun/deploy-existing.sh':entry==='raw'?'deploy/trialrun/deploy.sh':'scripts/qa/run-release-gate.sh';
const argv=[path.join(root,script),...(['ci-release','all','deploy'].includes(entry)?[entry]:[])];
const childEnv={...env,...overrides};
const executable=entry==='bootstrap'?process.execPath:'/bin/bash';
if(entry==='bootstrap')argv.unshift('--require',path.join(root,'bin/fs-guard.cjs'));
const result=spawnSync(executable,argv,{cwd:root,env:childEnv,encoding:'utf8',timeout:30000});
return {...result,commands:commands()};
},run(mode='verify-candidate') {
rmSync(path.join(root,'artifacts'),{recursive:true,force:true});
const result=spawnSync('/bin/bash',[path.join(root,'scripts/qa/run-release-gate.sh'),mode],{cwd:root,env,encoding:'utf8',timeout:30000});
const file=path.join(env.RELEASE_GATE_ARTIFACT_DIR,'release-result.json');
return {...result,record:existsSync(file)?JSON.parse(readFileSync(file)):null,commands:existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').map(JSON.parse):[]};
},verify() {return spawnSync(process.execPath,[path.join(root,'scripts/qa/release-gate-result.mjs'),'verify',env.RELEASE_GATE_ARTIFACT_DIR],{cwd:root,env,encoding:'utf8',timeout:30000});}};
}
@@ -0,0 +1,211 @@
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);
}
});
+2 -70
查看文件
@@ -1,77 +1,9 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { existsSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { fixture } from './fixtures/release-gate-fixture.mjs';
const projectRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../..');
const receiptFiles={environment:'environment/result.json',phases:'results/full-local-stages.json',discovery:'results/discovery.json',plan:'results/coverage-plan.json',execution:'results/playwright-results.json',selection:'results/execution-selection.json'};
function fixture(context,overrides={}) {
const root=mkdtempSync(path.join(os.tmpdir(),'hyb-release-shell-'));
context.after(()=>rmSync(root,{recursive:true,force:true}));
for(const dir of ['scripts/qa','deploy/trialrun','client/tests/e2e','server','bin','client/node_modules/@playwright/test','client/node_modules/playwright-core'])mkdirSync(path.join(root,dir),{recursive:true});
for(const file of readdirSync(path.join(projectRoot,'scripts/qa')).filter(file=>file.endsWith('.mjs')||file.endsWith('.sh')||file.endsWith('.py')))copyFileSync(path.join(projectRoot,'scripts/qa',file),path.join(root,'scripts/qa',file));
const rows=Array.from({length:10},(_,i)=>({file:`client/tests/e2e/flow-${i}.spec.ts`,id:`flow-${i}`,title:`flow-${i} @local-candidate:flow-${i}`,line:1}));
rows.push({file:'client/tests/e2e/ordinary.spec.ts',id:'ordinary',title:'ordinary',line:1});
for(const row of rows)writeFileSync(path.join(root,row.file),'synthetic');
const manifest={localCandidateFlows:rows.slice(0,10).map((row,i)=>({bundle:`flow-${i}`,witnessTag:`@local-candidate:flow-${i}`,witnessSpec:row.file})),acceptanceSpecs:rows.map(row=>row.file)};
const contract={schemaVersion:1,approvalReference:'synthetic',exceptions:[]};
writeFileSync(path.join(root,'scripts/qa/route-manifest.json'),JSON.stringify(manifest));
writeFileSync(path.join(root,'scripts/qa/full-local-coverage-contract.json'),JSON.stringify(contract));
for(const file of ['client/package.json','client/package-lock.json','server/package.json','server/package-lock.json','client/node_modules/playwright-core/browsers.json'])writeFileSync(path.join(root,file),'{}');
writeFileSync(path.join(root,'client/node_modules/playwright-core/browsers.json'),JSON.stringify({browsers:['chromium','webkit'].map(name=>({name,browserVersion:'synthetic'}))}));
writeFileSync(path.join(root,'client/node_modules/@playwright/test/package.json'),JSON.stringify({version:'1.58.2'}));
writeFileSync(path.join(root,'client/playwright.config.ts'),'// synthetic policy');
writeFileSync(path.join(root,'deploy/trialrun/validate-env-contract.js'),'// fake tooling owns this command');
writeFileSync(path.join(root,'.gitignore'),'artifacts/\nbin/\nnode_modules/\n');
const env={PATH:`${path.join(root,'bin')}:${process.env.PATH}`,LANG:'C.UTF-8',HYB_RELEASE_FIXTURE_ROOT:root,RELEASE_GATE_ARTIFACT_DIR:path.join(root,'artifacts/release-gate/run'),...overrides};
const fake=`#!${process.execPath}
const fs=require('node:fs'),path=require('node:path'),{spawnSync}=require('node:child_process');
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,args=process.argv.slice(2),name=path.basename(process.argv[1]);
fs.mkdirSync(path.join(root,'artifacts'),{recursive:true});fs.appendFileSync(path.join(root,'artifacts/commands.jsonl'),JSON.stringify({name,args})+'\\n');
if(name==='node' && args[0]?.endsWith('release-gate-result.mjs')){const result=spawnSync(process.execPath,args,{env:process.env,stdio:'inherit'});process.exit(result.status??1);}
if(name==='node' && args[0]?.endsWith('validate-env-contract.js') && process.env.FAIL_STATIC)process.exit(23);
if(name==='bash' && args[0]?.endsWith('run-web-gates.sh') && args[1]==='full-local'){if(process.env.OMIT_EVIDENCE)process.exit(0);const result=spawnSync(process.execPath,[path.join(root,'bin/evidence.mjs')],{env:process.env,stdio:'inherit'});process.exit(result.status??1);}
if(name==='bash' && args[0]?.includes('deploy/trialrun'))process.exit(91);
if(!['node','npm','bash','python3'].includes(name))throw Error('unexpected command');
`;
writeFileSync(path.join(root,'bin/fake'),fake,{mode:0o755});
for(const name of ['node','npm','bash','python3'])symlinkSync('fake',path.join(root,'bin',name));
writeFileSync(path.join(root,'bin/evidence.mjs'),`
import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';
import path from 'node:path';
import {captureReleaseSource,currentPolicy} from '../scripts/qa/release-gate-result.mjs';
import {FULL_LOCAL_STAGES,newStageRecord,updateStageRecord} from '../scripts/qa/qa-stage-record.mjs';
import {coveragePlan,localSpecArgs} from '../scripts/qa/full-local-coverage.mjs';
const root=process.env.HYB_RELEASE_FIXTURE_ROOT,base=process.env.QA_FULL_REPORT_DIR,now=()=>new Date().toISOString();
const source=captureReleaseSource(root),policy=currentPolicy(root),startedAt=now();
const phases=newStageRecord(FULL_LOCAL_STAGES,'full-local-stages');
const rows=${JSON.stringify(rows)},contract=${JSON.stringify(contract)};
const report=executed=>({errors:[],suites:rows.map(row=>({file:row.file,specs:[{...row,tests:[{projectName:'',repeatEachIndex:0,...(executed?{expectedStatus:'passed',status:'expected',results:[{status:'passed',retry:0}]}:{})}]}]}))});
const discovery=report(false),execution=report(true);
execution.config={forbidOnly:true,projects:[{name:'',retries:0,repeatEach:1,timeout:180000,testIgnore:['**/manual/**']}],shard:null,maxFailures:0,workers:policy.toolchain.workers,version:policy.toolchain.playwright};
for(const stage of FULL_LOCAL_STAGES){updateStageRecord(phases,'start',stage);if(stage==='browser')execution.stats={startTime:now(),duration:0,expected:rows.length,unexpected:0,flaky:0,skipped:0};updateStageRecord(phases,'end',stage,0);}updateStageRecord(phases,'finish',null,0);
const environment={schemaVersion:1,profile:'isolated-online-v1',mode:'full-local',policyDigest:policy.environmentDigest,platform:process.platform,status:'PASS',exitCode:0,childExit:0,childSignal:null,interrupted:null,startedAt,endedAt:now(),sourceBefore:source,sourceAfter:source,sourceUnchanged:true,repositoryUnchanged:true,candidateIntegrityStatus:'UNCHANGED',cleanup:{containerOwnerVerified:true,containerRemoved:true,scratchRemoved:true},environment:{schemaVersion:1,profile:'isolated-online-v1',policyDigest:policy.environmentDigest,platform:process.platform,runnerDigest:policy.runnerDigest,probeDigest:policy.probeDigest,supervisorObservation:{source:'live-supervisor-unix-ipc',sampledAt:now(),namespace:'parent'},namespace:{inheritedRunner:'parent',ownedChild:'child',runnerCreated:true,currentMatchesOwnedChild:true,topologyMatched:true},nativeOnlineProbe:{valid:true,online:true,browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,engines:['chromium','webkit'].map(name=>({name,version:'synthetic',online:true,valid:true,noNavigatorOverride:true,browserNamespace:'child',browserMatchesOwnedChild:true}))},bootstrapObservation:{valid:true,observationPoint:'guest-entry-black-box',freshContext:true,initialSessionHint:false,listenerBeforeNavigation:true,noRouteMocks:true,loginFormVisible:true,refreshRequestCount:0,loginRequestCount:0,nativeOnlineSamples:{beforeNavigation:true,documentStart:true,afterForm:true},browserNamespace:'child',browserMatchesOwnedChild:true,noNavigatorOverride:true,documentStatus:200,stayedOnLogin:true}}};
const bundle={environment,phases,discovery,execution,plan:coveragePlan(root,discovery,contract,source)};
bundle.selection={schemaVersion:1,profile:'full-local-selection',specArgs:localSpecArgs(bundle.plan),additionalArgs:[],retries:0,forbidOnly:true,workers:policy.toolchain.workers,mode:'local'};
if(process.env.BAD_PROFILE)environment.mode='retained-local';
if(process.env.BAD_SOURCE)environment.sourceBefore={...source,digest:'0'.repeat(64)};
if(process.env.BAD_COVERAGE)execution.suites.pop();
if(process.env.BAD_CLEANUP)environment.cleanup.containerRemoved=false;
if(process.env.BAD_STAGE)phases.stages.splice(2,1);
for(const [key,relative] of Object.entries(${JSON.stringify(receiptFiles)})){const file=path.join(base,relative);mkdirSync(path.dirname(file),{recursive:true});writeFileSync(file,JSON.stringify(bundle[key]));}
if(process.env.FAIL_FULL)process.exit(17);
`);
for(const args of [['init','-q'],['add','.'],['-c','user.name=QA Fixture','-c','[email protected]','commit','-qm','synthetic fixture']])assert.equal(spawnSync('git',args,{cwd:root}).status,0);
return {root,env,run(mode='verify-candidate') {
rmSync(path.join(root,'artifacts'),{recursive:true,force:true});
const result=spawnSync('/bin/bash',[path.join(root,'scripts/qa/run-release-gate.sh'),mode],{cwd:root,env,encoding:'utf8',timeout:30000});
const file=path.join(env.RELEASE_GATE_ARTIFACT_DIR,'release-result.json');
return {...result,record:existsSync(file)?JSON.parse(readFileSync(file)):null,commands:existsSync(path.join(root,'artifacts/commands.jsonl'))?readFileSync(path.join(root,'artifacts/commands.jsonl'),'utf8').trim().split('\n').map(JSON.parse):[]};
},verify() {return spawnSync(process.execPath,[path.join(root,'scripts/qa/release-gate-result.mjs'),'verify',env.RELEASE_GATE_ARTIFACT_DIR],{cwd:root,env,encoding:'utf8',timeout:30000});}};
}
function noDeploy(result) {assert.ok(!result.commands.some(c=>c.args.some(arg=>arg.includes('deploy/trialrun/deploy'))));}
test('真实 shell 复用 static/full-local、回执离线重验、部署命令零调用',context=>{