feat(P41): per-environment CI jobs + environment workflow input

---ci---
project: acdl
phase: 41
milestone: v1.9
status: execute
---/ci---

Phase 41 — per-environment-ci-jobs (REQ-105, REQ-106, D-082):

Per-env contracts (REQ-105):
- contracts/static-assets.{dev,qa,prod,dr}.yaml + microservice.{dev,qa,prod,dr}.yaml
  (8 files, each sets environment: to its own name, uses interpolation).
- Default contracts/static-assets.yaml + microservice.yaml preserved (backwards compat).

Deploy workflow environment input (REQ-106):
- .github/workflows/deploy.yml + .gitea/workflows/deploy.yml (byte-identical):
  new 'environment' workflow_call input (default empty, override).
- scripts/run_platform.sh: --environment <name> flag; exports
  ACDL_ENVIRONMENT_OVERRIDE; re-runs env check against the override.
- core/contract_resolver.py: resolve(environment_override=...) (D-088);
  CLI honors --environment flag + ACDL_ENVIRONMENT_OVERRIDE env var.

Consumer guide (REQ-106):
- docs/consumer-guide.md: 'Per-environment deployment' section with 4
  caller-workflow examples (dev/qa/prod/dr), HITL gate structure
  (approve_qa/approve_prod/approve_dr, D-042), interpolation reference table.
- Documents promotion-without-editing + hybrid model (per-env contracts
  OR single contract + env input).

Tests: +40 (test_per_env_contracts.py, test_deploy_workflow_env_input.py,
test_consumer_guide_per_env_section.py). 446 passed; run_ci.sh green;
deploy workflows byte-identical.
This commit is contained in:
Jon Chery
2026-07-23 04:34:10 +00:00
parent 481cfe760c
commit cd637808f5
17 changed files with 438 additions and 10 deletions
+81 -1
View File
@@ -376,4 +376,84 @@ window if needed. The CMK is permanently deleted after the window expires.
The uptime monitoring stack (deployed with separate state) is not
automatically destroyed by the decommission. It must be destroyed
separately (or left running to monitor the decommissioned stack's
endpoints going dark).
endpoints going dark).
## Per-environment deployment
ACDL supports a **promotion-without-editing** model: you do not edit the
`environment:` field in a contract to promote dev → qa → prod → dr.
Instead, there is **one CI job per environment**, each pointing at its
respective contract (or the same contract + the `environment` workflow
input). Promotion = running the matching job.
### Two shapes (both supported)
**Shape 1 — per-environment contract files:** a consumer repo has one
contract per environment (e.g. `.acdl/static-assets.dev.yaml`,
`.acdl/static-assets.qa.yaml`, …). Each sets `environment:` to its own
name and uses interpolation so env-specific values differ automatically:
```yaml
# .acdl/static-assets.qa.yaml
uses: acdl/pipelines/deploy.yaml@v1.9
module: static-assets
environment: qa
inputs:
bucket_name: acdl-${env.environment}-${contract.module}-${env.account_id}-${env.region}
region: ${env.region}
```
**Shape 2 — single contract + `environment` workflow input:** the
reusable deploy workflow (`acdl/.github/workflows/deploy.yml@v1.9`)
declares an `environment` input. When non-empty, it overrides the
contract's `environment` field at load time (before interpolation), so
the same contract can be promoted by passing a different environment:
```yaml
# .github/workflows/deploy-qa.yml (caller workflow)
on: workflow_dispatch:
inputs:
approve_qa:
description: "Set to true to approve the QA promotion"
type: boolean
required: true
jobs:
deploy-qa:
uses: acdl/.github/workflows/deploy.yml@v1.9
with:
environment: qa
contract: .acdl/contract.yaml
```
### One job per environment
A consumer repo's `.github/workflows/` directory has one caller workflow
per environment:
| File | Environment | Gate |
|------|-------------|------|
| `deploy-dev.yml` | dev | autonomous (no gate, confidence ≥ 0.50) |
| `deploy-qa.yml` | qa | QA HITL (`approve_qa` workflow_dispatch input; `github.actor` is the approver of record) |
| `deploy-prod.yml` | prod | SRE HITL (`approve_prod`; separation-of-duties enforced) |
| `deploy-dr.yml` | dr | SRE HITL (`approve_dr`) |
**Promotion = running the matching job.** No `environment:` field editing.
The approver identity is recorded to the DynamoDB outbox
(`approver_qa` / `approver_prod` / `approver_dr`) and the separation-of-
duties check blocks a prod promotion when `approver_qa == approver_prod`
(see `core/hitl_matrix_design.md`).
### Interpolation reference
| Token | Resolves to | Example |
|-------|-------------|---------|
| `${env.environment}` | the environment name (dev/qa/prod/dr) | `qa` |
| `${env.region}` | the environment's AWS region | `us-east-1` |
| `${env.account_id}` | the environment's AWS account id | `123456789012` |
| `${env.state_backend.bucket}` | the environment's state bucket | `acdl-qa-state` |
| `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.1.0.0/16` |
| `${contract.module}` | the contract's module name | `static-assets` |
| `${contract.environment}` | the contract's environment field | `qa` |
| `${contract.inputs.<name>}` | a contract input value | (as declared) |
Unknown tokens raise `ValueError` (fail loud). Expansion is recursive
(nested map/list values expand too).