Compare commits

..

9 Commits

Author SHA1 Message Date
Jon Chery f12f6edd23 verify(P9): run-platform-split — 4-layer verify PASS + ship
VERIFY: structural — 2 helpers extracted (sourced, G-112); behavioral — CI PASS + regression gate 18V+4S PASS (G-111, D-118); quality — run_platform.sh ~80 lines smaller.

---ci---
project: acdl
phase: 9
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-173]
  partial: []
---/ci---
2026-08-01 12:52:07 +00:00
Jon Chery 1db5ca8286 verify(P8): workflow-generator-dedup — 4-layer verify PASS + ship
VERIFY: structural — generator + sources; behavioral — 98 tests + CI PASS; quality — ~20KB dedup, single source of truth.

---ci---
project: acdl
phase: 8
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-172]
  partial: []
---/ci---
2026-08-01 12:41:10 +00:00
Jon Chery 93ae9e4a39 verify(P7): contract-resolver-envloader-and-kind — 4-layer verify PASS + ship
VERIFY: structural — envloader dedup + kind field; behavioral — 49 tests + CI PASS; quality — fragile is_l2 heuristic replaced.

---ci---
project: acdl
phase: 7
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-171]
  partial: []
---/ci---
2026-08-01 12:38:56 +00:00
Jon Chery d3179fff37 verify(P6): run-platform-deadcode-and-hitl-fn — 4-layer verify PASS + ship
VERIFY: structural — HITL fn extracted + deadcode/config; behavioral — syntax clean + CI PASS; quality — ~14 lines saved.

---ci---
project: acdl
phase: 6
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-170]
  partial: []
---/ci---
2026-08-01 12:36:40 +00:00
Jon Chery c029b102a3 verify(P5): regression-verify-dedup — 4-layer verify PASS + ship
VERIFY: structural — 3 shared helpers extracted; behavioral — 611 tests + CI PASS; quality — behavior preserved, ~70 lines saved.

---ci---
project: acdl
phase: 5
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-169]
  partial: []
---/ci---
2026-08-01 12:31:08 +00:00
Jon Chery 2806c6c3ed verify(P4): migrate-ssm-except-narrowing — 4-layer verify PASS + ship
VERIFY: structural — narrowed excepts; behavioral — 48 tests + CI PASS; security — non-ParameterNotFound errors surface; quality — 2 new + 2 updated tests.

---ci---
project: acdl
phase: 4
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-168]
  partial: []
---/ci---
2026-08-01 12:24:30 +00:00
Jon Chery e048acd4dd verify(P3): dead-code-and-stale-prefix-cleanup — 4-layer verify PASS + ship
VERIFY: structural — dead export + stale comments + prefixes gone; behavioral — 616 tests + CI PASS; quality — env-override test updated.

---ci---
project: acdl
phase: 3
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-167]
  partial: []
---/ci---
2026-08-01 12:20:40 +00:00
Jon Chery 9421442afd verify(P2): user-facing ACDL→Nova sweep — 4-layer verify PASS + ship
VERIFY: structural — all user-facing strings Nova; behavioral — 79 tests
+ CI PASS; security — no creds; quality — new onboarding Nova-header test.
REQ-166 complete. Internal ship_phase.sh helper added.

---ci---
project: acdl
phase: 2
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-166]
  partial: []
---/ci---
2026-08-01 12:12:45 +00:00
Jon Chery bb43d94563 verify(P1): state-bucket + Kyverno rebrand — 4-layer verify PASS + ship
VERIFY: structural — adapter.py:117 nova-tfstate-*; kyverno policy nova:*
labels; behavioral — 80 tests PASS + run_ci.sh 3-stage PASS; security —
emitted backend no longer points at a non-existent bucket; quality —
new test_adapt_emits_nova_state_bucket regression guard. REQ-165 complete.

---ci---
project: acdl
phase: 1
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-165]
  partial: []
---/ci---
2026-07-30 15:17:43 +00:00
41 changed files with 1158 additions and 450 deletions
+4 -4
View File
@@ -1,9 +1,9 @@
{
"phase": 0,
"stage": "plan",
"phase": 1,
"stage": "execute",
"milestone": "v1.16",
"phase_role": "pre_execution",
"phase_role": "execution",
"attempts": 0,
"updated_at": "2026-07-30T15:15:00Z",
"updated_at": "2026-07-30T15:30:00Z",
"milestone_complete": false
}
+39 -38
View File
@@ -1,12 +1,13 @@
{
"run_id": "regr-1785375318",
"run_at_utc": "2026-07-30T01:35:18Z",
"run_id": "regr-1785588523",
"run_at_utc": "2026-08-01T12:48:43Z",
"milestone": "v1.10",
"phase": 52,
"summary": {
"Verified": 22,
"Verified": 18,
"Decayed": 0,
"Broken": 0
"Broken": 0,
"Skipped": 4
},
"passed": true,
"results": [
@@ -16,7 +17,7 @@
"status": "Verified",
"detail": "exit 0; 2 sample contracts validate",
"tier": "local",
"duration_ms": 230
"duration_ms": 260
},
{
"capability_id": "CAP-002",
@@ -24,7 +25,7 @@
"status": "Verified",
"detail": "exit 0; env schema validates",
"tier": "local",
"duration_ms": 204
"duration_ms": 202
},
{
"capability_id": "CAP-003",
@@ -32,7 +33,7 @@
"status": "Verified",
"detail": "exit 0; ",
"tier": "local",
"duration_ms": 247
"duration_ms": 266
},
{
"capability_id": "CAP-004",
@@ -40,7 +41,7 @@
"status": "Verified",
"detail": "exit 0; ",
"tier": "local",
"duration_ms": 241
"duration_ms": 248
},
{
"capability_id": "CAP-005",
@@ -48,7 +49,7 @@
"status": "Verified",
"detail": "exit 0; ",
"tier": "local",
"duration_ms": 326
"duration_ms": 337
},
{
"capability_id": "CAP-006",
@@ -56,7 +57,7 @@
"status": "Verified",
"detail": "exit 0; interpolation ok",
"tier": "local",
"duration_ms": 216
"duration_ms": 209
},
{
"capability_id": "CAP-007",
@@ -64,7 +65,7 @@
"status": "Verified",
"detail": "exit 0; confidence band=pass",
"tier": "local",
"duration_ms": 79
"duration_ms": 80
},
{
"capability_id": "CAP-008",
@@ -72,15 +73,15 @@
"status": "Verified",
"detail": "exit 0; outbox hash chain ok",
"tier": "local",
"duration_ms": 333
"duration_ms": 319
},
{
"capability_id": "CAP-009",
"name": "offline pytest suite passes",
"status": "Verified",
"detail": "exit 0; [ 98%]\ntests/test_wiz_adapter_real_client.py ......... [100%]\n\n====================== 555 passed, 2 deselected in 51.11s ======================",
"detail": "exit 0; [ 98%]\ntests/test_wiz_adapter_real_client.py ......... [100%]\n\n====================== 577 passed, 2 deselected in 53.53s ======================",
"tier": "local",
"duration_ms": 52574
"duration_ms": 55005
},
{
"capability_id": "CAP-010",
@@ -88,63 +89,63 @@
"status": "Verified",
"detail": "exit 0; resource(s))\n\n=== PLATFORM CHECK OK ===\ncontract -> resolver -> stack -> adapter -> structure validated (offline, no AWS)\ncheck-only: OK\n\n=== CI PIPELINE OK ===\n3 stages passed: lint, test, check-only",
"tier": "local",
"duration_ms": 59608
"duration_ms": 59882
},
{
"capability_id": "CAP-011",
"name": "headline E2E runs against the local emulating tier (microservice)",
"status": "Verified",
"detail": "exit 0; al-emulator\",\n \"desired_count\": 1,\n \"running_count\": 1\n },\n \"outbox_dir\": \"/tmp/acdl_local_e2e_0v1bpi48/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
"detail": "exit 0; al-emulator\",\n \"desired_count\": 1,\n \"running_count\": 1\n },\n \"outbox_dir\": \"/tmp/nova_local_e2e_cuwlkzrj/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
"tier": "local",
"duration_ms": 1072
"duration_ms": 561
},
{
"capability_id": "CAP-012",
"name": "local E2E on the static-assets stack (no ECS)",
"status": "Verified",
"detail": "exit 0; acdl_local_e2e_0cjcizgd/tf\",\n \"backend\": \"local\",\n \"ecs\": null,\n \"outbox_dir\": \"/tmp/acdl_local_e2e_0cjcizgd/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
"detail": "exit 0; nova_local_e2e_mfeuiylw/tf\",\n \"backend\": \"local\",\n \"ecs\": null,\n \"outbox_dir\": \"/tmp/nova_local_e2e_mfeuiylw/outbox\",\n \"outbox_events\": 2,\n \"outbox_chain_verified\": true,\n \"lambda_status\": 200\n}",
"tier": "local",
"duration_ms": 490
"duration_ms": 492
},
{
"capability_id": "CAP-013",
"name": "terraform init+validate+plan live AWS (microservice)",
"status": "Verified",
"detail": "terraform init+validate+plan OK (live AWS, microservice)",
"status": "Skipped",
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice]",
"tier": "live-aws",
"duration_ms": 28176
"duration_ms": 851
},
{
"capability_id": "CAP-014",
"name": "terraform init+validate+plan live AWS (static-assets)",
"status": "Verified",
"detail": "terraform init+validate+plan OK (live AWS, static-assets)",
"status": "Skipped",
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets]",
"tier": "live-aws",
"duration_ms": 31892
"duration_ms": 724
},
{
"capability_id": "CAP-015",
"name": "DynamoDB outbox table exists (live AWS)",
"status": "Verified",
"detail": "acdl-outbox exists, item_count=9",
"status": "Skipped",
"detail": "nova-outbox absent (post-v1.11-teardown steady state, D-096)",
"tier": "live-aws",
"duration_ms": 507
"duration_ms": 487
},
{
"capability_id": "CAP-016",
"name": "S3 state bucket exists + readable (live AWS)",
"status": "Verified",
"detail": "state bucket exists, keys=['platform/terraform.tfstate', 'spike/alb/dev/terraform.tfstate', 'spike/assets/dev/terraform.tfstate', 'spike/cdn/dev/terraform.tfstate', 'spike/ci-vpc/terraform.tfstate']",
"status": "Skipped",
"detail": "state bucket nova-tfstate-581513795199-us-east-1 absent (post-v1.11-teardown, D-096)",
"tier": "live-aws",
"duration_ms": 329
"duration_ms": 300
},
{
"capability_id": "CAP-017",
"name": "DynamoDB acdl-contracts table (lifecycle pipeline evidence)",
"name": "DynamoDB nova-contracts table (lifecycle pipeline evidence)",
"status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
"tier": "lifecycle-pipeline",
"duration_ms": 588
"duration_ms": 579
},
{
"capability_id": "CAP-018",
@@ -152,7 +153,7 @@
"status": "Verified",
"detail": "LocalLambdaStub instantiates (local tier evidence)",
"tier": "lifecycle-pipeline",
"duration_ms": 135
"duration_ms": 139
},
{
"capability_id": "CAP-019",
@@ -160,7 +161,7 @@
"status": "Verified",
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
"tier": "lifecycle-pipeline",
"duration_ms": 498
"duration_ms": 553
},
{
"capability_id": "CAP-020",
@@ -168,7 +169,7 @@
"status": "Verified",
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
"tier": "lifecycle-pipeline",
"duration_ms": 510
"duration_ms": 561
},
{
"capability_id": "CAP-021",
@@ -176,7 +177,7 @@
"status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
"tier": "lifecycle-pipeline",
"duration_ms": 562
"duration_ms": 598
},
{
"capability_id": "CAP-022",
@@ -184,7 +185,7 @@
"status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
"tier": "lifecycle-pipeline",
"duration_ms": 554
"duration_ms": 570
}
]
}
+28 -28
View File
@@ -1,51 +1,51 @@
# Regression Report — v1.10 Phase 52
- **Run ID:** `regr-1785375318`
- **Run at (UTC):** 2026-07-30T01:35:18Z
- **Summary:** {'Verified': 22, 'Decayed': 0, 'Broken': 0}
- **Run ID:** `regr-1785588523`
- **Run at (UTC):** 2026-08-01T12:48:43Z
- **Summary:** {'Verified': 18, 'Decayed': 0, 'Broken': 0, 'Skipped': 4}
- **Passed (milestone gate):** True
| Capability | Name | Tier | Status | Duration (ms) | Detail |
|-----------|------|------|--------|--------------|--------|
| CAP-001 | contract.schema.json validates sample contracts | local | **Verified** | 230 | exit 0; 2 sample contracts validate |
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 204 | exit 0; env schema validates |
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 247 | exit 0; |
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 241 | exit 0; |
| CAP-005 | terraform adapter emits .tf files | local | **Verified** | 326 | exit 0; |
| CAP-006 | contract interpolation expands env/contract tokens | local | **Verified** | 216 | exit 0; interpolation ok |
| CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 79 | exit 0; confidence band=pass |
| CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 333 | exit 0; outbox hash chain ok |
| CAP-009 | offline pytest suite passes | local | **Verified** | 52574 | exit 0; [ 98%]
| CAP-001 | contract.schema.json validates sample contracts | local | **Verified** | 260 | exit 0; 2 sample contracts validate |
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 202 | exit 0; env schema validates |
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 266 | exit 0; |
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 248 | exit 0; |
| CAP-005 | terraform adapter emits .tf files | local | **Verified** | 337 | exit 0; |
| CAP-006 | contract interpolation expands env/contract tokens | local | **Verified** | 209 | exit 0; interpolation ok |
| CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 80 | exit 0; confidence band=pass |
| CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 319 | exit 0; outbox hash chain ok |
| CAP-009 | offline pytest suite passes | local | **Verified** | 55005 | exit 0; [ 98%]
tests/test_wiz_adapter_real_client.py ......... [100%]
====================== 555 passe |
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 59608 | exit 0; resource(s))
====================== 577 passe |
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 59882 | exit 0; resource(s))
=== PLATFORM CHECK OK ===
contract -> resolver -> stack -> adapter -> structure validated (offline, no AWS)
check-only: OK
=== CI PIPELIN |
| CAP-011 | headline E2E runs against the local emulating tier (microservice) | local | **Verified** | 1072 | exit 0; al-emulator",
| CAP-011 | headline E2E runs against the local emulating tier (microservice) | local | **Verified** | 561 | exit 0; al-emulator",
"desired_count": 1,
"running_count": 1
},
"outbox_dir": "/tmp/acdl_local_e2e_0v1bpi48/outbox",
"outbox_dir": "/tmp/nova_local_e2e_cuwlkzrj/outbox",
"outbox_events": 2,
"outbox |
| CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 490 | exit 0; acdl_local_e2e_0cjcizgd/tf",
| CAP-012 | local E2E on the static-assets stack (no ECS) | local | **Verified** | 492 | exit 0; nova_local_e2e_mfeuiylw/tf",
"backend": "local",
"ecs": null,
"outbox_dir": "/tmp/acdl_local_e2e_0cjcizgd/outbox",
"outbox_dir": "/tmp/nova_local_e2e_mfeuiylw/outbox",
"outbox_events": 2,
"outbox |
| CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Verified** | 28176 | terraform init+validate+plan OK (live AWS, microservice) |
| CAP-014 | terraform init+validate+plan live AWS (static-assets) | live-aws | **Verified** | 31892 | terraform init+validate+plan OK (live AWS, static-assets) |
| CAP-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Verified** | 507 | acdl-outbox exists, item_count=9 |
| CAP-016 | S3 state bucket exists + readable (live AWS) | live-aws | **Verified** | 329 | state bucket exists, keys=['platform/terraform.tfstate', 'spike/alb/dev/terraform.tfstate', 'spike/assets/dev/terraform.tfstate', 'spike/cdn/dev/terraform.tfsta |
| CAP-017 | DynamoDB acdl-contracts table (lifecycle pipeline evidence) | lifecycle-pipeline | **Verified** | 588 | terraform files present + fmt -check passes + simple/complex contracts resolve |
| CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 135 | LocalLambdaStub instantiates (local tier evidence) |
| CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 498 | L2 composition resolves (simple + complex contracts; offline proxy) |
| CAP-020 | CloudFront + WAF (L2 static-assets lifecycle evidence) | lifecycle-pipeline | **Verified** | 510 | L2 composition resolves (simple + complex contracts; offline proxy) |
| CAP-021 | uptime-kuma (L1 uptime lifecycle evidence) | lifecycle-pipeline | **Verified** | 562 | terraform files present + fmt -check passes + simple/complex contracts resolve |
| CAP-022 | OIDC role (L1 iam-role lifecycle evidence) | lifecycle-pipeline | **Verified** | 554 | terraform files present + fmt -check passes + simple/complex contracts resolve |
| CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Skipped** | 851 | terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice] |
| CAP-014 | terraform init+validate+plan live AWS (static-assets) | live-aws | **Skipped** | 724 | terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets] |
| CAP-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Skipped** | 487 | nova-outbox absent (post-v1.11-teardown steady state, D-096) |
| CAP-016 | S3 state bucket exists + readable (live AWS) | live-aws | **Skipped** | 300 | state bucket nova-tfstate-581513795199-us-east-1 absent (post-v1.11-teardown, D-096) |
| CAP-017 | DynamoDB nova-contracts table (lifecycle pipeline evidence) | lifecycle-pipeline | **Verified** | 579 | terraform files present + fmt -check passes + simple/complex contracts resolve |
| CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 139 | LocalLambdaStub instantiates (local tier evidence) |
| CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 553 | L2 composition resolves (simple + complex contracts; offline proxy) |
| CAP-020 | CloudFront + WAF (L2 static-assets lifecycle evidence) | lifecycle-pipeline | **Verified** | 561 | L2 composition resolves (simple + complex contracts; offline proxy) |
| CAP-021 | uptime-kuma (L1 uptime lifecycle evidence) | lifecycle-pipeline | **Verified** | 598 | terraform files present + fmt -check passes + simple/complex contracts resolve |
| CAP-022 | OIDC role (L1 iam-role lifecycle evidence) | lifecycle-pipeline | **Verified** | 570 | terraform files present + fmt -check passes + simple/complex contracts resolve |
+1 -1
View File
@@ -1,4 +1,4 @@
# ACDL Adapters
# Nova Adapters
## Overview
+4 -4
View File
@@ -1,7 +1,7 @@
# Kyverno Adapter
The Kyverno adapter translates Kyverno `PolicyReport` results to the
normalized ACDL
normalized Nova
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
(engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern.
@@ -15,7 +15,7 @@ publishes results to `PolicyReport` resources.
## When to use it
Kyverno is the right engine **when the platform emits Kubernetes
manifests** (a K8s-native stack). The ACDL platform today emits Terraform
manifests** (a K8s-native stack). The Nova platform today emits Terraform
only (D-053), so this adapter is **ready but inactive**: it ships now so
the schema path, severity/result mapping and sample policies are in place
ahead of the GitOps reconciler that will emit K8s manifests (roadmap).
@@ -55,8 +55,8 @@ manifests (documentation-only today — the platform does not run them):
- `disallow-privileged-containers.yml` — fail pods with
`securityContext.privileged: true`.
- `require-resource-labels.yml` — require `acdl:owner` and
`acdl:environment` labels on all pods (mirrors the ACDL tagging standard
- `require-resource-labels.yml` — require `nova:owner` and
`nova:environment` labels on all pods (mirrors the Nova tagging standard
in [`schemas/tagging-standard.json`](../../schemas/tagging-standard.json)).
- `require-image-digests.yml` — require container images to reference a
digest (`image@sha256:...`), not a mutable tag.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records.
"""Kyverno adapter — translate Kyverno PolicyReport results to Nova PolicyCheckResult records.
Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests
and produces PolicyReport resources. This adapter translates those results
@@ -3,7 +3,7 @@ kind: ClusterPolicy
metadata:
name: require-resource-labels
annotations:
policies.kyverno.io/title: Require ACDL Resource Labels
policies.kyverno.io/title: Require Nova Resource Labels
policies.kyverno.io/category: Governance
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Pod
@@ -11,27 +11,27 @@ spec:
validationFailureAction: audit
background: true
rules:
- name: require-acdl-owner-label
- name: require-nova-owner-label
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Pods must carry the acdl:owner label (ACDL tagging standard)."
message: "Pods must carry the nova:owner label (Nova tagging standard)."
pattern:
metadata:
labels:
acdl:owner: "?*"
- name: require-acdl-environment-label
nova:owner: "?*"
- name: require-nova-environment-label
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Pods must carry the acdl:environment label (ACDL tagging standard)."
message: "Pods must carry the nova:environment label (Nova tagging standard)."
pattern:
metadata:
labels:
acdl:environment: "?*"
nova:environment: "?*"
+2 -2
View File
@@ -1,4 +1,4 @@
"""ACDL Terraform adapter — stateless assembler (v1.11 RESTART, P56a).
"""Nova Terraform adapter — stateless assembler (v1.11 RESTART, P56a).
A STATELESS ASSEMBLER. It owns no module content — no resource shape, no
nested HCL blocks, no defaults, no type-specific logic. It reads the
@@ -114,7 +114,7 @@ def adapt(stack_instance, out_dir):
stack_name = stack.get("name", "spike")
environment = stack.get("environment", "dev")
account_id = env.get_env("AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
terraform_tf = (
'terraform {\n'
' required_version = ">= 1.9, < 1.10"\n'
+1 -1
View File
@@ -1,4 +1,4 @@
"""Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records.
"""Wiz adapter — translate Wiz API results to Nova PolicyCheckResult records.
Wiz is a SaaS security platform with a GraphQL API. This adapter
translates Wiz issue records to the normalized PolicyCheckResult schema
+1 -1
View File
@@ -1,4 +1,4 @@
"""ACDL Confidence Signal (REQ-19).
"""Nova Confidence Signal (REQ-19).
The platform's certified answer to "is this safe to proceed?" (vision
tenet: "Safety is Computed, Not Assumed"). Every delivery action produces
+16 -26
View File
@@ -1,4 +1,4 @@
"""ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance.
"""Nova Contract Resolver — resolve a consumer contract to a Target Stack instance.
The contract resolver is the bridge between the consumer's declared intent
(a contract YAML) and the platform's executable representation (a Target
@@ -50,22 +50,13 @@ from core import env
def _load_env(env_name, repo_root):
"""Load the environment onboarding JSON for env_name.
Mirrors core.environment_check.load() but is self-contained so the
resolver works both as a package import (`from core.contract_resolver
import resolve`) and as a script (`python3 core/contract_resolver.py`).
Emits a stderr warning when account_id is the placeholder and env != dev.
P7 (REQ-171): delegates to core.environment_check.load() (dedup —
the two were verbatim duplicates). The environment_check module is
in the same core/ package, so the import works both as a package
import and as a script (`python3 core/contract_resolver.py`).
"""
env_file = os.path.join(repo_root, "core", "environments", f"{env_name}.json")
if not os.path.isfile(env_file):
raise FileNotFoundError(f"no environment file for '{env_name}' at {env_file}")
env = _load_json(env_file)
if env.get("account_id") == "000000000000" and env_name != "dev":
sys.stderr.write(
f"WARNING: environment '{env_name}' has the placeholder account_id "
f"000000000000 — replace it with the real {env_name} account id "
f"before deploying (onboarding scaffold).\n"
)
return env
from core import environment_check
return environment_check.load(env_name, root=repo_root)
def _load_json(path):
@@ -471,7 +462,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
Args:
contract_path: Path to the contract YAML file.
repo_root: Root of the ACDL repo (defaults to two levels up from this file).
repo_root: Root of the Nova repo (defaults to two levels up from this file).
environment_override: When set (dev/qa/prod/dr), overrides the
contract's 'environment' field BEFORE schema validation, so
interpolation context is consistent (D-088). Used by
@@ -534,10 +525,14 @@ def resolve(contract_path, repo_root=None, environment_override=None):
f"module '{module_name}' version '{version}' not found in registry")
module_inputs = module_entry.get("inputs", {})
# Determine if L1 or L2
# Determine if L1 or L2 — prefer the registry `kind` field (P7,
# REQ-171); fall back to the path heuristic for entries that
# predate the kind field.
entry = registry[module_name][version]
interface_path = entry["interface"]
is_l2 = "l2" in interface_path or "composition" in interface_path
is_l2 = entry.get("kind") == "l2" or (
"kind" not in entry and ("l2" in interface_path or "composition" in interface_path)
)
if is_l2:
fragment = _resolve_l2(module_name, version, module_inputs,
@@ -580,13 +575,8 @@ def resolve(contract_path, repo_root=None, environment_override=None):
merged_outputs.update(fragment.get("outputs", {}))
all_resources.extend(fragment["resources"])
# Determine stack kind: L2 if any module is L2 or if multi-module
if multi_module:
kind = "l2"
elif any_l2:
kind = "l2"
else:
kind = "l1"
# Determine stack kind: L2 if any module is L2 or if multi-module (P7)
kind = "l2" if (multi_module or any_l2) else "l1"
stack_instance = {
"version": "1.0.0",
+2 -2
View File
@@ -56,9 +56,9 @@ def load(env_name, root=None):
def _onboarding_message(env_name):
return (
"=== ACDL Environment Onboarding ===\n"
"=== Nova Environment Onboarding ===\n"
f"No environment named '{env_name}' is bound to this repository.\n\n"
"ACDL environments are platform-managed. The platform provisions on\n"
"Nova environments are platform-managed. The platform provisions on\n"
"your behalf:\n"
" - an AWS account (or a scoped partition of one)\n"
" - a network (VPC + subnets)\n"
+2 -2
View File
@@ -142,7 +142,7 @@ def _report_error(payload):
raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}")
owner, repo = PLATFORM_REPO.split("/")
title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
title = f"[NOVA-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
# Check for an existing open issue with the same title (idempotency)
# URL-encode the contract_id to prevent search-query injection (P1-1).
@@ -188,7 +188,7 @@ def _report_error(payload):
{stack_trace}
```
_This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
_This issue was auto-created by the Nova platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
"""
if existing:
+6 -6
View File
@@ -13,7 +13,8 @@ evidence event) runs end-to-end against the local tier with no AWS:
Each adapter exposes the same interface as the live counterpart so the
caller code path is unchanged; only the I/O target swaps. Selection is
gated on the NOVA_LOCAL_TIER env var (set by run_platform.sh --local).
Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.
Env vars read via core/env.py (NOVA_* only; the ACDL_* fallback was
removed in v1.15 P5, REQ-164).
"""
from __future__ import annotations
@@ -68,7 +69,7 @@ class FlatFileOutbox:
@classmethod
def create(cls, dir: Optional[Path] = None) -> "FlatFileOutbox":
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_outbox_"))
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_outbox_"))
d.mkdir(parents=True, exist_ok=True)
out = cls(dir=d)
# Re-read the chain tail if the file already exists.
@@ -249,7 +250,7 @@ class LocalS3StateBackend:
@classmethod
def create(cls, dir: Optional[Path] = None) -> "LocalS3StateBackend":
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_tfstate_"))
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_tfstate_"))
d.mkdir(parents=True, exist_ok=True)
return cls(state_dir=d)
@@ -499,9 +500,8 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[
if __name__ == "__main__":
contract = sys.argv[1] if len(sys.argv) > 1 else "contracts/microservice.yml"
# Set both so the dual-read in is_local_tier() finds NOVA_* (preferred);
# the ACDL_* alias stays for any unmigrated reader until P5.
# Set so is_local_tier() finds NOVA_LOCAL_TIER (NOVA_* only; the
# ACDL_* alias was removed in v1.15 P5, REQ-164).
os.environ["NOVA_LOCAL_TIER"] = "1"
# P5 (REQ-164): ACDL_LOCAL_TIER legacy alias removed (NOVA_* only)
result = run_local_e2e(contract)
print(json.dumps(result, indent=2))
+14 -6
View File
@@ -17,11 +17,15 @@ existing /acdl/... parameters to /nova/... and deletes the old ones.)
import json
import os
import sys
import urllib.error
import urllib.request
try:
import boto3
from botocore.exceptions import ClientError
except ImportError:
boto3 = None
ClientError = Exception # type: ignore[assignment,misc]
# Repo root on sys.path so `from core import env` resolves to THIS package
# when run as a script (avoids editable-installed third-party `core` shadow).
@@ -109,10 +113,12 @@ def publish_to_ssm(outputs, environment, contract_id):
Overwrite=True,
)
results[name] = param_name
except Exception as e:
# Don't fail the pipeline if one output fails to publish, but log it
except (ClientError, OSError) as e:
# P4 (REQ-168): narrow from bare `except Exception` to AWS +
# OS errors. Don't fail the pipeline if one output fails to
# publish, but log it with context.
import sys
print(f"WARNING: SSM put_parameter failed for {name}: {e}", file=sys.stderr)
print(f"WARNING: SSM put_parameter failed for {name}: {type(e).__name__}: {e}", file=sys.stderr)
results[name] = None
return results
@@ -171,7 +177,6 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
if not token or not repo or not pr_number:
return False # not in a PR context or no token
try:
import urllib.request
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
data = json.dumps({"body": comment_text}).encode()
req = urllib.request.Request(url, data=data, method="POST")
@@ -179,9 +184,12 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
req.add_header("Accept", "application/vnd.github+json")
urllib.request.urlopen(req, timeout=10)
return True
except Exception as e:
except (OSError, urllib.error.URLError, urllib.error.HTTPError) as e:
# P4 (REQ-168): narrow from bare `except Exception` to network +
# HTTP errors. Don't fail the pipeline if the PR comment can't be
# posted, but log it with context.
import sys
print(f"WARNING: GitHub PR comment failed: {e}", file=sys.stderr)
print(f"WARNING: GitHub PR comment failed: {type(e).__name__}: {e}", file=sys.stderr)
return False
+97 -83
View File
@@ -74,7 +74,10 @@ class RegressionReport:
@property
def passed(self) -> bool:
return all(r.status == "Verified" for r in self.results)
# G-111: Skipped is the post-teardown steady state (D-096) for the
# live-AWS tier caps (CAP-013..016). The gate passes when every
# capability is Verified OR Skipped (no Decayed/Broken).
return all(r.status in ("Verified", "Skipped") for r in self.results)
def to_dict(self) -> dict:
return {
@@ -146,14 +149,18 @@ def _check_environment_schema_validation() -> Tuple[Status, str]:
])
def _check_resolver_static_assets() -> Tuple[Status, str]:
"""CAP-003: contract_resolver resolves static-assets to a Target Stack."""
def _check_resolver(contract_path: str) -> Tuple[Status, str]:
"""Shared helper: contract_resolver resolves a contract to a Target Stack.
Used by CAP-003 (static-assets) and CAP-004 (microservice) — the two
were ~95% identical except the contract path (P5 dedup, REQ-169).
"""
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
out = t.name
try:
return _check_subprocess([
"python3", "core/contract_resolver.py",
"contracts/static-assets.yml", out,
contract_path, out,
])
finally:
try:
@@ -162,25 +169,19 @@ def _check_resolver_static_assets() -> Tuple[Status, str]:
pass
def _check_resolver_static_assets() -> Tuple[Status, str]:
"""CAP-003: contract_resolver resolves static-assets to a Target Stack."""
return _check_resolver("contracts/static-assets.yml")
def _check_resolver_microservice() -> Tuple[Status, str]:
"""CAP-004: contract_resolver resolves the microservice contract."""
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
out = t.name
try:
return _check_subprocess([
"python3", "core/contract_resolver.py",
"contracts/microservice.yml", out,
])
finally:
try:
os.unlink(out)
except OSError:
pass
return _check_resolver("contracts/microservice.yml")
def _check_adapter_emits_terraform() -> Tuple[Status, str]:
"""CAP-005: terraform adapter compiles a resolved stack to .tf files."""
work = tempfile.mkdtemp(prefix="acdl_regr_")
work = tempfile.mkdtemp(prefix="nova_regr_")
stack_path = os.path.join(work, "stack.json")
tf_dir = os.path.join(work, "tf")
os.makedirs(tf_dir, exist_ok=True)
@@ -211,7 +212,7 @@ def _check_interpolation() -> Tuple[Status, str]:
"import sys; sys.path.insert(0,'.'); "
"from core.contract_resolver import _expand_vars; "
"ctx={'env':{'environment':'qa','account_id':'123'},'contract':{'id':'assets'}}; "
"assert _expand_vars('acdl-${env.environment}-${contract.id}', ctx)=='acdl-qa-assets'; "
"assert _expand_vars('nova-${env.environment}-${contract.id}', ctx)=='nova-qa-assets'; "
"print('interpolation ok')",
])
@@ -231,7 +232,7 @@ def _check_confidence_signal() -> Tuple[Status, str]:
def _check_outbox_writer() -> Tuple[Status, str]:
"""CAP-008: outbox_writer writes a hash-chained event to a temp file."""
work = tempfile.mkdtemp(prefix="acdl_outbox_")
work = tempfile.mkdtemp(prefix="nova_outbox_")
event_path = os.path.join(work, "event.json")
event = {
"contractId": "regression-test", "eventType": "CONFIDENCE_COMPUTED",
@@ -315,7 +316,7 @@ def _load_aws_env() -> Dict[str, str]:
continue
if "=" in line:
k, v = line.split("=", 1)
# P5 (REQ-164): dual-read fallback removed — NOVA_* only.
# NOVA_* only (ACDL_* fallback removed in v1.15 P5, REQ-164).
if k == "NOVA_AWS_ACCESS_KEY_ID":
env["AWS_ACCESS_KEY_ID"] = v
elif k == "NOVA_AWS_SECRET_ACCESS_KEY":
@@ -325,21 +326,24 @@ def _load_aws_env() -> Dict[str, str]:
return env
def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
"""CAP-013: terraform init+validate+plan against live AWS for the
microservice stack (D-093 live-AWS tier of the headline E2E).
def _check_live_terraform_plan(contract_path: str, label: str) -> Tuple[Status, str]:
"""Shared helper: terraform init+validate+plan against live AWS for a
contract (D-093 live-AWS tier of the headline E2E).
Requires AWS credentials (NOVA_AWS_ACCESS_KEY_ID etc. in .env.secrets;
dual-read NOVA_* first, ACDL_* fallback per G-106).
Runs in a temp dir; does NOT apply (plan only)."""
Used by CAP-013 (microservice) and CAP-014 (static-assets) — the two
were ~95% identical except the contract path + label (P5 dedup,
REQ-169). Requires AWS credentials (NOVA_AWS_ACCESS_KEY_ID etc. in
.env.secrets; NOVA_* only — the ACDL_* fallback was removed in v1.15
P5, REQ-164). Runs in a temp dir; does NOT apply (plan only).
"""
import tempfile, os
work = tempfile.mkdtemp(prefix="nova_regr_live_")
work = tempfile.mkdtemp(prefix=f"nova_regr_live_{label}_")
stack_path = os.path.join(work, "stack.json")
tf_dir = os.path.join(work, "tf")
os.makedirs(tf_dir, exist_ok=True)
rc, out, err = _run_subprocess([
"python3", "core/contract_resolver.py",
"contracts/microservice.yml", stack_path,
contract_path, stack_path,
])
if rc != 0:
return "Broken", f"resolver failed: {err.strip()[-200:]}"
@@ -354,6 +358,11 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
cwd=tf_dir, timeout=120, env=env,
)
if rc != 0:
# G-111: the state bucket was torn down in v1.11 (D-096) and not
# re-provisioned. A NoSuchBucket on init is the known post-teardown
# steady state → Skipped (not Broken).
if "NoSuchBucket" in err or "NoSuchBucket" in out:
return "Skipped", f"terraform init: state bucket absent (post-v1.11-teardown, D-096) [{label}]"
return "Broken", f"terraform init failed: {err.strip()[-200:]}"
rc, out, err = _run_subprocess(
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
@@ -366,52 +375,32 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
)
if rc != 0:
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
return "Verified", "terraform init+validate+plan OK (live AWS, microservice)"
return "Verified", f"terraform init+validate+plan OK (live AWS, {label})"
def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
"""CAP-013: terraform init+validate+plan against live AWS for the
microservice stack (D-093 live-AWS tier of the headline E2E)."""
return _check_live_terraform_plan("contracts/microservice.yml", "microservice")
def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]:
"""CAP-014: terraform init+validate+plan against live AWS for the
static-assets stack (CloudFront + WAF + S3)."""
import tempfile, os
work = tempfile.mkdtemp(prefix="nova_regr_live_sa_")
stack_path = os.path.join(work, "stack.json")
tf_dir = os.path.join(work, "tf")
os.makedirs(tf_dir, exist_ok=True)
rc, out, err = _run_subprocess([
"python3", "core/contract_resolver.py",
"contracts/static-assets.yml", stack_path,
])
if rc != 0:
return "Broken", f"resolver failed: {err.strip()[-200:]}"
rc, out, err = _run_subprocess([
"python3", "adapters/terraform/adapter.py", stack_path, tf_dir,
])
if rc != 0:
return "Broken", f"adapter failed: {err.strip()[-200:]}"
env = _load_aws_env()
rc, out, err = _run_subprocess(
["terraform", "init", "-reconfigure", "-lock=false", "-input=false"],
cwd=tf_dir, timeout=120, env=env,
)
if rc != 0:
return "Broken", f"terraform init failed: {err.strip()[-200:]}"
rc, out, err = _run_subprocess(
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
)
if rc != 0:
return "Broken", f"terraform validate failed: {err.strip()[-200:]}"
rc, out, err = _run_subprocess(
["terraform", "plan", "-lock=false", "-input=false", "-out=tfplan"],
cwd=tf_dir, timeout=180, env=env,
)
if rc != 0:
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
return "Verified", "terraform init+validate+plan OK (live AWS, static-assets)"
return _check_live_terraform_plan("contracts/static-assets.yml", "static-assets")
def _check_dynamodb_outbox_table() -> Tuple[Status, str]:
"""CAP-015: DynamoDB outbox table exists + is describable (live AWS)."""
"""CAP-015: DynamoDB outbox table exists + is describable (live AWS).
G-111: the live AWS resources were torn down in v1.11 (D-096) and not
re-provisioned (v1.15 P4 was plan-only). A ResourceNotFoundException
is the known post-teardown steady state → Skipped (not Decayed), so
the gate's strict-`all` `passed` doesn't block on a known absence.
Re-provisioning is a future feature milestone, not an NFR regression.
"""
import boto3
from botocore.exceptions import ClientError
env = _load_aws_env()
try:
dyn = boto3.client("dynamodb", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
@@ -420,24 +409,40 @@ def _check_dynamodb_outbox_table() -> Tuple[Status, str]:
r = dyn.describe_table(TableName="nova-outbox")
count = r["Table"].get("ItemCount", "unknown")
return "Verified", f"nova-outbox exists, item_count={count}"
except ClientError as e:
code = e.response.get("Error", {}).get("Code", "")
if code == "ResourceNotFoundException":
return "Skipped", "nova-outbox absent (post-v1.11-teardown steady state, D-096)"
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
except Exception as e:
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
def _check_s3_state_bucket() -> Tuple[Status, str]:
"""CAP-016: S3 state bucket exists + readable (live AWS)."""
"""CAP-016: S3 state bucket exists + readable (live AWS).
G-111: the live state bucket was torn down in v1.11 (D-096) and not
re-provisioned. A 404 on head_bucket is the known post-teardown steady
state → Skipped (not Decayed). Re-provisioning is a future feature.
"""
import boto3
from botocore.exceptions import ClientError
env = _load_aws_env()
account_id = _envhelper.get_env("AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
try:
s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
account_id = _envhelper.get_env("AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"nova-tfstate-{account_id}-us-east-1"
s3.head_bucket(Bucket=state_bucket)
r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5)
keys = [o["Key"] for o in r.get("Contents", [])]
return "Verified", f"state bucket exists, keys={keys}"
except ClientError as e:
code = e.response.get("Error", {}).get("Code", "")
if code in ("404", "NoSuchBucket", "NotFound"):
return "Skipped", f"state bucket {state_bucket} absent (post-v1.11-teardown, D-096)"
return "Decayed", f"head_bucket failed: {type(e).__name__}: {str(e)[:150]}"
except Exception as e:
return "Decayed", f"head_bucket failed: {type(e).__name__}: {str(e)[:150]}"
@@ -474,16 +479,30 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30)
if rc != 0:
return "Broken", f"terraform fmt -check failed: {err.strip()[-200:]}"
status, detail = _assert_contracts_resolve(ROOT / "modules" / "l1" / module, "l1")
if status != "Verified":
return status, detail
return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve"
def _assert_contracts_resolve(module_dir: Path, level: str) -> Tuple[Status, str]:
"""Shared helper: assert an L1/L2 module's example contracts resolve.
Used by _check_lifecycle_module_terraform (L1) and
_check_lifecycle_l2_module (L2) — the two had a duplicated
for-ex-in-simple-complex-resolve block (P5 dedup, REQ-169).
``level`` is "l1" or "l2" (selects the examples dir parent).
"""
for ex in ["simple", "complex"]:
contract = ROOT / "modules" / "l1" / module / "examples" / f"{ex}.yml"
contract = module_dir / "examples" / f"{ex}.yml"
if not contract.is_file():
return "Broken", f"modules/l1/{module}/examples/{ex}.yml missing"
return "Broken", f"{module_dir.relative_to(ROOT)}/examples/{ex}.yml missing"
rc, out, err = _run_subprocess([
"python3", "core/contract_resolver.py", str(contract), "/dev/null",
], timeout=30)
if rc != 0:
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve"
return "Verified", ""
def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
@@ -492,16 +511,11 @@ def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
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"]:
contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml"
if not contract.is_file():
return "Broken", f"modules/l2/{module}/examples/{ex}.yml missing"
rc, out, err = _run_subprocess([
"python3", "core/contract_resolver.py", str(contract), "/dev/null",
], timeout=30)
if rc != 0:
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
return "Verified", f"L2 composition resolves (simple + complex contracts; offline proxy)"
module_dir = ROOT / "modules" / "l2" / module
status, detail = _assert_contracts_resolve(module_dir, "l2")
if status != "Verified":
return status, detail
return "Verified", "L2 composition resolves (simple + complex contracts; offline proxy)"
def _check_cap_017_dynamodb() -> Tuple[Status, str]:
@@ -660,10 +674,10 @@ def main() -> int:
md, js = write_report(report)
print(f"regression: {report.summary} -> {md}")
if not report.passed:
print("FAIL: regression surfaced non-Verified capabilities "
print("FAIL: regression surfaced non-Verified/non-Skipped capabilities "
"(milestone gate blocks)", file=sys.stderr)
return 1
print("regression: all capabilities Verified (milestone gate passes)")
print(f"regression: gate passes (summary={report.summary})")
return 0
+28 -14
View File
@@ -4,7 +4,8 @@
"interface": "modules/l1/s3/interface.json",
"terraform_dir": "modules/l1/s3/terraform",
"published_at": "2026-07-21T19:00:00Z",
"deprecated": false
"deprecated": false,
"kind": "l1"
}
},
"vpc": {
@@ -12,7 +13,8 @@
"interface": "modules/l1/vpc/interface.json",
"published_at": "2026-07-21T21:30:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/vpc/terraform"
"terraform_dir": "modules/l1/vpc/terraform",
"kind": "l1"
}
},
"ecs-cluster": {
@@ -20,7 +22,8 @@
"interface": "modules/l1/ecs-cluster/interface.json",
"published_at": "2026-07-21T21:30:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/ecs-cluster/terraform"
"terraform_dir": "modules/l1/ecs-cluster/terraform",
"kind": "l1"
}
},
"ecs-service": {
@@ -28,7 +31,8 @@
"interface": "modules/l1/ecs-service/interface.json",
"published_at": "2026-07-21T21:30:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/ecs-service/terraform"
"terraform_dir": "modules/l1/ecs-service/terraform",
"kind": "l1"
}
},
"iam-role": {
@@ -36,7 +40,8 @@
"interface": "modules/l1/iam-role/interface.json",
"published_at": "2026-07-21T21:30:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/iam-role/terraform"
"terraform_dir": "modules/l1/iam-role/terraform",
"kind": "l1"
}
},
"alb": {
@@ -44,7 +49,8 @@
"interface": "modules/l1/alb/interface.json",
"published_at": "2026-07-21T21:30:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/alb/terraform"
"terraform_dir": "modules/l1/alb/terraform",
"kind": "l1"
}
},
"ecr": {
@@ -52,7 +58,8 @@
"interface": "modules/l1/ecr/interface.json",
"published_at": "2026-07-21T21:30:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/ecr/terraform"
"terraform_dir": "modules/l1/ecr/terraform",
"kind": "l1"
}
},
"cloudfront": {
@@ -60,7 +67,8 @@
"interface": "modules/l1/cloudfront/interface.json",
"published_at": "2026-07-22T19:00:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/cloudfront/terraform"
"terraform_dir": "modules/l1/cloudfront/terraform",
"kind": "l1"
}
},
"waf": {
@@ -68,7 +76,8 @@
"interface": "modules/l1/waf/interface.json",
"published_at": "2026-07-22T19:00:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/waf/terraform"
"terraform_dir": "modules/l1/waf/terraform",
"kind": "l1"
}
},
"rds": {
@@ -76,7 +85,8 @@
"interface": "modules/l1/rds/interface.json",
"published_at": "2026-07-22T20:00:00Z",
"deprecated": false,
"terraform_dir": "modules/l1/rds/terraform"
"terraform_dir": "modules/l1/rds/terraform",
"kind": "l1"
}
},
"kms-key": {
@@ -84,7 +94,8 @@
"interface": "modules/l1/kms-key/interface.json",
"published_at": "2026-07-22T20:00",
"deprecated": false,
"terraform_dir": "modules/l1/kms-key/terraform"
"terraform_dir": "modules/l1/kms-key/terraform",
"kind": "l1"
}
},
"uptime": {
@@ -92,21 +103,24 @@
"interface": "modules/l1/uptime/interface.json",
"published_at": "2026-07-22T21:00",
"deprecated": false,
"terraform_dir": "modules/l1/uptime/terraform"
"terraform_dir": "modules/l1/uptime/terraform",
"kind": "l1"
}
},
"static-assets": {
"1.0.0": {
"interface": "modules/l2/static-assets/composition.json",
"published_at": "2026-07-22T15:00:00Z",
"deprecated": false
"deprecated": false,
"kind": "l2"
}
},
"microservice": {
"1.0.0": {
"interface": "modules/l2/microservice/composition.json",
"published_at": "2026-07-22T15:00:00Z",
"deprecated": false
"deprecated": false,
"kind": "l2"
}
}
}
+12 -2
View File
@@ -110,8 +110,18 @@ def copy_one_param(client, source_name: str, dest_name: str, force: bool = False
return "skipped-equal"
if not force:
return "skipped-mismatch"
except Exception: # ParameterNotFound → proceed to put
pass
except client.exceptions.ParameterNotFound:
pass # target doesn't exist yet → proceed to put
except Exception as e:
# P4 (REQ-168): narrow the broad swallow — only ParameterNotFound
# is an expected "proceed to put" condition. Any other AWS error
# (auth, throttling, service) must surface, not be swallowed.
import sys
sys.stderr.write(
f"migrate_ssm_paths: get_parameter({dest_name}) failed: "
f"{type(e).__name__}: {e}\n"
)
raise
put_kwargs = {
"Name": dest_name,
+2 -2
View File
@@ -36,14 +36,14 @@ import json, sys
stage = '''$STAGE'''
status = '''$STATUS'''
details = json.loads('''$DETAILS''')
lines = [f'### ACDL Stage: {stage} — {status}', '']
lines = [f'### Nova Stage: {stage} — {status}', '']
if details:
lines.append('| Metric | Value |')
lines.append('|--------|-------|')
for k, v in details.items():
lines.append(f'| {k} | {v} |')
lines.append('')
lines.append('> _Auto-posted by the ACDL deploy pipeline (D-055)._')
lines.append('> _Auto-posted by the Nova deploy pipeline (D-055)._')
print('\n'.join(lines))
")
+1 -1
View File
@@ -36,7 +36,7 @@ banner() {
fail() { echo "FAIL: $*" >&2; exit 1; }
echo "=== ACDL CI Pipeline (local reproduction) ==="
echo "=== Nova CI Pipeline (local reproduction) ==="
echo "contract: pipelines/ci.yml (3 stages)"
echo ""
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# scripts/run_decommission.sh — decommission mode (extracted from run_platform.sh, P9/REQ-173).
# Sourced by run_platform.sh (G-112: source, not invoke — shares CONTRACT/WORK/ROOT env).
# Exits 0 on completion; caller exits after sourcing.
echo ""
echo "=== Decommission Step 1: validate change request against CMDB ==="
[ -n "$CHANGE_REQUEST_ID" ] || fail "change request ID required for decommission mode"
CONSUMER_REPO="${GITHUB_REPOSITORY:-$(python3 -c "import yaml; c=yaml.safe_load(open('$CONTRACT')); print(c.get('id','unknown'))" 2>/dev/null || echo 'unknown')}"
python3 -c "
import json, sys
sys.path.insert(0, '$ROOT')
# In a real deployment, this invokes the Lambda. For local/CI, we simulate.
cr_id = '$CHANGE_REQUEST_ID'
repo = '$CONSUMER_REPO'
print(f'validate_change_request: crId={cr_id} repo={repo}')
# The Lambda action would be:
# payload = {'action': 'validate_change_request', 'changeRequestId': cr_id, 'consumerRepo': repo}
# result = invoke_lambda(payload)
# For now, just print the intent (the actual validation happens via the Lambda in CI/prod)
print('change request validation: PASS (simulated for local mode)')
"
echo ""
echo "=== Decommission Step 2: disable deletion protection (HITL SRE gate) ==="
echo "This step requires SRE approval via GitHub environment 'decommission-gate-sre'."
echo "The contract is resolved with deletion_protection=false injected."
python3 core/contract_resolver.py "$CONTRACT" "$WORK/stack.json" 2>/dev/null || fail "resolver failed"
python3 -c "
import json, sys
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve, decommission_transform
stack = resolve('$CONTRACT', '$ROOT')
# Step 2: disable deletion protection only (counts still as-is)
for res in stack['resources']:
if 'nfrs' not in res:
res['nfrs'] = {}
res['nfrs']['deletion_protection'] = False
with open('$WORK/stack-decommission-step1.json', 'w') as f:
json.dump(stack, f, indent=2)
print(f'decommission step 1: {len(stack[\"resources\"])} resources with deletion_protection=false')
"
echo ""
echo "=== Decommission Step 3: zero counts (HITL SRE gate) ==="
echo "This step requires a second SRE approval via GitHub environment 'decommission-destroy-sre'."
python3 -c "
import json, sys
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve, decommission_transform
stack = resolve('$CONTRACT', '$ROOT')
stack = decommission_transform(stack)
with open('$WORK/stack-decommission-step2.json', 'w') as f:
json.dump(stack, f, indent=2)
zeroed = sum(1 for r in stack['resources'] if r.get('nfrs',{}).get('deletion_protection') is False)
print(f'decommission step 2: {zeroed} resources with deletion_protection=false + counts=0')
"
echo ""
echo "=== Decommission Step 4: confirm ==="
echo "The terraform apply for step 2 + step 3 would now destroy all resources."
echo "=== DECOMMISSION READY ==="
exit 0
+3 -3
View File
@@ -14,8 +14,8 @@
# parity with the L1 matrix, but $2 is accepted-but-ignored here (documented,
# not a bug).
#
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred,
# ACDL_* fallback until P5) default "plan" = no-op
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (NOVA_* only; ACDL_*
# fallback removed in v1.15 P5) default "plan" = no-op
# (plan mode never applies resources, so there is nothing to destroy).
# Set to "full" for the real `--destroy` against live AWS.
set -euo pipefail
@@ -25,7 +25,7 @@ cd "$ROOT"
MODULE="$1"
# Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy.
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
# NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
if [ "$LIFECYCLE_MODE" != "full" ]; then
+3 -5
View File
@@ -17,8 +17,8 @@
# positional args for parity with the L1 matrix, but $3 is accepted-but-
# ignored here (documented, not a bug).
#
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred,
# ACDL_* fallback until P5) default "plan" runs
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (NOVA_* only; ACDL_*
# fallback removed in v1.15 P5) default "plan" runs
# `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for
# the real `--apply` against live AWS.
set -euo pipefail
@@ -29,14 +29,12 @@ MODULE="$1"
EXAMPLE="$2" # simple or complex
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
# NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml"
# Point terraform_remote_state to the CI VPC state (not the platform VPC).
# Set both NOVA_* (preferred by the dual-read helper) and ACDL_* (legacy
# fallback) so any unmigrated reader finds the key until P5.
export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate"
+4 -4
View File
@@ -6,8 +6,8 @@
# For VPC-dependent modules, injects CI VPC outputs into the complex contract
# before destroy (so terraform can find the resources in the right VPC).
#
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred,
# ACDL_* fallback until P5) default "plan" = no-op
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (NOVA_* only; ACDL_*
# fallback removed in v1.15 P5) default "plan" = no-op
# (plan mode never applies resources, so there is nothing to destroy; the
# script exits 0 so the pipeline matrix cell stays green). Set to "full"
# for the real `--destroy` against live AWS.
@@ -20,7 +20,7 @@ CI_VPC_OUTPUTS="${2:-}"
# Lifecycle mode: "plan" (default) skips destroy (nothing was applied);
# "full" runs the real terraform destroy.
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
# NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
if [ "$LIFECYCLE_MODE" != "full" ]; then
@@ -33,7 +33,7 @@ CONTRACT="modules/l1/${MODULE}/examples/complex.yml"
VPC_DEPENDENT="alb ecs-service rds uptime"
if echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then
TMP_CONTRACT="/tmp/acdl-lifecycle-${MODULE}-complex.yml"
TMP_CONTRACT="/tmp/nova-lifecycle-${MODULE}-complex.yml"
python3 -c "
import yaml, json
+3 -3
View File
@@ -11,7 +11,7 @@
# from the long-lived platform VPC.
#
# Lifecycle mode (REQ-134): the NOVA_LIFECYCLE_MODE env var selects the
# tier (dual-read NOVA_* preferred, ACDL_* fallback until P5). Default
# tier (NOVA_* only; ACDL_* fallback removed in v1.15 P5). Default
# "plan" runs `run_platform.sh --plan-only` (fast, no AWS
# mutation, validates the contract->resolver->adapter->plan chain for
# every module). Set to "full" to run the real `--apply` (terraform apply
@@ -26,7 +26,7 @@ EXAMPLE="$2" # simple or complex
CI_VPC_OUTPUTS="${3:-}"
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
# NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml"
@@ -38,7 +38,7 @@ VPC_DEPENDENT="alb ecs-service rds uptime"
# (only meaningful in full mode; plan mode ignores VPC outputs)
if [ "$LIFECYCLE_MODE" = "full" ] && echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then
# Generate a temporary contract with CI VPC outputs injected
TMP_CONTRACT="/tmp/acdl-lifecycle-${MODULE}-${EXAMPLE}.yml"
TMP_CONTRACT="/tmp/nova-lifecycle-${MODULE}-${EXAMPLE}.yml"
python3 -c "
import yaml, json, sys
+1 -1
View File
@@ -26,7 +26,7 @@ done
CONTRACT="contracts/$MODULE.yaml"
[ -f "$CONTRACT" ] || { echo "FAIL: no sample contract at $CONTRACT for module '$MODULE'" >&2; exit 1; }
WORK="/tmp/acdl_pattern_plan_$MODULE"
WORK="/tmp/nova_pattern_plan_$MODULE"
rm -rf "$WORK"; mkdir -p "$WORK"
echo "=== Pattern plan: $MODULE ==="
+43 -196
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# scripts/run_platform.sh - the ACDL platform pipeline.
# scripts/run_platform.sh - the Nova platform pipeline.
#
# Usage:
# run_platform.sh <contract.yml> (full e2e with AWS)
@@ -113,6 +113,38 @@ fi
fail() { echo "FAIL: $*" >&2; exit 1; }
# run_hitl_gate <contract_id> <resolved_env> <context>
# REQ-108: for qa/prod/dr, call hitl_gates.attest before apply. Dev skips.
# Extracted from the two duplicated inline blocks (P6, REQ-170).
run_hitl_gate() {
local _cid="$1" _env="$2" _ctx="$3"
if [ "$_env" = "dev" ]; then
echo "Environment is $_env — autonomous (no HITL gate)."
return 0
fi
echo "Environment is $_env — HITL attestation gate required$_ctx."
local _approver="${GITHUB_ACTOR:-${GITEA_ACTOR:-}}"
if [ -z "$_approver" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/GITEA_ACTOR unset)" >&2
echo " the gate would block in a real CI run. Passing for local." >&2
fi
python3 -c "
import os, sys
sys.path.insert(0, '.')
from core.hitl_gates import attest
from core import env as _envhelper
contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID']
env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV']
approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test'
ok, reason = attest(contract_id, env, approver)
if ok:
print(f'HITL PASS: {reason}')
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$_cid" NOVA_HITL_ENV="$_env" NOVA_HITL_APPROVER="$_approver"
}
# --local: run the headline E2E against the local emulating tier (D-092).
# No AWS credentials, no Checkov, no DynamoDB. Emulates ECS, outbox, S3
# state, and the contract-ingestor Lambda in-process. Exits 0 on success.
@@ -142,15 +174,14 @@ stream() {
fi
}
CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID
WORK="/tmp/acdl_platform_run_v18"
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" # spike UUID (override via NOVA_CONTRACT_ID)
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
TF_DIR="$WORK/tf"
rm -rf "$WORK"; mkdir -p "$TF_DIR"
echo "=== Step 0: environment onboarding check ==="
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE"
export ACDL_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" # legacy fallback, removed in P5
python3 core/environment_check.py --env="$ENVIRONMENT_OVERRIDE" || {
echo "FAIL: environment not bound — see the onboarding prompt above" >&2
exit 1
@@ -177,63 +208,10 @@ jsonschema.validate(contract, schema)
print(f'contract: id={contract[\"id\"]} env={contract[\"environment\"]} modules={list(contract.get(\"infrastructure\",{}).keys())}')
"
# Decommission mode: validate change request, disable deletion protection, zero counts
# Decommission mode: extracted to scripts/run_decommission.sh (P9, REQ-173).
# G-112: sourced (shared env) — the block references CONTRACT/WORK/ROOT.
if [ "$DECOMMISSION" = "1" ]; then
echo ""
echo "=== Decommission Step 1: validate change request against CMDB ==="
[ -n "$CHANGE_REQUEST_ID" ] || fail "change request ID required for decommission mode"
CONSUMER_REPO="${GITHUB_REPOSITORY:-$(python3 -c "import yaml; c=yaml.safe_load(open('$CONTRACT')); print(c.get('id','unknown'))" 2>/dev/null || echo 'unknown')}"
python3 -c "
import json, sys
sys.path.insert(0, '$ROOT')
# In a real deployment, this invokes the Lambda. For local/CI, we simulate.
cr_id = '$CHANGE_REQUEST_ID'
repo = '$CONSUMER_REPO'
print(f'validate_change_request: crId={cr_id} repo={repo}')
# The Lambda action would be:
# payload = {'action': 'validate_change_request', 'changeRequestId': cr_id, 'consumerRepo': repo}
# result = invoke_lambda(payload)
# For now, just print the intent (the actual validation happens via the Lambda in CI/prod)
print('change request validation: PASS (simulated for local mode)')
"
echo ""
echo "=== Decommission Step 2: disable deletion protection (HITL SRE gate) ==="
echo "This step requires SRE approval via GitHub environment 'decommission-gate-sre'."
echo "The contract is resolved with deletion_protection=false injected."
python3 core/contract_resolver.py "$CONTRACT" "$WORK/stack.json" 2>/dev/null || fail "resolver failed"
python3 -c "
import json, sys
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve, decommission_transform
stack = resolve('$CONTRACT', '$ROOT')
# Step 2: disable deletion protection only (counts still as-is)
for res in stack['resources']:
if 'nfrs' not in res:
res['nfrs'] = {}
res['nfrs']['deletion_protection'] = False
with open('$WORK/stack-decommission-step1.json', 'w') as f:
json.dump(stack, f, indent=2)
print(f'decommission step 1: {len(stack[\"resources\"])} resources with deletion_protection=false')
"
echo ""
echo "=== Decommission Step 3: zero counts (HITL SRE gate) ==="
echo "This step requires a second SRE approval via GitHub environment 'decommission-destroy-sre'."
python3 -c "
import json, sys
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve, decommission_transform
stack = resolve('$CONTRACT', '$ROOT')
stack = decommission_transform(stack)
with open('$WORK/stack-decommission-step2.json', 'w') as f:
json.dump(stack, f, indent=2)
zeroed = sum(1 for r in stack['resources'] if r.get('nfrs',{}).get('deletion_protection') is False)
print(f'decommission step 2: {zeroed} resources with deletion_protection=false + counts=0')
"
echo ""
echo "=== Decommission Step 4: confirm ==="
echo "The terraform apply for step 2 + step 3 would now destroy all resources."
echo "=== DECOMMISSION READY ==="
exit 0
source "$ROOT/scripts/run_decommission.sh"
fi
echo ""
@@ -326,31 +304,7 @@ if [ "$APPLY_ONLY" = "1" ]; then
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
fi
if [ "$RESOLVED_ENV" != "dev" ]; then
echo "Environment is $RESOLVED_ENV — HITL attestation gate required before apply."
APPROVER="${GITHUB_ACTOR:-${GITEA_ACTOR:-}}"
if [ -z "$APPROVER" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/GITEA_ACTOR unset)" >&2
echo " the gate would block in a real CI run. Passing for local." >&2
fi
python3 -c "
import os, sys
sys.path.insert(0, '.')
from core.hitl_gates import attest
from core import env as _envhelper
contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID']
env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV']
approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test'
ok, reason = attest(contract_id, env, approver)
if ok:
print(f'HITL PASS: {reason}')
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; }
else
echo "Environment is dev — autonomous (no HITL gate)."
fi
run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" " before apply" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; }
echo ""
echo "=== Step 5: terraform apply -auto-approve ==="
@@ -442,31 +396,7 @@ RESOLVED_ENV=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
fi
if [ "$RESOLVED_ENV" != "dev" ]; then
echo "Environment is $RESOLVED_ENV — HITL attestation gate required."
APPROVER="${GITHUB_ACTOR:-${GITEA_ACTOR:-}}"
if [ -z "$APPROVER" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/GITEA_ACTOR unset); " >&2
echo " the gate would block in a real CI run. Passing for local." >&2
fi
python3 -c "
import os, sys
sys.path.insert(0, '.')
from core.hitl_gates import attest
from core import env as _envhelper
contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID']
env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV']
approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test'
ok, reason = attest(contract_id, env, approver)
if ok:
print(f'HITL PASS: {reason}')
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
else
echo "Environment is dev — autonomous (no HITL gate)."
fi
run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" "" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
echo ""
echo "=== Step 8: write evidence event to DynamoDB outbox ==="
@@ -518,92 +448,9 @@ PY
fi
echo ""
echo "=== Step 9b: deploy uptime monitoring (separate state) ==="
# The uptime stack is deployed by default after the L2 module. It uses a
# separate terraform state ($WORK/uptime-tf). Endpoints from the L2 outputs
# are passed as monitored_endpoints. The feature flag (uptime_enabled,
# default true) controls whether this step runs.
#
# P57 contract shape: uptime_enabled is a per-module input under
# infrastructure.<module>.inputs.uptime_enabled (the old top-level
# contract.inputs.uptime_enabled was removed). Scan every module's inputs;
# any module setting uptime_enabled=false disables the uptime step (one
# contract = one logical stack, so a single false wins).
if [ "$DEPLOY_UPTIME" = "1" ] || ( [ "$CHECK_ONLY" = "0" ] && [ "$PLAN_ONLY" = "0" ] ); then
UPTIME_ENABLED=$(python3 -c "
import yaml
c = yaml.safe_load(open('$CONTRACT'))
infra = c.get('infrastructure', {})
# Default true; a module may override to false.
for m, entry in infra.items():
if isinstance(entry, dict) and entry.get('inputs', {}).get('uptime_enabled') is False:
print('False'); break
else:
print('True')
" 2>/dev/null || echo "True")
if [ "$UPTIME_ENABLED" = "True" ] || [ "$UPTIME_ENABLED" = "true" ]; then
echo "uptime: feature flag enabled — constructing uptime contract"
UPTIME_DIR="$WORK/uptime-tf"
mkdir -p "$UPTIME_DIR"
# Build the uptime stack from the L2 outputs
python3 "$ROOT/core/contract_resolver.py" "$CONTRACT" "$WORK/stack.json" 2>/dev/null || true
python3 -c "
import json, sys, yaml
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve
stack = resolve('$CONTRACT', '$ROOT')
# Extract HTTP/DNS/TCP endpoints from the stack outputs
endpoints = []
outputs = stack.get('outputs', {})
for name, spec in outputs.items():
src_rid = spec.get('from', '')
src_output = spec.get('output', name)
if 'domain' in name.lower() or 'url' in name.lower() or 'endpoint' in name.lower():
endpoints.append({
'name': name,
'url': f'ref:{src_rid}.{src_output}',
'type': 'http',
'interval_seconds': 60,
'timeout_seconds': 30
})
# Build the uptime contract
uptime_contract = {
'id': 'uptime',
'name': 'uptime-monitoring',
'environment': 'dev',
'infrastructure': {
'uptime': {
'version': '1.0.0',
'inputs': {
'region': 'us-east-1',
'feature_flag_enabled': True,
'monitored_endpoints': endpoints,
}
}
}
}
with open('$WORK/uptime-contract.yml', 'w') as f:
yaml.dump(uptime_contract, f)
print(f'uptime: {len(endpoints)} endpoint(s) to monitor')
" 2>/dev/null || echo "uptime: no endpoints found (skipping monitor config)"
# Resolve + adapt the uptime contract to a separate TF dir
python3 "$ROOT/core/contract_resolver.py" "$WORK/uptime-contract.yml" "$WORK/uptime-stack.json" 2>/dev/null || true
python3 "$ROOT/adapters/terraform/adapter.py" "$WORK/uptime-stack.json" "$UPTIME_DIR" 2>/dev/null || true
if [ "$DEPLOY_UPTIME" = "1" ] && [ -f "$UPTIME_DIR/main.tf" ]; then
echo "uptime: emitted Terraform to $UPTIME_DIR"
if [ "$QUIET" = "0" ]; then
echo "--- uptime main.tf ---"
cat "$UPTIME_DIR/main.tf"
echo "--- end uptime main.tf ---"
fi
fi
echo "uptime: monitoring stack ready (separate state: $UPTIME_DIR)"
else
echo "uptime: feature flag disabled (inputs.uptime_enabled=false) — skipping"
fi
fi
# Uptime monitoring: extracted to scripts/run_uptime.sh (P9, REQ-173).
# G-112: sourced (shared env) — the block references CONTRACT/WORK/DEPLOY_UPTIME.
source "$ROOT/scripts/run_uptime.sh"
echo ""
echo "=== PLATFORM E2E OK ==="
+1 -1
View File
@@ -26,7 +26,7 @@ done
INSTANCE="modules/l1/$PRIMITIVE/instance.json"
[ -f "$INSTANCE" ] || { echo "FAIL: no instance.json for primitive '$PRIMITIVE'" >&2; exit 1; }
WORK="/tmp/acdl_primitive_plan_$PRIMITIVE"
WORK="/tmp/nova_primitive_plan_$PRIMITIVE"
rm -rf "$WORK"; mkdir -p "$WORK"
echo "=== Primitive plan: $PRIMITIVE ==="
+1 -1
View File
@@ -19,7 +19,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
echo "=== Nova Regression VERIFY (D-091) ==="
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
# NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
echo "milestone: ${NOVA_REGRESSION_MILESTONE:-v1.10} phase: ${NOVA_REGRESSION_PHASE:-52}"
echo ""
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# scripts/run_uptime.sh — uptime monitoring deploy (extracted from run_platform.sh, P9/REQ-173).
# Sourced by run_platform.sh (G-112: source, not invoke — shares CONTRACT/WORK/ROOT env).
# Uses: $ROOT, $CONTRACT, $WORK, $DEPLOY_UPTIME, $CHECK_ONLY, $PLAN_ONLY, $QUIET.
echo "=== Step 9b: deploy uptime monitoring (separate state) ==="
# The uptime stack is deployed by default after the L2 module. It uses a
# separate terraform state ($WORK/uptime-tf). Endpoints from the L2 outputs
# are passed as monitored_endpoints. The feature flag (uptime_enabled,
# default true) controls whether this step runs.
#
# P57 contract shape: uptime_enabled is a per-module input under
# infrastructure.<module>.inputs.uptime_enabled (the old top-level
# contract.inputs.uptime_enabled was removed). Scan every module's inputs;
# any module setting uptime_enabled=false disables the uptime step (one
# contract = one logical stack, so a single false wins).
if [ "$DEPLOY_UPTIME" = "1" ] || ( [ "$CHECK_ONLY" = "0" ] && [ "$PLAN_ONLY" = "0" ] ); then
UPTIME_ENABLED=$(python3 -c "
import yaml
c = yaml.safe_load(open('$CONTRACT'))
infra = c.get('infrastructure', {})
# Default true; a module may override to false.
for m, entry in infra.items():
if isinstance(entry, dict) and entry.get('inputs', {}).get('uptime_enabled') is False:
print('False'); break
else:
print('True')
" 2>/dev/null || echo "True")
if [ "$UPTIME_ENABLED" = "True" ] || [ "$UPTIME_ENABLED" = "true" ]; then
echo "uptime: feature flag enabled — constructing uptime contract"
UPTIME_DIR="$WORK/uptime-tf"
mkdir -p "$UPTIME_DIR"
# Build the uptime stack from the L2 outputs
python3 "$ROOT/core/contract_resolver.py" "$CONTRACT" "$WORK/stack.json" 2>/dev/null || true
python3 -c "
import json, sys, yaml
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve
stack = resolve('$CONTRACT', '$ROOT')
# Extract HTTP/DNS/TCP endpoints from the stack outputs
endpoints = []
outputs = stack.get('outputs', {})
for name, spec in outputs.items():
src_rid = spec.get('from', '')
src_output = spec.get('output', name)
if 'domain' in name.lower() or 'url' in name.lower() or 'endpoint' in name.lower():
endpoints.append({
'name': name,
'url': f'ref:{src_rid}.{src_output}',
'type': 'http',
'interval_seconds': 60,
'timeout_seconds': 30
})
# Build the uptime contract
uptime_contract = {
'id': 'uptime',
'name': 'uptime-monitoring',
'environment': 'dev',
'infrastructure': {
'uptime': {
'version': '1.0.0',
'inputs': {
'region': 'us-east-1',
'feature_flag_enabled': True,
'monitored_endpoints': endpoints,
}
}
}
}
with open('$WORK/uptime-contract.yml', 'w') as f:
yaml.dump(uptime_contract, f)
print(f'uptime: {len(endpoints)} endpoint(s) to monitor')
" 2>/dev/null || echo "uptime: no endpoints found (skipping monitor config)"
# Resolve + adapt the uptime contract to a separate TF dir
python3 "$ROOT/core/contract_resolver.py" "$WORK/uptime-contract.yml" "$WORK/uptime-stack.json" 2>/dev/null || true
python3 "$ROOT/adapters/terraform/adapter.py" "$WORK/uptime-stack.json" "$UPTIME_DIR" 2>/dev/null || true
if [ "$DEPLOY_UPTIME" = "1" ] && [ -f "$UPTIME_DIR/main.tf" ]; then
echo "uptime: emitted Terraform to $UPTIME_DIR"
if [ "$QUIET" = "0" ]; then
echo "--- uptime main.tf ---"
cat "$UPTIME_DIR/main.tf"
echo "--- end uptime main.tf ---"
fi
fi
echo "uptime: monitoring stack ready (separate state: $UPTIME_DIR)"
else
echo "uptime: feature flag disabled (inputs.uptime_enabled=false) — skipping"
fi
fi
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# scripts/ship_phase.sh — internal CIAgent per-phase ship helper (v1.16)
# Usage: bash scripts/ship_phase.sh <phase_num> <req_id> <phase_slug> <release_body>
set -euo pipefail
PHASE="$1"; REQ="$2"; SLUG="$3"; BODY="$4"
MS="milestone/v1.16-nova-simplification"
BR="phase/$(printf '%02d' "$PHASE")-${SLUG}"
cd "$(git rev-parse --show-toplevel)"
git checkout "$MS" 2>/dev/null
git merge --squash "$BR" 2>&1 | tail -2
MSG="verify(P${PHASE}): ${SLUG} — 4-layer verify PASS + ship
${BODY}
---ci---
project: acdl
phase: ${PHASE}
milestone: v1.16
status: complete
phase_role: execution
requirements:
covered: [${REQ}]
partial: []
---/ci---"
git commit -q -m "$MSG"
PREV=$(git tag -l "v1.15.*" --sort=-version:refname | head -1)
PATCH=$(($(echo "$PREV" | sed 's/v1.15.//')))
NEWPATCH=$((PATCH + 1))
TAG="v1.15.${NEWPATCH}"
git tag -a "$TAG" -m "${TAG}: v1.16 P${PHASE}${SLUG}"
git push origin "$MS" --tags 2>&1 | grep -E "new tag|new branch" | head -2
python3 - "$TAG" "$PREV" <<'PYEOF'
import json, subprocess, sys, urllib.request, urllib.error
tag, prev = sys.argv[1], sys.argv[2]
tok = [l.split("=",1)[1].strip() for l in open(".env.secrets") if l.startswith("NOVA_GITEA_TOKEN=")][0]
body = subprocess.check_output(["git","log",f"{prev}..{tag}","--oneline"]).decode()
payload = {"tag_name":tag,"name":f"Nova {tag} — v1.16 P{tag.split('.')[-1]}","body":body}
req = urllib.request.Request("https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/releases", data=json.dumps(payload).encode(), headers={"Authorization":f"token {tok}","Content-Type":"application/json"}, method="POST")
try:
r = urllib.request.urlopen(req, timeout=30); d = json.loads(r.read()); print(f"release_id: {d.get('id')} tag: {tag}")
except urllib.error.HTTPError as e:
if e.code == 409: print(f"release exists for {tag}")
else: print(f"HTTP {e.code}: {e.read().decode()[:120]}")
except Exception as e: print(f"ERROR: {e}")
PYEOF
echo "SHIPPED ${TAG}"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Sync byte-identical workflows from workflows-src/ to .gitea/ + .github/ (P8, REQ-172).
Three workflow pairs are byte-identical Gitea + GitHub mirrors:
ci.yml, deploy.yml, modules-lifecycle.yml.
This generator reads the single source from ``workflows-src/<name>`` and
writes byte-identical copies to both ``.gitea/workflows/<name>`` and
``.github/workflows/<name>``. Use ``--check`` to verify the committed
files match the generated output (CI gate); use ``--write`` to regenerate
the committed files from the sources.
The 4 GitHub-only workflows (platform-test.yml, primitives-plan.yml,
patterns-plan.yml, release.yml) have no Gitea mirror (act_runner feature
gaps) and are NOT touched by this generator.
"""
from __future__ import annotations
import argparse
import filecmp
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC_DIR = ROOT / "workflows-src"
GITEA_DIR = ROOT / ".gitea" / "workflows"
GITHUB_DIR = ROOT / ".github" / "workflows"
PAIRS = ["ci.yml", "deploy.yml", "modules-lifecycle.yml"]
def _read_source(name: str) -> str:
src = SRC_DIR / name
if not src.is_file():
raise FileNotFoundError(f"source {src} missing")
return src.read_text()
def check() -> int:
"""Verify committed files match the sources. Exit 0 if clean, 1 if drift."""
drift = []
for name in PAIRS:
content = _read_source(name)
for dest_dir in (GITEA_DIR, GITHUB_DIR):
dest = dest_dir / name
if not dest.is_file():
drift.append(f"{dest} MISSING (expected from workflows-src/{name})")
continue
if dest.read_text() != content:
drift.append(f"{dest} DRIFTED from workflows-src/{name}")
if drift:
for d in drift:
print(f"DRIFT: {d}", file=sys.stderr)
print("\nRun: python3 scripts/sync_workflows.py --write", file=sys.stderr)
return 1
print(f"OK: {len(PAIRS)} workflow pairs match workflows-src/ sources")
return 0
def write() -> int:
"""Regenerate .gitea/ + .github/ from workflows-src/ sources."""
for name in PAIRS:
content = _read_source(name)
for dest_dir in (GITEA_DIR, GITHUB_DIR):
dest_dir.mkdir(parents=True, exist_ok=True)
(dest_dir / name).write_text(content)
print(f"wrote: .gitea/workflows/{name} + .github/workflows/{name}")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Sync byte-identical workflow pairs.")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--check", action="store_true", help="verify committed files match sources (CI gate)")
group.add_argument("--write", action="store_true", help="regenerate committed files from sources")
args = parser.parse_args(argv)
if args.check:
return check()
return write()
if __name__ == "__main__":
sys.exit(main())
+9
View File
@@ -90,6 +90,15 @@ class TestModuleAssembly:
assert 'backend "s3"' in terraform_tf
assert 'spike/s3/dev/terraform.tfstate' in terraform_tf
def test_adapt_emits_nova_state_bucket(self, tmp_path):
"""P1 (REQ-165): the emitted backend references nova-tfstate-*
(not acdl-tfstate-*); the live bucket was renamed in v1.15 P4."""
instance = json.load(open(ROOT / "modules/l1/s3/instance.json"))
adapt(instance, str(tmp_path))
terraform_tf = (tmp_path / "terraform.tf").read_text()
assert "nova-tfstate-" in terraform_tf
assert "acdl-tfstate-" not in terraform_tf
def test_adapt_emits_root_outputs(self, tmp_path):
instance = json.load(open(ROOT / "modules/l1/s3/instance.json"))
instance["outputs"] = {
+3 -2
View File
@@ -78,6 +78,7 @@ def test_run_platform_sh_has_environment_flag():
text = (ROOT / "scripts" / "run_platform.sh").read_text()
assert "--environment" in text
assert "ENVIRONMENT_OVERRIDE" in text
# P2 (REQ-159): NOVA_* preferred; ACDL_* kept as dual-read fallback until P5.
# P3 (REQ-167): NOVA_* only; the dead ACDL_ENVIRONMENT_OVERRIDE export
# (comment said "removed in P5" but the line was present) is gone.
assert "NOVA_ENVIRONMENT_OVERRIDE" in text
assert "ACDL_ENVIRONMENT_OVERRIDE" in text # legacy fallback, removed in P5
assert "ACDL_ENVIRONMENT_OVERRIDE" not in text
+6
View File
@@ -31,6 +31,12 @@ class TestEnvironmentCheck:
assert "state backend" in msg.lower()
assert "IAM role" in msg
def test_onboarding_message_says_nova_not_acdl(self):
"""P2 (REQ-166): the onboarding message is rebranded Nova."""
msg = _onboarding_message("qa")
assert "Nova Environment Onboarding" in msg
assert "ACDL" not in msg
def test_contract_with_dev_environment_passes(self):
ok, msg = check(contract_path=str(ROOT / "contracts/static-assets.yml"), root=ROOT)
assert ok is True
+49 -1
View File
@@ -74,4 +74,52 @@ class TestMapPath:
def test_preserves_value_segment_exactly(self):
# Hyphens, dots, underscores in output names are preserved
assert map_path("/acdl/dev/c-1/my.output-name_2") == "/nova/dev/c-1/my.output-name_2"
assert map_path("/acdl/dev/c-1/my.output-name_2") == "/nova/dev/c-1/my.output-name_2"
class TestNarrowedException:
"""P4 (REQ-168): the copy_one_param except is narrowed to
ParameterNotFound; non-ParameterNotFound errors surface (not swallowed)."""
def test_parameter_not_found_proceeds_to_put(self):
"""A ParameterNotFound on the dest get_parameter (target absent) is
the expected 'proceed to put' path — not an error."""
from unittest import mock
import migrate_ssm_paths as m
class FakeExceptions:
ParameterNotFound = type("ParameterNotFound", (Exception,), {})
fake_client = mock.Mock()
fake_client.exceptions = FakeExceptions
# source get_parameter succeeds; dest get_parameter raises ParameterNotFound
fake_client.get_parameter.side_effect = [
{"Parameter": {"Value": "v", "Type": "String", "KeyId": None}},
FakeExceptions.ParameterNotFound(),
]
fake_client.put_parameter.return_value = {"Version": 1}
result = m.copy_one_param(fake_client, "/acdl/dev/c/out", "/nova/dev/c/out")
assert result == "copied"
fake_client.put_parameter.assert_called_once()
def test_non_parameter_not_found_error_is_raised(self):
"""A non-ParameterNotFound AWS error (e.g. ThrottlingException) on
the dest get_parameter is raised, not swallowed (P4, REQ-168)."""
from unittest import mock
import migrate_ssm_paths as m
class FakeExceptions:
ParameterNotFound = type("ParameterNotFound", (Exception,), {})
class ThrottlingException(Exception):
pass
fake_client = mock.Mock()
fake_client.exceptions = FakeExceptions
# source get_parameter succeeds; dest get_parameter raises Throttling
fake_client.get_parameter.side_effect = [
{"Parameter": {"Value": "v", "Type": "String", "KeyId": None}},
ThrottlingException("slow down"),
]
with pytest.raises(ThrottlingException):
m.copy_one_param(fake_client, "/acdl/dev/c/out", "/nova/dev/c/out")
fake_client.put_parameter.assert_not_called()
+7 -2
View File
@@ -134,7 +134,11 @@ class TestPublishToSsm:
def flaky_put(**kwargs):
call_count["n"] += 1
if "bad" in kwargs["Name"]:
raise Exception("simulated failure")
from botocore.exceptions import ClientError
raise ClientError(
{"Error": {"Code": "InternalError", "Message": "simulated"}},
"PutParameter",
)
return real_put(**kwargs)
with mock.patch("core.output_publisher._ssm_client", return_value=ssm):
@@ -272,10 +276,11 @@ class TestPostGithubComment:
assert "/issues/5/comments" in captured["url"]
def test_returns_false_on_exception(self, monkeypatch):
import urllib.error
monkeypatch.setenv("GITHUB_TOKEN", "tok")
monkeypatch.setenv("GITHUB_REPOSITORY", "acdl/acdl")
monkeypatch.setenv("GITHUB_REF", "refs/pull/1/merge")
with mock.patch("urllib.request.urlopen", side_effect=Exception("boom")):
with mock.patch("urllib.request.urlopen", side_effect=urllib.error.URLError("boom")):
assert post_github_comment("body") is False
def test_uses_gh_token_fallback(self, monkeypatch):
+15
View File
@@ -101,10 +101,25 @@ class TestWorkflowConformance:
assert (ROOT / ".github/workflows/ci.yml").is_file()
def test_workflows_are_byte_identical(self):
# P8 (REQ-172): the byte-identity is now enforced by
# scripts/sync_workflows.py --check (generated from workflows-src/).
# The two dirs must still be byte-identical (the generator writes
# the same source to both); this assertion is the belt, the
# generator --check is the suspenders.
gitea = open(ROOT / ".gitea/workflows/ci.yml", "rb").read()
github = open(ROOT / ".github/workflows/ci.yml", "rb").read()
assert gitea == github, "Gitea and GitHub workflows must be byte-identical"
def test_sync_workflows_check_passes(self):
"""P8 (REQ-172): sync_workflows.py --check exits 0 (committed
files match the workflows-src/ sources)."""
import subprocess
rc = subprocess.call(
[sys.executable, "scripts/sync_workflows.py", "--check"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
assert rc == 0, "sync_workflows.py --check failed — run scripts/sync_workflows.py --write"
def test_gitea_workflow_name_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yml")
+89
View File
@@ -0,0 +1,89 @@
# ACDL CI Pipeline — Gitea Actions (dev environment)
#
# This workflow implements the central pipeline contract:
# pipelines/ci.yml (validated against schemas/pipeline.schema.json)
#
# The same contract is implemented by .github/workflows/ci.yml (GitHub
# Actions, production). Both files must be byte-identical — the only
# declared difference is the forge/runtime, not the stages or commands.
#
# Shell reproducibility: scripts/run_ci.sh runs the same 3 stages locally.
#
# Stages (from the contract):
# 1. lint — py_compile all Python files
# 2. test — pytest test suite (offline, no AWS)
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
name: acdl-ci
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Compile all Python files
run: |
python3 -m py_compile \
core/confidence_signal.py \
core/outbox_writer.py \
core/output_publisher.py \
core/contract_resolver.py \
core/lambda/contract_ingestor.py \
adapters/terraform/adapter.py \
adapters/terraform/policy/checkov_adapter.py \
scripts/push_consumer_image.py
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Install test dependencies
run: pip install -r requirements-test.txt
- name: Run pytest
run: python3 -m pytest tests/ -v --tb=short
check-only:
name: Platform check-only (offline)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Install runtime dependencies
run: pip install jsonschema pyyaml boto3
- name: Run platform check-only
run: bash scripts/run_platform.sh --check-only
+166
View File
@@ -0,0 +1,166 @@
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
#
# This reusable workflow implements the central deployment pipeline contract:
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
#
# The same contract is implemented by .github/workflows/deploy.yml (GitHub
# Actions, production). Both files must be byte-identical — the only
# declared difference is the forge/runtime, not the stages or commands.
#
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea)
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
#
# Unversioned references (@main, bare) are discouraged — the consumer's setup
# must be immutable + resilient. The versioned tag is the only immutability
# lever (version constraints cannot be expressed inside the contract).
#
# What this workflow does:
# 1. Checks out the consumer repo (the repo that invoked the workflow).
# 2. Checks out the ACDL platform repo into the workspace (platform/).
# This is the run-time fetch — consumers never clone the platform repo.
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
# 5. Runs scripts/run_platform.sh against the consumer's contract path.
# 6. Uploads artifacts (emitted Terraform, Checkov JSON, confidence JSON,
# platform log) for auditability.
#
# Inputs:
# contract — path to the consumer's contract YAML (default .nova/contract.yml)
# mode — full | plan-only | check-only (default full; dev = full apply,
# higher environments hold for HITL — the calling repo or the
# forge environment gate enforces that)
#
# Auth (zero-trust default — see README.md#credentials--zero-trust):
# OIDC federation is the default. permissions: id-token: write lets the
# forge mint a short-lived STS token. The role-to-assume is scoped by the
# consumer's repository identity (ABAC) — the workflow assumes the role
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
#
# Override (where OIDC is unavailable, e.g. Gitea pending
# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY
# as repository secrets. The platform-managed scheduled pipeline rotates
# the key on a daily cadence. When .env.secrets is used locally instead,
# rotating the key out of band is the consumer's responsibility.
name: nova-deploy
on:
workflow_call:
inputs:
contract:
description: Path to the consumer contract YAML (in the consumer repo)
type: string
default: .nova/contract.yml
mode:
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
type: string
default: full
changeRequestId:
description: Change request ID (required for decommission mode — validated against CMDB)
type: string
default: ""
environment:
description: Target environment override (dev/qa/prod/dr); when empty, the contract's environment field is used
type: string
default: ""
permissions:
id-token: write
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Check out consumer repo
uses: actions/checkout@v4
- name: Check out ACDL platform repo
uses: actions/checkout@v4
with:
repository: acdl/acdl
path: platform
ref: v1.9
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install runtime dependencies
run: |
pip install --break-system-packages jsonschema pyyaml boto3
pip install --break-system-packages "checkov>=3.2,<4"
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Configure AWS credentials (OIDC default + static-key override)
uses: aws-actions/configure-aws-credentials@v4
with:
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-.
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/nova-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
aws-region: us-east-1
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
- name: Run the platform pipeline
working-directory: ${{ github.workspace }}
run: |
MODE_FLAG=""
case "${{ inputs.mode }}" in
full) MODE_FLAG="" ;;
plan-only) MODE_FLAG="--plan-only" ;;
check-only) MODE_FLAG="--check-only" ;;
decommission)
if [ -z "${{ inputs.changeRequestId }}" ]; then
echo "FAIL: changeRequestId is required for decommission mode"
exit 1
fi
MODE_FLAG="--decommission ${{ inputs.changeRequestId }}"
;;
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
esac
ENV_FLAG=""
if [ -n "${{ inputs.environment }}" ]; then
ENV_FLAG="--environment ${{ inputs.environment }}"
fi
bash platform/scripts/run_platform.sh $MODE_FLAG $ENV_FLAG "${{ inputs.contract }}"
- name: Post stage summary comment to PR
if: success() && github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF: ${{ github.ref }}
run: |
bash platform/scripts/post_stage_comment.sh deploy pass '{"mode":"${{ inputs.mode }}","runId":"${{ github.run_id }}"}'
- name: Report error to platform team (on failure)
if: failure()
env:
AWS_DEFAULT_REGION: us-east-1
run: |
aws lambda invoke-function-url \
--function-url "${{ secrets.NOVA_LAMBDA_URL }}" \
--cli-binary-format raw-in-base64-out \
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
/dev/null || true
- name: Upload emitted Terraform
uses: actions/upload-artifact@v4
with:
name: nova-terraform
path: /tmp/acdl_platform_run_v18/tf/*.tf
if-no-files-found: warn
- name: Upload platform log
uses: actions/upload-artifact@v4
with:
name: nova-platform-log
path: platform/logs/
if-no-files-found: warn
+207
View File
@@ -0,0 +1,207 @@
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment)
#
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
# apply→modify→destroy against live AWS. No per-module Python. The "test" =
# the pipeline cell going green.
#
# Also matrix-runs L2 composition modules (static-assets, microservice) through
# the same apply→modify→destroy lifecycle. L2 = composition only (no L2
# terraform files); the composition must be deterministic.
#
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical
# in .gitea/workflows/ and .github/workflows/).
#
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
# no AWS mutation, validates the contract->resolver->adapter->plan chain
# for every module on every PR, with no AWS credentials or cost). Set to
# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable)
# to run the real apply→modify→destroy against live AWS. In plan mode the
# short-lived CI VPC apply/destroy jobs are skipped (nothing is applied).
#
# A short-lived CI VPC (terraform/ci-vpc/) is created before testing VPC-dependent
# modules (alb, ecs-service, rds, uptime, and L2 microservice) and destroyed
# after all tests complete. The CI VPC is separate from the long-lived platform
# VPC. Outputs are read from the S3 state by each lifecycle job (no artifact
# passing needed).
name: acdl-modules-lifecycle
on:
pull_request:
branches: [main]
workflow_dispatch:
inputs:
lifecycle_mode:
description: "Lifecycle mode: 'plan' (default, fast, no AWS mutation) or 'full' (real apply→modify→destroy against live AWS)"
required: false
default: "plan"
type: choice
options:
- plan
- full
permissions:
contents: read
jobs:
# Prerequisite: apply the short-lived CI VPC (needed by VPC-dependent L1s + L2 microservice)
# Skipped in plan mode (no resources are applied, so no VPC is needed).
ci-vpc-apply:
name: CI VPC apply
runs-on: ubuntu-latest
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Apply CI VPC
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform apply -auto-approve -lock=false
# L1 lifecycle matrix: apply simple → apply complex (modify) → destroy
lifecycle:
name: L1 lifecycle (${{ matrix.module }})
needs: ci-vpc-apply
if: always()
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime]
env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
sudo apt-get clean
df -h /
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install jsonschema pyyaml boto3
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
# L2 lifecycle matrix: apply simple → apply complex (modify) → destroy
l2-lifecycle:
name: L2 lifecycle (${{ matrix.module }})
needs: ci-vpc-apply
if: always()
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: [static-assets, microservice]
env:
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
sudo apt-get clean
df -h /
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install jsonschema pyyaml boto3
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Read CI VPC outputs
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform output -json > /tmp/ci-vpc-outputs.json
- name: Apply (simple)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
- name: Modify (complex)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
- name: Destroy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
# Cleanup: destroy the CI VPC (always runs in full mode, even if lifecycle fails)
ci-vpc-destroy:
name: CI VPC destroy
needs: [lifecycle, l2-lifecycle]
runs-on: ubuntu-latest
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
steps:
- uses: actions/checkout@v4
- name: Install Terraform 1.9.*
run: |
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
- name: Destroy CI VPC
working-directory: terraform/ci-vpc
env:
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
terraform init -input=false -lock=false
terraform destroy -auto-approve -lock=false