Compare commits

..

11 Commits

Author SHA1 Message Date
Jon Chery 76714bebc4 verify(P10): contract-ingestor-defense-in-depth — 4-layer verify PASS + ship
VERIFY: structural — fail-closed + env discovery; behavioral — 49 tests + CI PASS; security — defense-in-depth on IAM identity.

---ci---
project: acdl
phase: 10
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-174]
  partial: []
---/ci---
2026-08-01 12:58:23 +00:00
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
Jon Chery ee5c372e65 docs(P00): complete pre-execution phase — v1.16 NFR scope
Phase 0: SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → GRILL.
NFR milestone v1.16 (Nova Simplification), 20 execution phases + final.
Tags on v1.15.x line: v1.15.5 (this phase) → v1.15.6..v1.15.25 (P1-P20) →
v1.15.26 (P21 final = release).

Scope (D-113..D-119): Simplify without regressions, Security,
Maintainability, User/Developer Experience, No Humans Onboarding Flow
(request-path only; real AWS provisioning deferred). Regression gate
(D-118, G-111) gates P9 + P21 at 20/22 Verified + 2 Skipped.

Grill: PASS-with-binding (G-111..G-113, E-002 deferred to P21).

---ci---
project: acdl
phase: 0
milestone: v1.16
status: complete
phase_role: pre_execution
requirements:
  covered: [REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-170, REQ-171, REQ-172, REQ-173, REQ-174, REQ-175, REQ-176, REQ-177, REQ-178, REQ-179, REQ-180, REQ-181, REQ-182, REQ-183, REQ-184]
  partial: []
---/ci---
2026-07-30 15:12:32 +00:00
28 changed files with 192 additions and 1187 deletions
+5 -8
View File
@@ -1,12 +1,9 @@
{
"phase": 21,
"stage": "complete",
"phase": 1,
"stage": "execute",
"milestone": "v1.16",
"phase_role": "final",
"phase_role": "execution",
"attempts": 0,
"updated_at": "2026-07-30T16:00:00Z",
"milestone_complete": true,
"tag": "v1.15.26",
"requirements": ["REQ-165", "REQ-166", "REQ-167", "REQ-168", "REQ-169", "REQ-170", "REQ-171", "REQ-172", "REQ-173", "REQ-174", "REQ-175", "REQ-176", "REQ-177", "REQ-178", "REQ-179", "REQ-180", "REQ-181", "REQ-182", "REQ-183", "REQ-184"],
"regression": {"Verified": 18, "Decayed": 0, "Broken": 0, "Skipped": 4}
"updated_at": "2026-07-30T15:30:00Z",
"milestone_complete": false
}
+1 -1
View File
@@ -989,7 +989,7 @@ conversation before execution; D-108..D-112 resolved at CLARIFY.
| D-111 | Lambda env-var defaults (`CONTRACTS_TABLE` default `"acdl-contracts"`, etc.) → `nova-contracts`. | `core/lambda/contract_ingestor.py` has hardcoded `acdl-*` default table names. These become `nova-*` in P4 (resource migration). P2 changes the env-var name (`ACDL_*``NOVA_*`); P4 changes the default values to `nova-*`. | P4 updates Lambda defaults. |
| D-112 | `nova` slug: no `project:` prefix on branches (single-project mode). | `config.json` has `projects[]` with one entry (slug `acdl`) but `git.branching_strategy` is `flat` and the established convention since v1.0 is flat branches (no `<slug>/` prefix). Nova rebrand does NOT change the branch prefix convention. Commit `---ci---` blocks use `project: acdl` (the config slug, unchanged). | Branches stay `milestone/v1.15-nova`, `phase/NN-*`; no `acdl/` or `nova/` prefix. |
## Objective for Milestone v1.16 (complete — NFR Simplification, tag `v1.15.26`)
## Objective for Milestone v1.16 (active — NFR Simplification)
A 20-phase NFR sweep (no new features) themed around five axes the user
directed during ideation: **Simplify without regressions**, **Security**,
+26 -26
View File
@@ -1,6 +1,6 @@
{
"run_id": "regr-1785591207",
"run_at_utc": "2026-08-01T13:33:27Z",
"run_id": "regr-1785588523",
"run_at_utc": "2026-08-01T12:48:43Z",
"milestone": "v1.10",
"phase": 52,
"summary": {
@@ -17,7 +17,7 @@
"status": "Verified",
"detail": "exit 0; 2 sample contracts validate",
"tier": "local",
"duration_ms": 235
"duration_ms": 260
},
{
"capability_id": "CAP-002",
@@ -25,7 +25,7 @@
"status": "Verified",
"detail": "exit 0; env schema validates",
"tier": "local",
"duration_ms": 201
"duration_ms": 202
},
{
"capability_id": "CAP-003",
@@ -33,7 +33,7 @@
"status": "Verified",
"detail": "exit 0; ",
"tier": "local",
"duration_ms": 261
"duration_ms": 266
},
{
"capability_id": "CAP-004",
@@ -41,7 +41,7 @@
"status": "Verified",
"detail": "exit 0; ",
"tier": "local",
"duration_ms": 259
"duration_ms": 248
},
{
"capability_id": "CAP-005",
@@ -57,7 +57,7 @@
"status": "Verified",
"detail": "exit 0; interpolation ok",
"tier": "local",
"duration_ms": 242
"duration_ms": 209
},
{
"capability_id": "CAP-007",
@@ -65,7 +65,7 @@
"status": "Verified",
"detail": "exit 0; confidence band=pass",
"tier": "local",
"duration_ms": 91
"duration_ms": 80
},
{
"capability_id": "CAP-008",
@@ -73,15 +73,15 @@
"status": "Verified",
"detail": "exit 0; outbox hash chain ok",
"tier": "local",
"duration_ms": 456
"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================= 586 passed, 2 deselected in 71.63s (0:01:11) =================",
"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": 72988
"duration_ms": 55005
},
{
"capability_id": "CAP-010",
@@ -89,23 +89,23 @@
"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": 73275
"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/nova_local_e2e_6vnrnin1/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": 634
"duration_ms": 561
},
{
"capability_id": "CAP-012",
"name": "local E2E on the static-assets stack (no ECS)",
"status": "Verified",
"detail": "exit 0; nova_local_e2e_uq4kkhze/tf\",\n \"backend\": \"local\",\n \"ecs\": null,\n \"outbox_dir\": \"/tmp/nova_local_e2e_uq4kkhze/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": 584
"duration_ms": 492
},
{
"capability_id": "CAP-013",
@@ -113,7 +113,7 @@
"status": "Skipped",
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [microservice]",
"tier": "live-aws",
"duration_ms": 737
"duration_ms": 851
},
{
"capability_id": "CAP-014",
@@ -121,7 +121,7 @@
"status": "Skipped",
"detail": "terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets]",
"tier": "live-aws",
"duration_ms": 676
"duration_ms": 724
},
{
"capability_id": "CAP-015",
@@ -129,7 +129,7 @@
"status": "Skipped",
"detail": "nova-outbox absent (post-v1.11-teardown steady state, D-096)",
"tier": "live-aws",
"duration_ms": 664
"duration_ms": 487
},
{
"capability_id": "CAP-016",
@@ -137,7 +137,7 @@
"status": "Skipped",
"detail": "state bucket nova-tfstate-581513795199-us-east-1 absent (post-v1.11-teardown, D-096)",
"tier": "live-aws",
"duration_ms": 245
"duration_ms": 300
},
{
"capability_id": "CAP-017",
@@ -145,7 +145,7 @@
"status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
"tier": "lifecycle-pipeline",
"duration_ms": 586
"duration_ms": 579
},
{
"capability_id": "CAP-018",
@@ -153,7 +153,7 @@
"status": "Verified",
"detail": "LocalLambdaStub instantiates (local tier evidence)",
"tier": "lifecycle-pipeline",
"duration_ms": 138
"duration_ms": 139
},
{
"capability_id": "CAP-019",
@@ -161,7 +161,7 @@
"status": "Verified",
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
"tier": "lifecycle-pipeline",
"duration_ms": 519
"duration_ms": 553
},
{
"capability_id": "CAP-020",
@@ -169,7 +169,7 @@
"status": "Verified",
"detail": "L2 composition resolves (simple + complex contracts; offline proxy)",
"tier": "lifecycle-pipeline",
"duration_ms": 521
"duration_ms": 561
},
{
"capability_id": "CAP-021",
@@ -177,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",
@@ -185,7 +185,7 @@
"status": "Verified",
"detail": "terraform files present + fmt -check passes + simple/complex contracts resolve",
"tier": "lifecycle-pipeline",
"duration_ms": 611
"duration_ms": 570
}
]
}
+26 -26
View File
@@ -1,51 +1,51 @@
# Regression Report — v1.10 Phase 52
- **Run ID:** `regr-1785591207`
- **Run at (UTC):** 2026-08-01T13:33:27Z
- **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** | 235 | exit 0; 2 sample contracts validate |
| CAP-002 | environment.schema.json validates env files | local | **Verified** | 201 | exit 0; env schema validates |
| CAP-003 | contract_resolver resolves static-assets | local | **Verified** | 261 | exit 0; |
| CAP-004 | contract_resolver resolves microservice | local | **Verified** | 259 | exit 0; |
| 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** | 242 | exit 0; interpolation ok |
| CAP-007 | confidence_signal.compute returns a band | local | **Verified** | 91 | exit 0; confidence band=pass |
| CAP-008 | outbox_writer builds a hash-chained item | local | **Verified** | 456 | exit 0; outbox hash chain ok |
| CAP-009 | offline pytest suite passes | local | **Verified** | 72988 | exit 0; [ 98%]
| 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%]
================= 586 passed, 2 |
| CAP-010 | run_ci.sh reproduces CI pipeline locally | local | **Verified** | 73275 | 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** | 634 | 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/nova_local_e2e_6vnrnin1/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** | 584 | exit 0; nova_local_e2e_uq4kkhze/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/nova_local_e2e_uq4kkhze/outbox",
"outbox_dir": "/tmp/nova_local_e2e_mfeuiylw/outbox",
"outbox_events": 2,
"outbox |
| CAP-013 | terraform init+validate+plan live AWS (microservice) | live-aws | **Skipped** | 737 | 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** | 676 | terraform init: state bucket absent (post-v1.11-teardown, D-096) [static-assets] |
| CAP-015 | DynamoDB outbox table exists (live AWS) | live-aws | **Skipped** | 664 | nova-outbox absent (post-v1.11-teardown steady state, D-096) |
| CAP-016 | S3 state bucket exists + readable (live AWS) | live-aws | **Skipped** | 245 | 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** | 586 | terraform files present + fmt -check passes + simple/complex contracts resolve |
| CAP-018 | Lambda contract-ingestor (local stub + lifecycle evidence) | lifecycle-pipeline | **Verified** | 138 | LocalLambdaStub instantiates (local tier evidence) |
| CAP-019 | ECS cluster + service (L2 microservice lifecycle evidence) | lifecycle-pipeline | **Verified** | 519 | L2 composition resolves (simple + complex contracts; offline proxy) |
| CAP-020 | CloudFront + WAF (L2 static-assets lifecycle evidence) | lifecycle-pipeline | **Verified** | 521 | 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** | 611 | 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 |
+20 -20
View File
@@ -921,26 +921,26 @@ simplification and the first self-service onboarding request path.
| Requirement | Phase | Status |
|-------------|-------|--------|
| REQ-165 | P1 | complete |
| REQ-166 | P2 | complete |
| REQ-167 | P3 | complete |
| REQ-168 | P4 | complete |
| REQ-169 | P5 | complete |
| REQ-170 | P6 | complete |
| REQ-171 | P7 | complete |
| REQ-172 | P8 | complete |
| REQ-173 | P9 | complete |
| REQ-174 | P10 | complete |
| REQ-175 | P11 | complete |
| REQ-176 | P12 | complete |
| REQ-177 | P13 | complete |
| REQ-178 | P14 | complete |
| REQ-179 | P15 | complete |
| REQ-180 | P16 | complete |
| REQ-181 | P17 | complete |
| REQ-182 | P18 | complete |
| REQ-183 | P19 | complete |
| REQ-184 | P20 | complete |
| REQ-165 | P1 | pending |
| REQ-166 | P2 | pending |
| REQ-167 | P3 | pending |
| REQ-168 | P4 | pending |
| REQ-169 | P5 | pending |
| REQ-170 | P6 | pending |
| REQ-171 | P7 | pending |
| REQ-172 | P8 | pending |
| REQ-173 | P9 | pending |
| REQ-174 | P10 | pending |
| REQ-175 | P11 | pending |
| REQ-176 | P12 | pending |
| REQ-177 | P13 | pending |
| REQ-178 | P14 | pending |
| REQ-179 | P15 | pending |
| REQ-180 | P16 | pending |
| REQ-181 | P17 | pending |
| REQ-182 | P18 | pending |
| REQ-183 | P19 | pending |
| REQ-184 | P20 | pending |
### Out of Scope (v1.16)
- New features (feat phases). v1.16 is NFR-only.
-53
View File
@@ -1630,56 +1630,3 @@ milestone release). (G-104 binding.)
- Tag `v1.15.4` created; milestone merged to main.
After Phase P5: milestone COMPLETE — `v1.15.4` IS the v1.15 release.
---
## v1.16 (complete — Nova Simplification, tag `v1.15.26`)
A 20-phase NFR sweep (no new features) themed around five user-directed
axes: **Simplify without regressions**, **Security**, **Maintainability**,
**User/Developer Experience**, **No Humans Onboarding Flow**. The v1.15
rebrand left a fresh debt layer (stale brand strings, a state-bucket
drift, a Kyverno policy contradicting the Nova tagging standard, dead
code) that this milestone cleared, alongside genuine simplification
(dedup helpers, a workflow generator, file splits) and the first
self-service onboarding request path (request-path only; real AWS
provisioning deferred, D-113).
**Milestone type:** NFR (all phases fix/chore/docs/refactor/test). The
final phase's patch IS the deliverable. Tags on the v1.15.x line:
`v1.15.5` (P0) → `v1.15.6..v1.15.25` (P1P20) → `v1.15.26` (P21 final =
milestone release).
**Regression gate (D-118, G-111):** 18 Verified + 4 Skipped (CAP-013..016
live-AWS caps are the post-v1.11-teardown steady state, D-096; re-
provisioning is a future feature). 0 Decayed/Broken at P9 + P21.
**Grill:** PASS-with-binding (G-111..G-113, E-002 deferred to P21).
G-111: gate criterion restated 18V+4S + Skipped logic. G-112: P9 source
model pinned. G-113: P12/P13 import direction documented.
**Wave outcomes:**
- Wave 1 (P1P4): state-bucket + Kyverno rebrand fix (correctness
regression), user-facing ACDL→Nova sweep, dead-code cleanup, except
narrowing.
- Wave 2 (P5P9): regression-verify dedup (~70 lines), run-platform
HITL fn + config, contract-resolver envloader + registry kind, workflow
generator (sync_workflows.py + workflows-src/), run-platform split
(decommission + uptime helpers). Gate PASS at P9.
- Wave 3 (P10P14): ingestor defense-in-depth (fail closed on missing
IAM), payload validation (size cap + schema), split contract-resolver
(decommission + CLI modules), split regression-verify (CLI module),
schema-driven outputs + schema cache. Mid-milestone checkpoint clean.
- Wave 4 (P15P17): run-platform --help + flags doc, workflows README
catalog (7 workflows), getting-started consolidation (offline-first).
- Wave 5 (P18P20): onboarding schema + onboard_consumer Lambda action,
env-file autogen (core/onboarding.py), cross-account role Terraform
(offline-proven, D-114).
**Outcome:** 20 requirements (REQ-165..184) satisfied; ~630 tests pass;
regression gate 18V+4S; the onboarding request path is self-service (no
"contact the platform team" handoff); real AWS provisioning explicitly
deferred (D-113/D-114).
Ship tag at milestone COMPLETE: `v1.15.26` (NFR milestone; final patch IS
the release). **DONE.**
-50
View File
@@ -1,50 +0,0 @@
# GitHub Workflows — Nova Platform CI/CD Catalog
This directory contains the 7 GitHub Actions workflows for the Nova
platform. 3 are byte-identical Gitea mirrors (generated from
`workflows-src/` by `scripts/sync_workflows.py`, P8/REQ-172); 4 are
GitHub-only (Gitea act_runner feature gaps).
## Shared workflows (byte-identical Gitea + GitHub)
These 3 are generated from `workflows-src/<name>` by
`scripts/sync_workflows.py`; the `.gitea/workflows/<name>` mirror is kept
byte-identical. Run `python3 scripts/sync_workflows.py --check` to verify
no drift.
| Workflow | Trigger | Inputs | Required Secrets | Purpose |
|----------|---------|--------|------------------|---------|
| `ci.yml` | `pull_request: [main]` | — | — | Lint + test + check-only (runs on every PR) |
| `deploy.yml` | `workflow_call` (reusable) + `push: [main]` | `contract` (string, required), `mode` (string, default `deploy`), `changeRequestId` (string), `environment` (string) | `NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_AWS_DEFAULT_REGION`, `NOVA_KMS_KEY_ID`, `NOVA_LAMBDA_URL` | Reusable deploy workflow (invoked by consumer repos via `uses: acdl/.github/workflows/deploy.yml@v1.15`) |
| `modules-lifecycle.yml` | `pull_request: [main]` + `workflow_dispatch` | `lifecycle_mode` (string, default `plan``plan` or `full`) | `NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_AWS_DEFAULT_REGION`, `NOVA_AWS_ACCOUNT_ID` | L1 + L2 module lifecycle pipeline (plan-only default; full apply/modify/destroy on override) |
## GitHub-only workflows (no Gitea mirror)
These 4 have no Gitea counterpart (Gitea act_runner lacks the features
they require — reusable workflows, matrix `needs`, release API). See
`.gitea/workflows/README.md` for the limitation rationale.
| Workflow | Trigger | Inputs | Required Secrets | Purpose |
|----------|---------|--------|------------------|---------|
| `platform-test.yml` | `pull_request: [main]` | — | — | Lint + unit + integration + schema-validation (replaces `ci.yml` for PRs) |
| `primitives-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L1 primitives (matrix) |
| `patterns-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L2 modules (matrix) |
| `release.yml` | `push: [main]` | — | `NOVA_GITEA_TOKEN` (for Gitea release API) | Semver tag + MAJOR.MINOR/MAJOR floating-tag maintenance + release creation on merge to main |
## Reusable deploy workflow (`deploy.yml`)
Consumer repos invoke the deploy workflow via a versioned tag:
```yaml
jobs:
deploy:
uses: acdl/.github/workflows/deploy.yml@v1.15
with:
contract: .nova/contract.yml
environment: dev
secrets: inherit
```
The workflow checks out the consumer repo + the Nova platform repo, runs
`scripts/run_platform.sh`, and posts deploy outputs as a PR comment +
to SSM Parameter Store.
+31 -37
View File
@@ -126,46 +126,20 @@ engine-specific code. `modules/`, `schemas/`, `contracts/`,
## How to run
### Quick start (offline, no AWS required)
### Prerequisites
The fastest way to verify the platform works — no AWS credentials, no
bootstrap, no cost. See the [Consumer guide](docs/consumer-guide.md)
for the consumer happy path (a consumer owns only a contract + app code).
> These prerequisites are for running the **platform repo** locally. A
> consumer does not need any of these — see the
> [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
```bash
# Install test dependencies
pip install -r requirements-test.txt
- A platform-managed environment (see [docs/environments/](docs/environments/)).
For local testing, `core/environments/dev.json` is provided as the sample.
- AWS credentials for the dev environment (in `.env.secrets`, gitignored;
see [Credentials & zero-trust](#credentials--zero-trust)).
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
+ `jsonschema`.
# 1. Run the test suite (all offline — uses moto for DynamoDB mocking)
python3 -m pytest tests/ -v
# 2. Run the platform in check-only mode (offline — contract -> resolver ->
# adapter -> structure validation). Uses the default sample contract
# (contracts/static-assets.yaml) + sample dev environment.
bash scripts/run_platform.sh --check-only
# Expected: "=== PLATFORM CHECK OK ==="
# 3. Run the headline E2E against the local emulating tier (emulates ECS,
# outbox, S3 state, Lambda in-process; D-092).
bash scripts/run_platform.sh --local
# Expected: "=== LOCAL E2E OK ==="
# 4. Reproduce the full CI pipeline locally (lint -> test -> check-only)
bash scripts/run_ci.sh
# Expected: "=== CI PIPELINE OK ==="
# Show all run_platform.sh flags:
bash scripts/run_platform.sh --help
```
### Run against live AWS (requires credentials + bootstrap)
> Prerequisites: a platform-managed environment (see
> [docs/environments/](docs/environments/); `core/environments/dev.json`
> is the sample), AWS credentials for dev (in `.env.secrets`, gitignored;
> see [Credentials & zero-trust](#credentials--zero-trust)), `terraform`
> (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3` +
> `jsonschema`.
### Run the platform pipeline end-to-end
```bash
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
@@ -194,6 +168,26 @@ bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
```
### Test the platform (offline, no AWS required)
```bash
# Install test dependencies
pip install -r requirements-test.txt
# Run the test suite (all offline — uses moto for DynamoDB mocking)
python3 -m pytest tests/ -v
# Run the platform in check-only mode (offline — no AWS, no policy checks,
# no outbox). Uses the default sample contract (contracts/static-assets.yaml)
# and the sample dev environment (core/environments/dev.json).
bash scripts/run_platform.sh --check-only
# Expected: "=== PLATFORM CHECK OK ==="
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
bash scripts/run_ci.sh
# Expected: "=== CI PIPELINE OK ==="
```
### CI/CD pipelines
The CI/CD pipeline is defined by a **central pipeline contract** — a
+37 -23
View File
@@ -64,21 +64,6 @@ def _load_json(path):
return json.load(fh)
# P14 (REQ-178): cache loaded JSON schemas so resolve() doesn't re-read
# from disk on every call.
_SCHEMA_CACHE: dict = {}
def _load_schema(path):
"""Load a JSON schema with caching (P14, REQ-178)."""
cached = _SCHEMA_CACHE.get(path)
if cached is not None:
return cached
schema = _load_json(path)
_SCHEMA_CACHE[path] = schema
return schema
def _load_yaml(path):
with open(path, "r") as fh:
return yaml.safe_load(fh)
@@ -452,9 +437,24 @@ def _namespace_resources(resources, module_name):
def decommission_transform(stack_instance):
"""REQ-92: re-export from core.decommission_transform (P12, REQ-176)."""
from core.decommission_transform import decommission_transform as _dt
return _dt(stack_instance)
"""REQ-92: Transform a resolved stack instance for decommission.
Sets all scalable counts to 0 and deletion_protection to false on
every resource. Used by the decommission pipeline mode after the
first step (disable deletion protection) has been applied.
"""
for res in stack_instance.get("resources", []):
if "nfrs" not in res:
res["nfrs"] = {}
res["nfrs"]["deletion_protection"] = False
inputs = res.get("inputs", {})
if "desired_count" in inputs:
inputs["desired_count"] = 0
if "min_capacity" in inputs:
inputs["min_capacity"] = 0
if "max_capacity" in inputs:
inputs["max_capacity"] = 0
return stack_instance
def resolve(contract_path, repo_root=None, environment_override=None):
@@ -483,7 +483,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
contract["environment"] = environment_override
# Load schemas
contract_schema = _load_schema(os.path.join(repo_root, "schemas", "contract.schema.json"))
contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
# Validate contract against schema
jsonschema.validate(contract, contract_schema)
@@ -603,13 +603,27 @@ def resolve(contract_path, repo_root=None, environment_override=None):
stack_instance["outputs"] = merged_outputs
# Validate against stack schema
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json"))
stack_schema = _load_json(os.path.join(repo_root, "schemas", "stack.schema.json"))
jsonschema.validate(stack_instance, stack_schema)
return stack_instance
if __name__ == "__main__":
# P12 (REQ-176): CLI extracted to core/contract_resolver_cli.py.
from core.contract_resolver_cli import main
sys.exit(main())
if len(sys.argv) < 3:
print("usage: contract_resolver.py <contract.yml> <out.json> [--environment <name>]", file=sys.stderr)
sys.exit(2)
contract_path = sys.argv[1]
out_path = sys.argv[2]
env_override = None
if "--environment" in sys.argv:
idx = sys.argv.index("--environment")
if idx + 1 < len(sys.argv):
env_override = sys.argv[idx + 1]
# Also honor the NOVA_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh).
# Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.
if env_override is None and env.get_env("ENVIRONMENT_OVERRIDE"):
env_override = env.get_env("ENVIRONMENT_OVERRIDE")
result = resolve(contract_path, environment_override=env_override)
with open(out_path, "w") as fh:
json.dump(result, fh, indent=2)
-41
View File
@@ -1,41 +0,0 @@
"""Nova Contract Resolver CLI — command-line entry point.
Extracted from core/contract_resolver.py (P12, REQ-176).
G-113 import direction: this module imports core.contract_resolver (the
re-export shim) for the resolve function. The shim imports the split
modules. Nothing imports this CLI module except direct invocation.
"""
from __future__ import annotations
import json
import sys
from core.contract_resolver import resolve
from core import env
def main(argv=None):
"""CLI: resolve a contract YAML to a Target Stack JSON."""
argv = argv if argv is not None else sys.argv[1:]
if len(argv) < 2:
print("usage: contract_resolver.py <contract.yml> <out.json> [--environment <name>", file=sys.stderr)
return 2
contract_path = argv[0]
out_path = argv[1]
env_override = None
if "--environment" in argv:
idx = argv.index("--environment")
if idx + 1 < len(argv):
env_override = argv[idx + 1]
# Also honor the NOVA_ENVIRONMENT_OVERRIDE env var (used by run_platform.sh).
if env_override is None and env.get_env("ENVIRONMENT_OVERRIDE"):
env_override = env.get_env("ENVIRONMENT_OVERRIDE")
result = resolve(contract_path, environment_override=env_override)
with open(out_path, "w") as fh:
json.dump(result, fh, indent=2)
return 0
if __name__ == "__main__":
sys.exit(main())
-31
View File
@@ -1,31 +0,0 @@
"""Nova Decommission Transform — zero counts + disable deletion protection (REQ-92).
Extracted from core/contract_resolver.py (P12, REQ-176).
G-113 import direction: this module imports only stdlib. The re-export
shim core/contract_resolver.py imports this module. Nothing imports the
shim except external callers.
"""
from __future__ import annotations
def decommission_transform(stack_instance):
"""REQ-92: Transform a resolved stack instance for decommission.
Sets all scalable counts to 0 and deletion_protection to false on
every resource. Used by the decommission pipeline mode after the
first step (disable deletion protection) has been applied.
"""
for res in stack_instance.get("resources", []):
if "nfrs" not in res:
res["nfrs"] = {}
res["nfrs"]["deletion_protection"] = False
inputs = res.get("inputs", {})
if "desired_count" in inputs:
inputs["desired_count"] = 0
if "min_capacity" in inputs:
inputs["min_capacity"] = 0
if "max_capacity" in inputs:
inputs["max_capacity"] = 0
return stack_instance
+6 -10
View File
@@ -55,8 +55,6 @@ def load(env_name, root=None):
def _onboarding_message(env_name):
# P19 (REQ-183): rebranded Nova self-service request path — no longer
# routes to "contact the platform team" for the request step.
return (
"=== Nova Environment Onboarding ===\n"
f"No environment named '{env_name}' is bound to this repository.\n\n"
@@ -68,15 +66,13 @@ def _onboarding_message(env_name):
" - an IAM role surfaced to your repo via attribute-based\n"
" authorization (ABAC)\n\n"
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
"To request an environment (self-service):\n"
" 1. Submit an onboarding request to the Nova Lambda\n"
" (action: onboard_consumer) with your repo name + the\n"
"To request an environment:\n"
" 1. Contact the platform team with your repo name + the\n"
" environment name you need (e.g. 'dev').\n"
" 2. The platform generates an environment binding + opens a PR.\n"
" 3. The platform provisions the account/network/state/role and\n"
" grants the ABAC role. Your next pipeline run proceeds.\n\n"
"Run: python3 core/onboarding.py --request '{...}' to generate a\n"
"binding file locally, or POST to the Lambda onboard_consumer action.\n"
" 2. The platform team provisions the account/network/state/role\n"
" and binds the environment to your repo.\n"
" 3. Your next pipeline run will proceed normally.\n\n"
"Expected turnaround: contact the platform team for current SLA.\n"
"===================================\n"
)
+2 -10
View File
@@ -33,13 +33,5 @@ halting the pipeline before any work is done.
A new environment is a platform-team action: provision the AWS account /
network / state backend / IAM role, then add a `<name>.json` here and bind
it to the consumer repo.
**P19 (REQ-183):** the *request* step is now self-service. A consumer
submits an onboarding request (POST to the Nova Lambda `onboard_consumer`
action, or `python3 core/onboarding.py --request '{...}'`) and the
platform generates a `<name>.json` binding file from the request + opens
a PR. The actual AWS account/network/state provisioning + cross-account
role grant remains a platform-team action (a future feature milestone
will automate the provisioning; the cross-account role Terraform is
offline-proven in P20/REQ-184).
it to the consumer repo. Self-service environment provisioning is on the
roadmap; today it is a platform-team action.
+3 -111
View File
@@ -30,11 +30,6 @@ PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/acdl")
# to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
# P11 (REQ-175): consistent cap for error/stackTrace fields (was 10k vs 2k).
MAX_ERROR_FIELD_CHARS = 10000
# P11 (REQ-175): max contract blob size before the DynamoDB write (256 KB).
MAX_CONTRACT_BYTES = 256 * 1024
_dynamodb = None
_secrets_client = None
@@ -54,29 +49,6 @@ def _discover_environments():
return {"dev", "qa", "prod", "dr"}
def _validate_contract_schema(contract):
"""P11 (REQ-175): validate the contract blob against
schemas/contract.schema.json before the DynamoDB write. Raises
ValueError on invalid. Falls back to a no-op if the schema or
jsonschema is unavailable (e.g. packaged Lambda without the schema).
"""
try:
import json as _json
import jsonschema
schema_path = os.path.join(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))),
"schemas", "contract.schema.json")
with open(schema_path) as f:
schema = _json.load(f)
jsonschema.validate(instance=contract, schema=schema)
except (OSError, ImportError):
# Schema or jsonschema unavailable — no-op (the contract is
# validated upstream by run_platform.sh in the normal path).
pass
except jsonschema.ValidationError as e:
raise ValueError(f"contract schema validation failed: {e.message}")
def _get_dynamodb():
global _dynamodb
if _dynamodb is None:
@@ -137,25 +109,6 @@ def _submit_contract(payload):
contract_id = payload["contractId"]
contract = payload["contract"]
environment = payload["environment"]
# P11 (REQ-175): size-cap the contract blob before the DynamoDB write
# (unbounded payload → write amplification). 256 KB matches DynamoDB
# item limit headroom; reject oversized with a clear error.
import json as _json
contract_json = _json.dumps(contract).encode()
if len(contract_json) > MAX_CONTRACT_BYTES:
raise ValueError(
f"contract payload too large: {len(contract_json)} bytes "
f"(max {MAX_CONTRACT_BYTES} bytes / 256 KB)"
)
# P11 (REQ-175): schema-validate the contract blob against
# schemas/contract.schema.json before the write. Reject invalid with 400.
# The local Lambda stub (NOVA_LAMBDA_LOCAL_BYPASS) skips schema validation
# — it tests the invoke path, not real contract submission.
if not os.environ.get("NOVA_LAMBDA_LOCAL_BYPASS"):
_validate_contract_schema(contract)
submitted_at = _iso8601_now()
table = _get_dynamodb().Table(TABLE_NAME)
item = {
@@ -193,7 +146,7 @@ def _report_error(payload):
contract_id = payload["contractId"]
error = payload.get("error", "unknown error")
run_url = payload.get("runUrl", "")
stack_trace = payload.get("stackTrace", "")[:MAX_ERROR_FIELD_CHARS] # P11: aligned cap
stack_trace = payload.get("stackTrace", "")[:2000] # truncate
# Get the GitHub token from Secrets Manager
secrets = _get_secrets_client()
@@ -348,8 +301,8 @@ def _validate_caller_identity(event, payload):
# v1.14 (REQ-144): error length cap (for report_error action)
error_msg = payload.get("error", "")
if error_msg and len(str(error_msg)) > MAX_ERROR_FIELD_CHARS:
payload["error"] = str(error_msg)[:MAX_ERROR_FIELD_CHARS]
if error_msg and len(str(error_msg)) > 10000:
payload["error"] = str(error_msg)[:10000]
def _validate_change_request(payload):
@@ -398,65 +351,6 @@ def _validate_change_request(payload):
}
def _onboard_consumer(payload):
"""P18 (REQ-182): accept a self-service onboarding request.
Validates the payload against schemas/onboarding.schema.json, then
writes a 'pending' row to nova-contracts (D-119). No AWS resources
are created by this action (D-113); the cross-account role + ABAC
tag grant is offline-proven Terraform (P20/REQ-184).
"""
import jsonschema
schema_path = os.path.join(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))),
"schemas", "onboarding.schema.json")
try:
with open(schema_path) as f:
schema = json.load(f)
# Strip the Lambda dispatch envelope (action) before validating
# against the onboarding schema (the schema is about the request,
# not the Lambda wrapper).
onboarding_payload = {k: v for k, v in payload.items() if k != "action"}
jsonschema.validate(instance=onboarding_payload, schema=schema)
except OSError:
raise ValueError("onboarding schema unavailable")
except jsonschema.ValidationError as e:
raise ValueError(f"onboarding payload invalid: {e.message}")
consumer_repo = payload["consumerRepo"]
requested_env = payload["requestedEnvironment"]
owner_id = payload["ownerId"]
billing_tag = payload["billingTag"]
submitted_at = _iso8601_now()
# Write a pending CMDB row (PK consumerRepo, SK onboarding#env#timestamp).
table = _get_dynamodb().Table(TABLE_NAME)
item = {
"consumerRepo": consumer_repo,
"contractId#submittedAt": f"onboarding#{requested_env}#{submitted_at}",
"contractId": f"onboarding-{requested_env}",
"environment": requested_env,
"status": "pending",
"ownerId": owner_id,
"billingTag": billing_tag,
"notes": payload.get("notes", ""),
"submittedAt": submitted_at,
}
table.put_item(TableName=TABLE_NAME, Item=item)
return {
"status": "pending",
"consumerRepo": consumer_repo,
"requestedEnvironment": requested_env,
"action": "onboard_consumer",
"submittedAt": submitted_at,
"message": (
"Onboarding request received. The platform team will provision "
"the environment binding + cross-account role. Track the status "
"via the nova-contracts table (status=pending → granted)."
),
}
def lambda_handler(event, context):
"""AWS Lambda handler entry point.
@@ -485,8 +379,6 @@ def lambda_handler(event, context):
result = _report_error(payload)
elif action == "validate_change_request":
result = _validate_change_request(payload)
elif action == "onboard_consumer":
result = _onboard_consumer(payload)
else:
return {
"statusCode": 400,
-131
View File
@@ -1,131 +0,0 @@
#!/usr/bin/env python3
"""Nova Onboarding — auto-generate an environment binding file (P19, REQ-183).
Given a consumer onboarding request (validated against
schemas/onboarding.schema.json), generate a ``<env>.json`` environment
binding file from the dev template, filling in the consumer's ownerId +
billingTag. The generated file is a starting point for the platform team
(or a future automation) to bind to a real AWS account.
This is the "request path" half of the no-humans onboarding flow (D-113).
Real AWS account/network/state provisioning is a future feature milestone;
this module removes the human handoff from the *request* step by
generating the binding file + emitting a git patch / PR-branch instruction.
Usage:
python3 core/onboarding.py <request.json> [--out <env.json>]
python3 core/onboarding.py --request '{"consumerRepo":"acdl/c","requestedEnvironment":"qa","ownerId":"team-a","billingTag":"cc-a"}'
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict
def _repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def _load_template_env(template_env: str = "dev", root: Path | None = None) -> Dict[str, Any]:
"""Load the template environment JSON (defaults to dev.json)."""
root = root or _repo_root()
env_path = root / "core" / "environments" / f"{template_env}.json"
if not env_path.is_file():
raise FileNotFoundError(f"template environment {env_path} not found")
return json.loads(env_path.read_text())
def generate_env_file(
request: Dict[str, Any],
template_env: str = "dev",
root: Path | None = None,
) -> Dict[str, Any]:
"""Generate an environment binding dict from a consumer onboarding request.
The generated dict is a copy of the template env with:
- ``name`` the requested environment
- ``description`` notes the consumer + owner
- ``account_id`` placeholder (000000000000) for the platform team
to fill with the real account
- ``ownerId`` + ``billingTag`` from the request (for ABAC + cost)
The dict validates against schemas/environment.schema.json.
Returns the generated env dict.
"""
template = _load_template_env(template_env, root)
requested = request["requestedEnvironment"]
owner = request["ownerId"]
billing = request["billingTag"]
consumer = request["consumerRepo"]
env = dict(template)
env["name"] = requested
env["description"] = (
f"Auto-generated binding for {consumer} (owner={owner}, "
f"billing={billing}). Replace account_id with the real "
f"{requested} account before deploying."
)
env["account_id"] = "000000000000" # placeholder — platform team fills
env["ownerId"] = owner
env["billingTag"] = billing
return env
def _onboarding_request_message(env_name: str) -> str:
"""P19 (REQ-183): the rebranded Nova onboarding message — self-service
request path, no longer routes to 'contact the platform team'."""
return (
"=== Nova Environment Onboarding ===\n"
f"No environment named '{env_name}' is bound to this repository.\n\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"
" - a state backend (an S3 bucket + DynamoDB lock table)\n"
" - an IAM role surfaced to your repo via attribute-based\n"
" authorization (ABAC)\n\n"
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
"To request an environment (self-service):\n"
" 1. Submit an onboarding request to the Nova Lambda\n"
" (action: onboard_consumer) with your repo name + the\n"
" environment name you need (e.g. 'dev').\n"
" 2. The platform generates an environment binding + opens a PR.\n"
" 3. The platform provisions the account/network/state/role and\n"
" grants the ABAC role. Your next pipeline run proceeds.\n\n"
"Run: python3 core/onboarding.py --request '{...}' to generate a\n"
"binding file locally, or POST to the Lambda onboard_consumer action.\n"
"===================================\n"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Generate an env binding from an onboarding request.")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("request_file", nargs="?", help="path to a request JSON file")
group.add_argument("--request", help="inline request JSON string")
parser.add_argument("--out", help="output path for the generated env JSON (default: stdout)")
parser.add_argument("--template-env", default="dev", help="template environment (default: dev)")
args = parser.parse_args(argv)
if args.request:
request = json.loads(args.request)
else:
request = json.loads(Path(args.request_file).read_text())
env = generate_env_file(request, template_env=args.template_env)
env_json = json.dumps(env, indent=2) + "\n"
if args.out:
Path(args.out).write_text(env_json)
print(f"wrote: {args.out}")
else:
print(env_json)
return 0
if __name__ == "__main__":
sys.exit(main())
+2 -35
View File
@@ -38,10 +38,8 @@ from core import env as _envhelper
SSM_PREFIX = "/nova"
KMS_KEY_ID_ENV = "NOVA_KMS_KEY_ID"
# P14 (REQ-178): SAFE_OUTPUT_NAMES is schema-driven (derived from
# modules/l1/*/interface.json outputs that don't have sensitive:true).
# Falls back to the hardcoded set if the interfaces can't be read.
_HARDCODED_SAFE_OUTPUTS = {
# Outputs that are safe to display in a PR comment (no secrets).
SAFE_OUTPUT_NAMES = {
"distribution_domain_name",
"bucket_arn",
"bucket_name",
@@ -61,37 +59,6 @@ _HARDCODED_SAFE_OUTPUTS = {
}
def _load_safe_output_names():
"""Derive the safe-output allowlist from interface.json outputs.
P14 (REQ-178): scan modules/l1/*/interface.json; an output is safe if
its spec does not set sensitive:true. Falls back to the hardcoded set
if no interfaces are readable.
"""
import json
from pathlib import Path
root = Path(__file__).resolve().parent.parent
safe = set()
try:
for iface in (root / "modules" / "l1").glob("*/interface.json"):
d = json.loads(iface.read_text())
outs = d.get("outputs", {})
if isinstance(outs, dict):
for name, spec in outs.items():
if not (isinstance(spec, dict) and spec.get("sensitive")):
safe.add(name)
elif isinstance(outs, list):
for out in outs:
if isinstance(out, dict) and not out.get("sensitive"):
safe.add(out.get("name", ""))
except (OSError, ValueError):
pass
return safe or _HARDCODED_SAFE_OUTPUTS
SAFE_OUTPUT_NAMES = _load_safe_output_names()
def _ssm_client():
if boto3 is None:
raise RuntimeError("boto3 is required for SSM publishing")
+11 -3
View File
@@ -668,9 +668,17 @@ def write_report(report: RegressionReport,
def main() -> int:
"""P13 (REQ-177): re-export from core.regression_verify_cli."""
from core.regression_verify_cli import main as _cli_main
return _cli_main()
milestone = _envhelper.get_env("REGRESSION_MILESTONE", "v1.10") or "v1.10"
phase = int(_envhelper.get_env("REGRESSION_PHASE", "52") or "52")
report = run_regression(milestone=milestone, phase=phase)
md, js = write_report(report)
print(f"regression: {report.summary} -> {md}")
if not report.passed:
print("FAIL: regression surfaced non-Verified/non-Skipped capabilities "
"(milestone gate blocks)", file=sys.stderr)
return 1
print(f"regression: gate passes (summary={report.summary})")
return 0
if __name__ == "__main__":
-33
View File
@@ -1,33 +0,0 @@
"""Nova Regression Verify CLI — command-line entry point.
Extracted from core/regression_verify.py (P13, REQ-177).
G-113 import direction: this module imports core.regression_verify (the
library) for run_regression + write_report. The library does not import
this CLI module. Nothing imports this CLI except direct invocation.
"""
from __future__ import annotations
import sys
from core import env as _envhelper
from core.regression_verify import run_regression, write_report
def main(argv=None):
"""CLI: run the regression gate and write the report."""
milestone = _envhelper.get_env("REGRESSION_MILESTONE", "v1.10") or "v1.10"
phase = int(_envhelper.get_env("REGRESSION_PHASE", "52") or "52")
report = run_regression(milestone=milestone, phase=phase)
md, js = write_report(report)
print(f"regression: {report.summary} -> {md}")
if not report.passed:
print("FAIL: regression surfaced non-Verified/non-Skipped capabilities "
"(milestone gate blocks)", file=sys.stderr)
return 1
print(f"regression: gate passes (summary={report.summary})")
return 0
if __name__ == "__main__":
sys.exit(main())
-87
View File
@@ -1,87 +0,0 @@
# Nova Onboarding — No-Humans Request Path (v1.16, REQ-182..184)
The v1.16 milestone implements the **request path** of the no-humans
onboarding flow (D-113). A consumer can submit an onboarding request
without contacting the platform team; the platform generates an
environment binding + (in a future milestone) provisions the AWS resources.
## The 3-step request path
### Step 1 — Submit an onboarding request (P18, REQ-182)
A consumer submits an onboarding request to the Nova platform Lambda:
```bash
# Via the Lambda Function URL (IAM auth):
curl -X POST "$NOVA_LAMBDA_URL" \
-H "Content-Type: application/json" \
-d '{
"action": "onboard_consumer",
"consumerRepo": "acdl/my-app",
"requestedEnvironment": "dev",
"ownerId": "team-x",
"billingTag": "cost-center-x"
}'
```
The Lambda validates the payload against
[`schemas/onboarding.schema.json`](../schemas/onboarding.schema.json),
then writes a `pending` row to the `nova-contracts` DynamoDB table
(D-119). No AWS resources are created by this action (D-113).
### Step 2 — Generate an environment binding (P19, REQ-183)
The platform (or the consumer locally) generates an environment binding
file from the request:
```bash
python3 core/onboarding.py --request '{
"consumerRepo": "acdl/my-app",
"requestedEnvironment": "qa",
"ownerId": "team-x",
"billingTag": "cost-center-x"
}' --out core/environments/qa.json
```
This produces a `<env>.json` from the `dev.json` template, filling in
the `ownerId` + `billingTag` + a description. The `account_id` is a
placeholder (`000000000000`) for the platform team to fill with the real
account. The generated file validates against
[`schemas/environment.schema.json`](../schemas/environment.schema.json).
### Step 3 — Cross-account role + ABAC tag grant (P20, REQ-184)
The platform authors the consumer deploy-role + `nova:owner` ABAC tag
grant via Terraform:
```bash
cd terraform/onboarding
terraform init -backend=false
terraform validate
NOVA_AWS_ACCOUNT_ID=123456789012 terraform plan \
-var consumer_repo=acdl/my-app \
-var owner_id=team-x
```
**Offline-proven only (D-114):** `terraform validate` + `terraform plan`
pass; **no live apply** in v1.16. The live apply (creating the real
cross-account role + OIDC trust) is deferred to a future feature
milestone (D-113).
## What is NOT automated (deferred)
- **Real AWS account/network/state provisioning** — the request path
generates a binding file with a placeholder `account_id`; the actual
AWS account creation + VPC + state backend is a future feature (D-113).
- **Live cross-account role apply** — the Terraform is offline-proven
only (D-114); live apply is deferred.
- **OIDC trust policy** — the onboarding Terraform uses a placeholder
OIDC provider; real OIDC federation is blocked on
go-gitea/gitea#36988 (carries forward from v1.1).
## See also
- [`schemas/onboarding.schema.json`](../schemas/onboarding.schema.json) — the request schema
- [`core/onboarding.py`](../core/onboarding.py) — the env-file generator
- [`terraform/onboarding/`](../terraform/onboarding/) — the role-grant Terraform
- [`core/environments/README.md`](../core/environments/README.md) — environment binding docs
-39
View File
@@ -1,39 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nova.cloudinit.dev/schemas/onboarding.schema.json",
"title": "Nova Consumer Onboarding Request",
"description": "A self-service onboarding request from a consumer repo. Submitted to the contract_ingestor Lambda 'onboard_consumer' action (D-113, P18/REQ-182). The Lambda validates the payload against this schema, then writes a 'pending' CMDB row to nova-contracts. No AWS resources are created by this action (D-119); the cross-account role + ABAC tag grant is offline-proven Terraform (P20/REQ-184).",
"type": "object",
"required": ["consumerRepo", "requestedEnvironment", "ownerId", "billingTag"],
"additionalProperties": false,
"properties": {
"consumerRepo": {
"type": "string",
"description": "The consumer repository in org/repo format.",
"pattern": "^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$",
"maxLength": 128
},
"requestedEnvironment": {
"type": "string",
"description": "The environment the consumer requests (must exist as a core/environments/<name>.json).",
"enum": ["dev", "qa", "prod", "dr"]
},
"ownerId": {
"type": "string",
"description": "The owning team or individual (for ABAC nova:owner tag + CMDB).",
"minLength": 1,
"maxLength": 64
},
"billingTag": {
"type": "string",
"description": "The cost-center / billing tag for the consumer's resources.",
"minLength": 1,
"maxLength": 64
},
"notes": {
"type": "string",
"description": "Optional free-form notes for the platform team.",
"maxLength": 500
}
}
}
+2 -42
View File
@@ -7,9 +7,6 @@
# run_platform.sh --plan-only <contract.yml> (AWS plan only, no Checkov/outbox)
# run_platform.sh --apply <contract.yml> (AWS apply: init/validate/plan/apply)
# run_platform.sh --destroy <contract.yml> (AWS destroy: init/validate/destroy)
# run_platform.sh --local [contract.yml] (local emulating tier, no AWS)
# run_platform.sh --decommission <CR> <contract.yml> (gated teardown)
# run_platform.sh --help (show all flags)
#
# Modes:
# --check-only (offline, no AWS/Checkov/DynamoDB — for CI)
@@ -20,19 +17,13 @@
# contract -> resolver -> stack -> adapter -> terraform init/validate/plan/apply -> exit 0
# --destroy (requires AWS creds; use --decommission <CR> for gated production teardown)
# contract -> resolver -> stack -> adapter -> terraform init/validate/destroy -> exit 0
# --local (no AWS creds; local emulating tier D-092)
# contract -> resolver -> adapter -> local S3/ECS/outbox/Lambda stubs -> exit 0
# (default) (requires AWS creds + Checkov + DynamoDB)
# contract -> resolver -> stack -> adapter -> terraform plan -> Checkov ->
# confidence -> outbox
#
# Flags:
# --quiet suppress terraform/checkov streaming (output to log only)
# --decommission gate --destroy with D-070 two-step CR validation (requires <CR>)
# --deploy-uptime deploy the uptime monitoring stack (separate state)
# --local run the headline E2E against the local emulating tier (D-092)
# --environment <name> override the contract's environment at load time (D-088)
# --help, -h show all flags + a one-line description
# --quiet suppress terraform/checkov streaming (output to log only)
# --decommission gate --destroy with D-070 two-step CR validation (requires <CR>)
#
# The contract file is a YAML file validated against schemas/contract.schema.json.
# The resolver (core/contract_resolver.py) resolves it to a Target Stack
@@ -68,36 +59,6 @@ CHANGE_REQUEST_ID=""
ENVIRONMENT_OVERRIDE=""
CONTRACT=""
# P15 (REQ-179): --help / -h prints all flags + a one-line description.
_print_help() {
cat <<'HELP'
Nova platform pipeline — run_platform.sh
Usage:
run_platform.sh <contract.yml> (full e2e with AWS)
run_platform.sh --check-only [contract.yml] (offline, no AWS)
run_platform.sh --plan-only <contract.yml> (AWS plan only)
run_platform.sh --apply <contract.yml> (AWS apply)
run_platform.sh --destroy <contract.yml> (AWS destroy)
run_platform.sh --local [contract.yml] (local emulating tier)
run_platform.sh --decommission <CR> <contract.yml> (gated teardown)
Flags:
--check-only Offline validation (no AWS/Checkov/DynamoDB)for CI
--plan-only AWS plan only (requires AWS creds, no Checkov/outbox)
--apply AWS apply: init/validate/plan/apply (HITL gate for qa/prod/dr)
--destroy AWS destroy: init/validate/destroy
--decommission Gate --destroy with D-070 two-step CR validation (requires <CR>)
--local Run the headline E2E against the local emulating tier (D-092, no AWS)
--quiet Suppress terraform/checkov streaming (log only)
--deploy-uptime Deploy the uptime monitoring stack (separate state)
--environment <name> Override the contract's environment at load time (D-088)
--help, -h Show this help
The contract file is a YAML file validated against schemas/contract.schema.json.
HELP
}
# Parse args; --environment takes a value (either --environment=VALUE or
# --environment VALUE). The contract / changeRequestId are the remaining
# positional args.
@@ -108,7 +69,6 @@ for arg in "$@"; do
continue
fi
case "$arg" in
--help|-h) _print_help; exit 0 ;;
--check-only) CHECK_ONLY=1 ;;
--plan-only) PLAN_ONLY=1 ;;
--apply) APPLY_ONLY=1 ;;
-42
View File
@@ -1,42 +0,0 @@
# terraform/onboarding/ — Consumer deploy-role + ABAC tag grant (P20, REQ-184)
Offline-proven Terraform for the cross-account consumer deploy-role +
`nova:owner` ABAC tag grant. This is the "role grant" half of the
no-humans onboarding flow (D-113); the "request" half is P18 (Lambda
action) + P19 (env-file autogen).
## Scope (D-114)
This Terraform is **offline-proven only** in v1.16:
- `terraform validate` passes.
- `terraform plan` (with `NOVA_AWS_ACCOUNT_ID` set) produces the expected
role + policy.
- **No live apply**`NOVA_LIFECYCLE_MODE=plan` default. Live apply is
deferred to a future feature milestone (D-113/D-114).
## Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `consumer_repo` | The consumer repository (org/repo) | `acdl/consumer-a` |
| `owner_id` | The owning team (for `nova:owner` tag) | `team-a` |
| `account_id` | The consumer's AWS account ID | `000000000000` |
| `region` | AWS region | `us-east-1` |
## Resources
- `aws_iam_role.consumer_deploy` — the consumer's deploy role with a
trust policy (assumed by the consumer's CI runner).
- `aws_iam_role_policy.consumer_invoke` — inline policy granting
`lambda:InvokeFunctionUrl` on the platform Lambda, scoped via
`aws:PrincipalTag/nova:owner == var.owner_id` (ABAC).
- `aws_iam_tag.owner` — tags the role with `nova:owner` + `nova:contract`.
## Usage (offline)
```bash
cd terraform/onboarding
terraform init -backend=false
terraform validate
NOVA_AWS_ACCOUNT_ID=123456789012 terraform plan -var consumer_repo=acdl/my-app -var owner_id=team-x
```
-120
View File
@@ -1,120 +0,0 @@
terraform {
required_version = ">= 1.9, < 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
variable "consumer_repo" {
description = "The consumer repository (org/repo) — for the nova:contract tag."
type = string
default = "acdl/consumer-a"
}
variable "owner_id" {
description = "The owning team (for the nova:owner ABAC tag)."
type = string
default = "team-a"
}
variable "account_id" {
description = "The consumer's AWS account ID (where the deploy role is created)."
type = string
default = "000000000000"
}
variable "region" {
description = "AWS region."
type = string
default = "us-east-1"
}
provider "aws" {
region = var.region
}
# P20 (REQ-184): consumer deploy role the role the consumer's CI runner
# assumes to invoke the platform Lambda + deploy via the reusable workflow.
# The trust policy allows the consumer's CI runner (GitHub Actions /
# Gitea act_runner) to assume this role. In a real deployment, the trust
# policy is scoped to the consumer's OIDC provider; for offline-proven
# mode, a placeholder trust is used.
resource "aws_iam_role" "consumer_deploy" {
name = "nova-${replace(var.consumer_repo, "/", "-")}-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
# Placeholder: in a real deployment, this is the consumer's
# OIDC provider ARN. Offline-proven mode uses a wildcard.
Federated = "arn:aws:iam::${var.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:${var.consumer_repo}:*"
}
}
}
]
})
tags = {
"nova:owner" = var.owner_id
"nova:contract" = var.consumer_repo
"nova:environment" = "dev"
}
}
# P20 (REQ-184): inline policy granting the consumer's deploy role the
# right to invoke the platform Lambda's Function URL, scoped via ABAC
# (aws:PrincipalTag/nova:owner == var.owner_id). The platform Lambda's
# resource-based policy + the consumer_invoke_policy.json template
# enforce the ABAC scope at the Lambda side; this policy grants the
# invoke permission on the consumer side.
resource "aws_iam_role_policy" "consumer_invoke" {
name = "nova-consumer-invoke"
role = aws_iam_role.consumer_deploy.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"lambda:InvokeFunctionUrl",
]
Resource = [
# The platform Lambda ARN (cross-account). The account_id is
# the platform account, not the consumer account. For offline-
# proven mode, a placeholder ARN is used.
"arn:aws:lambda:${var.region}:000000000000:function:nova-contract-ingestor"
]
Condition = {
StringEquals = {
"aws:PrincipalTag/nova:owner" = var.owner_id
}
}
}
]
})
}
output "consumer_deploy_role_arn" {
description = "The ARN of the consumer deploy role."
value = aws_iam_role.consumer_deploy.arn
}
output "consumer_deploy_role_name" {
description = "The name of the consumer deploy role."
value = aws_iam_role.consumer_deploy.name
}
+17 -91
View File
@@ -48,18 +48,10 @@ def _local_lambda_bypass(monkeypatch):
@pytest.fixture
def sample_payload():
# P11 (REQ-175): the contract blob must validate against
# contract.schema.json (requires id/name/environment/infrastructure;
# id matches ^[a-z][a-z0-9-]{2,5}$).
return {
"consumerRepo": "acdl/consumer-a",
"contractId": "contract-001",
"contract": {
"id": "test",
"name": "test-contract",
"environment": "dev",
"infrastructure": {"s3": {"version": "1.0.0", "inputs": {}}},
},
"contract": {"stack": "s3", "environment": "dev"},
"environment": "dev",
"action": "submit_contract",
}
@@ -141,15 +133,16 @@ class TestSubmitContract:
assert item["submittedAt"]["S"] == result["submittedAt"]
# The contract attribute holds the full contract object. boto3's
# resource API serializes a dict as a DynamoDB Map (type "M"); each
# leaf scalar is wrapped in its own type tag. P11 (REQ-175): the
# fixture contract has a nested infrastructure map; assert the
# top-level keys are present (full deep-equality is fragile with
# moto's recursive type wrapping).
actual_contract = item["contract"]["M"]
assert set(actual_contract.keys()) == set(sample_payload["contract"].keys())
assert actual_contract["id"]["S"] == sample_payload["contract"]["id"]
assert actual_contract["name"]["S"] == sample_payload["contract"]["name"]
assert actual_contract["environment"]["S"] == sample_payload["contract"]["environment"]
# leaf scalar is wrapped in its own type tag.
expected_contract = sample_payload["contract"]
actual_contract = item["contract"]
# The resource API stores scalars inside the map with their own type
# tags (e.g. {"S": ...}); unwrap one level for the two known leaves.
unwrapped = {
k: list(v.values())[0] if isinstance(v, dict) and len(v) == 1 else v
for k, v in actual_contract["M"].items()
}
assert unwrapped == expected_contract
def test_submit_contract_sk_contains_contract_id_and_timestamp(self, moto_contracts_table, sample_payload):
result = ingestor._submit_contract(sample_payload)
@@ -160,24 +153,6 @@ class TestSubmitContract:
ts = sk.split("#", 1)[1]
datetime.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ")
def test_oversized_contract_rejected(self, moto_contracts_table, sample_payload):
"""P11 (REQ-175): a contract blob > 256 KB is rejected."""
sample_payload["contract"] = {"blob": "x" * (300 * 1024)}
with pytest.raises(ValueError, match="contract payload too large"):
ingestor._submit_contract(sample_payload)
def test_schema_invalid_contract_rejected(self, moto_contracts_table, sample_payload, monkeypatch):
"""P11 (REQ-175): a contract that fails contract.schema.json
validation is rejected with a clear error."""
# The autouse fixture sets NOVA_LAMBDA_LOCAL_BYPASS; unset it so
# the schema validation runs (the bypass skips schema validation).
monkeypatch.delenv("NOVA_LAMBDA_LOCAL_BYPASS", raising=False)
# The contract schema requires id/name/environment/infrastructure;
# an empty dict fails validation.
sample_payload["contract"] = {}
with pytest.raises(ValueError, match="contract schema validation failed"):
ingestor._submit_contract(sample_payload)
# ---------------------------------------------------------------------------
# report_error (D-055) — GitHub issue creation via the GitHub API
@@ -299,21 +274,20 @@ class TestReportError:
ingestor._report_error(error_payload)
def test_report_error_truncates_stack_trace(self, monkeypatch, error_payload, patched_secrets):
# P11 (REQ-175): a very long stack trace is truncated to
# MAX_ERROR_FIELD_CHARS (10000) in the body (was 2000; aligned).
error_payload["stackTrace"] = "x" * 20000
# A very long stack trace should be truncated to 2000 chars in the body.
error_payload["stackTrace"] = "x" * 5000
calls = self._mock_urlopen(monkeypatch, [
(200, json.dumps({"items": []})),
(201, json.dumps({"number": 1, "html_url": "u"})),
])
result = ingestor._report_error(error_payload)
assert result["status"] == "issue_created"
# The create request body should contain exactly 10000 'x' chars.
# The create request body should contain exactly 2000 'x' chars.
create_req = calls[1]
body = json.loads(create_req.data.decode())
# The body markdown contains the (truncated) stack trace.
assert "x" * 10000 in body["body"]
assert "x" * 10001 not in body["body"]
assert "x" * 2000 in body["body"]
assert "x" * 2001 not in body["body"]
def test_lambda_handler_routes_report_error(self, monkeypatch, error_payload, patched_secrets):
# End-to-end via lambda_handler: action=report_error → 200.
@@ -593,52 +567,4 @@ class TestV14IdentityValidation:
"""The _validate_caller_identity docstring documents the ABAC reliance."""
docstring = ingestor._validate_caller_identity.__doc__
assert "ABAC" in docstring
assert "PrincipalTag" in docstring
class TestOnboardConsumer:
"""P18 (REQ-182): the onboard_consumer action writes a pending CMDB row."""
_ARN = "arn:aws:sts::000:assumed-role/nova-deploy/test"
def test_valid_onboarding_writes_pending_row(self, moto_contracts_table):
payload = {
"action": "onboard_consumer",
"consumerRepo": "acdl/consumer-b",
"requestedEnvironment": "dev",
"ownerId": "team-b",
"billingTag": "cost-center-b",
}
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 200
body = json.loads(resp["body"])
assert body["status"] == "pending"
assert body["action"] == "onboard_consumer"
assert body["requestedEnvironment"] == "dev"
def test_invalid_onboarding_rejected(self, moto_contracts_table):
# An invalid consumerRepo (no /) fails the identity format check
# (which runs for all actions) before the onboarding schema.
payload = {
"action": "onboard_consumer",
"consumerRepo": "not-a-repo-format",
"requestedEnvironment": "dev",
"ownerId": "team-b",
"billingTag": "cost-center-b",
}
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 400
assert "invalid consumerRepo" in json.loads(resp["body"])["error"]
def test_missing_onboarding_field_rejected(self, moto_contracts_table):
payload = {
"action": "onboard_consumer",
"consumerRepo": "acdl/consumer-b",
"requestedEnvironment": "dev",
# ownerId + billingTag missing
}
event = {"body": json.dumps(payload), "requestContext": {"identity": {"userArn": self._ARN}}}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 400
assert "onboarding payload invalid" in json.loads(resp["body"])["error"]
assert "PrincipalTag" in docstring
+1 -12
View File
@@ -34,15 +34,4 @@ class TestDocsCoverage:
assert "How to Write an Adapter" in content
assert "How to Wire" in content
assert "How to Test" in content
assert "Existing Adapters" in content
def test_github_workflows_readme_catalogs_all_workflows():
"""P16 (REQ-180): .github/workflows/README.md catalogs all 7 workflows."""
from pathlib import Path
readme = Path(__file__).resolve().parent.parent / ".github" / "workflows" / "README.md"
assert readme.is_file(), ".github/workflows/README.md missing"
text = readme.read_text()
for wf in ["ci.yml", "deploy.yml", "modules-lifecycle.yml",
"platform-test.yml", "primitives-plan.yml", "patterns-plan.yml",
"release.yml"]:
assert wf in text, f"{wf} not cataloged in .github/workflows/README.md"
assert "Existing Adapters" in content
+2 -17
View File
@@ -21,10 +21,7 @@ class TestEnvironmentCheck:
assert ok is False
assert "nonexistent-env" in msg
assert "onboarding" in msg.lower() or "Environment Onboarding" in msg
# P19 (REQ-183): the message now routes to the self-service
# request path (onboard_consumer), not "contact the platform team".
assert "platform team" not in msg.lower()
assert "onboard_consumer" in msg or "self-service" in msg.lower()
assert "platform team" in msg.lower()
def test_onboarding_message_lists_platform_provisions(self):
msg = _onboarding_message("qa")
@@ -105,16 +102,4 @@ class TestRunPlatformWireIn:
)
assert result.returncode == 0, f"stdout: {result.stdout}\nstderr: {result.stderr}"
assert "PLATFORM CHECK OK" in result.stdout
assert "environment" in result.stdout.lower() or "Step 0" in result.stdout
class TestOnboardingMessageSelfService:
"""P19 (REQ-183): the onboarding message is self-service, not 'contact
the platform team'."""
def test_no_contact_platform_team(self):
msg = _onboarding_message("qa")
assert "contact the platform team" not in msg.lower()
def test_mentions_self_service_request(self):
msg = _onboarding_message("qa")
assert "self-service" in msg.lower() or "onboard_consumer" in msg
assert "environment" in result.stdout.lower() or "Step 0" in result.stdout
-50
View File
@@ -1,50 +0,0 @@
"""Unit tests for core/onboarding.py (P19, REQ-183)."""
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from core.onboarding import generate_env_file, _onboarding_request_message
class TestGenerateEnvFile:
"""P19 (REQ-183): generate_env_file produces a valid env JSON."""
def test_generates_env_with_request_fields(self):
request = {
"consumerRepo": "acdl/consumer-b",
"requestedEnvironment": "qa",
"ownerId": "team-b",
"billingTag": "cost-center-b",
}
env = generate_env_file(request, template_env="dev")
assert env["name"] == "qa"
assert env["ownerId"] == "team-b"
assert env["billingTag"] == "cost-center-b"
assert env["account_id"] == "000000000000" # placeholder
assert "consumer-b" in env["description"]
def test_preserves_template_network_and_state(self):
request = {
"consumerRepo": "acdl/c",
"requestedEnvironment": "prod",
"ownerId": "team-a",
"billingTag": "cc-a",
}
env = generate_env_file(request, template_env="dev")
assert "vpc_cidr" in env["network"]
assert "bucket" in env["state_backend"]
assert env["region"] == "us-east-1"
class TestOnboardingRequestMessage:
"""P19 (REQ-183): the request message is self-service."""
def test_message_mentions_onboard_consumer(self):
msg = _onboarding_request_message("dev")
assert "onboard_consumer" in msg
assert "Nova" in msg
-38
View File
@@ -1,38 +0,0 @@
"""Unit tests for terraform/onboarding (P20, REQ-184)."""
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
ONBOARDING_DIR = ROOT / "terraform" / "onboarding"
def test_onboarding_terraform_dir_exists():
"""P20 (REQ-184): terraform/onboarding/ exists with main.tf + README."""
assert ONBOARDING_DIR.is_dir()
assert (ONBOARDING_DIR / "main.tf").is_file()
assert (ONBOARDING_DIR / "README.md").is_file()
def test_onboarding_terraform_validates():
"""P20 (REQ-184): terraform validate passes for the onboarding module
(offline-proven, D-114). Skipped if terraform is not installed."""
if not subprocess.call(["which", "terraform"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
pytest.skip("terraform not installed")
rc = subprocess.call(
["terraform", "validate"],
cwd=str(ONBOARDING_DIR),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
assert rc == 0, "terraform validate failed for terraform/onboarding/"
def test_onboarding_main_tf_has_nova_tags():
"""P20 (REQ-184): the deploy role is tagged with nova:owner + nova:contract."""
main_tf = (ONBOARDING_DIR / "main.tf").read_text()
assert '"nova:owner"' in main_tf
assert '"nova:contract"' in main_tf
assert "aws_iam_role" in main_tf
assert "lambda:InvokeFunctionUrl" in main_tf