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:
+15
-10
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user