From 7a93a92c68c0ac9775345eec90b663f5f04eed6f Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Tue, 21 Jul 2026 13:40:50 +0000 Subject: [PATCH] feat(P04): Phase 04 verification script + finalize_evidence URLError fix (T-4.4) ---ci--- phase: 4 milestone: v1.0 status: execute persona: lead-developer task: T-4.4 requirements: covered: [REQ-10, REQ-12] review: p0: 1 p1: 0 p0_items: - id: P0-001 location: scripts/finalize_evidence.py _request() issue: urllib.error.URLError (connection refused, DNS, timeout) was uncaught and stack-traced instead of returning clean JSON severity: P0 (the pipeline would crash on a Gitea outage instead of writing a clean failure event) fix: catch URLError in _request(); return (0, 'URLError: ') so callers report cleanly ---/ci--- Wave 2, task T-4.4. scripts/verify_phase04.sh checks: (1) typecheck; (2) pipeline.yml structure (3 inputs + 4 jobs + correct if: conditions + finalize.needs=prod-gate); (3) pipeline.yml references all 5 core scripts + branch-pin doc; (4) issue-to-contract.yml structure (issues[opened] + parse-and-trigger); (5) issue-to-contract.yml references (l3b_agent_stub + dispatch endpoint + contract-ref + issue number + GITEA_TOKEN + new_branch); (6) finalize_evidence.py --help + missing file + missing token (all exit 1 clean, no stack trace); (7) finalize_evidence.py dead-host dry-run (exit 1, no stack trace). All 12 checks PASS. The dead-host check caught a P0: finalize_evidence.py did not catch urllib.error.URLError, so a connection-refused would stack-trace in the pipeline. Fixed by catching URLError in _request() and returning (0, 'URLError: ') so callers report a clean JSON failure. --- scripts/finalize_evidence.py | 5 + scripts/verify_phase04.sh | 186 +++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100755 scripts/verify_phase04.sh diff --git a/scripts/finalize_evidence.py b/scripts/finalize_evidence.py index ad2e76d..fdde84e 100755 --- a/scripts/finalize_evidence.py +++ b/scripts/finalize_evidence.py @@ -63,6 +63,11 @@ def _request(method: str, url: str, token: str, body: dict = None): except Exception: body_text = "" return exc.code, body_text + except urllib.error.URLError as exc: + # Network-level failure (connection refused, DNS, timeout). Return + # a synthetic 0 status + the reason so callers can report cleanly + # without a stack trace. + return 0, f"URLError: {exc.reason}" def get_existing_sha(host: str, owner: str, repo: str, path: str, diff --git a/scripts/verify_phase04.sh b/scripts/verify_phase04.sh new file mode 100755 index 0000000..be98497 --- /dev/null +++ b/scripts/verify_phase04.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# Phase 04 verification script. +# Confirms the pipeline workflow + issue trigger + finalize_evidence.py +# conform to the Phase 04 plan and the Gitea Actions topology in +# ARCHITECTURE.md. Does NOT execute a real Gitea Actions run (act_runner +# is not registered in this environment); validates structure + syntax +# + a dry-run of finalize_evidence.py against a dead host. +# +# Usage: scripts/verify_phase04.sh +# Exit codes: 0 = all checks passed; 1 = one or more checks failed. + +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +fail_count=0 +pass() { printf ' [PASS] %s\n' "$1"; } +fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); } + +echo "== Phase 04 verification ==" +echo "Root: ${ROOT}" +echo + +# --- Check 1: typecheck --- +echo "-- Check 1: typecheck --" +if bash -n scripts/finalize_evidence.py 2>/dev/null || python3 -m py_compile scripts/finalize_evidence.py 2>/dev/null; then + pass "py_compile finalize_evidence.py" +else + fail "py_compile finalize_evidence.py" +fi +if python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/pipeline.yml')); yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml'))" 2>/dev/null; then + pass "yaml load both workflows" +else + fail "yaml load workflows" +fi + +# --- Check 2: pipeline.yml structure --- +echo "-- Check 2: pipeline.yml structure (D-027, D-028) --" +p_struct=$(python3 << 'PYEOF' +import yaml, sys +try: + d = yaml.safe_load(open('.gitea/workflows/pipeline.yml')) + on = d.get('on', d.get(True)) or {} + assert 'workflow_dispatch' in on, 'no workflow_dispatch trigger' + inputs = on['workflow_dispatch']['inputs'] + assert set(inputs.keys()) == {'contract-ref', 'approve_qa', 'approve_prod'}, f'inputs: {set(inputs.keys())}' + assert inputs['contract-ref']['type'] == 'string', 'contract-ref type' + assert inputs['approve_qa']['type'] == 'boolean', 'approve_qa type' + assert inputs['approve_prod']['type'] == 'boolean', 'approve_prod type' + jobs = d['jobs'] + assert set(jobs.keys()) == {'dev', 'qa-gate', 'prod-gate', 'finalize'}, f'jobs: {set(jobs.keys())}' + dev_if = jobs['dev'].get('if', '') + assert 'approve_qa' in dev_if and 'approve_prod' in dev_if, f'dev.if: {dev_if}' + qa_if = jobs['qa-gate'].get('if', '') + assert 'approve_qa' in qa_if, f'qa-gate.if: {qa_if}' + prod_if = jobs['prod-gate'].get('if', '') + assert 'approve_prod' in prod_if, f'prod-gate.if: {prod_if}' + fin_needs = jobs['finalize'].get('needs', []) + assert fin_needs == ['prod-gate'] or fin_needs == 'prod-gate', f'finalize.needs: {fin_needs}' + # All jobs runs-on ubuntu-latest + for name, job in jobs.items(): + assert job.get('runs-on') == 'ubuntu-latest', f'{name} runs-on: {job.get("runs-on")}' + print('OK') +except AssertionError as ex: + print(f'FAIL: {ex}') + sys.exit(1) +except Exception as ex: + print(f'FAIL: {ex}') + sys.exit(1) +PYEOF +) +if [ "$p_struct" = "OK" ]; then + pass "pipeline.yml: 3 inputs + 4 jobs + correct if: conditions + finalize.needs=prod-gate" +else + fail "pipeline.yml structure: $p_struct" +fi + +# --- Check 3: pipeline.yml references core scripts --- +echo "-- Check 3: pipeline.yml references core scripts (D-029) --" +text=$(cat .gitea/workflows/pipeline.yml) +missing="" +for ref in policy_checker.py confidence_signal.py mock_executor.sh evidence_writer.py finalize_evidence.py; do + if ! echo "$text" | grep -qF "$ref"; then + missing="$missing $ref" + fi +done +if [ -z "$missing" ]; then + pass "pipeline.yml references all 5 core scripts" +else + fail "pipeline.yml missing references:$missing" +fi +# Branch-pin documentation +if echo "$text" | grep -q 'milestone/v1.0-initial'; then + pass "pipeline.yml documents branch-pin to milestone/v1.0-initial" +else + fail "pipeline.yml missing branch-pin reference" +fi + +# --- Check 4: issue-to-contract.yml structure --- +echo "-- Check 4: issue-to-contract.yml structure (D-030) --" +i_struct=$(python3 << 'PYEOF' +import yaml, sys +try: + d = yaml.safe_load(open('contracts-repo/.gitea/workflows/issue-to-contract.yml')) + on = d.get('on', d.get(True)) or {} + assert 'issues' in on, 'no issues trigger' + assert on['issues']['types'] == ['opened'], f'types: {on["issues"]["types"]}' + assert 'parse-and-trigger' in d['jobs'], 'no parse-and-trigger job' + assert d['jobs']['parse-and-trigger'].get('runs-on') == 'ubuntu-latest', 'runs-on' + print('OK') +except AssertionError as ex: + print(f'FAIL: {ex}') + sys.exit(1) +PYEOF +) +if [ "$i_struct" = "OK" ]; then + pass "issue-to-contract.yml: issues[opened] + parse-and-trigger job" +else + fail "issue-to-contract.yml structure: $i_struct" +fi + +# --- Check 5: issue-to-contract.yml references + dispatch endpoint --- +echo "-- Check 5: issue-to-contract.yml references + dispatch (D-014, D-030) --" +text=$(cat contracts-repo/.gitea/workflows/issue-to-contract.yml) +missing="" +for ref in l3b_agent_stub.py 'actions/workflows/pipeline.yml/dispatches' 'contract-ref' 'gitea.event.issue.number' 'GITEA_TOKEN' 'new_branch'; do + if ! echo "$text" | grep -qF "$ref"; then + missing="$missing $ref" + fi +done +if [ -z "$missing" ]; then + pass "issue-to-contract.yml: l3b_agent_stub + dispatch + contract-ref + issue number + token + new_branch" +else + fail "issue-to-contract.yml missing references:$missing" +fi + +# --- Check 6: finalize_evidence.py --help + clean failure --- +echo "-- Check 6: finalize_evidence.py CLI + clean failure modes ---" +out=$(python3 scripts/finalize_evidence.py --help 2>&1); rc=$? +if [ "$rc" = "0" ] && echo "$out" | grep -qi 'usage\|--audit\|--owner'; then + pass "finalize_evidence.py --help exits 0 with usage" +else + fail "finalize_evidence.py --help: rc=$rc" +fi + +# Missing audit file (with a fake token so it gets past the env check) → exit 1, no stack trace +out=$(ACDL_GITEA_TOKEN=fake python3 scripts/finalize_evidence.py --audit /tmp/definitely_nonexistent_audit.json 2>&1); rc=$? +if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then + pass "finalize_evidence.py missing file → exit 1, no stack trace" +else + fail "finalize_evidence.py missing file: rc=$rc, out='$out'" +fi + +# Missing token env (audit file present) → exit 1, no stack trace +printf '[]\n' > /tmp/empty_audit.json +out=$(env -u ACDL_GITEA_TOKEN python3 scripts/finalize_evidence.py --audit /tmp/empty_audit.json 2>&1); rc=$? +if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then + pass "finalize_evidence.py missing token env → exit 1, no stack trace" +else + fail "finalize_evidence.py missing token: rc=$rc, out='$out'" +fi + +# --- Check 7: finalize_evidence.py dry-run against a dead host (clean failure) --- +echo "-- Check 7: finalize_evidence.py dry-run against dead host ---" +# Use a real audit.json but point at a host that will refuse the connection. +printf '[{"seq":0,"ts":"2026-07-21T00:00:00Z","stage":"genesis","event":"init","prev_hash":"GENESIS","hash":"x"}]\n' > /tmp/real_audit.json +out=$(ACDL_GITEA_TOKEN=fake GITEA_HOST=http://127.0.0.1:0 python3 scripts/finalize_evidence.py --audit /tmp/real_audit.json --host http://127.0.0.1:0 2>&1); rc=$? +if [ "$rc" = "1" ] && ! echo "$out" | grep -q 'Traceback'; then + pass "finalize_evidence.py dead host → exit 1, no stack trace (clean API failure)" +else + fail "finalize_evidence.py dead host: rc=$rc, out='$out'" +fi + +# Cleanup +rm -f /tmp/empty_audit.json /tmp/real_audit.json + +echo +echo "== Summary ==" +if [ "$fail_count" -eq 0 ]; then + echo "Phase 04 verification PASSED (pipeline + issue trigger + finalize helper, all checks ok)" + exit 0 +else + echo "Phase 04 verification FAILED (${fail_count} check(s) failed)" + exit 1 +fi \ No newline at end of file