ship: phase-04 pipeline-and-approval-gates #4

Merged
grimacing merged 9 commits from phase/04-pipeline-and-approval-gates into milestone/v1.0-initial 2026-07-21 13:42:25 +00:00
Owner

Phase 04 ship

Replaces the workflow skeletons with real implementations.

Pipeline (.gitea/workflows/pipeline.yml): 3-dispatch approval-gate topology (D-027/D-028):

  • workflow_dispatch inputs: contract-ref, approve_qa, approve_prod
  • 4 jobs: dev (initial dispatch, runs policy_checker + confidence_signal + mock_executor + writes evidence), qa-gate (approve_qa=true), prod-gate (approve_prod=true), finalize (chained to prod-gate)
  • Evidence persisted to acdl-evidence via finalize_evidence.py between dispatches (D-028)

Issue trigger (contracts-repo/.gitea/workflows/issue-to-contract.yml): on: issues [opened] → checkout acdl → run l3b_agent_stub.py → commit contract.yaml to contract/<n> branch on acdl-contracts → comment + close Issue → dispatch pipeline via API (D-014/D-030)

Helper (scripts/finalize_evidence.py): GET→sha + PUT/POST upload of audit.json to acdl-evidence, with clean URLError handling (P0-001 fixed during verify).

Requirements covered: REQ-10, REQ-12
Verification: 4 layers passed, 1 P0 fixed inline (P0-002: shell injection in issue body interpolation → use env var).

Version target: v1.0.4 (feature milestone, phase 4 patch)

## Phase 04 ship Replaces the workflow skeletons with real implementations. **Pipeline (`.gitea/workflows/pipeline.yml`):** 3-dispatch approval-gate topology (D-027/D-028): - `workflow_dispatch` inputs: `contract-ref`, `approve_qa`, `approve_prod` - 4 jobs: `dev` (initial dispatch, runs policy_checker + confidence_signal + mock_executor + writes evidence), `qa-gate` (approve_qa=true), `prod-gate` (approve_prod=true), `finalize` (chained to prod-gate) - Evidence persisted to `acdl-evidence` via `finalize_evidence.py` between dispatches (D-028) **Issue trigger (`contracts-repo/.gitea/workflows/issue-to-contract.yml`):** `on: issues [opened]` → checkout `acdl` → run `l3b_agent_stub.py` → commit `contract.yaml` to `contract/<n>` branch on `acdl-contracts` → comment + close Issue → dispatch pipeline via API (D-014/D-030) **Helper (`scripts/finalize_evidence.py`):** GET→sha + PUT/POST upload of `audit.json` to `acdl-evidence`, with clean `URLError` handling (P0-001 fixed during verify). **Requirements covered:** REQ-10, REQ-12 **Verification:** 4 layers passed, 1 P0 fixed inline (P0-002: shell injection in issue body interpolation → use env var). **Version target:** v1.0.4 (feature milestone, phase 4 patch)
grimacing added 9 commits 2026-07-21 13:42:24 +00:00
---ci---
phase: 4
milestone: v1.0
status: clarify
decisions:
  - id: D-027
    decision: Two workflows: pipeline.yml (workflow_call + workflow_dispatch) and issue-to-contract.yml (issues.opened); gates via workflow_dispatch approve_qa/approve_prod inputs
    rationale: Gitea has no environment reviewers, no repository_dispatch, no native approval UI; D-013 fallback
    confidence: 0.85
    alternatives: [single long-running workflow that polls, external approval bot]
  - id: D-028
    decision: All 4 stages in one workflow run; state passed via upload/download-artifact; gates re-dispatched with approve inputs
    rationale: Gitea supports upload/download-artifact; alternative is committing state between jobs (heavier)
    confidence: 0.80
    alternatives: [commit state.json + audit.json to a state branch between jobs]
  - id: D-029
    decision: Finalize commits audit.json to acdl-evidence main via the Gitea file-contents API (same mechanism as Phase 01 index.html push)
    rationale: D-012 raw-URL requires the file to be on main; the API is the only way to put it there from a step
    confidence: 0.90
    alternatives: [clone acdl-evidence in the step, copy file, commit, push]
  - id: D-030
    decision: Issue-to-contract workflow checks out l3b_agent_stub from acdl, parses Issue, commits contract.yaml to contract/<issue-number> branch on acdl-contracts, then dispatches pipeline via workflow_dispatch API
    rationale: Gitea Actions cannot trigger cross-repo without an explicit API call; the branch carries the contract ref
    confidence: 0.85
    alternatives: [push contract to a sentinel branch on the acdl repo itself]
---/ci---

Phase 04 has real pipeline-topology ambiguities (Gitea's gaps vs the spec's
approval-gate intent). Four decisions logged: D-027 two-workflow topology
+ dispatch-input gates, D-028 artifact-based state passing, D-029 finalize
commits via file-contents API, D-030 issue workflow checks out
l3b_agent_stub from acdl and dispatches the pipeline.
---ci---
phase: 4
milestone: v1.0
status: research
research:
  gitea_actions_capabilities:
    upload_artifact_v3: supported (v4 NOT supported by act_runner)
    artifacts_cross_dispatch: NOT preserved (re-dispatch starts a new run)
    workflow_dispatch_api: POST /actions/workflows/{filename}.yml/dispatches
    workflow_call: supported; uses: <owner>/<repo>/.gitea/workflows/<file>@<ref>
    checkout_cross_repo: actions/checkout@v4 with repository + ref + token
    secrets: ${{ secrets.GITEA_TOKEN }} must be a manually-created PAT (auto-token is current-repo only)
    approval_gate: workflow_dispatch input (approve_qa, approve_prod); no native pause-and-wait
  pipeline_design:
    topology: single pipeline.yml with workflow_dispatch inputs (contract-ref, approve_qa, approve_prod)
    stages: 3 separate dispatches (initial dev, qa-approve, prod-approve+finalize)
    state_persistence: each stage writes evidence to acdl-evidence via file-contents API (PUT audit.json)
    job_if_conditions: dev runs when !approve_qa && !approve_prod; qa-gate when approve_qa && !approve_prod; prod-gate+finalize when approve_prod
  personas: no change; backend-engineer owns the workflow YAML + finalize + issue trigger; lead-developer owns verify_phase04.sh
---/ci---

ARCHITECTURE.md gains a 'Phase 04 pipeline topology (research)' section:
the artifact-vs-re-dispatch limitation forces 3-dispatch topology with
evidence persisted to acdl-evidence (PUT audit.json) between dispatches.
PERSONAS.md is unchanged for Phase 04 (backend-engineer + lead-developer
active; infra-stub-engineer idle; frontend-engineer still off).
---ci---
phase: 4
milestone: v1.0
status: plan
plan:
  waves: 2
  wave_1_backend: [T-4.1 finalize_evidence.py, T-4.2 pipeline.yml real, T-4.3 issue-to-contract.yml real]
  wave_2_coordination: [T-4.4 verify_phase04.sh, T-4.5 traceability]
  requirements_covered: [REQ-10, REQ-12]
  must_haves: 7
  verification:
    typecheck: bash -n + py_compile + yaml load both workflows
    test: scripts/verify_phase04.sh (structure + syntax + finalize_evidence dry-run; full end-to-end run is Phase 05)
  risk: full pipeline cannot be exercised without an act_runner; verify_phase04 validates structure + syntax + dry-run only
---/ci---

PLAN.md replaced with Phase 04 plan. Wave 1 (backend) builds the pipeline
real implementation + finalize_evidence.py helper + issue-to-contract.yml
real implementation. Wave 2 (lead-developer) wires verify_phase04.sh +
traceability. Full end-to-end run is deferred to Phase 05 (act_runner
not registered in this environment).
---ci---
phase: 4
milestone: v1.0
status: execute
persona: backend-engineer
task: T-4.1
requirements:
  covered: [REQ-10]
---/ci---

Wave 1, task T-4.1. Adds scripts/finalize_evidence.py, a stdlib-only helper
that uploads a local audit.json to the acdl-evidence Gitea repo via the
file-contents API. It GETs the existing file to discover its sha (update
mode) and falls back to create mode on 404, then PUTs/POSTs the base64-encoded
content with the Authorization: token <token> header. This is the per-stage
state-persistence mechanism (D-028) used by the pipeline jobs between
re-dispatches, and the finalize-step writer (D-029).
---ci---
phase: 4
milestone: v1.0
status: execute
persona: backend-engineer
task: T-4.2
requirements:
  covered: [REQ-10]
---/ci---

Wave 1, task T-4.2. Replaces the Phase 01 pipeline.yml skeleton with the
real 3-dispatch approval-gate topology (D-027/D-028). workflow_dispatch
inputs contract-ref/approve_qa/approve_prod drive four jobs: dev (policy +
confidence gate + mock_executor + evidence, persist via finalize_evidence),
qa-gate (records approval), prod-gate (records approval), finalize
(needs: prod-gate, commits final audit.json to acdl-evidence). Each stage
persists its audit.json to acdl-evidence via the file-contents API since
Gitea Actions artifacts do not survive re-dispatch. The `on:` key is quoted
as "on": so PyYAML parses it as a string key (it would otherwise be coerced
to the boolean True).
---ci---
phase: 4
milestone: v1.0
status: execute
persona: backend-engineer
task: T-4.3
requirements:
  covered: [REQ-12]
---/ci---

Wave 1, task T-4.3. Replaces the Phase 01 issue-to-contract.yml skeleton
with the real L3B trigger (D-030). On issue opened, the workflow checks out
the acdl repo at milestone/v1.0-initial (branch-pin rule) to obtain
l3b_agent_stub.py, parses the Issue body into contract.yaml, commits it to
a new branch contract/<issue-number> on acdl-contracts via the Gitea
file-contents API using the new_branch field (single POST creates the
branch + file), comments on and closes the Issue, then dispatches the
pipeline workflow on acdl via POST .../actions/workflows/pipeline.yml/
dispatches with inputs contract-ref=contract/<issue-number>
(cross-repo trigger per D-014). The `on:` key is quoted as "on": so
PyYAML parses it as a string key.
---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: <reason>') 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: <reason>') so callers report a clean JSON failure.
---ci---
phase: 4
milestone: v1.0
status: execute
persona: lead-developer
task: T-4.5
requirements:
  covered: [REQ-10, REQ-12]
---/ci---

Wave 2, task T-4.5. REQUIREMENTS REQ-10/12 -> covered (pending VERIFY).
ROADMAP Phase 04 -> executing.
---ci---
phase: 4
milestone: v1.0
status: verify
requirements:
  covered: [REQ-10, REQ-12]
  partial: []
lessons:
  - 'on: <trigger>' YAML key must be quoted ('on':) because PyYAML
    interprets bare 'on:' as the boolean True (YAML 1.1 spec quirk).
    Structural validators using 'd.get("on")' fail without the quote.
  - Gitea Actions has no native approval-gate UI; the 3-dispatch
    topology (initial dev, qa-approve, prod-approve+finalize) is the
    documented fallback. State persists via audit.json on acdl-evidence
    between dispatches (D-028), NOT via artifacts (artifacts do not
    survive re-dispatch).
  - urllib.error.URLError is a separate exception from HTTPError and
    must be caught explicitly; otherwise a network failure (connection
    refused, DNS, timeout) stack-traces instead of returning a clean
    JSON failure.
review:
  p0: 1
  p1: 0
  p0_items:
    - id: P0-002
      location: contracts-repo/.gitea/workflows/issue-to-contract.yml 'Parse Issue body' step
      issue: gitea.event.issue.body was interpolated directly into a double-quoted shell argument ('"${{ gitea.event.issue.body }}"'), enabling shell injection from arbitrary Issue text
      severity: P0 (a malicious Issue could execute arbitrary commands in the workflow step)
      stride: Elevation of Privilege
      fix: pass the body via an env var (ISSUE_BODY: ${{ gitea.event.issue.body }}) and reference $ISSUE_BODY in the run: step; Gitea Actions escapes env var values, preventing shell metacharacter interpretation
---/ci---

Layer 1 STRUCTURAL: PASS (4/4 files; bash -n + py_compile + yaml load clean; no TODO/FIXME).
Layer 2 BEHAVIORAL: PASS (verify_phase04.sh 12/12 checks).
Layer 3 SECURITY: PASS with 1 P0 fixed in this commit (P0-002: shell injection in issue body interpolation -> use env var).
Layer 4 QUALITY: PASS.

Note: P0-001 (finalize_evidence URLError) was fixed inline during T-4.4 (commit 7a93a92). Both P0s are now resolved.
grimacing merged commit 72b359c9a9 into milestone/v1.0-initial 2026-07-21 13:42:25 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: continuous-intelligence/acdl#4