feat(P19): central pipeline contract + shell reproducibility + output streaming (v1.4.1)

---ci---
project: acdl
phase: 19
milestone: v1.4
status: execute
---

Add declarative pipeline contract (schemas/pipeline.schema.json +
pipelines/ci.yaml) as single source of truth for both Gitea Actions (dev)
and GitHub Actions (production) workflows. Both workflow files are
byte-identical and validated against the contract by 32 new tests.

Add scripts/run_ci.sh for shell reproducibility — mirrors the CI pipeline
locally (lint → test → check-only), exits 0 with 'CI PIPELINE OK'.

Update scripts/run_platform.sh to stream output by default: terraform
init/validate/plan via tee, Checkov compliance results with per-record
severity/rule/pass-fail, and emitted Terraform in --check-only. New
--quiet flag for log-only mode.

Requirements: REQ-43 (central pipeline contract), REQ-44 (shell
reproducibility), REQ-45 (output streaming). 122 tests pass (90 + 32).
This commit is contained in:
Jon Chery
2026-07-22 15:10:54 +00:00
parent 6e23c168f1
commit deb075b61e
12 changed files with 687 additions and 37 deletions
+48 -2
View File
@@ -84,7 +84,7 @@ id 202 published. D-034 closed (root key deactivated by user).**
Milestone COMPLETE gate: review → ship `v1.2.0` (feature milestone, next
minor per ship.md) → audit. **DONE.**
## Objective for Milestone v1.2 (active)
## Objective for Milestone v1.2 (prior — complete)
Platform hardening + first real consumer deployment. The v1.1 spike proved
the IR commitments hold on a single dev-only `terraform plan` for one S3
@@ -133,6 +133,40 @@ microservice), not just a plan.
Milestone COMPLETE gate: review → ship `v1.3.0` (feature milestone, next
minor per ship.md — v1.1 shipped `v1.2.0`) → audit.
## Objective for Milestone v1.4 (active)
Central pipeline contract + shell reproducibility + output streaming. The
v1.3 milestone (Phases 1718) created identical CI/CD pipelines for Gitea
and GitHub but they were duplicated copies with no single source of truth.
v1.4 makes the pipeline a declarative contract, enables full shell
reproducibility, and streams terraform/checkov output so users can see
what the platform is doing.
Three scope axes:
1. **Central pipeline contract.** A JSON Schema
(`schemas/pipeline.schema.json`) + YAML instance (`pipelines/ci.yaml`)
declares the pipeline stages, commands, triggers, and runner. Both
`.gitea/workflows/ci.yml` (Gitea Actions, dev) and
`.github/workflows/ci.yml` (GitHub Actions, production) implement the
contract. A test validates conformance.
2. **Shell reproducibility.** `scripts/run_ci.sh` mirrors the CI pipeline
locally — runs the same 3 stages (lint, test, check-only) in sequence.
The pipeline is fully reproducible from the shell, not just in CI.
3. **Output streaming.** `scripts/run_platform.sh` streams terraform
init/validate/plan output, Checkov compliance results, and
PolicyCheckResult records to stdout by default, so the user sees what
is happening. A `--quiet` flag suppresses streaming for log-only mode.
## Milestone v1.4 Phases
| Phase | Name | Goal |
|-------|------|------|
| 19 | central-pipeline-contract-and-shell-reproducibility | Create the central pipeline contract (JSON Schema + YAML instance). Create `scripts/run_ci.sh` for shell reproducibility. Update `run_platform.sh` to stream terraform/checkov output. Update both workflow YAMLs with contract references (staying byte-identical). Add tests for contract validation, workflow conformance, and streaming. |
Milestone COMPLETE gate: review → ship `v1.4.1` (feature milestone, next
minor per ship.md — v1.3 shipped `v1.3.2`) → audit.
## Requirements
### v1.0 (Prior milestone — the demo)
@@ -168,7 +202,7 @@ New requirements REQ-16..REQ-28 — see `REQUIREMENTS.md` §v1.1. Summary:
- **REQ-28:** Spike verification proves the IR-shaped commitments hold (no
polyglot mess; the adapter is the only substrate-specific code).
### v1.2 (Active milestone — platform hardening + first real consumer deployment)
### v1.2 (Prior milestone — platform hardening + first real consumer deployment, complete)
New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
@@ -189,6 +223,18 @@ New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
- **REQ-35:** End-to-end verification — consumer commit → live ECS service
(HTTP 200) → evidence event → timeline.
### v1.4 (Active milestone — central pipeline contract + shell reproducibility + streaming)
New requirements REQ-43..REQ-45 — see `REQUIREMENTS.md` §v1.4. Summary:
- **REQ-43:** Central pipeline contract — `schemas/pipeline.schema.json` +
`pipelines/ci.yaml`. Both Gitea and GitHub workflows implement the
contract; a test validates conformance.
- **REQ-44:** `scripts/run_ci.sh` mirrors the CI pipeline locally (lint →
test → check-only), exiting 0 with "CI PIPELINE OK".
- **REQ-45:** `scripts/run_platform.sh` streams terraform/checkov output by
default (with `--quiet` for log-only mode). Both workflows byte-identical.
## Constraints
- **Forge:** Gitea at `https://git.cloudinit.dev`, org `continuous-intelligence`.
+22 -3
View File
@@ -91,7 +91,7 @@
### Category: End-to-End Verification
- **REQ-35:** One end-to-end flow: consumer commit to `acdl-consumer-microservice` → pipeline triggered → contract→IR resolution → `terraform plan``terraform apply` (dev) → a live ECS Fargate service serving HTTP 200 on its ALB → evidence event written to the DynamoDB outbox → the event renders on the `acdl-evidence` timeline. `scripts/verify_phase16.sh` proves the full flow green.
## v1.3 (Active — module documentation + thin-composition removal)
## v1.3 (Prior — module documentation + thin-composition removal, complete)
### Category: Thin-Composition Removal
- **REQ-36:** The L2 thin-composition layer is removed completely: `composition.json` files, `acdl_platform/contract_resolver.py`, `schemas/contract.schema.json`, `contracts/spike.yaml`, `contracts/microservice.yaml`, and L2 entries in `modules-ir/registry.json` are deleted. The L2 directories are kept as placeholders with READMEs. The downstream pipeline (adapter → checkov → confidence → outbox) is patched to load a pre-existing IR instance instead of resolving a contract.
@@ -109,6 +109,17 @@
- **REQ-42:** `pyproject.toml` exists at the repo root with pytest configuration (testpaths, markers) and the project metadata. `requirements-test.txt` pins test-only dependencies separate from runtime dependencies.
## v1.4 (Active — central pipeline contract + shell reproducibility + streaming)
### Category: Central Pipeline Contract
- **REQ-43:** A central pipeline contract exists as `schemas/pipeline.schema.json` (JSON Schema draft 2020-12) + `pipelines/ci.yaml` (YAML instance). The contract declares the pipeline name, triggers (push/PR branches), runner, Python version, and stages (name + command + required + install + description). Both `.gitea/workflows/ci.yml` (Gitea Actions, dev) and `.github/workflows/ci.yml` (GitHub Actions, production) implement the same stages, commands, triggers, and runner as declared in the contract. A test (`tests/test_pipeline_contract.py`) validates the contract against the schema and asserts both workflows conform (same jobs, same commands, same triggers, same runner, byte-identical).
### Category: Shell Reproducibility
- **REQ-44:** `scripts/run_ci.sh` reproduces the CI pipeline locally — runs the same 3 stages (lint, test, check-only) in sequence with proper exit codes, failing on first error. The script exits 0 with "CI PIPELINE OK" on success. A `--quiet` flag suppresses per-stage banners. The script mirrors the central pipeline contract (`pipelines/ci.yaml`) so the shell and CI environments produce identical outcomes.
### Category: Pipeline Streaming
- **REQ-45:** `scripts/run_platform.sh` streams output by default: terraform init/validate/plan output is piped to stdout via `tee` (visible to the user and logged), Checkov results are printed in human-readable form, and PolicyCheckResult records are displayed with severity, rule ID, and pass/fail status per record. The `--check-only` mode streams the emitted Terraform file content. A `--quiet` flag suppresses streaming (output to log files only) for backwards compatibility. Both gitea and github workflows are byte-identical (identical outcomes — the only difference is the forge runtime).
## Out of Scope (v1.2)
| REQ | Original criterion | Clarified criterion (effective) | Decision |
@@ -179,7 +190,7 @@
| REQ-34 | 15 | complete (v1.2.5) |
| REQ-35 | 16 | partial (v1.2.6, IAM-blocked) |
### v1.3 (active — module documentation + thin-composition removal)
### v1.3 (prior — module documentation + thin-composition removal, complete)
| Requirement | Phase | Status |
|-------------|-------|--------|
@@ -189,4 +200,12 @@
| REQ-39 | 18 | complete (v1.3.2) |
| REQ-40 | 18 | complete (v1.3.2) |
| REQ-41 | 18 | complete (v1.3.2) |
| REQ-42 | 18 | complete (v1.3.2) |
| REQ-42 | 18 | complete (v1.3.2) |
### v1.4 (active — central pipeline contract + shell reproducibility + streaming)
| Requirement | Phase | Status |
|-------------|-------|--------|
| REQ-43 | 19 | complete (v1.4.1) |
| REQ-44 | 19 | complete (v1.4.1) |
| REQ-45 | 19 | complete (v1.4.1) |
+29 -3
View File
@@ -5,7 +5,8 @@
- **v1.0 (demo):** complete — tag `v1.1.0`, 2026-07-21. All 5 phases shipped + audited PASS.
- **v1.1 (complete):** architecture finalization + v1 spike. 5 phases (0610). Tag `v1.2.0`, 2026-07-21. All 5 phases shipped + verified; review READY TO SHIP (0 P0); audit CLEAN. Gitea release id 202.
- **v1.2 (complete):** platform hardening + first real consumer deployment. 6 phases (1116). Tag `v1.3.0`, 2026-07-21. All 6 phases shipped + verified; review READY TO SHIP (1 P0 operator action, 1 P1 deferred); audit CLEAN.
- **v1.3 (active):** module documentation + thin-composition removal. The L2 composition layer is removed; module READMEs are built out.
- **v1.3 (complete):** module documentation + thin-composition removal. The L2 composition layer is removed; module READMEs are built out. Tag `v1.3.2`.
- **v1.4 (active):** central pipeline contract + shell reproducibility + output streaming. A declarative pipeline contract (`schemas/pipeline.schema.json` + `pipelines/ci.yaml`) binds the Gitea and GitHub workflows to a single source of truth. `scripts/run_ci.sh` mirrors the CI pipeline locally. `scripts/run_platform.sh` streams terraform/checkov output by default.
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
---
@@ -221,7 +222,7 @@ After Phase 16: COMPLETE gate — review → ship `v1.3.0` → audit.
---
## v1.3 (Active — module documentation + thin-composition removal)
## v1.3 (Complete — module documentation + thin-composition removal)
The v1.3 milestone starts with simplification: removing the unsatisfactory
thin-composition layer and building out proper module documentation. The
@@ -249,4 +250,29 @@ L2 composition mechanism will be redesigned in a later phase.
- `pytest` runs and passes offline (no AWS, no Checkov, no DynamoDB).
- `run_platform.sh --check-only` runs offline and exits 0.
- `.gitea/workflows/ci.yml` and `.github/workflows/ci.yml` exist with identical job stages (lint, test, check-only).
- `pyproject.toml` + `requirements-test.txt` pin test dependencies.
- `pyproject.toml` + `requirements-test.txt` pin test dependencies.
After Phase 18: COMPLETE gate — review → ship `v1.3.2` → audit.
---
## v1.4 (Active — central pipeline contract + shell reproducibility + streaming)
The v1.4 milestone makes the CI/CD pipeline a declarative contract rather
than duplicated workflow copies, enables full shell reproducibility of the
CI pipeline, and streams terraform/checkov output so users can see what
the platform is doing.
### Phase 19 — central-pipeline-contract-and-shell-reproducibility
- **Description:** Create a central pipeline contract (`schemas/pipeline.schema.json` JSON Schema + `pipelines/ci.yaml` YAML instance) that both `.gitea/workflows/ci.yml` (Gitea Actions, dev) and `.github/workflows/ci.yml` (GitHub Actions, production) implement. Create `scripts/run_ci.sh` that mirrors the CI pipeline locally (lint → test → check-only). Update `scripts/run_platform.sh` to stream terraform init/validate/plan output, Checkov compliance results, and PolicyCheckResult records to stdout by default (with `--quiet` for log-only mode). Add `tests/test_pipeline_contract.py` validating the contract schema, workflow conformance, and run_ci.sh. Update both workflow YAMLs with contract reference headers (staying byte-identical).
- **Status:** complete (v1.4.1)
- **Depends on:** [18]
- **Requirements:** REQ-43, REQ-44, REQ-45
- **Success Criteria:**
- `pipelines/ci.yaml` validates against `schemas/pipeline.schema.json`.
- Both `.gitea/workflows/ci.yml` and `.github/workflows/ci.yml` are byte-identical.
- A test parses both workflows and asserts their stages/commands match the contract.
- `scripts/run_ci.sh` exits 0 and outputs "CI PIPELINE OK".
- `scripts/run_platform.sh --check-only` streams the emitted Terraform to stdout.
- `scripts/run_platform.sh --check-only --quiet` suppresses the Terraform stream.
- `pytest` total count increases from 90 to 122 (32 new contract/streaming tests).
+1 -1
View File
@@ -4,7 +4,7 @@
{
"slug": "acdl",
"name": "Agentic Cloud Delivery Platform",
"milestone": "v1.3",
"milestone": "v1.4",
"status": "active"
}
],
+15
View File
@@ -1,3 +1,18 @@
# ACDL CI Pipeline — Gitea Actions (dev environment)
#
# This workflow implements the central pipeline contract:
# pipelines/ci.yaml (validated against schemas/pipeline.schema.json)
#
# The same contract is implemented by .github/workflows/ci.yml (GitHub
# Actions, production). Both files must be byte-identical — the only
# declared difference is the forge/runtime, not the stages or commands.
#
# Shell reproducibility: scripts/run_ci.sh runs the same 3 stages locally.
#
# Stages (from the contract):
# 1. lint — py_compile all Python files
# 2. test — pytest test suite (offline, no AWS)
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
name: acdl-ci
on:
+15
View File
@@ -1,3 +1,18 @@
# ACDL CI Pipeline — Gitea Actions (dev environment)
#
# This workflow implements the central pipeline contract:
# pipelines/ci.yaml (validated against schemas/pipeline.schema.json)
#
# The same contract is implemented by .github/workflows/ci.yml (GitHub
# Actions, production). Both files must be byte-identical — the only
# declared difference is the forge/runtime, not the stages or commands.
#
# Shell reproducibility: scripts/run_ci.sh runs the same 3 stages locally.
#
# Stages (from the contract):
# 1. lint — py_compile all Python files
# 2. test — pytest test suite (offline, no AWS)
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
name: acdl-ci
on:
+64 -17
View File
@@ -14,11 +14,21 @@ a configuration file, or a Terraform module.
## Status
- **v1.2 (active):** platform hardening + first real consumer deployment.
Harden the v1.1 spike's NFRs, simplify the setup, rewrite the docs, and
prove the platform delivers real value by deploying a basic microservice
to AWS ECS Fargate end-to-end (`terraform apply`, dev autonomous). Ship
tag `v1.3.0`.
- **v1.4 (active):** central pipeline contract + shell reproducibility +
output streaming. A declarative pipeline contract
(`schemas/pipeline.schema.json` + `pipelines/ci.yaml`) binds the Gitea
and GitHub workflows to a single source of truth. `scripts/run_ci.sh`
mirrors the CI pipeline locally. `scripts/run_platform.sh` streams
terraform/checkov output by default. Ship tag `v1.4.1`.
- **v1.3 (complete, tag `v1.3.2`):** module documentation + thin-composition
removal. The L2 composition layer is removed; module READMEs are built
out. Testing + CI/CD pipelines (pytest, `--check-only`, Gitea + GitHub
workflows).
- **v1.2 (complete, tag `v1.3.0`):** platform hardening + first real
consumer deployment. Harden the v1.1 spike's NFRs, simplify the setup,
rewrite the docs, and prove the platform delivers real value by
deploying a basic microservice to AWS ECS Fargate end-to-end (`terraform
apply`, dev autonomous).
- **v1.1 (complete, tag `v1.2.0`):** architecture finalization + v1 spike.
Finalized the architecture to v1.0 (resolved all 11 open design
decisions) and proved the IR commitments hold with one end-to-end spike
@@ -111,12 +121,15 @@ ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \
bash scripts/rotate_spike_key.sh
# 3. Run the full platform pipeline (IR -> adapter -> plan -> Checkov ->
# confidence -> outbox)
# confidence -> outbox). Output is streamed to stdout by default.
bash scripts/run_platform.sh
# Expected: "=== PLATFORM E2E OK ==="
# Or plan-only (IR -> adapter -> terraform plan; no Checkov/outbox):
bash scripts/run_platform.sh --plan-only
# Add --quiet to suppress streaming (output to log files only):
bash scripts/run_platform.sh --quiet
```
### Test the platform (offline, no AWS required)
@@ -125,25 +138,58 @@ bash scripts/run_platform.sh --plan-only
# Install test dependencies
pip install -r requirements-test.txt
# Run the test suite (90 tests, all offline — uses moto for DynamoDB mocking)
# Run the test suite (122 tests, all offline — uses moto for DynamoDB mocking)
python3 -m pytest tests/ -v
# Run the platform in check-only mode (offline — no AWS, no Checkov, no outbox)
# Streams the emitted Terraform to stdout by default; --quiet suppresses it
bash scripts/run_platform.sh --check-only
# Expected: "=== PLATFORM CHECK OK ==="
# Reproduce the full CI pipeline locally (lint → test → check-only)
bash scripts/run_ci.sh
# Expected: "=== CI PIPELINE OK ==="
```
### CI/CD pipelines
Identical pipelines run on both Gitea Actions (dev) and GitHub Actions
(production):
The CI/CD pipeline is defined by a **central pipeline contract** — a
declarative YAML instance (`pipelines/ci.yaml`) validated against a JSON
Schema (`schemas/pipeline.schema.json`). Both forge workflows implement
the same contract:
- `.gitea/workflows/ci.yml` — Gitea Actions (dev environment)
- `.github/workflows/ci.yml` — GitHub Actions (production)
Both run three stages: **lint** (py_compile), **test** (pytest), and
**check-only** (`run_platform.sh --check-only`). Both trigger on push to
`main` and on pull requests.
Both workflow files are **byte-identical** — the only difference is the
forge runtime. Both run three stages: **lint** (py_compile), **test**
(pytest), and **check-only** (`run_platform.sh --check-only`). Both
trigger on push to `main` and on pull requests. A test
(`tests/test_pipeline_contract.py`) validates that both workflows conform
to the contract.
`scripts/run_ci.sh` mirrors the CI pipeline locally — running the same
three stages in sequence. This makes the pipeline fully reproducible from
the shell, not just in CI:
```bash
bash scripts/run_ci.sh # run all 3 stages (lint, test, check-only)
bash scripts/run_ci.sh --quiet # suppress per-stage banners
```
### Output streaming (run_platform.sh)
`scripts/run_platform.sh` streams output by default so the user can see
what the platform is doing:
- **`--check-only`**: streams the emitted Terraform file content to stdout
- **`--plan-only`** and **full mode**: streams `terraform init`, `terraform
validate`, and `terraform plan` output via `tee` (visible and logged)
- **Full mode**: prints Checkov compliance results and each
PolicyCheckResult record with severity, rule ID, and pass/fail status
A `--quiet` flag suppresses streaming (output to log files only) for
backwards-compatible log-only mode.
### Re-run the archived v1.0 demo (stubs only, no AWS)
@@ -159,13 +205,14 @@ behavior rather than provisioning real cloud resources.
| Path | Purpose | Status |
|------|---------|--------|
| `acdl_platform/` | Platform code: confidence signal, contract resolver, outbox writer, HITL/ledger/SoD designs (renamed from `platform/` in Phase 08 to avoid shadowing the stdlib `platform` module) | v1.1 complete; v1.2 extends |
| `schemas/` | JSON Schemas: IR, PolicyCheckResult, contract (draft 2020-12) | v1.1 complete; v1.2 extends contract schema |
| `acdl_platform/` | Platform code: confidence signal, outbox writer, separation of duties, HITL/ledger designs | v1.1 complete; v1.3 removes contract_resolver |
| `schemas/` | JSON Schemas: IR, PolicyCheckResult, pipeline contract (draft 2020-12) | v1.1 complete; v1.4 adds pipeline schema |
| `pipelines/` | Central pipeline contract: `ci.yaml` (YAML instance validated against `schemas/pipeline.schema.json`) | v1.4 |
| `adapters/` | Substrate adapters — Terraform adapter (the only substrate-specific code per §12) + Checkov policy adapter | v1.1 complete; v1.2 expands `TYPE_MAP` |
| `terraform/` | State backend (S3 + DynamoDB) + spike TF (`terraform/spike/`) + bootstrap scripts (`terraform/bootstrap/`) | v1.1 complete; v1.2 adds ECS apply |
| `modules-ir/` | IR-typed L1/L2 modules + `registry.json`. v1.1: `l1-s3`, `l2-static-asset`. v1.2: + 6 ECS L1s, `l2-microservice` | v1.1 complete; v1.2 expands |
| `contracts/` | Sample contracts (`spike.yaml` for `l2-static-asset`) | v1.1 complete; v1.2 adds `microservice.yaml` |
| `scripts/` | Verify scripts (`verify_phaseNN.sh`), platform run script (`run_platform.sh`; `--plan-only` for plan subset), key rotation | v1.1 complete; v1.2 consolidates |
| `modules-ir/` | IR-typed L1/L2 modules + `registry.json`. v1.1: `l1-s3`. v1.2: + 6 ECS L1s. v1.3: L2 removed (placeholders) | v1.3 |
| `scripts/` | Platform run script (`run_platform.sh` with `--check-only`/`--plan-only`/`--quiet`), CI pipeline script (`run_ci.sh`), verify scripts, key rotation | v1.4 |
| `tests/` | Pytest suite (122 tests, all offline — adapter, confidence signal, checkov adapter, outbox writer, pipeline contract, streaming) | v1.4 |
| `demo/` | Archived v1.0 executive demo (tag `v1.1.0`); runs locally via `demo/scripts/run_demo.sh --no-upload` | complete (archived) |
| `.ciagent/` | CIAgent metadata (config, project, architecture, requirements, roadmap, personas, plans, research, verify, review, audit) | active |
| `docs/` | Upstream vision + architecture sources (`vision.md`, `architecture.md`) | active |
+49
View File
@@ -0,0 +1,49 @@
# ACDL Central Pipeline Contract (v1.4)
#
# This is the single source of truth for the CI/CD pipeline. Both
# .gitea/workflows/ci.yml (Gitea Actions, dev) and
# .github/workflows/ci.yml (GitHub Actions, production) implement the
# stages, commands, triggers, and runner declared here.
# scripts/run_ci.sh mirrors the same stages for shell reproducibility.
#
# A test (tests/test_pipeline_contract.py) validates that both workflow
# YAMLs conform to this contract and that run_ci.sh runs the same commands.
#
# The contract does NOT replace workflow YAML syntax — it declares the
# *intent* that the forge-specific workflows implement. The workflow files
# use Gitea/GitHub Actions syntax (checkout, setup-python, run blocks);
# this contract declares what those blocks must contain.
#
# Validated against schemas/pipeline.schema.json.
name: acdl-ci
environment: dev
triggers:
push: [main]
pull_request: [main]
runner: ubuntu-latest
python_version: "3.12"
stages:
- name: lint
description: Compile all Python files (py_compile)
command: |
python3 -m py_compile \
acdl_platform/confidence_signal.py \
acdl_platform/outbox_writer.py \
adapters/terraform/adapter.py \
adapters/terraform/policy/checkov_adapter.py \
scripts/push_consumer_image.py
required: true
- name: test
description: Run the pytest test suite offline
command: python3 -m pytest tests/ -v --tb=short
install: pip install -r requirements-test.txt
required: true
- name: check-only
description: Run the platform pipeline offline (no AWS/Checkov/DynamoDB)
command: bash scripts/run_platform.sh --check-only
install: pip install jsonschema pyyaml boto3
required: true
+77
View File
@@ -0,0 +1,77 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://acdl.cloudinit.dev/schemas/pipeline.schema.json",
"title": "ACDL Central Pipeline Contract",
"description": "Declarative contract for a CI/CD pipeline. Both Gitea Actions (.gitea/workflows/ci.yml, dev) and GitHub Actions (.github/workflows/ci.yml, production) implement the stages, commands, triggers, and runner declared here. The shell script scripts/run_ci.sh mirrors the same stages for local reproducibility. The contract is the single source of truth; the workflow YAMLs and run_ci.sh are generated/validated against it.",
"$comment": "The pipeline contract does not replace workflow YAML syntax — it declares the *intent* (stages, commands, triggers, runner) that both Gitea and GitHub workflows implement. A test (tests/test_pipeline_contract.py) validates conformance: the workflow YAMLs must declare the same jobs/stages/commands as the contract, and run_ci.sh must run the same commands in the same order.",
"type": "object",
"required": ["name", "triggers", "runner", "stages"],
"properties": {
"name": {
"type": "string",
"description": "Pipeline name (matches the workflow 'name:' field)."
},
"environment": {
"type": "string",
"enum": ["dev", "production"],
"description": "Declared environment. dev = Gitea Actions; production = GitHub Actions. Does not change job commands — only documents which forge runs this instance."
},
"triggers": {
"type": "object",
"required": ["push", "pull_request"],
"properties": {
"push": {
"type": "array",
"items": {"type": "string"},
"description": "Branches that trigger the pipeline on push."
},
"pull_request": {
"type": "array",
"items": {"type": "string"},
"description": "Branches that trigger the pipeline on PR."
}
}
},
"runner": {
"type": "string",
"description": "Runner image (e.g. 'ubuntu-latest'). Both Gitea and GitHub use the same runner label."
},
"python_version": {
"type": "string",
"description": "Python version for setup-python action."
},
"stages": {
"type": "array",
"minItems": 1,
"items": {"$ref": "#/$defs/stage"}
}
},
"$defs": {
"stage": {
"type": "object",
"required": ["name", "command", "required"],
"properties": {
"name": {
"type": "string",
"description": "Stage name (maps to the workflow job name)."
},
"command": {
"type": "string",
"description": "The shell command to run for this stage. Must be identical in the workflow YAML 'run:' block and in scripts/run_ci.sh."
},
"required": {
"type": "boolean",
"description": "If true, a non-zero exit code fails the pipeline."
},
"install": {
"type": "string",
"description": "Optional: pip install command to run before the stage command."
},
"description": {
"type": "string",
"description": "Optional: human-readable description of what this stage does."
}
}
}
}
}
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# scripts/run_ci.sh - reproduce the CI pipeline locally.
#
# Mirrors the central pipeline contract (pipelines/ci.yaml) which both
# .gitea/workflows/ci.yml (Gitea Actions, dev) and
# .github/workflows/ci.yml (GitHub Actions, production) implement.
#
# Runs the same three stages in the same order:
# 1. lint — py_compile all Python files
# 2. test — pytest test suite (offline)
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
#
# Fails on the first stage that errors. Exits 0 with "CI PIPELINE OK"
# when all stages pass.
#
# Usage:
# bash scripts/run_ci.sh # run all stages
# bash scripts/run_ci.sh --quiet # suppress per-stage banners
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
QUIET=0
for arg in "$@"; do
case "$arg" in
--quiet) QUIET=1 ;;
*) echo "FAIL: unknown argument: $arg" >&2; exit 1 ;;
esac
done
banner() {
[ "$QUIET" = "1" ] || echo ""
echo "── $1 ──"
[ "$QUIET" = "1" ] || echo ""
}
fail() { echo "FAIL: $*" >&2; exit 1; }
echo "=== ACDL CI Pipeline (local reproduction) ==="
echo "contract: pipelines/ci.yaml (3 stages)"
echo ""
banner "Stage 1/3: lint (py_compile)"
python3 -m py_compile \
acdl_platform/confidence_signal.py \
acdl_platform/outbox_writer.py \
adapters/terraform/adapter.py \
adapters/terraform/policy/checkov_adapter.py \
scripts/push_consumer_image.py \
|| fail "lint: py_compile failed"
echo "lint: OK"
banner "Stage 2/3: test (pytest)"
python3 -m pytest tests/ -v --tb=short || fail "test: pytest failed"
echo "test: OK"
banner "Stage 3/3: check-only (run_platform.sh --check-only)"
bash scripts/run_platform.sh --check-only || fail "check-only: run_platform.sh failed"
echo "check-only: OK"
echo ""
echo "=== CI PIPELINE OK ==="
echo "3 stages passed: lint, test, check-only"
exit 0
+67 -11
View File
@@ -3,11 +3,16 @@
#
# Modes:
# --check-only (offline, no AWS/Checkov/DynamoDB — for CI)
# load IR -> adapter -> validate output structure -> exit 0
# load IR -> adapter -> stream emitted TF -> validate structure -> exit 0
# --plan-only (requires AWS creds, no Checkov/outbox)
# load IR -> adapter -> terraform init/validate/plan -> exit 0
# load IR -> adapter -> terraform init/validate/plan (streamed) -> exit 0
# (default) (requires AWS creds + Checkov + DynamoDB)
# load IR -> adapter -> terraform plan -> Checkov -> confidence -> outbox
# load IR -> adapter -> terraform plan (streamed) -> Checkov (streamed) ->
# confidence -> outbox
#
# Flags:
# --quiet suppress terraform/checkov streaming (output to log only)
# default: stream to stdout so the user sees what is happening
#
# NOTE: contract resolution (contract_resolver.py) was removed when the
# thin-composition layer was taken out. The pipeline now starts from a
@@ -22,16 +27,29 @@ cd "$ROOT"
CHECK_ONLY=0
PLAN_ONLY=0
QUIET=0
for arg in "$@"; do
case "$arg" in
--check-only) CHECK_ONLY=1 ;;
--plan-only) PLAN_ONLY=1 ;;
--quiet) QUIET=1 ;;
*) echo "FAIL: unknown argument: $arg" >&2; exit 1 ;;
esac
done
fail() { echo "FAIL: $*" >&2; exit 1; }
# stream: pipe a command's stdout+stderr to both a log file and the
# terminal (unless --quiet). Usage: stream <logfile> -- <command...>
stream() {
local log="$1"; shift
if [ "$QUIET" = "1" ]; then
"$@" > "$log" 2>&1
else
"$@" 2>&1 | tee "$log"
fi
}
CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID
WORK="/tmp/spike_e2e"
rm -rf "$WORK"; mkdir -p "$WORK"
@@ -46,6 +64,13 @@ echo "=== Step 3: adapter compiles IR -> terraform/spike/*.tf (regenerate) ==="
python3 adapters/terraform/adapter.py "$WORK/spike_ir.json" terraform/spike || fail "adapter failed"
echo "adapter: emitted terraform/spike/{main.tf,terraform.tf,providers.tf}"
if [ "$QUIET" = "0" ]; then
echo ""
echo "--- emitted terraform/spike/main.tf ---"
cat terraform/spike/main.tf
echo "--- end main.tf ---"
fi
if [ "$CHECK_ONLY" = "1" ]; then
echo ""
echo "=== Step 3b: validate adapter output structure (offline) ==="
@@ -86,9 +111,20 @@ export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
echo "=== Step 4: terraform init + validate + plan -lock=false (real AWS) ==="
cd terraform/spike
terraform init -reconfigure -lock=false -input=false >> "$WORK/tf.log" 2>&1 || fail "terraform init failed"
terraform validate >> "$WORK/tf.log" 2>&1 || fail "terraform validate failed"
terraform plan -lock=false -input=false -out=tfplan >> "$WORK/tf.log" 2>&1 || fail "terraform plan failed"
echo ""
echo "--- terraform init ---"
stream "$WORK/tf-init.log" terraform init -reconfigure -lock=false -input=false || fail "terraform init failed"
echo ""
echo "--- terraform validate ---"
stream "$WORK/tf-validate.log" terraform validate || fail "terraform validate failed"
echo ""
echo "--- terraform plan ---"
stream "$WORK/tf-plan.log" terraform plan -lock=false -input=false -out=tfplan || fail "terraform plan failed"
echo ""
echo "terraform plan OK (1 to add, 0 to change, 0 to destroy expected)"
cd "$ROOT"
@@ -98,16 +134,35 @@ if [ "$PLAN_ONLY" = "1" ]; then
exit 0
fi
echo ""
echo "=== Step 5: run Checkov on terraform/spike/main.tf ==="
checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail > "$WORK/checkov.json" 2> "$WORK/checkov.err"
if [ "$QUIET" = "0" ]; then
checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail 2>&1 | tee "$WORK/checkov.json"
else
checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail > "$WORK/checkov.json" 2> "$WORK/checkov.err"
fi
[ -s "$WORK/checkov.json" ] || fail "checkov produced no output"
echo "checkov: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo ""
echo "checkov summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo "=== Step 6: Checkov adapter -> PolicyCheckResult list ==="
echo ""
echo "=== Step 6: Checkov adapter -> PolicyCheckResult (compliance details) ==="
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov.json" "$CONTRACT_ID" > "$WORK/pcr.json" || fail "checkov adapter failed"
PCR_COUNT=$(python3 -c "import json; print(len(json.load(open('$WORK/pcr.json'))))")
echo "PolicyCheckResult: $PCR_COUNT record(s)"
python3 -c "
import json
pcrs = json.load(open('$WORK/pcr.json'))
print(f'PolicyCheckResult: {len(pcrs)} record(s)')
print()
for pcr in pcrs:
sev = pcr.get('severity', 'info')
res = pcr.get('result', 'unknown')
rule = pcr.get('ruleId', 'unknown')
msg = pcr.get('message', '')
marker = 'PASS' if res == 'pass' else 'FAIL' if res == 'fail' else 'SKIP' if res == 'skipped' else res.upper()
print(f' [{marker}] {sev:8s} {rule:30s} {msg}')
"
echo ""
echo "=== Step 7: confidence signal compute ==="
python3 <<PY > "$WORK/signal.json" || fail "confidence signal failed"
import json
@@ -129,6 +184,7 @@ SCORE=$(python3 -c "import json; print(round(json.load(open('$WORK/signal.json')
echo "confidence: score=$SCORE band=$BAND"
[ "$BAND" = "pass" ] || fail "confidence band is $BAND, expected pass for dev"
echo ""
echo "=== Step 8: write evidence event to DynamoDB outbox ==="
python3 <<PY > "$WORK/event.json" || fail "event build failed"
import json, datetime
+236
View File
@@ -0,0 +1,236 @@
import json
import os
import subprocess
import sys
from pathlib import Path
import jsonschema
import pytest
import yaml
ROOT = Path(__file__).resolve().parent.parent
def _load_yaml(path):
with open(ROOT / path) as f:
return yaml.safe_load(f)
def _load_workflow(path):
wf = _load_yaml(path)
if True in wf:
wf["on"] = wf[True]
return wf
class TestPipelineSchema:
def test_schema_is_valid_json_schema(self):
schema = json.load(open(ROOT / "schemas/pipeline.schema.json"))
jsonschema.Draft202012Validator.check_schema(schema)
def test_schema_has_required_fields(self):
schema = json.load(open(ROOT / "schemas/pipeline.schema.json"))
assert "name" in schema["required"]
assert "triggers" in schema["required"]
assert "runner" in schema["required"]
assert "stages" in schema["required"]
def test_schema_stage_def_has_command_and_required(self):
schema = json.load(open(ROOT / "schemas/pipeline.schema.json"))
stage_def = schema["$defs"]["stage"]
assert "command" in stage_def["required"]
assert "required" in stage_def["required"]
class TestPipelineContract:
def test_contract_validates_against_schema(self):
schema = json.load(open(ROOT / "schemas/pipeline.schema.json"))
contract = _load_yaml("pipelines/ci.yaml")
jsonschema.validate(contract, schema)
def test_contract_has_three_stages(self):
contract = _load_yaml("pipelines/ci.yaml")
stage_names = [s["name"] for s in contract["stages"]]
assert stage_names == ["lint", "test", "check-only"]
def test_contract_runner_is_ubuntu_latest(self):
contract = _load_yaml("pipelines/ci.yaml")
assert contract["runner"] == "ubuntu-latest"
def test_contract_python_version(self):
contract = _load_yaml("pipelines/ci.yaml")
assert contract["python_version"] == "3.12"
def test_contract_triggers_push_main(self):
contract = _load_yaml("pipelines/ci.yaml")
assert "main" in contract["triggers"]["push"]
def test_contract_triggers_pr_main(self):
contract = _load_yaml("pipelines/ci.yaml")
assert "main" in contract["triggers"]["pull_request"]
def test_contract_all_stages_required(self):
contract = _load_yaml("pipelines/ci.yaml")
for stage in contract["stages"]:
assert stage["required"] is True
def test_contract_lint_command_compiles_python(self):
contract = _load_yaml("pipelines/ci.yaml")
lint = next(s for s in contract["stages"] if s["name"] == "lint")
assert "py_compile" in lint["command"]
assert "acdl_platform/confidence_signal.py" in lint["command"]
assert "adapters/terraform/adapter.py" in lint["command"]
def test_contract_test_command_runs_pytest(self):
contract = _load_yaml("pipelines/ci.yaml")
test_stage = next(s for s in contract["stages"] if s["name"] == "test")
assert "pytest" in test_stage["command"]
def test_contract_check_only_runs_platform(self):
contract = _load_yaml("pipelines/ci.yaml")
check = next(s for s in contract["stages"] if s["name"] == "check-only")
assert "run_platform.sh" in check["command"]
assert "--check-only" in check["command"]
class TestWorkflowConformance:
def test_gitea_workflow_exists(self):
assert (ROOT / ".gitea/workflows/ci.yml").is_file()
def test_github_workflow_exists(self):
assert (ROOT / ".github/workflows/ci.yml").is_file()
def test_workflows_are_byte_identical(self):
gitea = open(ROOT / ".gitea/workflows/ci.yml", "rb").read()
github = open(ROOT / ".github/workflows/ci.yml", "rb").read()
assert gitea == github, "Gitea and GitHub workflows must be byte-identical"
def test_gitea_workflow_name_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yaml")
assert wf["name"] == contract["name"]
def test_gitea_workflow_has_three_jobs(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
assert set(wf["jobs"].keys()) == {"lint", "test", "check-only"}
def test_gitea_workflow_triggers_match_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yaml")
assert wf["on"]["push"]["branches"] == contract["triggers"]["push"]
assert wf["on"]["pull_request"]["branches"] == contract["triggers"]["pull_request"]
def test_gitea_workflow_runner_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yaml")
for job in wf["jobs"].values():
assert job["runs-on"] == contract["runner"]
def test_gitea_workflow_python_version_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yaml")
for job in wf["jobs"].values():
setup_step = next(
s for s in job["steps"] if "setup-python" in s.get("uses", "")
)
assert setup_step["with"]["python-version"] == contract["python_version"]
def test_gitea_lint_command_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
lint_job = wf["jobs"]["lint"]
run_step = next(s for s in lint_job["steps"] if "run" in s)
assert "py_compile" in run_step["run"]
for py_file in [
"acdl_platform/confidence_signal.py",
"acdl_platform/outbox_writer.py",
"adapters/terraform/adapter.py",
"adapters/terraform/policy/checkov_adapter.py",
"scripts/push_consumer_image.py",
]:
assert py_file in run_step["run"], f"{py_file} missing from lint command"
def test_gitea_test_command_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
test_job = wf["jobs"]["test"]
run_step = next(s for s in test_job["steps"] if "run" in s and "pytest" in s["run"])
assert "pytest" in run_step["run"]
def test_gitea_check_only_command_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
check_job = wf["jobs"]["check-only"]
run_step = next(
s for s in check_job["steps"] if "run" in s and "run_platform" in s["run"]
)
assert "run_platform.sh" in run_step["run"]
assert "--check-only" in run_step["run"]
class TestRunCiScript:
def test_run_ci_script_exists_and_executable(self):
path = ROOT / "scripts/run_ci.sh"
assert path.is_file()
assert os.access(path, os.X_OK)
def test_run_ci_script_contains_lint_stage(self):
content = open(ROOT / "scripts/run_ci.sh").read()
assert "py_compile" in content
assert "acdl_platform/confidence_signal.py" in content
assert "adapters/terraform/adapter.py" in content
def test_run_ci_script_contains_test_stage(self):
content = open(ROOT / "scripts/run_ci.sh").read()
assert "pytest" in content
assert "tests/" in content
def test_run_ci_script_contains_check_only_stage(self):
content = open(ROOT / "scripts/run_ci.sh").read()
assert "run_platform.sh" in content
assert "--check-only" in content
def test_run_ci_script_has_success_message(self):
content = open(ROOT / "scripts/run_ci.sh").read()
assert "CI PIPELINE OK" in content
def test_run_ci_lint_and_check_only_pass(self):
result = subprocess.run(
["bash", "-c",
f"cd {ROOT} && "
"python3 -m py_compile "
"acdl_platform/confidence_signal.py "
"acdl_platform/outbox_writer.py "
"adapters/terraform/adapter.py "
"adapters/terraform/policy/checkov_adapter.py "
"scripts/push_consumer_image.py && "
"echo 'lint: OK' && "
"bash scripts/run_platform.sh --check-only && "
"echo 'check-only: OK'"],
capture_output=True, text=True, cwd=str(ROOT),
timeout=30,
)
assert result.returncode == 0, f"stdout: {result.stdout}\nstderr: {result.stderr}"
assert "lint: OK" in result.stdout
assert "check-only: OK" in result.stdout
assert "PLATFORM CHECK OK" in result.stdout
class TestRunPlatformStreaming:
def test_check_only_streams_emitted_terraform(self):
result = subprocess.run(
["bash", str(ROOT / "scripts/run_platform.sh"), "--check-only"],
capture_output=True, text=True, cwd=str(ROOT),
timeout=30,
)
assert result.returncode == 0
assert "PLATFORM CHECK OK" in result.stdout
assert "--- emitted terraform/spike/main.tf ---" in result.stdout
assert "aws_s3_bucket" in result.stdout
def test_check_only_quiet_suppresses_terraform(self):
result = subprocess.run(
["bash", str(ROOT / "scripts/run_platform.sh"), "--check-only", "--quiet"],
capture_output=True, text=True, cwd=str(ROOT),
timeout=30,
)
assert result.returncode == 0
assert "PLATFORM CHECK OK" in result.stdout
assert "--- emitted terraform/spike/main.tf ---" not in result.stdout