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:
@@ -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
|
||||
Reference in New Issue
Block a user