Files
acdl/docs/consumer-guide.md
Jon Chery 25427250ad
acdl-ci / Lint (push) Successful in 9s
acdl-ci / Test (push) Failing after 22s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
Nova Slides Render / render (push) Failing after 24s
docs(P1): consumer guide accuracy fixes — REQ-276..281,290
- Step 3 contract fields table: stale uses/module → real id/name/environment/infrastructure (REQ-276)
- Step 4 caller: add environment: dev to match Step 2 (REQ-277)
- Step 5 stage 8: (dev only) → (autonomous in dev; higher envs apply after HITL) (REQ-278)
- Step 8: rewrite with Shape A destroy-then-rebuild + Shape B cross-ref (REQ-279)
- Per-env section: add Shape B lead sentence (REQ-280)
- Reference table: @v1.19 wording + .yaml→.yml extension fix (REQ-281)
- Tests: rename no-field-editing → both-promotion-shapes + new destroy-on-env-change test (REQ-290)

---ci---
project: acdl
phase: 1
milestone: v1.24
status: execute
requirements: [REQ-276,REQ-277,REQ-278,REQ-279,REQ-280,REQ-281,REQ-290]
---/ci---
2026-08-12 14:26:22 +00:00

21 KiB

Consumer Guide — Declare intent, deploy to AWS

This guide walks a consumer through creating their pipeline and defining a contract that deploys any Nova module to AWS. It is generic across all modules in the registry; static-assets is the worked example, but every step applies to microservice and any future module.

The model

Consumers have their own repos and consume Nova by writing a contract that declares infrastructure (one or more modules), an environment, and inputs. The consumer declares a contract (which infrastructure, which environment, which inputs); the Nova platform owns the pipelines, modules, engine adapter, and evidence stream.

You do not write infrastructure modules, workflow YAML, or adapter code. You write a contract YAML file and the platform does the rest. Your repository contains only your application code, your contracts, and your CI definitions.

flowchart LR
    A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: nova/.github/workflows/deploy.yml@v1.19| B
    B["platform runners<br/>(modules + pipelines + adapters + schemas)"] -->|contract -&gt; resolver -&gt; stack -&gt; adapter<br/>-&gt; security checks -&gt; infrastructure plan -&gt; policy checks<br/>-&gt; confidence -&gt; apply -&gt; evidence event| C
    C["your resources in AWS"]

Versioning the uses: reference

The central deployment pipeline is always versioned with floating MAJOR and MINOR tags (e.g. nova/pipelines/contract.yml@v1.19). Version constraints cannot be expressed inside the contract, so the tag in uses: is the only immutability lever a consumer has. See Versioning for the full rationale.

Unversioned references are discouraged. Do not use @main or a bare nova/pipelines/contract.yml.

Prerequisites

These are the only prerequisites for a consumer repo. You do not need an AWS account, infrastructure tooling, or a runner key — those are platform-managed. See Environments.

  • A consumer GitHub repository for your application code + contracts.
  • A platform-managed environment bound to your repo. The platform team provisions the AWS account, network, state backend, and IAM role. If no environment is bound, your first pipeline run emits a friendly onboarding prompt. See Environments.
  • Authorization to reference the central pipeline. Onboarding grants your repo the right to uses: nova/.github/workflows/deploy.yml@v1.19. Contact the platform team if you have not been onboarded.

Step 1 — Create a consumer repo

Create a repository for your application. The top level holds your app code; your contract lives at .nova/contract.yml. Example for a static site:

my-static-site/
  index.html
  assets/
    style.css
    logo.png
  .nova/
    contract.yaml
  .github/
    workflows/
      deploy.yml

Example for a microservice:

my-microservice/
  app.py
  Dockerfile
  .nova/
    contract.yaml
  .github/
    workflows/
      deploy.yml

Your app code lives at the top level. Your contract lives at .nova/contract.yml regardless of the module you deploy. Your CI definition lives at .github/workflows/deploy.yml.

Step 2 — Reference the central pipeline

In your CI workflow (.github/workflows/deploy.yml), reference the central Nova deployment workflow with a versioned tag (floating MAJOR + MINOR):

jobs:
  deploy:
    uses: nova/.github/workflows/deploy.yml@v1.19
    with:
      contract: .nova/contract.yml
      environment: dev

The versioned tag is the only immutability lever — the consumer's CI workflow pins the platform version. The contract itself no longer carries a uses: field; the version pin lives in the CI workflow reference.

Step 3 — Define the contract

Write .nova/contract.yml. The static-assets example:

environment: dev
id: assets
infrastructure:
  static-assets:
    inputs:
      bucket_name: my-static-site-assets
      region: us-east-1
    version: 1.0.0
name: static-assets

A microservice example:

environment: dev
id: msvc
infrastructure:
  microservice:
    inputs:
      env:
        LOG_LEVEL: info
      image: my-registry/my-microservice:latest
      port: 8080
    version: 1.0.0
name: microservice

Contract fields

Field Type Required Description
id string yes Short operational acronym (3-6 chars, lowercase + digits + hyphens). Becomes stack.name: the Terraform state key (spike/<id>/<env>/terraform.tfstate), the outbox event identity, and the resource naming prefix. Stable across deploys and environment promotions.
name string yes Full human-readable stack name. Becomes stack.title: the display name in PR comments, evidence records, and dashboards.
environment string yes The platform-managed environment to deploy to (dev, qa, prod, or dr). See Environments.
infrastructure object yes Map of modules to deploy, keyed by module name (matching a registry key in modules/registry.json). Each entry carries an optional version (defaults to latest published) and per-module inputs. One entry = single-module deploy; N entries = multi-module manifest.

Module inputs

Each module declares its inputs in its interface.json (primitives) or composition.json (modules). Consult the module catalog for the full list, or read the module's own README under modules/l1/<name>/ or modules/l2/<name>/. Each module also has an examples/ directory with validated consumer contract examples (simple.yaml + complex.yaml

  • variation files) that demonstrate real usage — see the module's ## Examples section.

The contract is validated against the contract schema. An invalid contract (missing field, unknown module, wrong type) fails at the validate-contract stage with a clear error.

Step 4 — Run the pipeline

You do not run platform scripts locally for the happy path. The central deploy workflow is a reusable workflow that the platform runners fetch and execute for you.

The consumer CI definition

Add a thin workflow file to your repo that invokes the reusable Nova deploy workflow with a versioned tag (.github/workflows/deploy.yml):

name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    uses: nova/.github/workflows/deploy.yml@v1.19
    with:
      contract: .nova/contract.yml
      environment: dev

That is the entire consumer-side workflow. When you push to main:

  1. The platform runner resolves uses: nova/.github/workflows/deploy.yml@v1.19 to the reusable workflow at the pinned tag.
  2. A platform-provided runner checks out your repo.
  3. The runner checks out the Nova platform repo into the workspace — this is how the pipeline fetches the platform code at run time. You never clone the platform repo yourself.
  4. The runner installs the runtime dependencies the platform requires.
  5. The runner invokes scripts/run_platform.sh against your .nova/contract.yml.

You see the streamed output (infrastructure plan, policy-check results, confidence signal) in your run logs. The --check-only and --plan-only flags are platform-side modes visible in the pipeline logs; you do not pass them yourself — the reusable workflow selects the mode based on the environment in your contract (dev = full apply; higher environments hold for attestation).

Local validation (optional)

A consumer may clone the Nova platform repo to run --check-only against their contract before pushing — this is optional and not required for the happy path. If you do this, the runtime dependencies must be installed locally, and any AWS credentials follow the Credentials override model: a static key in .env.secrets (gitignored) is rotated out of band by you — the platform guarantees daily rotation for platform-runner runs, not for locally-held copies.

bash scripts/run_platform.sh --check-only path/to/your/.nova/contract.yml

Step 5 — What the pipeline does

Each stage of the central deployment pipeline:

flowchart TD
    S1["validate-contract<br/>schema check"] --> S2
    S2["resolve-stack<br/>contract -&gt; Target Stack"] --> S3
    S3["security checks<br/>(adapter)"] --> S4
    S4["infrastructure plan<br/>(adapter compiles the stack)"] --> S5
    S5["policy checks<br/>(adapter -&gt; PolicyCheckResult)"] --> S6
    S6["confidence<br/>score + band (dev &gt;= 0.50)"] --> S7
    S7["evidence event<br/>to the audit outbox"] --> S8
    S8["infrastructure apply<br/>(autonomous in dev;<br/>higher envs apply after HITL)"]
  1. validate-contract — validates your contract YAML against the contract schema. Fails fast on missing fields, unknown modules, or wrong types.
  2. resolve-stack — the contract resolver resolves your contract to a Target Stack instance. It loads the module's pattern, expands its children, wires your contract inputs to the children's inputs, and emits a stack JSON instance.
  3. security checks (adapter) — security checks run on the resolved stack before any infrastructure is planned.
  4. infrastructure plan (adapter) — the engine adapter compiles the stack to an infrastructure plan. You see the plan in your run logs.
  5. policy checks (adapter) — policy checks run on the plan. The results are normalized to PolicyCheckResult records. Each result has a severity, rule ID, and pass/fail status.
  6. confidence — the confidence signal computes a score from 6 inputs (policy, validation, freshness, source, history, NFRs). For dev, the threshold is ≥ 0.50. If the band is pass, the pipeline proceeds.
  7. evidence event — a hash-chained evidence event is written to the audit outbox.
  8. infrastructure apply (autonomous in dev; higher environments apply after HITL attestation) — the infrastructure plan is applied, creating the resources in your AWS account. An evidence event for the apply is recorded.

Step 6 — What gets created

After a successful dev run, the resources declared by your module's pattern exist in your AWS account, and an evidence event is recorded.

For the static-assets example:

  • An S3 bucket named my-static-site-assets in us-east-1 with versioning enabled.
  • A CloudFront distribution with the S3 bucket as the origin (via Origin Access Control) and HTTPS redirection.
  • A WAFv2 Web ACL (CloudFront-scoped) associated with the distribution.
  • An evidence event in the audit outbox with the contract ID, stack name (static-assets), confidence score, and band.
  • A confidence band of pass (score ≥ 0.50 for dev).

For other modules, consult the module's README (modules/l1/<name>/README.md or modules/l2/<name>/README.md) for the exact resources created.

Step 7 — Upload your content (static-assets example)

The platform provisions the infrastructure; you upload your content. For the static-assets module:

aws s3 sync ./assets s3://my-static-site-assets/ --acl public-read

For a microservice, the platform provisions the ECS service and ALB; you push your container image to the ECR repo the platform created.

Step 8 — Promote to qa / prod

There are two supported promotion shapes. Both are valid; pick the one that fits your repo's workflow.

Shape A — edit the environment field (destroy-then-rebuild)

Change environment in your contract (the infrastructure stays the same). The contract id stays stable, so the platform knows this is the same stack moving to a new environment:

id: assets
name: static-assets
environment: qa   # QA attestation + confidence >= 0.75
infrastructure:
  static-assets:
    version: "1.0.0"
    inputs: { ... }

What happens when you change environment: devenvironment: qa: the platform detects that the environment changed on a known contract id. Before building the new environment, it destroys the prior environment's resources (Terraform state key spike/{id}/dev/) and records an evidence event for the destroy. Only then does it apply the new environment (state key spike/{id}/qa/). There is no orphan path — if the destroy fails, the pipeline fails closed (no apply runs, no resources are left behind). This is full lifecycle management: the platform never creates a state where prior-environment resources are abandoned.

Higher environments require human attestation (a platform-runner deployment approval) and higher confidence thresholds. See Environments for the full table.

Note: the destroy-then-rebuild runs within the same AWS account (the current platform scaffold uses one account). Cross-account promotion (separate accounts per env) is a future milestone.

Shape B — per-environment caller workflows (no editing)

Alternatively, keep one contract per environment (or one contract + the environment workflow input) and run the matching CI job to promote. This avoids the destroy step because each environment has its own state from the first deploy. See Per-environment deployment below for the full pattern.

Step 9 — Compliance extensions

Each module lists compliance extension points for the future compliance milestone (GDPR, SOX, SOC2, DORA). See each module's README under modules/l1/<name>/README.md or modules/l2/<name>/README.md for the per-module extension points. Common examples:

  • KMS key — shared encryption key for SSE.
  • S3 access logs — access logging to a separate audit bucket.
  • Object Lock — 7-year immutable retention for evidence.
  • Public access block — prevent data exfiltration.

Reference

Resource Path Description
Central deployment pipeline contract pipelines/contract.yml The pipeline stages your contract references.
Reusable deploy workflow .github/workflows/deploy.yml The workflow your repo invokes via uses:.
Contract schema schemas/contract.schema.json JSON Schema for consumer contracts.
Stack schema schemas/stack.schema.json JSON Schema for the resolved stack instance.
Module catalog modules/ All primitives and modules.
Sample contract contracts/static-assets.yml The reference example contract (used with caller workflow @v1.19).
Sample contract contracts/microservice.yml The microservice example contract (used with caller workflow @v1.19).
Module examples modules/<name>/examples/ Validated per-module example contracts (simple.yaml + complex.yaml).
Contract resolver core/contract_resolver.py Resolves contracts to stack instances.
Angine adapter adapters/terraform/adapter.py Compiles stack instances to infrastructure.
Platform pipeline runner scripts/run_platform.sh The pipeline runner (platform-side; consumers do not invoke it directly).
Environments environments/ Platform-managed environments + onboarding.
Versioning pipeline/versioning The uses: tag + module versioning.
Platform README README.md How the platform works + how to run the platform repo locally.
Credentials & zero-trust README.md#credentials--zero-trust The OIDC/ABAC default + static-key override model.

Decommissioning a stack

When a consumer needs to tear down a deployed stack, the platform provides a decommission mode on the same deploy pipeline. The decommission process is a 2-step pipeline with HITL SRE gates to prevent accidental destruction:

  1. Request a change request (CR): Contact the platform team to create a change request in the platform CMDB (DynamoDB nova-change-requests table). The CR must be approved before decommission can proceed. The CR includes the consumer repo, contract ID, and the reason for decommission.

  2. Trigger decommission: Update the consumer's deploy workflow call to use mode: decommission with the changeRequestId input:

    uses: nova/.github/workflows/deploy.yml@v1.19
    with:
      contract: .nova/contract.yml
      mode: decommission
      changeRequestId: "CHG0678912"
    
  3. Step 1 — Disable deletion protection (HITL SRE gate): The pipeline validates the CR ID against the CMDB (status must be approved). Then it resolves the contract with deletion_protection: false injected into all resources and runs terraform plan + terraform apply. This removes the prevent_destroy lifecycle meta-argument from all resources. An SRE must approve this step via the GitHub environment decommission-gate-sre.

  4. Step 2 — Zero counts + destroy (HITL SRE gate): The pipeline applies decommission_transform which sets all scalable counts to 0 (desired_count=0, min_capacity=0, max_capacity=0) and deletion_protection=false on all resources. Then it runs terraform plan + terraform apply which destroys all resources (now that deletion protection is off and counts are zeroed). A second SRE must approve this step via the GitHub environment decommission-destroy-sre.

  5. Confirmation: The pipeline confirms the stack is destroyed (terraform state is empty for the stack).

What happens to the per-stack CMK?

The per-stack CMK is not immediately destroyed — it enters a deletion window (default 30 days, configurable via the deletion_window_days input). This ensures any encrypted data can still be decrypted during the deletion window if needed. The CMK is permanently deleted after the window expires.

What happens to the uptime monitoring?

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).

Per-environment deployment

This is Shape B (the alternative to Shape A's edit-and-destroy path in Step 8). Shape B avoids the destroy step because each environment has its own state from the first deploy — no prior environment to tear down.

Nova 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. .nova/static-assets.dev.yml, .nova/static-assets.qa.yml, …). Each sets environment: to its own name and uses interpolation so env-specific values differ automatically:

environment: qa
id: assets
infrastructure:
  static-assets:
    inputs:
      bucket_name: acdl-${env.environment}-${contract.id}-${env.account_id}-${env.region}
      region: ${env.region}
    version: 1.0.0
name: static-assets

Shape 2 — single contract + environment workflow input: the reusable deploy workflow (nova/.github/workflows/deploy.yml@v1.19) 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:

# .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: nova/.github/workflows/deploy.yml@v1.19
    with:
      environment: qa
      contract: .nova/contract.yml

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 nova-qa-state
${env.network.vpc_cidr} the environment's VPC CIDR 10.1.0.0/16
${contract.id} the contract's operational acronym 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).