feat(P20): consumer happy path + reusable deploy workflow (v1.5.0)
---ci--- project: acdl phase: 20 milestone: v1.5 status: verify ---/ci--- REQ-46: README rewritten — platform-source vs consumer-repo distinction up front; platform flow converted to mermaid flowchart TD; L3A/L3B + spike nomenclature scrubbed from prose (code paths kept verbatim); prereqs pointer to consumer guide added. REQ-47: docs/CONSUMER_GUIDE.md (generic, all L2 modules) replaces docs/consumer-guide-static-asset.md — mermaid diagrams (model LR + pipeline TD), versioned uses: (@v1.4 floating MAJOR+MINOR, bare/@main discouraged), consumer-scoped prerequisites (no Terraform/Checkov/boto3/runner-key), run- time platform fetch via reusable workflow (consumers never invoke scripts/run_platform.sh locally for the happy path), optional local validation note. REQ-48: Credentials section rewritten — zero-trust OIDC + ABAC default (repo-identity + resource-tag scoping, blast-radius containment); static-key override in GitHub Secrets or .env.secrets with platform-managed daily rotation; consumer rotates out of band when using .env.secrets locally. REQ-49: byte-identical .gitea/workflows/deploy.yml + .github/workflows/ deploy.yml — reusable (on: workflow_call), checks out consumer repo + ACDL platform repo, installs deps, runs run_platform.sh, uploads artifacts; OIDC default (permissions: id-token: write) + static-key override via secrets. REQ-50: contracts/static-asset.yaml uses: @v1.4 (MAJOR+MINOR). REQ-51: tests/test_pipeline_contract.py extended — TestDeployPipelineSchema, TestDeployPipelineContract, TestDeployWorkflowConformance (byte-identical, reusable, contract/mode inputs, run_platform invocation, platform-repo checkout, OIDC permissions), TestSampleContractVersioning. 154 tests pass (19 new); run_ci.sh green. Fixes: modules/l2/static-asset/README.md dangling link retargeted to docs/CONSUMER_GUIDE.md.
This commit is contained in:
@@ -143,6 +143,7 @@ class TestWorkflowConformance:
|
||||
for py_file in [
|
||||
"acdl_platform/confidence_signal.py",
|
||||
"acdl_platform/outbox_writer.py",
|
||||
"acdl_platform/contract_resolver.py",
|
||||
"adapters/terraform/adapter.py",
|
||||
"adapters/terraform/policy/checkov_adapter.py",
|
||||
"scripts/push_consumer_image.py",
|
||||
@@ -175,6 +176,7 @@ class TestRunCiScript:
|
||||
content = open(ROOT / "scripts/run_ci.sh").read()
|
||||
assert "py_compile" in content
|
||||
assert "acdl_platform/confidence_signal.py" in content
|
||||
assert "acdl_platform/contract_resolver.py" in content
|
||||
assert "adapters/terraform/adapter.py" in content
|
||||
|
||||
def test_run_ci_script_contains_test_stage(self):
|
||||
@@ -198,6 +200,7 @@ class TestRunCiScript:
|
||||
"python3 -m py_compile "
|
||||
"acdl_platform/confidence_signal.py "
|
||||
"acdl_platform/outbox_writer.py "
|
||||
"acdl_platform/contract_resolver.py "
|
||||
"adapters/terraform/adapter.py "
|
||||
"adapters/terraform/policy/checkov_adapter.py "
|
||||
"scripts/push_consumer_image.py && "
|
||||
@@ -233,4 +236,126 @@ class TestRunPlatformStreaming:
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "PLATFORM CHECK OK" in result.stdout
|
||||
assert "--- emitted terraform/spike/main.tf ---" not in result.stdout
|
||||
assert "--- emitted terraform/spike/main.tf ---" not in result.stdout
|
||||
|
||||
|
||||
class TestDeployPipelineSchema:
|
||||
def test_deploy_schema_is_valid_json_schema(self):
|
||||
schema = json.load(open(ROOT / "schemas/deploy-pipeline.schema.json"))
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
|
||||
def test_deploy_schema_has_required_fields(self):
|
||||
schema = json.load(open(ROOT / "schemas/deploy-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_deploy_schema_stage_def_has_command_and_required(self):
|
||||
schema = json.load(open(ROOT / "schemas/deploy-pipeline.schema.json"))
|
||||
stage_def = schema["$defs"]["stage"]
|
||||
assert "command" in stage_def["required"]
|
||||
assert "required" in stage_def["required"]
|
||||
|
||||
|
||||
class TestDeployPipelineContract:
|
||||
def test_deploy_contract_validates_against_schema(self):
|
||||
schema = json.load(open(ROOT / "schemas/deploy-pipeline.schema.json"))
|
||||
contract = _load_yaml("pipelines/deploy.yaml")
|
||||
jsonschema.validate(contract, schema)
|
||||
|
||||
def test_deploy_contract_has_six_stages(self):
|
||||
contract = _load_yaml("pipelines/deploy.yaml")
|
||||
stage_names = [s["name"] for s in contract["stages"]]
|
||||
assert stage_names == [
|
||||
"validate-contract",
|
||||
"resolve-stack",
|
||||
"terraform-plan",
|
||||
"checkov",
|
||||
"confidence",
|
||||
"apply",
|
||||
]
|
||||
|
||||
def test_deploy_contract_runner_is_ubuntu_latest(self):
|
||||
contract = _load_yaml("pipelines/deploy.yaml")
|
||||
assert contract["runner"] == "ubuntu-latest"
|
||||
|
||||
|
||||
class TestDeployWorkflowConformance:
|
||||
def test_gitea_deploy_workflow_exists(self):
|
||||
assert (ROOT / ".gitea/workflows/deploy.yml").is_file()
|
||||
|
||||
def test_github_deploy_workflow_exists(self):
|
||||
assert (ROOT / ".github/workflows/deploy.yml").is_file()
|
||||
|
||||
def test_deploy_workflows_are_byte_identical(self):
|
||||
gitea = open(ROOT / ".gitea/workflows/deploy.yml", "rb").read()
|
||||
github = open(ROOT / ".github/workflows/deploy.yml", "rb").read()
|
||||
assert gitea == github, "Gitea and GitHub deploy workflows must be byte-identical"
|
||||
|
||||
def test_deploy_workflow_name_matches_contract(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
contract = _load_yaml("pipelines/deploy.yaml")
|
||||
assert wf["name"] == contract["name"]
|
||||
|
||||
def test_deploy_workflow_is_reusable(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
assert "workflow_call" in wf["on"]
|
||||
|
||||
def test_deploy_workflow_has_contract_input(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
inputs = wf["on"]["workflow_call"]["inputs"]
|
||||
assert "contract" in inputs
|
||||
assert inputs["contract"]["default"] == ".acdl/contract.yaml"
|
||||
|
||||
def test_deploy_workflow_has_mode_input(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
inputs = wf["on"]["workflow_call"]["inputs"]
|
||||
assert "mode" in inputs
|
||||
assert inputs["mode"]["default"] == "full"
|
||||
|
||||
def test_deploy_workflow_runner_matches_contract(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
contract = _load_yaml("pipelines/deploy.yaml")
|
||||
for job in wf["jobs"].values():
|
||||
assert job["runs-on"] == contract["runner"]
|
||||
|
||||
def test_deploy_workflow_python_version_matches_contract(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
contract = _load_yaml("pipelines/deploy.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_deploy_workflow_invokes_run_platform(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
deploy_job = wf["jobs"]["deploy"]
|
||||
run_step = next(
|
||||
s for s in deploy_job["steps"] if "run" in s and "run_platform" in s["run"]
|
||||
)
|
||||
assert "run_platform.sh" in run_step["run"]
|
||||
|
||||
def test_deploy_workflow_checks_out_platform_repo(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
deploy_job = wf["jobs"]["deploy"]
|
||||
platform_checkout = next(
|
||||
s for s in deploy_job["steps"]
|
||||
if "checkout" in s.get("uses", "") and s.get("with", {}).get("path") == "acdl-platform"
|
||||
)
|
||||
assert platform_checkout["with"]["repository"] == "acdl/acdl"
|
||||
|
||||
def test_deploy_workflow_permissions_id_token_write(self):
|
||||
wf = _load_workflow(".gitea/workflows/deploy.yml")
|
||||
assert wf["permissions"]["id-token"] == "write"
|
||||
assert wf["permissions"]["contents"] == "read"
|
||||
|
||||
|
||||
class TestSampleContractVersioning:
|
||||
def test_sample_contract_uses_versioned_tag(self):
|
||||
contract = _load_yaml("contracts/static-asset.yaml")
|
||||
uses = contract["uses"]
|
||||
assert "@v" in uses, "sample contract must use a versioned @vX.Y tag"
|
||||
assert "@main" not in uses, "sample contract must not use @main"
|
||||
assert uses == "acdl/pipelines/deploy.yaml@v1.4"
|
||||
Reference in New Issue
Block a user