refactor(P57): contract surface redesign + rename + .yml repo-wide

Contract surface redesign:
- New top-level fields: id (3-6 char acronym → stack.name), name (full → stack.title),
  infrastructure (map keyed by module name, replaces module:)
- Drop uses: field (dead reference; version pin lives in CI workflow uses: line)
- Drop top-level module/inputs (now nested under infrastructure map)
- Per-module optional version (defaults to latest published from registry)
- Multi-module contracts: one file deploys N modules in one pipeline run,
  resource IDs namespaced with module name to avoid collisions
- stack.schema.json: add optional title field for display name

Rename:
- pipelines/deploy.yaml → pipelines/contract.yml (declarative spec, not a pipeline)
- pipelines/ci.yaml → pipelines/ci.yml
- All 44 .yaml files → .yml repo-wide (contracts, module examples, kyverno policies)
- .acdl/contract.yaml → .acdl/contract.yml

Resolver (core/contract_resolver.py):
- Rewrite resolve() to loop infrastructure map, default version to latest,
  merge module fragments into one stack with namespaced resource IDs
- _latest_version() picks highest non-deprecated from registry
- _namespace_resources() prefixes IDs + rewrites ref: expressions for multi-module
- Single-module path: unprefixed IDs (backward compatible)

Verification:
- 494 tests pass (0 contract-shape failures)
- Local E2E passes (contract → resolver → adapter → local ECS HTTP 200 → outbox)

---ci---
project: acdl
phase: 57
milestone: v1.10.2
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-07-27 21:37:40 +00:00
parent 7f36df5610
commit 031887ec56
127 changed files with 1597 additions and 1100 deletions
+15 -10
View File
@@ -243,7 +243,7 @@ class TestStaticAssetsStack:
@pytest.fixture
def static_assets_stack(self):
from core.contract_resolver import resolve
return resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
return resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
def test_static_assets_resolves_to_4_resources(self, static_assets_stack):
types = [r["type"] for r in static_assets_stack["resources"]]
@@ -381,7 +381,7 @@ class TestResolverOutputs:
def test_static_assets_has_stack_outputs(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
assert "outputs" in stack
outputs = stack["outputs"]
assert "distribution_domain_name" in outputs
@@ -390,7 +390,7 @@ class TestResolverOutputs:
def test_static_assets_output_has_from_and_output(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
dist_out = stack["outputs"]["distribution_domain_name"]
assert "from" in dist_out
assert "output" in dist_out
@@ -399,7 +399,7 @@ class TestResolverOutputs:
def test_static_assets_adapter_emits_stack_output_blocks(self, tmp_path):
"""P1-7: adapter emits `output` blocks from stack.outputs."""
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
out_dir = str(tmp_path / "tf_out")
adapt(stack, out_dir)
main_tf = open(os.path.join(out_dir, "main.tf")).read()
@@ -494,7 +494,7 @@ class TestEncryptionByDefault:
def test_static_assets_l2_wires_kms_key_to_s3(self):
"""REQ-85: L2 modules wire per-stack CMK to children."""
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
types = [r["type"] for r in stack["resources"]]
assert "aws:kms:key" in types
s3_res = next(r for r in stack["resources"] if r["type"] == "aws:s3:bucket")
@@ -577,12 +577,17 @@ class TestDeletionProtectionByDefault:
"""REQ-87: L2 feature flag deletion_protection=false propagates to all children."""
import yaml
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.8",
"module": "static-assets",
"id": "assets",
"name": "static-assets-dp-test",
"environment": "dev",
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1", "deletion_protection": False},
"infrastructure": {
"static-assets": {
"version": "1.0.0",
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1", "deletion_protection": False},
}
},
}
contract_path = tmp_path / "test-dp.yaml"
contract_path = tmp_path / "test-dp.yml"
with open(contract_path, "w") as fh:
yaml.dump(contract, fh)
from core.contract_resolver import resolve
@@ -656,7 +661,7 @@ class TestUptimePrimitive:
def test_deploy_pipeline_has_deploy_uptime_stage(self):
import yaml
with open(ROOT / "pipelines/deploy.yaml") as fh:
with open(ROOT / "pipelines/contract.yml") as fh:
contract = yaml.safe_load(fh)
stage_names = [s["name"] for s in contract["stages"]]
assert "deploy-uptime" in stage_names
+60 -38
View File
@@ -17,24 +17,25 @@ class TestContractSchema:
schema = json.load(open(ROOT / "schemas/contract.schema.json"))
jsonschema.Draft202012Validator.check_schema(schema)
def test_schema_requires_uses_module_environment_inputs(self):
def test_schema_requires_id_name_environment_infrastructure(self):
schema = json.load(open(ROOT / "schemas/contract.schema.json"))
for field in ["uses", "module", "environment", "inputs"]:
for field in ["id", "name", "environment", "infrastructure"]:
assert field in schema["required"]
class TestResolveStaticAsset:
def test_resolve_static_asset_contract(self, tmp_path):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
assert stack["stack"]["name"] == "static-assets"
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
assert stack["stack"]["name"] == "assets"
assert stack["stack"]["title"] == "static-assets"
assert stack["stack"]["kind"] == "l2"
assert stack["stack"]["depth"] == 1
assert len(stack["resources"]) >= 1
def test_resolve_static_asset_has_s3_cloudfront_waf_resources(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
types = [r["type"] for r in stack["resources"]]
assert "aws:s3:bucket" in types
assert "aws:cloudfront:distribution" in types
@@ -43,15 +44,15 @@ class TestResolveStaticAsset:
def test_resolve_static_asset_has_s3_resource(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
s3_res = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"]
assert len(s3_res) == 1
assert s3_res[0]["inputs"]["bucket_name"] == "acdl-dev-static-assets-000000000000-us-east-1"
assert s3_res[0]["inputs"]["bucket_name"] == "acdl-dev-assets-000000000000-us-east-1"
assert s3_res[0]["inputs"]["region"] == "us-east-1"
def test_resolve_static_asset_validates_against_stack_schema(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
schema = json.load(open(ROOT / "schemas/stack.schema.json"))
jsonschema.validate(stack, schema)
@@ -59,22 +60,28 @@ class TestResolveStaticAsset:
class TestResolveMicroservice:
def test_resolve_microservice_contract(self):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest",
"port": 8080,
"region": "us-east-1",
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest",
"port": 8080,
"region": "us-east-1",
},
}
},
}
contract_path = ROOT / "contracts" / "test-microservice.yaml"
contract_path = ROOT / "contracts" / "test-microservice.yml"
with open(contract_path, "w") as fh:
yaml.dump(contract, fh)
try:
from core.contract_resolver import resolve
stack = resolve(str(contract_path), str(ROOT))
assert stack["stack"]["name"] == "microservice"
assert stack["stack"]["name"] == "msvc"
assert stack["stack"]["title"] == "microservice-test"
assert stack["stack"]["kind"] == "l2"
assert len(stack["resources"]) >= 6
finally:
@@ -84,20 +91,25 @@ class TestResolveMicroservice:
class TestResolveL1Direct:
def test_resolve_s3_direct(self, tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1",
"module": "s3",
"id": "s3a",
"name": "s3-direct-test",
"environment": "dev",
"inputs": {
"bucket_name": "my-test-bucket",
"region": "us-east-1",
"infrastructure": {
"s3": {
"version": "1.0.0",
"inputs": {
"bucket_name": "my-test-bucket",
"region": "us-east-1",
},
}
},
}
contract_path = tmp_path / "test-s3.yaml"
contract_path = tmp_path / "test-s3.yml"
with open(contract_path, "w") as fh:
yaml.dump(contract, fh)
from core.contract_resolver import resolve
stack = resolve(str(contract_path), str(ROOT))
assert stack["stack"]["name"] == "s3"
assert stack["stack"]["name"] == "s3a"
assert stack["stack"]["kind"] == "l1"
assert len(stack["resources"]) == 1
assert stack["resources"][0]["type"] == "aws:s3:bucket"
@@ -105,12 +117,17 @@ class TestResolveL1Direct:
def test_resolve_s3_validates_against_stack_schema(self, tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1",
"module": "s3",
"id": "s3a",
"name": "s3-schema-test",
"environment": "dev",
"inputs": {"bucket_name": "test", "region": "us-east-1"},
"infrastructure": {
"s3": {
"version": "1.0.0",
"inputs": {"bucket_name": "test", "region": "us-east-1"},
}
},
}
contract_path = tmp_path / "test-s3-schema.yaml"
contract_path = tmp_path / "test-s3-schema.yml"
with open(contract_path, "w") as fh:
yaml.dump(contract, fh)
from core.contract_resolver import resolve
@@ -122,12 +139,17 @@ class TestResolveL1Direct:
class TestResolveErrors:
def test_unknown_module_raises(self, tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1",
"module": "nonexistent",
"id": "bad1",
"name": "unknown-module-test",
"environment": "dev",
"inputs": {},
"infrastructure": {
"nonexistent": {
"version": "1.0.0",
"inputs": {},
}
},
}
contract_path = tmp_path / "bad.yaml"
contract_path = tmp_path / "bad.yml"
with open(contract_path, "w") as fh:
yaml.dump(contract, fh)
from core.contract_resolver import resolve
@@ -135,8 +157,8 @@ class TestResolveErrors:
resolve(str(contract_path), str(ROOT))
def test_missing_required_field_fails_validation(self, tmp_path):
contract = {"uses": "acdl/pipelines/deploy.yaml@v1", "module": "s3"}
contract_path = tmp_path / "incomplete.yaml"
contract = {"id": "bad2", "name": "incomplete", "environment": "dev"}
contract_path = tmp_path / "incomplete.yml"
with open(contract_path, "w") as fh:
yaml.dump(contract, fh)
from core.contract_resolver import resolve
@@ -147,12 +169,12 @@ class TestResolveErrors:
class TestDeployPipelineContract:
def test_deploy_pipeline_validates_against_schema(self):
schema = json.load(open(ROOT / "schemas/deploy-pipeline.schema.json"))
with open(ROOT / "pipelines/deploy.yaml") as fh:
with open(ROOT / "pipelines/contract.yml") as fh:
contract = yaml.safe_load(fh)
jsonschema.validate(contract, schema)
def test_deploy_pipeline_has_six_stages(self):
with open(ROOT / "pipelines/deploy.yaml") as fh:
with open(ROOT / "pipelines/contract.yml") as fh:
contract = yaml.safe_load(fh)
stage_names = [s["name"] for s in contract["stages"]]
assert "validate-contract" in stage_names
@@ -168,7 +190,7 @@ class TestL2OutputsResolution:
def test_static_assets_outputs_present(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
assert "outputs" in stack, "stack.outputs must be present for L2 modules (P1-7)"
assert "distribution_domain_name" in stack["outputs"]
assert "bucket_arn" in stack["outputs"]
@@ -176,7 +198,7 @@ class TestL2OutputsResolution:
def test_static_assets_output_from_field_resolves_to_resource_id(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/static-assets.yml"), str(ROOT))
dist = stack["outputs"]["distribution_domain_name"]
assert "from" in dist
assert "output" in dist
@@ -184,7 +206,7 @@ class TestL2OutputsResolution:
def test_microservice_outputs_present(self):
from core.contract_resolver import resolve
stack = resolve(str(ROOT / "contracts/microservice.yaml"), str(ROOT))
stack = resolve(str(ROOT / "contracts/microservice.yml"), str(ROOT))
assert "outputs" in stack, "stack.outputs must be present for L2 modules (P1-7)"
assert "lb_arn" in stack["outputs"]
assert "service_arn" in stack["outputs"]
+5 -5
View File
@@ -40,35 +40,35 @@ def test_deploy_workflow_passes_env_flag_to_run_platform():
def test_resolver_environment_override_changes_env():
"""D-088: environment_override changes the resolved environment."""
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"),
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"),
environment_override="qa")
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert "qa" in s3["inputs"]["bucket_name"]
def test_resolver_environment_override_prod():
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"),
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"),
environment_override="prod")
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert "prod" in s3["inputs"]["bucket_name"]
def test_resolver_environment_override_dr():
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"),
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"),
environment_override="dr")
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert "dr" in s3["inputs"]["bucket_name"]
def test_resolver_no_override_uses_contract_env():
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"))
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert "dev" in s3["inputs"]["bucket_name"]
def test_resolver_override_none_uses_contract_env():
"""Passing environment_override=None uses the contract's environment."""
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"),
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"),
environment_override=None)
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert "dev" in s3["inputs"]["bucket_name"]
+9 -9
View File
@@ -32,17 +32,17 @@ class TestEnvironmentCheck:
assert "IAM role" in msg
def test_contract_with_dev_environment_passes(self):
ok, msg = check(contract_path=str(ROOT / "contracts/static-assets.yaml"), root=ROOT)
ok, msg = check(contract_path=str(ROOT / "contracts/static-assets.yml"), root=ROOT)
assert ok is True
assert "dev" in msg
def test_contract_with_missing_environment_fails(self, tmp_path):
contract = tmp_path / "contract.yaml"
contract = tmp_path / "contract.yml"
contract.write_text(
"uses: acdl/pipelines/deploy.yaml@v1.6\n"
"module: static-assets\n"
"id: assets\n"
"name: static-assets-test\n"
"environment: no-such-env\n"
"inputs:\n bucket_name: x\n region: us-east-1\n"
"infrastructure:\n static-assets:\n version: \"1.0.0\"\n inputs:\n bucket_name: x\n region: us-east-1\n"
)
ok, msg = check(contract_path=str(contract), root=ROOT)
assert ok is False
@@ -53,11 +53,11 @@ class TestEnvironmentCheck:
assert ok is False
def test_contract_without_environment_field_returns_false(self, tmp_path):
contract = tmp_path / "contract.yaml"
contract = tmp_path / "contract.yml"
contract.write_text(
"uses: acdl/pipelines/deploy.yaml@v1.6\n"
"module: static-assets\n"
"inputs:\n bucket_name: x\n region: us-east-1\n"
"id: assets\n"
"name: static-assets-test\n"
"infrastructure:\n static-assets:\n version: \"1.0.0\"\n inputs:\n bucket_name: x\n region: us-east-1\n"
)
ok, msg = check(contract_path=str(contract), root=ROOT)
assert ok is False
+14 -14
View File
@@ -24,21 +24,21 @@ def test_expand_env_dotted_path():
assert _expand_vars("${env.state_backend.bucket}", ctx) == "acdl-dev-state"
def test_expand_contract_module():
ctx = {"env": {}, "contract": {"module": "static-assets"}}
assert _expand_vars("${contract.module}", ctx) == "static-assets"
def test_expand_contract_id():
ctx = {"env": {}, "contract": {"id": "assets"}}
assert _expand_vars("${contract.id}", ctx) == "assets"
def test_expand_contract_dotted_path():
ctx = {"env": {}, "contract": {"inputs": {"bucket_name": "acdl-x"}}}
assert _expand_vars("${contract.inputs.bucket_name}", ctx) == "acdl-x"
ctx = {"env": {}, "contract": {"infrastructure": {"s3": {"inputs": {"bucket_name": "acdl-x"}}}}}
assert _expand_vars("${contract.infrastructure.s3.inputs.bucket_name}", ctx) == "acdl-x"
def test_expand_nested_in_string():
ctx = {"env": {"environment": "dev", "account_id": "000000000000", "region": "us-east-1"},
"contract": {"module": "static-assets"}}
result = _expand_vars("acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}", ctx)
assert result == "acdl-dev-static-assets-000000000000-us-east-1"
"contract": {"id": "assets"}}
result = _expand_vars("acdl-${env.environment}-${contract.id}-${env.account_id}-${env.region}", ctx)
assert result == "acdl-dev-assets-000000000000-us-east-1"
def test_expand_recursive_in_dict():
@@ -81,24 +81,24 @@ def test_expand_non_string_passthrough():
def test_resolve_static_assets_expands_bucket_name():
"""Resolving the sample contract produces the interpolated bucket name."""
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"))
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert s3["inputs"]["bucket_name"] == "acdl-dev-static-assets-000000000000-us-east-1"
assert s3["inputs"]["bucket_name"] == "acdl-dev-assets-000000000000-us-east-1"
assert s3["inputs"]["region"] == "us-east-1"
def test_resolve_microservice_expands_bucket_name():
stack = resolve(str(ROOT / "contracts" / "microservice.yaml"))
stack = resolve(str(ROOT / "contracts" / "microservice.yml"))
# The microservice L2 wires bucket_name to vpc.inputs.cidr (legacy wire);
# the interpolated value is a valid CIDR-like string. The key assertion
# is that resolution succeeds with interpolation (no unresolved tokens).
assert stack["stack"]["name"] == "microservice"
assert stack["stack"]["name"] == "msvc"
def test_resolve_with_environment_override_uses_overridden_env():
"""D-088: environment_override changes the interpolation context."""
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"),
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"),
environment_override="qa")
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
# qa env: environment=qa, account_id=000000000000, region=us-east-1
assert s3["inputs"]["bucket_name"] == "acdl-qa-static-assets-000000000000-us-east-1"
assert s3["inputs"]["bucket_name"] == "acdl-qa-assets-000000000000-us-east-1"
+2 -2
View File
@@ -181,7 +181,7 @@ def test_run_local_e2e_microservice():
-> local ECS (HTTP 200) -> flat-file outbox -> local Lambda. No AWS."""
os.environ["ACDL_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/microservice.yaml")
result = le.run_local_e2e("contracts/microservice.yml")
finally:
os.environ.pop("ACDL_LOCAL_TIER", None)
assert result["tier"] == "local-emulator"
@@ -198,7 +198,7 @@ def test_run_local_e2e_static_assets():
complete (ecs=None) and the outbox chain + Lambda stub must pass."""
os.environ["ACDL_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/static-assets.yaml")
result = le.run_local_e2e("contracts/static-assets.yml")
finally:
os.environ.pop("ACDL_LOCAL_TIER", None)
assert result["tier"] == "local-emulator"
+4 -4
View File
@@ -28,8 +28,8 @@ class TestModuleStandards:
assert (l1_dir / "interface.json").is_file(), f"{name}: interface.json missing"
assert (l1_dir / "instance.json").is_file(), f"{name}: instance.json missing"
assert (l1_dir / "README.md").is_file(), f"{name}: README.md missing"
assert (l1_dir / "examples" / "simple.yaml").is_file(), f"{name}: examples/simple.yaml missing"
assert (l1_dir / "examples" / "complex.yaml").is_file(), f"{name}: examples/complex.yaml missing"
assert (l1_dir / "examples" / "simple.yml").is_file(), f"{name}: examples/simple.yml missing"
assert (l1_dir / "examples" / "complex.yml").is_file(), f"{name}: examples/complex.yml missing"
def test_all_l2_have_required_files(self, registry):
for name, entry in registry.items():
@@ -39,8 +39,8 @@ class TestModuleStandards:
l2_dir = ROOT / "modules" / "l2" / name
assert (l2_dir / "composition.json").is_file(), f"{name}: composition.json missing"
assert (l2_dir / "README.md").is_file(), f"{name}: README.md missing"
assert (l2_dir / "examples" / "simple.yaml").is_file(), f"{name}: examples/simple.yaml missing"
assert (l2_dir / "examples" / "complex.yaml").is_file(), f"{name}: examples/complex.yaml missing"
assert (l2_dir / "examples" / "simple.yml").is_file(), f"{name}: examples/simple.yml missing"
assert (l2_dir / "examples" / "complex.yml").is_file(), f"{name}: examples/complex.yml missing"
def test_all_l1_have_deletion_protection_nfr(self, registry):
for name, entry in registry.items():
+65 -25
View File
@@ -34,16 +34,21 @@ def _tf_for_contract(contract_dict, tmp_path):
def test_desired_count_override_emits_overridden_value(tmp_path):
"""An L1 with desired_count: 3 in contract inputs emits desired_count = 3."""
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
"desired_count": 3,
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert "desired_count = 3" in tf
@@ -53,15 +58,20 @@ def test_desired_count_override_emits_overridden_value(tmp_path):
def test_desired_count_default_emits_one_via_interface(tmp_path):
"""Absent desired_count emits desired_count = 1 via interface default."""
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert "desired_count = 1" in tf
@@ -69,16 +79,21 @@ def test_desired_count_default_emits_one_via_interface(tmp_path):
def test_launch_type_override_emits_overridden_value(tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
"launch_type": "EC2",
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert 'launch_type = "EC2"' in tf
@@ -87,16 +102,21 @@ def test_launch_type_override_emits_overridden_value(tmp_path):
def test_target_type_override_emits_overridden_value(tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
"target_type": "instance",
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert 'target_type = "instance"' in tf
@@ -105,16 +125,21 @@ def test_target_type_override_emits_overridden_value(tmp_path):
def test_load_balancer_type_override_emits_overridden_value(tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
"load_balancer_type": "network",
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert 'load_balancer_type = "network"' in tf
@@ -123,16 +148,21 @@ def test_load_balancer_type_override_emits_overridden_value(tmp_path):
def test_family_override_emits_overridden_value(tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
"family": "myservice",
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert 'family = "myservice"' in tf
@@ -140,15 +170,20 @@ def test_family_override_emits_overridden_value(tmp_path):
def test_family_default_emits_app(tmp_path):
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert 'family = "app"' in tf
@@ -157,7 +192,7 @@ def test_family_default_emits_app(tmp_path):
def test_v1_1_s3_regression_still_passes(tmp_path):
"""The v1.1 S3 regression: the static-assets L1 (s3-only) must still
produce valid Terraform with no ECS/ALB/VPC defaults leaking in."""
contract_path = ROOT / "contracts" / "static-assets.yaml"
contract_path = ROOT / "contracts" / "static-assets.yml"
stack = resolve(str(contract_path))
out_dir = tmp_path / "tf"
adapt(stack, str(out_dir))
@@ -172,15 +207,20 @@ def test_no_hardcoded_microservice_name_in_route_table(tmp_path):
"""The hardcoded 'acdl-microservice-rt' / 'acdl-microservice-igw' Name
tags are removed (D-085); the name derives from the VPC name input."""
contract = {
"uses": "acdl/pipelines/deploy.yaml@v1.9",
"module": "microservice",
"id": "msvc",
"name": "microservice-test",
"environment": "dev",
"inputs": {
"infrastructure": {
"microservice": {
"version": "1.0.0",
"inputs": {
"bucket_name": "acdl-test",
"region": "us-east-1",
"image": "public.ecr.aws/docker/library/nginx:latest",
"port": 80,
},
}
}
}
tf = _tf_for_contract(contract, tmp_path)
assert "acdl-microservice-rt" not in tf
+21 -21
View File
@@ -15,14 +15,14 @@ from core.contract_resolver import resolve
SCHEMA = json.loads((ROOT / "schemas" / "contract.schema.json").read_text())
PER_ENV_CONTRACTS = [
"contracts/static-assets.dev.yaml",
"contracts/static-assets.qa.yaml",
"contracts/static-assets.prod.yaml",
"contracts/static-assets.dr.yaml",
"contracts/microservice.dev.yaml",
"contracts/microservice.qa.yaml",
"contracts/microservice.prod.yaml",
"contracts/microservice.dr.yaml",
"contracts/static-assets.dev.yml",
"contracts/static-assets.qa.yml",
"contracts/static-assets.prod.yml",
"contracts/static-assets.dr.yml",
"contracts/microservice.dev.yml",
"contracts/microservice.qa.yml",
"contracts/microservice.prod.yml",
"contracts/microservice.dr.yml",
]
@@ -35,26 +35,26 @@ def test_per_env_contract_validates_against_schema(rel):
@pytest.mark.parametrize("rel", PER_ENV_CONTRACTS)
def test_per_env_contract_resolves(rel):
stack = resolve(str(ROOT / rel))
assert stack["stack"]["name"] in ("static-assets", "microservice")
assert stack["stack"]["name"] in ("assets", "msvc")
def test_static_assets_dev_has_dev_environment():
c = yaml.safe_load((ROOT / "contracts/static-assets.dev.yaml").read_text())
c = yaml.safe_load((ROOT / "contracts/static-assets.dev.yml").read_text())
assert c["environment"] == "dev"
def test_static_assets_qa_has_qa_environment():
c = yaml.safe_load((ROOT / "contracts/static-assets.qa.yaml").read_text())
c = yaml.safe_load((ROOT / "contracts/static-assets.qa.yml").read_text())
assert c["environment"] == "qa"
def test_static_assets_prod_has_prod_environment():
c = yaml.safe_load((ROOT / "contracts/static-assets.prod.yaml").read_text())
c = yaml.safe_load((ROOT / "contracts/static-assets.prod.yml").read_text())
assert c["environment"] == "prod"
def test_static_assets_dr_has_dr_environment():
c = yaml.safe_load((ROOT / "contracts/static-assets.dr.yaml").read_text())
c = yaml.safe_load((ROOT / "contracts/static-assets.dr.yml").read_text())
assert c["environment"] == "dr"
@@ -67,21 +67,21 @@ def test_per_env_contracts_use_interpolation():
def test_per_env_qa_resolves_to_qa_bucket_name():
stack = resolve(str(ROOT / "contracts/static-assets.qa.yaml"))
stack = resolve(str(ROOT / "contracts/static-assets.qa.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert s3["inputs"]["bucket_name"] == "acdl-qa-static-assets-000000000000-us-east-1"
assert s3["inputs"]["bucket_name"] == "acdl-qa-assets-000000000000-us-east-1"
def test_per_env_prod_resolves_to_prod_bucket_name():
stack = resolve(str(ROOT / "contracts/static-assets.prod.yaml"))
stack = resolve(str(ROOT / "contracts/static-assets.prod.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert s3["inputs"]["bucket_name"] == "acdl-prod-static-assets-000000000000-us-east-1"
assert s3["inputs"]["bucket_name"] == "acdl-prod-assets-000000000000-us-east-1"
def test_default_dev_contract_still_works():
"""The existing contracts/static-assets.yaml remains the dev default."""
c = yaml.safe_load((ROOT / "contracts/static-assets.yaml").read_text())
"""The existing contracts/static-assets.yml remains the dev default."""
c = yaml.safe_load((ROOT / "contracts/static-assets.yml").read_text())
assert c["environment"] == "dev"
stack = resolve(str(ROOT / "contracts/static-assets.yaml"))
stack = resolve(str(ROOT / "contracts/static-assets.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert s3["inputs"]["bucket_name"] == "acdl-dev-static-assets-000000000000-us-east-1"
assert s3["inputs"]["bucket_name"] == "acdl-dev-assets-000000000000-us-east-1"
+40 -28
View File
@@ -45,49 +45,49 @@ class TestPipelineSchema:
class TestPipelineContract:
def test_contract_validates_against_schema(self):
schema = json.load(open(ROOT / "schemas/pipeline.schema.json"))
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
jsonschema.validate(contract, schema)
def test_contract_has_three_stages(self):
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
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")
contract = _load_yaml("pipelines/ci.yml")
assert contract["runner"] == "ubuntu-latest"
def test_contract_python_version(self):
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
assert contract["python_version"] == "3.12"
def test_contract_triggers_push_main(self):
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
assert "main" in contract["triggers"]["push"]
def test_contract_triggers_pr_main(self):
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
assert "main" in contract["triggers"]["pull_request"]
def test_contract_all_stages_required(self):
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
for stage in contract["stages"]:
assert stage["required"] is True
def test_contract_lint_command_compiles_python(self):
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
lint = next(s for s in contract["stages"] if s["name"] == "lint")
assert "py_compile" in lint["command"]
assert "core/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")
contract = _load_yaml("pipelines/ci.yml")
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")
contract = _load_yaml("pipelines/ci.yml")
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"]
@@ -107,7 +107,7 @@ class TestWorkflowConformance:
def test_gitea_workflow_name_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
assert wf["name"] == contract["name"]
def test_gitea_workflow_has_three_jobs(self):
@@ -116,19 +116,19 @@ class TestWorkflowConformance:
def test_gitea_workflow_triggers_match_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yaml")
contract = _load_yaml("pipelines/ci.yml")
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")
contract = _load_yaml("pipelines/ci.yml")
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")
contract = _load_yaml("pipelines/ci.yml")
for job in wf["jobs"].values():
setup_step = next(
s for s in job["steps"] if "setup-python" in s.get("uses", "")
@@ -262,11 +262,11 @@ class TestDeployPipelineSchema:
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")
contract = _load_yaml("pipelines/contract.yml")
jsonschema.validate(contract, schema)
def test_deploy_contract_has_nine_stages(self):
contract = _load_yaml("pipelines/deploy.yaml")
contract = _load_yaml("pipelines/contract.yml")
stage_names = [s["name"] for s in contract["stages"]]
assert stage_names == [
"validate-contract",
@@ -281,7 +281,7 @@ class TestDeployPipelineContract:
]
def test_deploy_contract_runner_is_ubuntu_latest(self):
contract = _load_yaml("pipelines/deploy.yaml")
contract = _load_yaml("pipelines/contract.yml")
assert contract["runner"] == "ubuntu-latest"
@@ -299,7 +299,7 @@ class TestDeployWorkflowConformance:
def test_deploy_workflow_name_matches_contract(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
contract = _load_yaml("pipelines/deploy.yaml")
contract = _load_yaml("pipelines/contract.yml")
assert wf["name"] == contract["name"]
def test_deploy_workflow_is_reusable(self):
@@ -310,7 +310,7 @@ class TestDeployWorkflowConformance:
wf = _load_workflow(".gitea/workflows/deploy.yml")
inputs = wf["on"]["workflow_call"]["inputs"]
assert "contract" in inputs
assert inputs["contract"]["default"] == ".acdl/contract.yaml"
assert inputs["contract"]["default"] == ".acdl/contract.yml"
def test_deploy_workflow_has_mode_input(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
@@ -320,13 +320,13 @@ class TestDeployWorkflowConformance:
def test_deploy_workflow_runner_matches_contract(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
contract = _load_yaml("pipelines/deploy.yaml")
contract = _load_yaml("pipelines/contract.yml")
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")
contract = _load_yaml("pipelines/contract.yml")
for job in wf["jobs"].values():
setup_step = next(
s for s in job["steps"] if "setup-python" in s.get("uses", "")
@@ -371,12 +371,24 @@ class TestDeployWorkflowConformance:
class TestSampleContractVersioning:
def test_sample_contract_uses_versioned_tag(self):
contract = _load_yaml("contracts/static-assets.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.9"
def test_ci_workflow_uses_versioned_tag(self):
"""The consumer CI workflow (the runtime dispatch) uses a versioned @vX.Y tag.
The contract no longer carries a `uses:` field (removed in P57); the
version pin lives in the consumer's CI workflow reference."""
import yaml
wf = yaml.safe_load((ROOT / ".github/workflows/deploy.yml").read_text())
# The workflow itself doesn't have a top-level uses; check the checkout
# ref of the platform repo (the versioned tag the consumer pins to).
deploy_job = wf["jobs"]["deploy"]
checkout_steps = [s for s in deploy_job["steps"]
if "checkout" in s.get("uses", "")]
platform_checkout = next(
(s for s in checkout_steps if s.get("with", {}).get("path") == "platform"),
None)
assert platform_checkout is not None, "must have a platform repo checkout"
ref = platform_checkout["with"]["ref"]
assert ref.startswith("v"), f"platform ref must be a versioned tag, got {ref}"
assert "@main" not in ref and ref != "main", "must not pin to @main"
class TestPlatformWorkflows:
@@ -433,7 +445,7 @@ class TestPlatformWorkflows:
)
assert "run_platform.sh" in run_step["run"]
assert "--check-only" in run_step["run"]
assert "contracts/*.yaml" in run_step["run"]
assert "contracts/*.yml" in run_step["run"]
def test_platform_test_schema_validation_validates_schemas(self):
wf = _load_workflow(".github/workflows/platform-test.yml")
+12 -12
View File
@@ -11,51 +11,51 @@ from core.contract_resolver import resolve
def test_static_assets_bucket_name_uses_naming_pattern():
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"))
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
bucket = s3["inputs"]["bucket_name"]
# The naming pattern: acdl-<env>-<module>-<account_id>-<region>
assert bucket.startswith("acdl-dev-static-assets-")
# The naming pattern: acdl-<env>-<id>-<account_id>-<region>
assert bucket.startswith("acdl-dev-assets-")
assert "000000000000" in bucket
assert bucket.endswith("us-east-1")
assert bucket == "acdl-dev-static-assets-000000000000-us-east-1"
assert bucket == "acdl-dev-assets-000000000000-us-east-1"
def test_static_assets_region_uses_env_region():
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"))
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
assert s3["inputs"]["region"] == "us-east-1"
def test_static_assets_contract_has_interpolation_tokens_pre_resolve():
"""The contract file itself contains the raw ${env.*} tokens (pre-resolution)."""
text = (ROOT / "contracts" / "static-assets.yaml").read_text()
text = (ROOT / "contracts" / "static-assets.yml").read_text()
assert "${env.environment}" in text
assert "${contract.module}" in text
assert "${contract.id}" in text
assert "${env.account_id}" in text
assert "${env.region}" in text
def test_microservice_contract_has_interpolation_tokens():
text = (ROOT / "contracts" / "microservice.yaml").read_text()
text = (ROOT / "contracts" / "microservice.yml").read_text()
assert "${env.environment}" in text
assert "${env.account_id}" in text
assert "${env.region}" in text
def test_microservice_resolves_with_interpolation():
stack = resolve(str(ROOT / "contracts" / "microservice.yaml"))
assert stack["stack"]["name"] == "microservice"
stack = resolve(str(ROOT / "contracts" / "microservice.yml"))
assert stack["stack"]["name"] == "msvc"
# Resolution succeeded — no unresolved tokens.
def test_interpolation_uses_all_naming_components():
"""The naming pattern includes region, account id, and environment (the binding requirement)."""
stack = resolve(str(ROOT / "contracts" / "static-assets.yaml"))
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"))
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
bucket = s3["inputs"]["bucket_name"]
# Verify all three required components are present in the resolved name.
assert "dev" in bucket # environment
assert "000000000000" in bucket # account_id
assert "us-east-1" in bucket # region
assert "static-assets" in bucket # module
assert "assets" in bucket # contract id