From 05761075cfa6c01113b4dad0b2dda82fca400d24 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Sat, 1 Aug 2026 02:46:16 +0800 Subject: [PATCH] fix: require Cloudflare deployment commit proof --- verification/src/checks/deployment.mjs | 25 +++++++++++++++++++++---- verification/src/core/command.mjs | 15 +++++++++------ verification/tests/core.test.mjs | 10 ++++++++++ verification/tests/files.test.mjs | 12 ++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/verification/src/checks/deployment.mjs b/verification/src/checks/deployment.mjs index 3b334fb..8e9a0bd 100644 --- a/verification/src/checks/deployment.mjs +++ b/verification/src/checks/deployment.mjs @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { runCommand } from '../core/command.mjs'; import { finding } from '../core/finding.mjs'; @@ -8,6 +9,13 @@ function digest(output) { return output?.match(/^Digest:\s+(sha256:[a-f0-9]+)/m)?.[1] || null; } +export function latestDeployment(output) { + try { + const rows = JSON.parse(output); + return Array.isArray(rows) ? rows[0] || null : null; + } catch { return null; } +} + export async function checkDeployment(ctx) { if (ctx.config.offline) return finding(ctx, { id: 'deploy.consistency', category: 'deployment', title: 'Local, GitHub, Cloudflare, and Docker release consistency', status: 'SKIP', severity: 'critical', @@ -21,15 +29,24 @@ export async function checkDeployment(ctx) { const latest = await runCommand(ctx, 'dockerhub-latest', 'docker', ['buildx', 'imagetools', 'inspect', 'kuekhaoyang/kvideo:latest'], { timeoutMs: 120_000 }); const versioned = await runCommand(ctx, 'dockerhub-version', 'docker', ['buildx', 'imagetools', 'inspect', `kuekhaoyang/kvideo:${ctx.state.version}`], { timeoutMs: 120_000 }); const wrangler = path.join(ctx.config.verifyDir, 'node_modules', '.bin', 'wrangler'); - const pages = await runCommand(ctx, 'cloudflare-deployments', wrangler, ['pages', 'deployment', 'list', '--project-name', 'kvideo'], { timeoutMs: 120_000 }); + const pages = await runCommand(ctx, 'cloudflare-deployments', wrangler, ['pages', 'deployment', 'list', '--project-name', 'kvideo', '--environment', 'production', '--json'], { timeoutMs: 120_000 }); + const deployment = latestDeployment(fs.readFileSync(pages.outputPath, 'utf8')); + const deployedRelease = deployment?.Deployment ? await request(`${deployment.Deployment}/api/app-update`) : null; const githubVersion = jsonBody(githubPackage)?.version; const cloudVersion = jsonBody(cloudflare)?.currentVersion; + const deployedVersion = jsonBody(deployedRelease)?.currentVersion; const latestDigest = digest(latest.tail); const versionDigest = digest(versioned.tail); - const facts = { localSha, remoteSha, localVersion: ctx.state.version, githubVersion, cloudVersion, latestDigest, versionDigest, pagesMentionsSha: pages.tail.includes(localSha.slice(0, 7)) }; + const deploymentSha = deployment?.Source || null; + const deploymentUrl = deployment?.Deployment || null; + const facts = { localSha, remoteSha, localVersion: ctx.state.version, githubVersion, cloudVersion, deployedVersion, + deploymentSha, deploymentUrl, latestDigest, versionDigest }; const target = path.join(ctx.dirs.raw, 'deployment-consistency.json'); - writeJson(target, { facts, githubPackage, cloudflare, evidence: { latest: latest.outputPath, versioned: versioned.outputPath, pages: pages.outputPath } }); - const ok = localSha === remoteSha && githubVersion === ctx.state.version && cloudVersion === ctx.state.version && latestDigest && latestDigest === versionDigest; + writeJson(target, { facts, githubPackage, cloudflare, deployedRelease, deployment, + evidence: { latest: latest.outputPath, versioned: versioned.outputPath, pages: pages.outputPath } }); + const ok = localSha === remoteSha && githubVersion === ctx.state.version && cloudVersion === ctx.state.version + && deploymentSha === localSha.slice(0, 7) && deployedVersion === ctx.state.version + && latestDigest && latestDigest === versionDigest; finding(ctx, { id: 'deploy.consistency', category: 'deployment', title: 'Local, GitHub main, Cloudflare, and both Docker tags agree', status: ok ? 'PASS' : 'FAIL', severity: 'critical', expected: 'Same Git commit/version; Docker latest and version tags share one digest', actual: JSON.stringify(facts), diff --git a/verification/src/core/command.mjs b/verification/src/core/command.mjs index f3494f0..a9d0deb 100644 --- a/verification/src/core/command.mjs +++ b/verification/src/core/command.mjs @@ -25,6 +25,13 @@ export function runCommand(ctx, name, command, args = [], options = {}) { }); let tail = ''; let timedOut = false; + let settled = false; + const finish = (result) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + stream.end(() => resolve(result)); + }; const capture = (chunk) => { const text = chunk.toString(); stream.write(text); @@ -39,14 +46,10 @@ export function runCommand(ctx, name, command, args = [], options = {}) { setTimeout(() => terminate(child, 'SIGKILL'), 3000).unref(); }, options.timeoutMs || ctx.config.commandTimeoutMs); child.on('error', (error) => { - clearTimeout(timeout); - stream.end(); - resolve({ code: 127, error: error.message, tail, outputPath, timedOut, durationMs: Date.now() - started }); + finish({ code: 127, error: error.message, tail, outputPath, timedOut, durationMs: Date.now() - started }); }); child.on('exit', (code, signal) => { - clearTimeout(timeout); - stream.end(); - resolve({ code: code ?? 1, signal, tail, outputPath, timedOut, durationMs: Date.now() - started }); + finish({ code: code ?? 1, signal, tail, outputPath, timedOut, durationMs: Date.now() - started }); }); }); } diff --git a/verification/tests/core.test.mjs b/verification/tests/core.test.mjs index 01fdf05..cb00859 100644 --- a/verification/tests/core.test.mjs +++ b/verification/tests/core.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { numericCandidate, prepareActionState } from '../src/browser/action-state.mjs'; import { getConfig } from '../src/config.mjs'; +import { latestDeployment } from '../src/checks/deployment.mjs'; import { redact, redactText } from '../src/core/redact.mjs'; import { escapeXml } from '../src/core/xml.mjs'; @@ -41,3 +42,12 @@ test('accepts explicit full-run action budgets', () => { assert.equal(config.maxActionStates, 1234); assert.equal(config.maxActionDepth, 7); }); + +test('selects the newest Cloudflare production deployment', () => { + const output = JSON.stringify([ + { Environment: 'Production', Source: 'abcdef1', Deployment: 'https://new.pages.dev' }, + { Environment: 'Production', Source: '1234567', Deployment: 'https://old.pages.dev' }, + ]); + assert.equal(latestDeployment(output)?.Source, 'abcdef1'); + assert.equal(latestDeployment('not json'), null); +}); diff --git a/verification/tests/files.test.mjs b/verification/tests/files.test.mjs index 3ad4250..82c1c1a 100644 --- a/verification/tests/files.test.mjs +++ b/verification/tests/files.test.mjs @@ -3,6 +3,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; +import { runCommand } from '../src/core/command.mjs'; import { lineCount, readJson, relative, walk, writeJson } from '../src/core/files.mjs'; test('file helpers inventory and serialize deterministic fixtures', () => { @@ -17,3 +18,14 @@ test('file helpers inventory and serialize deterministic fixtures', () => { assert.equal(walk(root, (file) => file.endsWith('.txt')).length, 1); fs.rmSync(root, { recursive: true, force: true }); }); + +test('command logs are flushed before command completion resolves', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kvideo-command-')); + const raw = path.join(root, 'raw'); + fs.mkdirSync(raw); + const ctx = { config: { root, commandTimeoutMs: 5000 }, dirs: { raw } }; + const result = await runCommand(ctx, 'flush', process.execPath, ['-e', "process.stdout.write('x'.repeat(50000))"]); + assert.equal(result.code, 0); + assert.equal(fs.readFileSync(result.outputPath, 'utf8').length, 50000); + fs.rmSync(root, { recursive: true, force: true }); +});