Compare commits

..

5 Commits

Author SHA1 Message Date
Jon Chery 81f111d462 docs(P04): complete regression-gate-evidence-hardening phase (v1.13.7)
---ci---
project: acdl
phase: 4
milestone: v1.14
status: complete
requirements:
  covered: [REQ-138]
  partial: []
---/ci---
2026-07-29 20:32:59 +00:00
Jon Chery 79e7a4a304 docs(P03): complete lifecycle-script-arg-cleanup phase (v1.13.6)
---ci---
project: acdl
phase: 3
milestone: v1.14
status: complete
requirements:
  covered: [REQ-137]
  partial: []
---/ci---
2026-07-29 20:24:13 +00:00
Jon Chery 8ae307affc docs(P02): complete static-assets-wiring-fix phase (v1.13.5)
---ci---
project: acdl
phase: 2
milestone: v1.14
status: complete
requirements:
  covered: [REQ-136]
  partial: []
---/ci---
2026-07-29 20:21:12 +00:00
Jon Chery 6e1a1bd7db docs(P01): complete adapter-dedup-diagnostic phase (v1.13.4)
---ci---
project: acdl
phase: 1
milestone: v1.14
status: complete
requirements:
  covered: [REQ-135]
  partial: []
---/ci---
2026-07-29 20:17:55 +00:00
Jon Chery 040abc0fb7 docs(ship): v1.13.3 complete — v1.14 pre-execution phase shipped (Gitea release id 255)
---ci---
project: acdl
phase: 0
milestone: v1.14
status: complete
---/ci---
2026-07-29 20:14:30 +00:00
8 changed files with 101 additions and 29 deletions
+9 -6
View File
@@ -93,22 +93,25 @@ down to zero-cost steady state (P64, D-096).
- **CAP-017 (Verified):** DynamoDB `acdl-contracts` table — Verified - **CAP-017 (Verified):** DynamoDB `acdl-contracts` table — Verified
live-aws via L1 rds module lifecycle pipeline (apply/modify/destroy live-aws via L1 rds module lifecycle pipeline (apply/modify/destroy
exit 0). Evidence: regression registry CAP-017 (lifecycle-pipeline tier). exit 0). Evidence: regression registry CAP-017 (offline proxy: terraform
files present + fmt -check passes + contracts resolve; live
apply/modify/destroy verified by the modules-lifecycle workflow run).
- **CAP-018 (Verified):** Lambda contract-ingestor — Verified via local - **CAP-018 (Verified):** Lambda contract-ingestor — Verified via local
Lambda stub (CAP-011, Phase 53) + lifecycle pipeline. Evidence: Lambda stub (CAP-011, Phase 53) + lifecycle pipeline. Evidence:
regression registry CAP-018. regression registry CAP-018 (offline proxy).
- **CAP-019 (Verified):** ECS cluster + service — Verified live-aws via - **CAP-019 (Verified):** ECS cluster + service — Verified live-aws via
L2 microservice lifecycle pipeline (apply/modify/destroy exit 0). L2 microservice lifecycle pipeline (apply/modify/destroy exit 0).
Evidence: regression registry CAP-019. Evidence: regression registry CAP-019 (offline proxy).
- **CAP-020 (Verified):** CloudFront + WAF production static-assets - **CAP-020 (Verified):** CloudFront + WAF production static-assets
stack — Verified live-aws via L2 static-assets lifecycle pipeline stack — Verified live-aws via L2 static-assets lifecycle pipeline
(apply/modify/destroy exit 0). Evidence: regression registry CAP-020. (apply/modify/destroy exit 0). Evidence: regression registry CAP-020
(offline proxy).
- **CAP-021 (Verified):** uptime-kuma monitoring primitive — Verified - **CAP-021 (Verified):** uptime-kuma monitoring primitive — Verified
live-aws via L1 uptime module lifecycle pipeline. Evidence: regression live-aws via L1 uptime module lifecycle pipeline. Evidence: regression
registry CAP-021. registry CAP-021 (offline proxy).
- **CAP-022 (Verified):** OIDC role for act_runner — Verified live-aws - **CAP-022 (Verified):** OIDC role for act_runner — Verified live-aws
via L1 iam-role module lifecycle pipeline. Evidence: regression via L1 iam-role module lifecycle pipeline. Evidence: regression
registry CAP-022. registry CAP-022 (offline proxy).
All CAP-017..022 are now in the regression registry All CAP-017..022 are now in the regression registry
(`core/regression_verify.py`) with "lifecycle-pipeline" tier evidence (`core/regression_verify.py`) with "lifecycle-pipeline" tier evidence
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"phase": 0, "phase": 0,
"stage": "grill", "stage": "complete",
"milestone": "v1.14", "milestone": "v1.14",
"phase_role": "pre_execution", "phase_role": "pre_execution",
"attempts": 0, "attempts": 0,
"updated_at": "2026-07-29T20:25:00Z" "updated_at": "2026-07-29T20:30:00Z"
} }
+24 -10
View File
@@ -431,13 +431,19 @@ def _check_s3_state_bucket() -> Tuple[Status, str]:
def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]: def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
"""Helper: verify an L1 module's terraform dir exists with the required """Helper: verify an L1 module's terraform dir exists with the required
files + its example contracts resolve. This is the offline proxy for files + its example contracts resolve + terraform fmt syntax check
'lifecycle pipeline green' — the pipeline cell going green requires passes. This is the offline proxy for 'lifecycle pipeline green' — the
terraform init+validate+apply+modify+destroy to succeed against live pipeline cell going green requires terraform init+validate+apply+modify+
AWS, which requires the terraform files to exist and contracts to destroy to succeed against live AWS, which requires the terraform files
resolve first. We avoid terraform init here (too slow for the to exist, contracts to resolve, and HCL syntax to be valid first.
regression gate); terraform validate is run by the lifecycle pipeline
itself.""" We run `terraform fmt -check` (fast, no init required) as a syntax probe.
We avoid `terraform validate` here (requires `terraform init`, which
downloads providers — too slow for the regression gate). Full
`terraform validate` is run by the lifecycle pipeline itself. This is
an offline proxy, not live pipeline evidence; the live apply/modify/
destroy is verified by the modules-lifecycle workflow run, not by this
gate."""
tf_dir = ROOT / "modules" / "l1" / module / "terraform" tf_dir = ROOT / "modules" / "l1" / module / "terraform"
if not tf_dir.is_dir(): if not tf_dir.is_dir():
return "Broken", f"modules/l1/{module}/terraform/ does not exist" return "Broken", f"modules/l1/{module}/terraform/ does not exist"
@@ -450,6 +456,11 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
tf_text = "".join((tf_dir / f).read_text() for f in ["variables.tf", "main.tf", "outputs.tf"] if (tf_dir / f).is_file()) tf_text = "".join((tf_dir / f).read_text() for f in ["variables.tf", "main.tf", "outputs.tf"] if (tf_dir / f).is_file())
if "local." in tf_text and not (tf_dir / "locals.tf").is_file(): if "local." in tf_text and not (tf_dir / "locals.tf").is_file():
return "Broken", "missing terraform files: ['locals.tf'] (referenced by module)" return "Broken", "missing terraform files: ['locals.tf'] (referenced by module)"
# terraform fmt -check: fast HCL syntax probe (no init required).
rc, out, err = _run_subprocess(
["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30)
if rc != 0:
return "Broken", f"terraform fmt -check failed: {err.strip()[-200:]}"
for ex in ["simple", "complex"]: for ex in ["simple", "complex"]:
contract = ROOT / "modules" / "l1" / module / "examples" / f"{ex}.yml" contract = ROOT / "modules" / "l1" / module / "examples" / f"{ex}.yml"
if not contract.is_file(): if not contract.is_file():
@@ -459,12 +470,15 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
], timeout=30) ], timeout=30)
if rc != 0: if rc != 0:
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}" return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
return "Verified", f"terraform files present + simple/complex contracts resolve" return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve"
def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]: def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
"""Helper: verify an L2 module's composition resolves + its example """Helper: verify an L2 module's composition resolves + its example
contracts resolve. Offline proxy for 'L2 lifecycle pipeline green'.""" contracts resolve. Offline proxy for 'L2 lifecycle pipeline green'.
This is an offline proxy, not live pipeline evidence; the live
apply/modify/destroy is verified by the modules-lifecycle workflow
run, not by this gate."""
for ex in ["simple", "complex"]: for ex in ["simple", "complex"]:
contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml" contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml"
if not contract.is_file(): if not contract.is_file():
@@ -474,7 +488,7 @@ def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
], timeout=30) ], timeout=30)
if rc != 0: if rc != 0:
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}" return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
return "Verified", f"L2 composition resolves (simple + complex contracts)" return "Verified", f"L2 composition resolves (simple + complex contracts; offline proxy)"
def _check_cap_017_dynamodb() -> Tuple[Status, str]: def _check_cap_017_dynamodb() -> Tuple[Status, str]:
+5 -1
View File
@@ -18,7 +18,11 @@
{"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"}, {"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"},
{"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"}, {"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"},
{"from": "contract.inputs.region", "to": "kms.inputs.region"}, {"from": "contract.inputs.region", "to": "kms.inputs.region"},
{"from": "kms.outputs.kms_key_arn", "to": "s3.inputs.kms_key_arn"} {"from": "kms.outputs.kms_key_arn", "to": "s3.inputs.kms_key_arn"},
{"from": "contract.inputs.default_ttl", "to": "cloudfront.inputs.default_ttl"},
{"from": "contract.inputs.max_ttl", "to": "cloudfront.inputs.max_ttl"},
{"from": "contract.inputs.price_class", "to": "cloudfront.inputs.price_class"},
{"from": "contract.inputs.viewer_protocol_policy", "to": "cloudfront.inputs.viewer_protocol_policy"}
], ],
"outputs": [ "outputs": [
{"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"}, {"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"},
+12 -7
View File
@@ -1,16 +1,21 @@
# Complex static-assets deployment (S3 + CloudFront + WAF) # Complex static-assets deployment (S3 + CloudFront + WAF)
# Modify variant: same bucket_name as simple (in-place modify, adds CDN + WAF) # Modify variant: same bucket_name as simple (in-place modify, tunes CDN
# TTLs + price class + viewer protocol policy). The simple example uses
# the cloudfront interface defaults (default_ttl=3600, max_ttl=86400,
# PriceClass_100, redirect-to-https); this complex example sets explicit
# non-default values so the lifecycle "modify" step exercises a real
# terraform diff on the cloudfront distribution, not an idempotent
# re-apply.
environment: dev environment: dev
id: assets id: assets
infrastructure: infrastructure:
static-assets: static-assets:
inputs: inputs:
bucket_name: my-static-site bucket_name: my-static-site
default_ttl: 3600 default_ttl: 7200
max_ttl: 86400 max_ttl: 172800
price_class: PriceClass_100 price_class: PriceClass_200
region: us-east-1 region: us-east-1
viewer_protocol_policy: redirect-to-https viewer_protocol_policy: https-only
waf_enabled: true
version: 1.0.0 version: 1.0.0
name: static assets name: static assets
+8 -1
View File
@@ -1,11 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# scripts/run_l2_lifecycle_destroy.sh — run a single L2 module lifecycle destroy. # scripts/run_l2_lifecycle_destroy.sh — run a single L2 module lifecycle destroy.
# #
# Usage: run_l2_lifecycle_destroy.sh <module> [ci-vpc-outputs.json] # Usage: run_l2_lifecycle_destroy.sh <module>
# #
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state. # pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state.
# #
# NOTE: unlike the L1 scripts (run_lifecycle_destroy.sh), the L2 path does
# NOT take a ci-vpc-outputs.json argument. L2 compositions reference the
# platform VPC via terraform_remote_state (a data source), not by injecting
# VPC outputs into the contract. The workflow passes 2 positional args for
# parity with the L1 matrix, but $2 is accepted-but-ignored here (documented,
# not a bug).
#
# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" = no-op # Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" = no-op
# (plan mode never applies resources, so there is nothing to destroy). # (plan mode never applies resources, so there is nothing to destroy).
# Set to "full" for the real `--destroy` against live AWS. # Set to "full" for the real `--destroy` against live AWS.
+9 -1
View File
@@ -1,13 +1,21 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# scripts/run_l2_lifecycle_test.sh — run a single L2 module lifecycle apply/modify. # scripts/run_l2_lifecycle_test.sh — run a single L2 module lifecycle apply/modify.
# #
# Usage: run_l2_lifecycle_test.sh <module> <example> [ci-vpc-outputs.json] # Usage: run_l2_lifecycle_test.sh <module> <example>
# #
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle # Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state so the # pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state so the
# microservice composition's terraform_remote_state data source reads from # microservice composition's terraform_remote_state data source reads from
# the short-lived CI VPC (not the long-lived platform VPC). # the short-lived CI VPC (not the long-lived platform VPC).
# #
# NOTE: unlike the L1 scripts (run_lifecycle_test.sh), the L2 path does NOT
# take a ci-vpc-outputs.json argument. L2 compositions reference the platform
# VPC via terraform_remote_state (a data source), not by injecting VPC
# outputs into the contract. The ACDL_REMOTE_STATE_KEY env var points the
# data source at the correct CI VPC state key. The workflow passes 3
# positional args for parity with the L1 matrix, but $3 is accepted-but-
# ignored here (documented, not a bug).
#
# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" runs # Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" runs
# `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for # `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for
# the real `--apply` against live AWS. # the real `--apply` against live AWS.
+32 -1
View File
@@ -330,4 +330,35 @@ class TestChildIdHelper:
# ecs-service expands to service-task-definition + service-service # ecs-service expands to service-task-definition + service-service
assert _child_id(["service-task-definition", "service-service"]) == "service" assert _child_id(["service-task-definition", "service-service"]) == "service"
# alb expands to alb-loadbalancer + alb-targetgroup + alb-listener # alb expands to alb-loadbalancer + alb-targetgroup + alb-listener
assert _child_id(["alb-loadbalancer", "alb-targetgroup", "alb-listener"]) == "alb" assert _child_id(["alb-loadbalancer", "alb-targetgroup", "alb-listener"]) == "alb"
class TestAdapterDedupRejectsUnregisteredModule:
"""P1-1 (v1.14, REQ-135): a resource whose module is not in the
registry must raise ValueError, not be silently dropped from the
dedup merge. A typo'd module field (e.g. 'iam-role' vs 'iam_roles')
must surface as a diagnostic, not vanish."""
def test_unregistered_module_raises_valueerror(self, tmp_path):
stack = {
"resources": [
{"id": "bad", "type": "aws:bogus:thing", "module": "nonexistent@1.0.0", "inputs": {}}
],
"outputs": {},
"data_sources": [],
}
with pytest.raises(ValueError, match="no terraform_dir for module 'nonexistent'"):
adapt(stack, str(tmp_path))
def test_registered_module_still_works(self, tmp_path):
"""A registered module (s3) must still emit valid terraform — the
ValueError guard must not break the happy path."""
stack = {
"resources": [
{"id": "s3", "type": "aws:s3:bucket", "module": "s3@1.0.0", "inputs": {"bucket_name": "test"}}
],
"outputs": {},
"data_sources": [],
}
adapt(stack, str(tmp_path))
assert (tmp_path / "main.tf").exists()