From ffb5d94d55bfa17d05462843689e2c82fe22ce2d Mon Sep 17 00:00:00 2001 From: eimyroot Date: Wed, 2 Sep 2026 14:26:07 +0200 Subject: [PATCH 1/2] ci: enforce full environment release gate --- .github/workflows/ci.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2aa332b..5639b68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,14 @@ on: permissions: contents: read +concurrency: + group: webforge-ci-${{ github.ref }} + cancel-in-progress: true + jobs: verify: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v4 @@ -19,6 +24,12 @@ jobs: with: node-version: 22 + - name: Environment + run: | + node --version + npm --version + google-chrome --version || google-chrome-stable --version || chromium --version || true + - name: Unit and contract tests run: npm test @@ -34,5 +45,8 @@ jobs: - name: Renderer coverage run: npm run renderer:coverage - - name: Release gate + - name: Package release gate run: npm run release:gate + + - name: Full environment release gate + run: npm run release:full From 4573ae3c8256ea440fbc3d259d7c4aeb68ddad82 Mon Sep 17 00:00:00 2001 From: eimyroot Date: Wed, 2 Sep 2026 14:26:23 +0200 Subject: [PATCH 2/2] fix: make deployment success evidence fail closed --- src/core/deployment-executor.mjs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/core/deployment-executor.mjs b/src/core/deployment-executor.mjs index 55ca83a..3436d29 100644 --- a/src/core/deployment-executor.mjs +++ b/src/core/deployment-executor.mjs @@ -6,6 +6,7 @@ import { visualReadinessChecks } from './visual-readiness.mjs'; const tail=s=>String(s||'').slice(-6000); const hasCommand=cmd=>spawnSync('sh',['-lc',`command -v ${cmd}`],{encoding:'utf8'}).status===0; +const sourceSha=()=>process.env.VERCEL_GIT_COMMIT_SHA||process.env.GITHUB_SHA||process.env.CI_COMMIT_SHA||null; function gateChecks(projectDir){ const evidence=JSON.parse(fs.readFileSync(path.join(projectDir,'evidence.receipt.json'),'utf8')); @@ -20,30 +21,41 @@ function gateChecks(projectDir){ ]; } +function deploymentResult({mode,provider,eligibility,checks,commandStatus,output,url}){ + const sha=sourceSha(); + if(commandStatus!==0){ + return {schema:'webforge.deployment-execution.v4',status:'FAIL',mode,provider,url:null,sourceSha:sha,detail:output,eligibility,checks}; + } + if(!url){ + return {schema:'webforge.deployment-execution.v4',status:'UNVERIFIED',mode,provider,url:null,sourceSha:sha,detail:`Deployment command exited 0 but no deployment URL was captured.\n${output}`,eligibility,checks}; + } + return {schema:'webforge.deployment-execution.v4',status:'PASS',mode,provider,url,sourceSha:sha,detail:output,eligibility,checks}; +} + export function executeDeployment(projectDir,{mode='preview',provider='vercel',productionApproved=false}={}){ if(!['preview','production'].includes(mode)) throw new Error('mode must be preview or production'); if(!['vercel','cloudflare'].includes(provider)) throw new Error('provider must be vercel or cloudflare'); const checks=gateChecks(projectDir); const eligibility=evaluateDeployment({checks},{productionApproved}); const eligible=mode==='preview'?eligibility.previewEligible:eligibility.productionEligible; - if(!eligible) return {schema:'webforge.deployment-execution.v3',status:'BLOCKED',mode,provider,eligibility,checks}; + if(!eligible) return {schema:'webforge.deployment-execution.v4',status:'BLOCKED',mode,provider,sourceSha:sourceSha(),eligibility,checks}; const runtimeRoot=path.join(projectDir,'runtime'); if(provider==='vercel'){ - if(!hasCommand('vercel')) return {schema:'webforge.deployment-execution.v3',status:'UNVERIFIED',mode,provider,detail:'vercel CLI unavailable',eligibility,checks}; + if(!hasCommand('vercel')) return {schema:'webforge.deployment-execution.v4',status:'UNVERIFIED',mode,provider,sourceSha:sourceSha(),detail:'vercel CLI unavailable',eligibility,checks}; const args=['deploy','--yes']; if(mode==='production')args.push('--prod'); const r=spawnSync('vercel',args,{cwd:runtimeRoot,encoding:'utf8',timeout:180000}); const output=tail((r.stdout||'')+(r.stderr||'')); const url=(output.match(/https:\/\/[^\s]+\.vercel\.app[^\s]*/)||[])[0]||null; - return {schema:'webforge.deployment-execution.v3',status:r.status===0?'PASS':'FAIL',mode,provider,url,detail:output,eligibility,checks}; + return deploymentResult({mode,provider,eligibility,checks,commandStatus:r.status,output,url}); } - if(!hasCommand('wrangler')) return {schema:'webforge.deployment-execution.v3',status:'UNVERIFIED',mode,provider,detail:'wrangler CLI unavailable',eligibility,checks}; + if(!hasCommand('wrangler')) return {schema:'webforge.deployment-execution.v4',status:'UNVERIFIED',mode,provider,sourceSha:sourceSha(),detail:'wrangler CLI unavailable',eligibility,checks}; const buildDir=fs.existsSync(path.join(runtimeRoot,'dist'))?'dist':fs.existsSync(path.join(runtimeRoot,'.vercel','output','static'))?'.vercel/output/static':null; - if(!buildDir) return {schema:'webforge.deployment-execution.v3',status:'FAIL',mode,provider,detail:'no static build output for Cloudflare Pages',eligibility,checks}; + if(!buildDir) return {schema:'webforge.deployment-execution.v4',status:'FAIL',mode,provider,sourceSha:sourceSha(),detail:'no static build output for Cloudflare Pages',eligibility,checks}; const args=['pages','deploy',buildDir]; if(mode==='production') args.push('--branch','main'); const r=spawnSync('wrangler',args,{cwd:runtimeRoot,encoding:'utf8',timeout:180000}); const output=tail((r.stdout||'')+(r.stderr||'')); const url=(output.match(/https:\/\/[^\s]+\.pages\.dev[^\s]*/)||[])[0]||null; - return {schema:'webforge.deployment-execution.v3',status:r.status===0?'PASS':'FAIL',mode,provider,url,detail:output,eligibility,checks}; + return deploymentResult({mode,provider,eligibility,checks,commandStatus:r.status,output,url}); }