mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-14 08:13:43 +08:00
fix: require Cloudflare deployment commit proof
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user