From b5b6660fdcd2a99e0b937a85e5149360b52d72cb Mon Sep 17 00:00:00 2001 From: Jens Caers Date: Wed, 26 Aug 2026 23:21:44 +0200 Subject: [PATCH] test(deploy): lock signed AppOps evidence contract --- tests/approved-deployment-evidence.test.mjs | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/approved-deployment-evidence.test.mjs diff --git a/tests/approved-deployment-evidence.test.mjs b/tests/approved-deployment-evidence.test.mjs new file mode 100644 index 0000000..0b1650c --- /dev/null +++ b/tests/approved-deployment-evidence.test.mjs @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const workflowUrl = new URL('../examples/gitea-actions/forgeflow-approved-deploy.yml', import.meta.url); +const deployUrl = new URL('../examples/server/forgeflow-deploy', import.meta.url); + + +test('approved workflow transports immutable evidence only to the root-owned deploy wrapper', async () => { + const workflow = await readFile(workflowUrl, 'utf8'); + + for (const input of [ + 'environment', + 'commit_sha', + 'request_id', + 'approval_id', + 'approval_fingerprint', + 'evidence_issued_at', + 'evidence_signature', + ]) { + assert.match(workflow, new RegExp(`\\b${input}:`)); + } + assert.match(workflow, /\$\{\{ gitea\.repository \}\}/); + assert.match(workflow, /FF_APPROVAL_ID.*FF_REQUEST_ID/s); + assert.match(workflow, /sudo \/usr\/local\/bin\/forgeflow-deploy/); + assert.doesNotMatch(workflow, /actions\/checkout/); + assert.doesNotMatch(workflow, /docker compose/); + assert.doesNotMatch(workflow, /git\s+-C/); +}); + + +test('server wrapper verifies Ed25519 evidence before any live git or compose mutation', async () => { + const script = await readFile(deployUrl, 'utf8'); + const verifyIndex = script.indexOf('openssl pkeyutl -verify'); + const resetIndex = script.indexOf('git -C "$APP_DIR" reset --hard "$SHA"'); + const composeIndex = script.indexOf('docker compose -f "$COMPOSE_FILE" up -d --build'); + + assert.ok(verifyIndex > 0, 'expected cryptographic verification'); + assert.ok(resetIndex > verifyIndex, 'git reset must happen after evidence verification'); + assert.ok(composeIndex > verifyIndex, 'compose mutation must happen after evidence verification'); + assert.match(script, /EVIDENCE_PUBLIC_KEY_FILE="\/etc\/forgeflow\/evidence\.pub"/); + assert.match(script, /evidence_owner.*root/s); + assert.match(script, /8#022/); + assert.match(script, /EVIDENCE_ISSUED_AT >= now_epoch - 1800/); + assert.match(script, /"evidence_verified": \$EVIDENCE_VERIFIED/); + assert.match(script, /\(\( \$# == 3 \|\| \$# == 4 \|\| \$# == 8 \)\)/); +}); + + +test('signed message fields match the AppOps evidence v1 contract and exclude runner-chosen workflow/ref', async () => { + const script = await readFile(deployUrl, 'utf8'); + const marker = "printf 'forgeflow-evidence-v1\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n%s\\n'"; + assert.ok(script.includes(marker)); + assert.match( + script, + /"\$APPROVAL_ID"[\s\\]+"\$APPROVAL_FINGERPRINT"[\s\\]+"\$REPOSITORY"[\s\\]+"\$ENVIRONMENT"[\s\\]+"\$\{SHA,,\}"[\s\\]+"\$REQUEST_ID"[\s\\]+"\$EVIDENCE_ISSUED_AT"/s, + ); + assert.doesNotMatch(script.slice(0, script.indexOf('APP_DIR=""')), /WORKFLOW|workflow|ref=/); +});