feat(P1): remove gitea/gitlab from synced files + simplify docs (REQ-230,231,232)

Genericize forge-detection code: gitea→forge/generic_forge, GITEA_ACTOR→FORGE_ACTOR.
Drop .gitea byte-identity test assertions (keep GitHub-side + contract conformance).
Add test_no_forge_mentions.py guard test (REQ-230).
Delete completed migration docs (NOVA_MIGRATION.md, NOVA_AWS_MIGRATION.md).
Move NO_HUMANS_THESIS.md to .ciagent/ (internal artifact).
Strip ciagent-internal provenance from synced docs (REQ-/D-/P-/CAP- IDs,
milestone headers, .ciagent/PROJECT.md citations).
Trim README.md (reusable deploy section, local key rotation paragraph).
Fix version-tag drift (@v1.13→@v1.19, acdl/→nova/).

---ci---
project: acdl
phase: 1
milestone: v1.20
status: execute
requirements: [REQ-230, REQ-231, REQ-232]
---/ci---
This commit is contained in:
Jon Chery
2026-08-07 18:20:29 +00:00
parent b418d429b5
commit 0d2cbdb423
51 changed files with 246 additions and 914 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# ACDL CI Pipeline — Gitea Actions (dev environment) # Nova CI Pipeline (dev environment)
# #
# This workflow implements the central pipeline contract: # This workflow implements the central pipeline contract:
# pipelines/ci.yml (validated against schemas/pipeline.schema.json) # pipelines/ci.yml (validated against schemas/pipeline.schema.json)
+4 -4
View File
@@ -1,4 +1,4 @@
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment) # Nova Reusable Deploy Workflow (dev environment)
# #
# This reusable workflow implements the central deployment pipeline contract: # This reusable workflow implements the central deployment pipeline contract:
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json) # pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
@@ -8,7 +8,7 @@
# declared difference is the forge/runtime, not the stages or commands. # declared difference is the forge/runtime, not the stages or commands.
# #
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR): # Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea) # uses: nova/.github/workflows/deploy.yml@v1.19
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub) # uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
# #
# Unversioned references (@main, bare) are discouraged — the consumer's setup # Unversioned references (@main, bare) are discouraged — the consumer's setup
@@ -38,8 +38,8 @@
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session # that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>. # policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
# #
# Override (where OIDC is unavailable, e.g. Gitea pending # Override (where OIDC is unavailable, e.g. pending
# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY
# as repository secrets. The platform-managed scheduled pipeline rotates # as repository secrets. The platform-managed scheduled pipeline rotates
# the key on a daily cadence. When .env.secrets is used locally instead, # the key on a daily cadence. When .env.secrets is used locally instead,
# rotating the key out of band is the consumer's responsibility. # rotating the key out of band is the consumer's responsibility.
+2 -2
View File
@@ -1,4 +1,4 @@
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment) # Nova Modules Lifecycle Pipeline (dev environment)
# #
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through # Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
# apply→modify→destroy against live AWS. No per-module Python. The "test" = # apply→modify→destroy against live AWS. No per-module Python. The "test" =
@@ -9,7 +9,7 @@
# terraform files); the composition must be deterministic. # terraform files); the composition must be deterministic.
# #
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical # This workflow implements pipelines/modules-lifecycle.yml (byte-identical
# in .gitea/workflows/ and .github/workflows/). # in .github/workflows/).
# #
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to # Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
+10 -15
View File
@@ -1,35 +1,30 @@
# GitHub Workflows — Nova Platform CI/CD Catalog # GitHub Workflows — Nova Platform CI/CD Catalog
This directory contains the 7 GitHub Actions workflows for the Nova This directory contains the GitHub Actions workflows for the Nova
platform. 3 are byte-identical Gitea mirrors (generated from platform. 3 are generated from `workflows-src/<name>`; 4 are GitHub-only.
`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) ## Shared workflows (generated from source)
These 3 are generated from `workflows-src/<name>` by These 3 are generated from `workflows-src/<name>`. Run `python3 scripts/sync_workflows.py --check` to verify
`scripts/sync_workflows.py`; the `.gitea/workflows/<name>` mirror is kept
byte-identical. Run `python3 scripts/sync_workflows.py --check` to verify
no drift. no drift.
| Workflow | Trigger | Inputs | Required Secrets | Purpose | | Workflow | Trigger | Inputs | Required Secrets | Purpose |
|----------|---------|--------|------------------|---------| |----------|---------|--------|------------------|---------|
| `ci.yml` | `pull_request: [main]` | — | — | Lint + test + check-only (runs on every PR) | | `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`) | | `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: nova/.github/workflows/deploy.yml@v1.19`) |
| `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) | | `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) ## GitHub-only workflows
These 4 have no Gitea counterpart (Gitea act_runner lacks the features These 4 have no counterpart (the dev forge lacks the features
they require — reusable workflows, matrix `needs`, release API). See they require — reusable workflows, matrix `needs`, release API).
`.gitea/workflows/README.md` for the limitation rationale.
| Workflow | Trigger | Inputs | Required Secrets | Purpose | | Workflow | Trigger | Inputs | Required Secrets | Purpose |
|----------|---------|--------|------------------|---------| |----------|---------|--------|------------------|---------|
| `platform-test.yml` | `pull_request: [main]` | — | — | Lint + unit + integration + schema-validation (replaces `ci.yml` for PRs) | | `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) | | `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) | | `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 | | `release.yml` | `push: [main]` | — | `NOVA_RELEASE_TOKEN` | Semver tag + MAJOR.MINOR/MAJOR floating-tag maintenance + release creation on merge to main |
## Reusable deploy workflow (`deploy.yml`) ## Reusable deploy workflow (`deploy.yml`)
@@ -38,7 +33,7 @@ Consumer repos invoke the deploy workflow via a versioned tag:
```yaml ```yaml
jobs: jobs:
deploy: deploy:
uses: acdl/.github/workflows/deploy.yml@v1.15 uses: nova/.github/workflows/deploy.yml@v1.19
with: with:
contract: .nova/contract.yml contract: .nova/contract.yml
environment: dev environment: dev
+1 -1
View File
@@ -1,4 +1,4 @@
# ACDL CI Pipeline — Gitea Actions (dev environment) # Nova CI Pipeline (dev environment)
# #
# This workflow implements the central pipeline contract: # This workflow implements the central pipeline contract:
# pipelines/ci.yml (validated against schemas/pipeline.schema.json) # pipelines/ci.yml (validated against schemas/pipeline.schema.json)
+4 -4
View File
@@ -1,4 +1,4 @@
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment) # Nova Reusable Deploy Workflow (dev environment)
# #
# This reusable workflow implements the central deployment pipeline contract: # This reusable workflow implements the central deployment pipeline contract:
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json) # pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
@@ -8,7 +8,7 @@
# declared difference is the forge/runtime, not the stages or commands. # declared difference is the forge/runtime, not the stages or commands.
# #
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR): # Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea) # uses: nova/.github/workflows/deploy.yml@v1.19
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub) # uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
# #
# Unversioned references (@main, bare) are discouraged — the consumer's setup # Unversioned references (@main, bare) are discouraged — the consumer's setup
@@ -38,8 +38,8 @@
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session # that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>. # policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
# #
# Override (where OIDC is unavailable, e.g. Gitea pending # Override (where OIDC is unavailable, e.g. pending
# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY
# as repository secrets. The platform-managed scheduled pipeline rotates # as repository secrets. The platform-managed scheduled pipeline rotates
# the key on a daily cadence. When .env.secrets is used locally instead, # the key on a daily cadence. When .env.secrets is used locally instead,
# rotating the key out of band is the consumer's responsibility. # rotating the key out of band is the consumer's responsibility.
+2 -2
View File
@@ -1,4 +1,4 @@
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment) # Nova Modules Lifecycle Pipeline (dev environment)
# #
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through # Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
# apply→modify→destroy against live AWS. No per-module Python. The "test" = # apply→modify→destroy against live AWS. No per-module Python. The "test" =
@@ -9,7 +9,7 @@
# terraform files); the composition must be deterministic. # terraform files); the composition must be deterministic.
# #
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical # This workflow implements pipelines/modules-lifecycle.yml (byte-identical
# in .gitea/workflows/ and .github/workflows/). # in .github/workflows/).
# #
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to # Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,
+1
View File
@@ -41,3 +41,4 @@ metrics/lifecycle/
*.crt *.crt
*.jks *.jks
*.keystore.coverage *.keystore.coverage
.coverage
+3 -23
View File
@@ -219,23 +219,9 @@ bash scripts/run_ci.sh --quiet # suppress per-stage banners
### Reusable deploy workflow ### Reusable deploy workflow
The deployment pipeline is defined by a **central deployment pipeline Consumer repos invoke the deploy pipeline via `.github/workflows/deploy.yml`
contract** (`pipelines/contract.yml`, validated against (a reusable GitHub Actions workflow, versioned tag `nova/.github/workflows/deploy.yml@v1.19`).
`schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a See the [Consumer guide](docs/consumer-guide.md) for the end-to-end happy path.
**reusable workflow**:
- `.github/workflows/deploy.yml` — GitHub Actions (production)
The workflow implements the same stages as `pipelines/contract.yml`
(validate-contract → resolve-stack → security checks → infrastructure plan
→ policy checks → confidence → evidence event → apply). A consumer repo
invokes the reusable workflow via a **versioned tag** (floating MAJOR +
MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.13`). The workflow checks
out the consumer repo, then checks out the Nova platform repo into the
runner workspace, and runs `scripts/run_platform.sh` against the consumer's
contract — the consumer never clones the platform repo or invokes its
scripts locally. See the [Consumer guide](docs/consumer-guide.md) for the
end-to-end happy path.
### Output streaming (run_platform.sh) ### Output streaming (run_platform.sh)
@@ -310,12 +296,6 @@ documented alternative:
runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing. runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing.
- The platform rotates platform-runner keys on a **daily cadence** - The platform rotates platform-runner keys on a **daily cadence**
rotation is not the consumer's burden in the platform-runner path. rotation is not the consumer's burden in the platform-runner path.
- **When `.env.secrets` is used locally**, rotating the key **out of band is
the consumer's responsibility**. The platform guarantees daily rotation
for platform-runner runs; it does not guarantee rotation for
locally-held copies. The consumer must rotate a local key via
`scripts/rotate_spike_key.sh` (or equivalent) on their own cadence.
No long-lived credential is permitted persistently — the platform-runner No long-lived credential is permitted persistently — the platform-runner
key's useful lifetime is one workflow run, and the local alternative is key's useful lifetime is one workflow run, and the local alternative is
rotated at least daily (platform-runner) or out of band (local). rotated at least daily (platform-runner) or out of band (local).
+3 -3
View File
@@ -62,7 +62,7 @@ path above remains the v1.9 production audit record.
**platform-level KMS key** (not per-contract — a per-contract key would **platform-level KMS key** (not per-contract — a per-contract key would
explode the key-management surface), rotated **quarterly**. The `jws` explode the key-management surface), rotated **quarterly**. The `jws`
field is added to the event shape when this ships. field is added to the event shape when this ships.
- **Async worker + DLQ:** a Lambda (or a Gitea Actions scheduled workflow) - **Async worker + DLQ:** a Lambda (or a forge Actions scheduled workflow)
reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an
SQS dead-letter queue for failed writes. RTO = DLQ replay. SQS dead-letter queue for failed writes. RTO = DLQ replay.
- **Daily checkpoints (§9):** a daily job reads the last event hash and - **Daily checkpoints (§9):** a daily job reads the last event hash and
@@ -86,7 +86,7 @@ log" anti-goal requires.
D-083 ships). D-083 ships).
- `prev_event_hash` (chain link; `GENESIS` for the first event). - `prev_event_hash` (chain link; `GENESIS` for the first event).
- `hash` (this event's SHA-256 over canonical JSON). - `hash` (this event's SHA-256 over canonical JSON).
- `approver_qa` (Gitea/GitHub username of the QA approver; populated on - `approver_qa` (CI username of the QA approver; populated on
qa-promotion by v1.9's `hitl_gates.attest` — D-042). qa-promotion by v1.9's `hitl_gates.attest` — D-042).
- `approver_prod` (SRE username; populated on prod-promotion by v1.9's - `approver_prod` (SRE username; populated on prod-promotion by v1.9's
`hitl_gates.attest`). `hitl_gates.attest`).
@@ -112,7 +112,7 @@ log" anti-goal requires.
- **D-042** — approver identities (`approver_qa`, `approver_prod`, - **D-042** — approver identities (`approver_qa`, `approver_prod`,
`approver_dr`) live in the outbox; the separation-of-duties check `approver_dr`) live in the outbox; the separation-of-duties check
(`core/separation_of_duties.py`) reads `approver_qa` and compares (`core/separation_of_duties.py`) reads `approver_qa` and compares
to the prod-dispatch `gitea.actor` / `github.actor`. v1.9's to the prod-dispatch CI actor. v1.9's
`hitl_gates.attest` populates these attributes. `hitl_gates.attest` populates these attributes.
- **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily - **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily
checkpoints deferred to a future milestone. Requires non-offline- checkpoints deferred to a future milestone. Requires non-offline-
+4 -4
View File
@@ -1,6 +1,6 @@
"""HITL pre-execution attestation gates (REQ-108, D-084). """HITL pre-execution attestation gates (REQ-108, D-084).
Records the approver identity (`gitea.actor` / `github.actor`) to the Records the approver identity (the CI actor (GITHUB_ACTOR or FORGE_ACTOR)) to the
DynamoDB outbox for the contractId (attribute `approver_qa` / DynamoDB outbox for the contractId (attribute `approver_qa` /
`approver_prod` / `approver_dr`), runs the separation-of-duties check on `approver_prod` / `approver_dr`), runs the separation-of-duties check on
prod, invokes the 8-concern attestation matrix for the target env, and prod, invokes the 8-concern attestation matrix for the target env, and
@@ -29,7 +29,7 @@ def attest(contract_id: str, env: str, approver: str,
Args: Args:
contract_id: the contract UUID. contract_id: the contract UUID.
env: dev/qa/prod/dr. env: dev/qa/prod/dr.
approver: the approver's username (`gitea.actor` / `github.actor`). approver: the approver's username (the CI actor (GITHUB_ACTOR or FORGE_ACTOR)).
evidence: optional operator-supplied evidence artifacts (for the evidence: optional operator-supplied evidence artifacts (for the
attestation matrix operator-supplied concerns). attestation matrix operator-supplied concerns).
outbox_client: optional moto-mocked DynamoDB outbox client for tests. outbox_client: optional moto-mocked DynamoDB outbox client for tests.
@@ -41,7 +41,7 @@ def attest(contract_id: str, env: str, approver: str,
return (True, "dev autonomous (no HITL gate)") return (True, "dev autonomous (no HITL gate)")
if not approver: if not approver:
return (False, f"no approver identity for {env} (GITHUB_ACTOR/GITEA_ACTOR unset)") return (False, f"no approver identity for {env} (GITHUB_ACTOR/FORGE_ACTOR unset)")
attr = _approver_attr(env) attr = _approver_attr(env)
if not attr: if not attr:
@@ -88,7 +88,7 @@ def attest(contract_id: str, env: str, approver: str,
def approver_from_env() -> Optional[str]: def approver_from_env() -> Optional[str]:
"""Read the approver identity from the environment.""" """Read the approver identity from the environment."""
return os.environ.get("GITHUB_ACTOR") or os.environ.get("GITEA_ACTOR") return os.environ.get("GITHUB_ACTOR") or os.environ.get("FORGE_ACTOR")
if __name__ == "__main__": if __name__ == "__main__":
+15 -15
View File
@@ -18,32 +18,32 @@ gates. No partial deployment to roll back on rejection (qa, prod); dr is
a separate deployment against a separate cluster/region. The a separate deployment against a separate cluster/region. The
canary/deployment-rollback model is explicitly not in scope for v1. canary/deployment-rollback model is explicitly not in scope for v1.
## Gitea-specific gate mechanics (D-042) ## Forge-specific gate mechanics (D-042)
Gitea has **no Environments API** and ignores `environment:` blocks The dev forge has **no Environments API** and ignores `environment:` blocks
(v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate (v1.0 D-013; re-confirmed in RESEARCH TARGET 1). The pre-execution gate
is modeled as a `workflow_dispatch` with approval inputs: is modeled as a `workflow_dispatch` with approval inputs:
- **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch - **qa gate:** `workflow_dispatch` with `approve_qa: true`; the dispatch
run's `gitea.actor` is the QA approver. run's `CI actor` is the QA approver.
- **prod gate:** `workflow_dispatch` with `approve_prod: true`; - **prod gate:** `workflow_dispatch` with `approve_prod: true`;
`gitea.actor` is the SRE approver. `CI actor` is the SRE approver.
- **dr gate:** `workflow_dispatch` with `approve_dr: true`; same. - **dr gate:** `workflow_dispatch` with `approve_dr: true`; same.
The approver identity of record = `gitea.actor` of the dispatch run The approver identity of record = `CI actor` of the dispatch run
(D-042). There is no other approval-identity signal in Gitea. The real (D-042). There is no other approval-identity signal in the dev forge. The real
OIDC path (blocked on go-gitea/gitea#36988) does not change this — OIDC path (blocked on upstream forge OIDC support) does not change this —
OIDC authorizes the *runner* to AWS, it does not change how the platform OIDC authorizes the *runner* to AWS, it does not change how the platform
records the *human* approver. records the *human* approver.
On GitHub, the equivalent is `github.actor` of the `workflow_dispatch` On GitHub, the equivalent is `CI actor` of the `workflow_dispatch`
run; GitHub Environments with required reviewers are the native gate, run; GitHub Environments with required reviewers are the native gate,
but the `workflow_dispatch` approval-input fallback is used for but the `workflow_dispatch` approval-input fallback is used for
byte-identical Gitea + GitHub workflows. byte-identical across forges.
## Reviewer routing (ARCHITECTURE.md §10.2) ## Reviewer routing (ARCHITECTURE.md §10.2)
Gitea CODEOWNERS routes the right reviewer to the right gate: CODEOWNERS routes the right reviewer to the right gate:
- qa → QA team - qa → QA team
- prod → SRE team - prod → SRE team
@@ -105,7 +105,7 @@ concern is missing or expired for prod/dr.
| 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit `PENDING_ATTESTATION_TIMEOUT_WARNING` event | | 1 business day | PENDING_ATTESTATION_WARNING | Notify team + platform on-call (elevated path); emit `PENDING_ATTESTATION_TIMEOUT_WARNING` event |
| 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit `PENDING_ATTESTATION_AUTO_FREEZE` event; new submission linked via `supersedes` | | 2 business days | PENDING_ATTESTATION_AUTO_FREEZE | Auto-freeze; require re-submission; emit `PENDING_ATTESTATION_AUTO_FREEZE` event; new submission linked via `supersedes` |
**Implementation:** a Gitea `on: schedule` workflow (runs hourly) that **Implementation:** an `on: schedule` workflow (runs hourly) that
scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts` scans the DynamoDB outbox for `PENDING_ATTESTATION` events with `ts`
older than 1/2 business days and emits the warn/freeze events. Not older than 1/2 business days and emits the warn/freeze events. Not
implemented in v1.9 (roadmap item; the attestation gates themselves are implemented in v1.9 (roadmap item; the attestation gates themselves are
@@ -126,11 +126,11 @@ The identity-distinctness check is platform-internal, not GitHub-native,
not Kyverno (in v1). Sequence: not Kyverno (in v1). Sequence:
1. On promotion dev → qa, the platform reads the QA approver's identity 1. On promotion dev → qa, the platform reads the QA approver's identity
from the `workflow_dispatch` run's `gitea.actor` (or `github.actor`) from the `workflow_dispatch` run's `CI actor`
and writes it to the DynamoDB outbox keyed by `contractId` (attribute and writes it to the DynamoDB outbox keyed by `contractId` (attribute
`approver_qa`). `approver_qa`).
2. On promotion qa → prod, the platform reads the stored `approver_qa` 2. On promotion qa → prod, the platform reads the stored `approver_qa`
from the outbox and the new SRE approver's `gitea.actor` from the from the outbox and the new SRE approver identity from the
prod-dispatch run. prod-dispatch run.
3. If `approver_qa == approver_prod`, the platform blocks the prod 3. If `approver_qa == approver_prod`, the platform blocks the prod
promotion, writes a `SEPARATION_OF_DUTIES_VIOLATION` event to the promotion, writes a `SEPARATION_OF_DUTIES_VIOLATION` event to the
@@ -163,8 +163,8 @@ v1.9 (Phase 41 + Phase 42) wires the gates end-to-end:
## Decision trail ## Decision trail
- **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch` - **D-042** — approver identity = `CI actor` of the `workflow_dispatch`
run; no Environments API in Gitea. On GitHub, `github.actor`. run; no Environments API in the dev forge.
- **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback, - **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback,
re-used for the real platform's pre-execution gate model. re-used for the real platform's pre-execution gate model.
- **D-084** (v1.9) — 8-concern attestation matrix: offline-testable - **D-084** (v1.9) — 8-concern attestation matrix: offline-testable
+8 -8
View File
@@ -27,7 +27,7 @@ CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "nova-change-req
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "nova/github-token") GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "nova/github-token")
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/acdl") PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/acdl")
# P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE # P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE
# to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea. # to a compatible forge API root (e.g. https://forge.example.com/api/v1).
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com") 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). # P11 (REQ-175): consistent cap for error/stackTrace fields (was 10k vs 2k).
@@ -96,22 +96,22 @@ def _iso8601_now():
def _forge_type(): def _forge_type():
"""P1-9: Detect whether the API base is GitHub or Gitea. """Detect whether the API base is GitHub or a compatible forge.
Gitea API roots contain '/api/v1'; GitHub's is 'api.github.com'. Compatible forge API roots contain '/api/v1'; GitHub's is 'api.github.com'.
""" """
if "/api/v1" in GITHUB_API_BASE: if "/api/v1" in GITHUB_API_BASE:
return "gitea" return "generic_forge"
return "github" return "github"
def _issues_search_url(owner, repo, encoded_query): def _issues_search_url(owner, repo, encoded_query):
"""P1-9: Build the issue search URL based on forge type. """Build the issue search URL based on forge type.
GitHub uses /search/issues?q=...; Gitea uses /repos/{owner}/{repo}/issues?... GitHub uses /search/issues?q=...; compatible forges use /repos/{owner}/{repo}/issues?...
with query params (no /search/issues endpoint). with query params (no /search/issues endpoint).
""" """
if _forge_type() == "gitea": if _forge_type() == "generic_forge":
return ( return (
f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues" f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
f"?state=open&type=issues&q={encoded_query}" f"?state=open&type=issues&q={encoded_query}"
@@ -123,7 +123,7 @@ def _issues_search_url(owner, repo, encoded_query):
def _issues_create_url(owner, repo): def _issues_create_url(owner, repo):
"""URL for creating an issue (same pattern for both GitHub + Gitea).""" """URL for creating an issue (same pattern across forges)."""
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues" return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
+1 -1
View File
@@ -1,6 +1,6 @@
"""Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md """Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md
§10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the §10.3, D-042). Reads `approver_qa` from the DynamoDB outbox for the
contractId, compares to the prod-dispatch `gitea.actor` / `github.actor`. contractId, compares to the prod-dispatch the CI actor.
Blocks on equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt Blocks on equality, emits `SEPARATION_OF_DUTIES_VIOLATION`, routes a halt
artifact to SRE on-call. artifact to SRE on-call.
-2
View File
@@ -1,7 +1,5 @@
# Nova Metrics Catalog # Nova Metrics Catalog
> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (REQ-195)
> Generated: 2026-08-04
This is the canonical catalog of every executive KPI in Nova's This is the canonical catalog of every executive KPI in Nova's
leadership metrics layer. Each metric carries a **status**: leadership metrics layer. Each metric carries a **status**:
-2
View File
@@ -1,7 +1,5 @@
# Nova Deferred Metrics Activation Roadmap # Nova Deferred Metrics Activation Roadmap
> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (REQ-210)
> Generated: 2026-08-04
This document lists all 8 deferred metrics + the onboarding-funnel This document lists all 8 deferred metrics + the onboarding-funnel
"granted" half, with their blocking decisions, unblock requirements, "granted" half, with their blocking decisions, unblock requirements,
-2
View File
@@ -1,7 +1,5 @@
# Nova Metrics Views — PowerBI Data Dictionary # Nova Metrics Views — PowerBI Data Dictionary
> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (REQ-190, REQ-209)
> Generated: 2026-08-04
This document is the column-level data dictionary for the PowerBI export This document is the column-level data dictionary for the PowerBI export
views in `metrics/powerbi/`. Each fact/dimension table and placeholder views in `metrics/powerbi/`. Each fact/dimension table and placeholder
-270
View File
@@ -1,270 +0,0 @@
# Nova AWS Resource Migration Runbook (REQ-163, P4)
> **Milestone:** v1.15-Nova (Wave 4, P4). Renames every `acdl-*` AWS
> resource name → `nova-*` via Terraform. This is the heaviest Terraform
> phase of the rebrand and requires a **maintenance window**.
>
> **Plan-validated only.** Per A1, `NOVA_LIFECYCLE_MODE` defaults to
> `plan` (no live AWS mutation from CI). `terraform validate` passes; the
> live apply steps below are executed by a platform operator during the
> scheduled maintenance window. Each step has a verification + rollback.
## Scope (renamed resources)
| AWS resource | Before | After | Strategy |
|---|---|---|---|
| KMS alias | `alias/acdl-platform` | `alias/nova-platform` | cheap rename |
| SNS topic | `acdl-sod-halt` | `nova-sod-halt` | recreate |
| Security group | `acdl-ecs-sg` | `nova-ecs-sg` | recreate |
| Lambda (role/policy/function) | `acdl-contract-ingestor` | `nova-contract-ingestor` | recreate |
| DynamoDB contracts | `acdl-contracts` | `nova-contracts` | scan + copy |
| DynamoDB change-requests | `acdl-change-requests` | `nova-change-requests` | scan + copy |
| Secrets Manager secret | `acdl/github-token` | `nova/github-token` | recreate + re-store |
| ECR repo | `acdl-microservice` | `nova-microservice` | re-push |
| ECS cluster/service/task/role | `acdl-microservice` | `nova-microservice` | recreate |
| IAM user + policy | `acdl-spike-runner` (+ `-policy`) | `nova-spike-runner` (+ `-policy`) | re-bootstrap |
| IAM act-runner role | `acdl-act-runner-role` | `nova-act-runner-role` | re-bootstrap |
| IAM deploy role | `acdl-deploy-<repo>` | `nova-deploy-<repo>` | re-bootstrap |
| S3 state bucket | `acdl-tfstate-581513795199-us-east-1` | `nova-tfstate-581513795199-us-east-1` | `-migrate-state` |
| DynamoDB outbox | `acdl-outbox` | `nova-outbox` | scan + copy |
| Platform VPC/subnet/IGW/RT | `acdl-shared*` | `nova-shared*` | recreate (brief downtime) |
| CI VPC/subnet/SG/cluster | `acdl-ci-*` | `nova-ci-*` | recreate (CI-only) |
| ALB name prefix | `acdl-alb` | `nova-alb` | recreate (brief downtime, LAST) |
## Migration ordering (binding)
Order: **KMS alias → SNS/SG → Lambda → DynamoDB → ECR → IAM → state bucket → ALB**.
Each step is independently rollback-able. The ALB is last because it
requires the briefest downtime window.
---
## Pre-flight
1. **Announce the maintenance window** (consumers are notified via the
P1 migration guide `docs/NOVA_MIGRATION.md`).
2. **Back up state** for every stack (see §State bucket — back up the
state JSON *before* `-migrate-state`).
3. Confirm `NOVA_LIFECYCLE_MODE=plan` (default) so CI does not mutate
AWS during the window.
4. Confirm the new `nova-*` destination tables/repos will be created by
the same Terraform apply (no manual pre-creation needed).
## Step 1 — KMS alias (`alias/acdl-platform` → `alias/nova-platform`)
- **Command (in `terraform/platform/`):**
```bash
terraform init -upgrade
terraform apply -replace=aws_kms_alias.nova_platform
```
(Terraform destroys the old alias + creates the new one — aliases are
cheap; the underlying key ID is unchanged.)
- **Verify:** `aws kms list-aliases --query 'Aliases[?AliasName==`alias/nova-platform`]'` returns the new alias; `alias/acdl-platform` is gone.
- **Rollback:** `terraform apply -replace=aws_kms_alias.nova_platform` against the prior revision (re-creates `alias/acdl-platform`). Resources encrypted by the key are unaffected (key ID unchanged).
## Step 2 — SNS topic + Security group (recreate)
- **Command:** `terraform apply` in `terraform/platform/`.
- SNS `acdl-sod-halt` → `nova-sod-halt` (the topic ARN changes; update `NOVA_SOD_HALT_TOPIC_ARN` wherever it is set).
- SG `acdl-ecs-sg` → `nova-ecs-sg` (the security group is re-attached to running ECS tasks; brief task restart).
- **Verify:** `aws sns list-topics` shows `nova-sod-halt`; `aws ec2 describe-security-groups` shows `nova-ecs-sg`.
- **Rollback:** `terraform apply` the prior revision re-creates the `acdl-*` names. The SNS topic has no message backlog (halt artifacts are fire-and-forget); the SG drift resolves on next task deploy.
## Step 3 — Lambda (recreate)
- **Command:** `terraform apply` in `terraform/platform/`.
- Lambda function `acdl-contract-ingestor` → `nova-contract-ingestor`.
- Execution role `acdl-contract-ingestor-role` → `nova-contract-ingestor-role`.
- Inline policy `acdl-contract-ingestor-policy` → `nova-contract-ingestor-policy`.
- The Lambda env vars (`CONTRACTS_TABLE`, `GITHUB_TOKEN_SECRET_ID`) now resolve to `nova-*` defaults.
- **Verify:** `aws lambda list-functions` shows `nova-contract-ingestor`; the Function URL returns 200 on a SigV4-signed invoke. The `consumer_invoke_policy.json` rendered output (Terraform `consumer_invoke_policy_rendered`) now references `function:nova-contract-ingestor` — re-distribute to consumer deploy roles.
- **Rollback:** `terraform apply` the prior revision re-creates `acdl-contract-ingestor`. Consumer deploy roles must point back at the old Function ARN (re-distribute the prior `consumer_invoke_policy.json`).
## Step 4 — DynamoDB (scan + copy)
DynamoDB table names are immutable post-creation, so the migration is a
**scan + copy** (not a rename). The new `nova-*` tables are created by
the same Terraform apply (Step 3). The data-migration script copies
every item and verifies row counts.
- **Command (from repo root):**
```bash
# Dry-run first (no writes):
python3 scripts/migrate_dynamodb_data.py
# Execute the copy:
python3 scripts/migrate_dynamodb_data.py --apply
# A single table:
python3 scripts/migrate_dynamodb_data.py --table contracts --apply
```
The script scans `acdl-contracts` → copies to `nova-contracts`, and
`acdl-change-requests` → `nova-change-requests`, then verifies the
destination row count == source row count (re-scan, not
`DescribeTable.ItemCount` which lags ~6h).
- **Verify:**
```bash
# Row counts must match (printed by the script). Manual cross-check:
aws dynamodb scan --table-name nova-contracts --select COUNT
aws dynamodb scan --table-name acdl-contracts --select COUNT
```
Then **point consumers at the new tables** (the Lambda already reads
`nova-*` defaults; any direct DynamoDB consumers update their env).
- **Keep the old tables** (`acdl-contracts`, `acdl-change-requests`)
until consumers are verified reading from `nova-*`. **Deletion is a
manual post-verification step:**
```bash
aws dynamodb delete-table --table-name acdl-contracts
aws dynamodb delete-table --table-name acdl-change-requests
```
Only delete after a full soak period confirms `nova-*` reads succeed.
- **Rollback:** Re-point consumers at `acdl-*` (the old tables are
retained). The copy is additive (no data loss). To roll back a partial
copy, re-run `--apply` (idempotent — `PutItem` overwrites).
### Outbox table (`acdl-outbox` → `nova-outbox`)
The evidence outbox table follows the same scan+copy pattern (it is
created by `terraform/bootstrap/create_state_backend.py`).
- **Command:** `python3 scripts/migrate_dynamodb_data.py --source acdl-outbox --dest nova-outbox --apply`
- The `core/outbox_writer.py` default + `core/regression_verify.py`
CAP-015 probe now reference `nova-outbox` (P4 updated both). The
regression gate's live-AWS CAP-015 will return `Verified` once the
`nova-outbox` table exists live; until then it is `Decayed` (the gate
is re-run at milestone complete after the live migration).
## Step 5 — ECR (re-push)
- **Command:** `terraform apply` in `terraform/microservice/` creates
the new `nova-microservice` ECR repo. Re-push the image:
```bash
python3 scripts/push_consumer_image.py # creates nova-microservice + prints docker tag/push
```
(The script's `ECR_REPO_NAME` is now `nova-microservice`.)
- **Verify:** `aws ecr describe-repositories` shows `nova-microservice`; `docker pull <acct>.dkr.ecr.us-east-1.amazonaws.com/nova-microservice:latest` succeeds.
- **Rollback:** The old `acdl-microservice` repo is retained until the
soak passes. Re-push to it if a rollback is needed. Delete it manually:
`aws ecr delete-repository --repository-name acdl-microservice --force`.
## Step 6 — IAM (re-bootstrap)
- **Command:**
```bash
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
python3 terraform/bootstrap/create_state_backend.py # creates nova-outbox (idempotent)
python3 terraform/bootstrap/create_iam_user.py # creates nova-spike-runner
python3 terraform/bootstrap/apply_iam_baseline.py # creates nova-spike-runner-policy + nova-act-runner-role
bash scripts/rotate_spike_key.sh # rotates the nova-spike-runner key
```
The deploy role `acdl-deploy-<repo>` → `nova-deploy-<repo>` is
created by the bootstrap (the deploy workflow
`.gitea/.github/workflows/deploy.yml` now references
`role/nova-deploy-{1}`).
- **Verify:** `aws iam get-user --user-name nova-spike-runner`;
`aws iam list-attached-user-policies --user-name nova-spike-runner`
shows `nova-spike-runner-policy`;
`aws iam get-role --role-name nova-act-runner-role`.
- **Rollback:** Re-run the prior bootstrap scripts (they create
`acdl-spike-runner` + `acdl-act-runner-role`). The deploy workflow's
`role-to-assume` must be reverted to `acdl-deploy-` (prior revision).
## Step 7 — State bucket (`acdl-tfstate-*` → `nova-tfstate-*`, `-migrate-state`)
The S3 state backend is renamed. Terraform's `-migrate-state` copies the
state objects to the new bucket. **Back up the state JSON first.**
- **Back up state (per stack):**
```bash
for stack in platform microservice ci-vpc; do
aws s3 cp s3://acdl-tfstate-581513795199-us-east-1/$stack/terraform.tfstate \
./backup-$stack.tfstate
done
```
- **Command (per stack):** the backend config in each
`terraform/*/terraform.tf` now points at `nova-tfstate-...`.
```bash
cd terraform/platform
terraform init -migrate-state # copies state acdl-tfstate → nova-tfstate
cd ../microservice
terraform init -migrate-state
cd ../ci-vpc
terraform init -migrate-state
```
- **Verify:** `aws s3 ls s3://nova-tfstate-581513795199-us-east-1/`
shows the state keys; `terraform state list` in each dir lists the
expected resources.
- **Rollback:** Point the backend back at `acdl-tfstate-*` and re-run
`terraform init -migrate-state` (restores from the backup bucket). The
old `acdl-tfstate-*` bucket is retained until the soak passes. Delete
it manually:
`aws s3 rb s3://acdl-tfstate-581513795199-us-east-1 --force`.
## Step 8 — ALB (recreate, brief downtime, LAST)
The ALB is last because its recreation requires the briefest downtime
window (the ECS service is re-attached to the new target group).
- **Command:** `terraform apply` in `terraform/microservice/`. The ALB
`acdl-microservice` / `acdl-alb` → `nova-microservice` / `nova-alb`.
- **Verify:** `aws elbv2 describe-load-balancers` shows the new ALB;
`curl http://<new-alb-dns>/` returns 200.
- **Rollback:** `terraform apply` the prior revision re-creates the
`acdl-*` ALB (brief downtime again). The old ALB DNS is retained until
consumers are re-pointed.
---
## Post-migration
1. **Soak:** run consumers against `nova-*` for a full verification
window (deploy a test contract end-to-end).
2. **Delete old resources** (manual, only after soak):
- DynamoDB: `acdl-contracts`, `acdl-change-requests`, `acdl-outbox`
- ECR: `acdl-microservice`
- IAM: `acdl-spike-runner` (+ policy), `acdl-act-runner-role`,
`acdl-deploy-<repo>`
- S3: `acdl-tfstate-581513795199-us-east-1`
- SNS: `acdl-sod-halt`
- SG: `acdl-ecs-sg`
- Secrets Manager: `acdl/github-token`
- KMS alias: `alias/acdl-platform`
- ALB: `acdl-alb` / `acdl-microservice`
3. **Regression gate:** re-run `bash scripts/run_regression.sh`. The
live-AWS CAP-013..016 probes should return `Verified` (the `nova-*`
tables + state bucket exist). CAP-015 (outbox) flips from `Decayed`
→ `Verified` once `nova-outbox` is live.
## What P5 owns (not P4)
- **Remove dual-read fallback:** `core/env.py` `get_env()` drops the
`ACDL_*` fallback; shell scripts drop `:-$ACDL_X`. P4 keeps the
dual-read (deployments don't break mid-window).
- **`nova_tagging.py` hard-fail on `acdl:*`:** P3 set hard mode (no
`acdl:*`-only tags); P5 tightens to fail on any `acdl:*` presence. P4
leaves P3's behavior.
- **Delete `ACDL_*` Gitea secrets:** the `NOVA_*` aliases created in P2
are now the only source.
- **Finalize `docs/NOVA_MIGRATION.md`:** mark the migration complete
(cutoff passed).
- **Milestone ship:** tag `v1.15.4`, merge to `main`, Gitea release.
## Files touched in P4
- `terraform/platform/main.tf`, `terraform/microservice/main.tf`,
`terraform/ci-vpc/main.tf` — resource renames + backend bucket.
- `terraform/{platform,microservice,ci-vpc}/terraform.tf` — state bucket.
- `terraform/platform/consumer_invoke_policy.json` — Lambda ARN.
- `terraform/bootstrap/{create_state_backend,create_iam_user,apply_iam_baseline}.py`,
`spike_runner_policy.json`, `.bootstrap_state.json`, `README.md` —
IAM/outbox/state-bucket renames.
- `modules/l1/*/terraform/**` + `modules/l1/alb/instance.json` — L1
resource-name defaults.
- `modules/l2/microservice/composition.json` — `nova-app-role` default.
- `core/lambda/contract_ingestor.py` — default table names (D-111).
- `core/outbox_writer.py`, `core/regression_verify.py`,
`core/local_emulators.py` — outbox table consistency (cross-territory,
minimal).
- `.gitea/workflows/deploy.yml` + `.github/workflows/deploy.yml` —
`nova-deploy-` role ARN + artifact names.
- `scripts/migrate_dynamodb_data.py` (NEW), `scripts/rotate_spike_key.sh`,
`scripts/push_consumer_image.py`.
- `tests/**` — fixtures updated to assert `nova-*`.
-177
View File
@@ -1,177 +0,0 @@
# Nova Migration Guide — What Consumers Must Know
> **STATUS: COMPLETE (milestone v1.15.4, 2026-07-30).** The Nova rebrand
> is fully rolled out. The dual-read / parallel-write grace period has
> ended (P5 cutoff passed). All `ACDL_*` env var fallbacks, `.acdl/`
> consumer-path fallbacks, `/acdl/` SSM-path fallbacks, `acdl:*` tag-key
> fallbacks, and `acdl-*` AWS resource names are removed. Consumers must
> use the `NOVA_*` / `.nova/` / `/nova/` / `nova:*` / `nova-*` names
> exclusively. If you have not yet migrated, follow the steps below.
> **Nova** is the new product brand for the platform formerly known as
> **ACDL** (Agentic Cloud Delivery Platform). This guide documents the
> breaking changes from the rebrand rollout (Phases P2P4, cutoff P5)
> and tells you exactly what to do.
## What is NOT changing
- **The Gitea repository name** (`continuous-intelligence/acdl`) is **not**
changing. Only the product brand is changing. The `uses:` reference
(`acdl/.github/workflows/deploy.yml@vX.Y`) and the GitHub `acdl/acdl` repo
path are unchanged for the duration of the rebrand; the workflow
`uses:` reference will be migrated in a later, separately-announced step.
- **The platform behavior** is unchanged. Same pipeline stages, same
contract schema, same confidence model, same evidence stream, same
modules. Only the brand, the on-disk path, the env var names, the SSM
path, the AWS tag keys, and the AWS resource names are changing.
## The 5 breaking changes
Five things that consumers may reference are being renamed. Each is
scheduled into a phase, ships with a grace period, and has a cutoff.
### 1. Consumer contract path — Phase P2
- **Old:** `.acdl/contract.yml`
- **New:** `.nova/contract.yml`
- **Phase:** P2 (env vars + consumer path)
- **Grace period:** during P2P4 the deploy workflow reads **both** paths
(`.nova/contract.yml` first, falling back to `.acdl/contract.yml` if the
new path is absent). Your existing contracts keep working until P5.
- **Cutoff:** P5 removes the `.acdl/` fallback. Move your contract file
before P5.
- **What you must do:** rename the directory in your consumer repo from
`.acdl/` to `.nova/` and update any `contract:` workflow input that
points at the old path. Nothing else changes in the contract content.
### 2. Environment variables — Phase P2
- **Old:** `ACDL_*` (e.g. `ACDL_LIFECYCLE_MODE`, `ACDL_AWS_ACCOUNT_ID`,
`ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID`, …)
- **New:** `NOVA_*` (e.g. `NOVA_LIFECYCLE_MODE`, `NOVA_AWS_ACCOUNT_ID`,
`NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID`, …)
- **Phase:** P2 (env vars + consumer path)
- **Grace period — dual-read fallback:** during P2P4 the platform reads
**`NOVA_*` first, then falls back to `ACDL_*`** if the Nova variable is
unset. This means your CI secrets, workflow env blocks, and local
`.env.secrets` keep working unchanged through P4. You do not need to
rename everything in one shot — rename a variable and the dual-read picks
it up; leave one old and it still resolves.
- **Cutoff:** P5 removes the `ACDL_*` fallback. After P5, only `NOVA_*`
is read.
- **What you must do:** rename your `ACDL_*` CI secrets, workflow `env:`
blocks, and any local `.env.secrets` entries to `NOVA_*`. Because of the
dual-read, you can do this incrementally across P2P4 — but it must be
complete before P5.
### 3. SSM parameter path — Phase P3 (DONE)
- **Old:** `/acdl/{env}/{contractId}/{output}`
- **New:** `/nova/{env}/{contractId}/{output}`
- **Phase:** P3 (SSM paths + tag keys) — **shipped in P3**
- **Grace period — parallel-write:** during P3P4 the platform **writes
every output to both** the `/acdl/…` and `/nova/…` SSM paths, and reads
from `/nova/…` first (falling back to `/acdl/…`). Any hardcoded SSM path
reads in your application code keep resolving through P4. The P3
migration script (`scripts/migrate_ssm_paths.py`) copies existing
`/acdl/…` parameters to `/nova/…`, verifies the copy, and deletes the
old ones.
- **Cutoff:** P5 stops writing to `/acdl/…` and removes the read fallback.
After P5 only `/nova/…` exists.
- **What you must do:** if your application code or runbooks read deploy
outputs from SSM by hardcoded path, update the path prefix from `/acdl/`
to `/nova/`. If you consume outputs only via the PR-comment / GitHub
issue surface, you do nothing — the platform republishes under the new
path automatically.
### 4. AWS tag keys — Phase P3 (DONE)
- **Old:** `acdl:owner`, `acdl:environment`, `acdl:contract`,
`acdl:cost-center`, `acdl:ref`
- **New:** `nova:owner`, `nova:environment`, `nova:contract`,
`nova:cost-center`, `nova:ref`
- **Phase:** P3 (SSM paths + tag keys) — **shipped in P3**
- **Grace period — parallel-tag period:** during P3P4 the platform
**tags every resource with both** the `acdl:*` and `nova:*` keys (same
values). The ABAC session policy matches on **either** key set, so your
existing scoped permissions keep working. The default cost-center value
moves from `acdl-default` to `nova-default` (both written during the
parallel-tag period). Terraform now emits `nova:*` keys; old `acdl:*`
tags on pre-P3 live resources are removed by the P4 runbook's
`scripts/untag_acdl_keys.py` step after the `nova:*` tags are applied
live.
- **Cutoff:** P5 stops writing the `acdl:*` keys and the ABAC policy matches
only on `nova:*`. After P5, resources created before P5 still carry the
old `acdl:*` tags (tags are not retroactively rewritten) but **new**
resources are tagged `nova:*` only, and the policy no longer grants
access via `acdl:*`.
- **What you must do:** if you have IAM policies, Cost Explorer filters,
or billing groupings that key off `acdl:*` tag keys, add a parallel
`nova:*` condition (or migrate to `nova:*`) before P5. The platform
handles the dual-tagging; you only need to update your own tag-key
references.
### 5. AWS resource names — Phase P4
- **Old:** `acdl-*` (DynamoDB tables `acdl-contracts`,
`acdl-change-requests`; Lambda `acdl-contract-ingestor`; SNS
`acdl-sod-halt`; security group `acdl-ecs-sg`; KMS alias
`alias/acdl-platform`; ECS services, ECR repos, IAM user
`acdl-spike-runner`, state bucket `acdl-tfstate-*`, ALB `acdl-alb`,
`acdl-deploy-*`)
- **New:** `nova-*` (the same resources, prefixed `nova-`)
- **Phase:** P4 (resource names) — **maintenance window**
- **Grace period:** P4 is a **planned maintenance window**. AWS resources
cannot be renamed in place, so P4 provisions the `nova-*` resources,
migrates data (DynamoDB tables, S3 state), repoints the platform, and
tears down the `acdl-*` resources. The platform team schedules and
announces the window; consumers do not provision or rename anything
themselves.
- **Cutoff:** the `acdl-*` resources are decommissioned at the end of the
P4 maintenance window. After P4, only `nova-*` resources exist.
- **What you must do:** nothing for the resource names themselves — the
platform owns the rename. If your application code or runbooks reference
a specific `acdl-*` resource by name (e.g. a hardcoded DynamoDB table
name or ECR URI), update it to the `nova-*` name during P4. The platform
publishes the exact old → new name mapping with the P4 announcement.
## Timeline at a glance
| Phase | What ships | Grace period | Cutoff |
|-------|------------|--------------|--------|
| **P1** (this phase) | Brand prose, docs, decks, schema `$id`, release titles | n/a (prose only) | n/a |
| **P2** | `.nova/` contract path + `NOVA_*` env vars | dual-read: `.nova/``.acdl/`, `NOVA_*``ACDL_*` | **P5** removes fallback |
| **P3** | `/nova/` SSM path + `nova:*` tag keys | parallel-write (SSM) + parallel-tag (ABAC matches either) | **P5** removes old path/tags |
| **P4** | `nova-*` AWS resource names | maintenance window (platform-owned migration) | end of P4 window |
| **P5** | Fallback removal | — | `ACDL_*` env vars, `.acdl/` path, `/acdl/` SSM, `acdl:*` tags stop working |
## What consumers must do (checklist)
1. **Before P5 — contract path:** move `.acdl/contract.yml`
`.nova/contract.yml` in your consumer repo; update the `contract:`
workflow input. *(Can be done any time in P2P4.)*
2. **Before P5 — env vars:** rename `ACDL_*` CI secrets / workflow `env:`
blocks / local `.env.secrets` to `NOVA_*`. *(Incremental during P2P4;
dual-read keeps you green.)*
3. **Before P5 — SSM reads:** if you read deploy outputs from SSM by
hardcoded `/acdl/…` path, update to `/nova/…`. *(Skip if you consume
outputs via PR comments only.)*
4. **Before P5 — tag-key references:** if you have IAM policies, Cost
Explorer filters, or billing groupings keyed off `acdl:*`, add or
migrate to `nova:*`. *(Platform handles dual-tagging.)*
5. **During P4 — resource-name references:** if your code or runbooks
reference a specific `acdl-*` AWS resource by name, update to the
`nova-*` name per the P4 mapping announcement. *(Platform owns the
rename itself.)*
## Questions
If anything in this guide is unclear, or you are unsure whether your
consumer repo references a renamed value, open an issue on the platform
repo. The platform team will confirm what you need to change and when.
> **Note:** the real Gitea repository name (`continuous-intelligence/acdl`)
> is **not** changing — only the product brand. The `uses:` workflow
> reference and repo path are migrated in a separately-announced later step;
> until then, keep your `uses: acdl/.github/workflows/deploy.yml@vX.Y`
> reference as-is.
+1 -1
View File
@@ -77,7 +77,7 @@ milestone (D-113).
only (D-114); live apply is deferred. only (D-114); live apply is deferred.
- **OIDC trust policy** — the onboarding Terraform uses a placeholder - **OIDC trust policy** — the onboarding Terraform uses a placeholder
OIDC provider; real OIDC federation is blocked on OIDC provider; real OIDC federation is blocked on
go-gitea/gitea#36988 (carries forward from v1.1). upstream forge OIDC support (carries forward from v1.1).
## See also ## See also
+1 -1
View File
@@ -230,7 +230,7 @@ change to the modules/stack/confidence/audit.
- A MAJOR bump requires a new registry entry (immutable publication); the - A MAJOR bump requires a new registry entry (immutable publication); the
old entry enters a 12-month deprecation window. old entry enters a 12-month deprecation window.
- The central deploy pipeline is referenced by a floating MAJOR + MINOR tag - The central deploy pipeline is referenced by a floating MAJOR + MINOR tag
(e.g. `@v1.13`); patch fixes flow within the tag, breaking changes land (e.g. `@v1.19`); patch fixes flow within the tag, breaking changes land
under the next MINOR tag. under the next MINOR tag.
See [Versioning](pipeline/versioning) for the consumer-facing details. See [Versioning](pipeline/versioning) for the consumer-facing details.
+13 -13
View File
@@ -19,7 +19,7 @@ definitions.
```mermaid ```mermaid
flowchart LR flowchart LR
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.13| B 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 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"] C["your resources in AWS"]
``` ```
@@ -27,13 +27,13 @@ flowchart LR
## Versioning the `uses:` reference ## Versioning the `uses:` reference
The central deployment pipeline is **always versioned with floating MAJOR The central deployment pipeline is **always versioned with floating MAJOR
and MINOR tags** (e.g. `acdl/pipelines/contract.yml@v1.13`). Version and MINOR tags** (e.g. `nova/pipelines/contract.yml@v1.19`). Version
constraints cannot be expressed inside the contract, so the tag in constraints cannot be expressed inside the contract, so the tag in
`uses:` is the only immutability lever a consumer has. See `uses:` is the only immutability lever a consumer has. See
[Versioning](pipeline/versioning) for the full rationale. [Versioning](pipeline/versioning) for the full rationale.
**Unversioned references are discouraged.** Do not use `@main` or a bare **Unversioned references are discouraged.** Do not use `@main` or a bare
`acdl/pipelines/contract.yml`. `nova/pipelines/contract.yml`.
## Prerequisites ## Prerequisites
@@ -47,7 +47,7 @@ platform-managed. See [Environments](environments/).
environment is bound, your first pipeline run emits a friendly onboarding environment is bound, your first pipeline run emits a friendly onboarding
prompt. See [Environments](environments/). prompt. See [Environments](environments/).
- **Authorization to reference the central pipeline.** Onboarding grants - **Authorization to reference the central pipeline.** Onboarding grants
your repo the right to `uses: acdl/.github/workflows/deploy.yml@v1.13`. your repo the right to `uses: nova/.github/workflows/deploy.yml@v1.19`.
Contact the platform team if you have not been onboarded. Contact the platform team if you have not been onboarded.
## Step 1 — Create a consumer repo ## Step 1 — Create a consumer repo
@@ -94,7 +94,7 @@ Nova deployment workflow with a **versioned tag** (floating MAJOR + MINOR):
```yaml ```yaml
jobs: jobs:
deploy: deploy:
uses: acdl/.github/workflows/deploy.yml@v1.13 uses: nova/.github/workflows/deploy.yml@v1.19
with: with:
contract: .nova/contract.yml contract: .nova/contract.yml
environment: dev environment: dev
@@ -140,7 +140,7 @@ name: microservice
| Field | Type | Required | Description | | Field | Type | Required | Description |
|-------|------|----------|-------------| |-------|------|----------|-------------|
| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/contract.yml@v1.13`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). | | `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `nova/pipelines/contract.yml@v1.19`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). |
| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](modules/). | | `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](modules/). |
| `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](environments/). | | `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](environments/). |
| `inputs` | object | yes | Module-specific inputs (see the module's README). | | `inputs` | object | yes | Module-specific inputs (see the module's README). |
@@ -177,14 +177,14 @@ on:
branches: [main] branches: [main]
jobs: jobs:
deploy: deploy:
uses: acdl/.github/workflows/deploy.yml@v1.13 uses: nova/.github/workflows/deploy.yml@v1.19
with: with:
contract: .nova/contract.yml contract: .nova/contract.yml
``` ```
That is the entire consumer-side workflow. When you push to `main`: That is the entire consumer-side workflow. When you push to `main`:
1. The platform runner resolves `uses: acdl/.github/workflows/deploy.yml@v1.13` 1. The platform runner resolves `uses: nova/.github/workflows/deploy.yml@v1.19`
to the reusable workflow **at the pinned tag**. to the reusable workflow **at the pinned tag**.
2. A **platform-provided runner** checks out **your** repo. 2. A **platform-provided runner** checks out **your** repo.
3. The runner checks out the **Nova platform repo** into the workspace — 3. The runner checks out the **Nova platform repo** into the workspace —
@@ -326,8 +326,8 @@ per-module extension points. Common examples:
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. | | Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. | | Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
| Module catalog | [modules/](modules/) | All primitives and modules. | | Module catalog | [modules/](modules/) | All primitives and modules. |
| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.13`). | | Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.19`). |
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.13`). | | Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.19`). |
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). | | 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. | | Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
| Angine adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. | | Angine adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. |
@@ -353,7 +353,7 @@ destruction:
use `mode: decommission` with the `changeRequestId` input: use `mode: decommission` with the `changeRequestId` input:
```yaml ```yaml
uses: acdl/.github/workflows/deploy.yml@v1.13 uses: nova/.github/workflows/deploy.yml@v1.19
with: with:
contract: .nova/contract.yml contract: .nova/contract.yml
mode: decommission mode: decommission
@@ -421,7 +421,7 @@ name: static-assets
``` ```
**Shape 2 — single contract + `environment` workflow input:** the **Shape 2 — single contract + `environment` workflow input:** the
reusable deploy workflow (`acdl/.github/workflows/deploy.yml@v1.13`) reusable deploy workflow (`nova/.github/workflows/deploy.yml@v1.19`)
declares an `environment` input. When non-empty, it overrides the declares an `environment` input. When non-empty, it overrides the
contract's `environment` field at load time (before interpolation), so contract's `environment` field at load time (before interpolation), so
the same contract can be promoted by passing a different environment: the same contract can be promoted by passing a different environment:
@@ -436,7 +436,7 @@ on: workflow_dispatch:
required: true required: true
jobs: jobs:
deploy-qa: deploy-qa:
uses: acdl/.github/workflows/deploy.yml@v1.13 uses: nova/.github/workflows/deploy.yml@v1.19
with: with:
environment: qa environment: qa
contract: .nova/contract.yml contract: .nova/contract.yml
-7
View File
@@ -78,10 +78,3 @@ Planned future features (no dates; tracked in the internal roadmap):
- [Consumer Guide](consumer-guide) — start here if you are a consumer. - [Consumer Guide](consumer-guide) — start here if you are a consumer.
- [Architecture](architecture) — start here if you are a platform engineer. - [Architecture](architecture) — start here if you are a platform engineer.
- The [README](https://github.com/nova/nova) describes the platform repo. - The [README](https://github.com/nova/nova) describes the platform repo.
> **Note:** The product brand is **Nova** (formerly ACDL — Agentic Cloud
> Delivery Platform). The Gitea repository name (`continuous-intelligence/acdl`)
> and the GitHub `uses:` reference (`acdl/.github/workflows/deploy.yml@…`)
> are unchanged during the rebrand transition; only the product name is
> changing. See the [Nova migration guide](NOVA_MIGRATION) for the
> scheduled breaking changes.
+1 -1
View File
@@ -39,7 +39,7 @@ It is exposed to consumer repos as a **reusable workflow**:
- `.github/workflows/deploy.yml` — GitHub Actions (production) - `.github/workflows/deploy.yml` — GitHub Actions (production)
A consumer repo invokes the reusable workflow via a **versioned tag** A consumer repo invokes the reusable workflow via a **versioned tag**
(floating MAJOR + MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.13`). (floating MAJOR + MINOR, e.g. `nova/.github/workflows/deploy.yml@v1.19`).
The workflow checks out the consumer repo, then checks out the Nova platform The workflow checks out the consumer repo, then checks out the Nova platform
repo into the runner workspace, and runs `scripts/run_platform.sh` against repo into the runner workspace, and runs `scripts/run_platform.sh` against
the consumer's contract. The consumer never clones the platform repo or the consumer's contract. The consumer never clones the platform repo or
+2 -2
View File
@@ -26,7 +26,7 @@ tag** in a consumer's CI workflow definition:
```yaml ```yaml
jobs: jobs:
deploy: deploy:
uses: acdl/.github/workflows/deploy.yml@v1.13 uses: nova/.github/workflows/deploy.yml@v1.19
with: with:
contract: .nova/contract.yml contract: .nova/contract.yml
``` ```
@@ -36,7 +36,7 @@ itself — the contract no longer carries a `uses:` field). The CI workflow
`uses:` tag is the only immutability lever a consumer has. `uses:` tag is the only immutability lever a consumer has.
**Unversioned references are discouraged.** Do not use `@main` or a bare **Unversioned references are discouraged.** Do not use `@main` or a bare
`acdl/.github/workflows/deploy.yml``main` is constantly updated and can `nova/.github/workflows/deploy.yml``main` is constantly updated and can
cause unexpected failures. Pinning to a MAJOR+MINOR tag means: cause unexpected failures. Pinning to a MAJOR+MINOR tag means:
- **Immutability** — the pipeline behavior you tested is the behavior you - **Immutability** — the pipeline behavior you tested is the behavior you
+7 -7
View File
@@ -69,7 +69,7 @@ Synthesize the full markdown into a lean Marp deck:
Both formats are derived from the Marp deck. **HTML is committed to the repo** Both formats are derived from the Marp deck. **HTML is committed to the repo**
(viewable in any browser, self-contained with base64-embedded images). **PPTX (viewable in any browser, self-contained with base64-embedded images). **PPTX
is uploaded to the Gitea release** as a downloadable attachment (binary, not is uploaded to the release** as a downloadable attachment (binary, not
committed to git). committed to git).
#### HTML export (committed to repo) #### HTML export (committed to repo)
@@ -90,7 +90,7 @@ file that renders the full deck with the S&P Global Energy theme.
committed artifacts, not generated on-the-fly — they must be re-rendered and committed artifacts, not generated on-the-fly — they must be re-rendered and
re-committed when the Marp deck is updated. re-committed when the Marp deck is updated.
#### PPTX export (uploaded to Gitea release) #### PPTX export (uploaded to release)
```bash ```bash
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \ CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
@@ -102,7 +102,7 @@ CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
The `--allow-local-files` flag is **required** for PPTX export so the local The `--allow-local-files` flag is **required** for PPTX export so the local
PNG diagrams are embedded in the file. As of v1.18 (REQ-228, D-141), PPTX PNG diagrams are embedded in the file. As of v1.18 (REQ-228, D-141), PPTX
files **are committed to the repo** as first-class binary artifacts (no LFS) files **are committed to the repo** as first-class binary artifacts (no LFS)
and are also attached to the phase's Gitea release via and are also attached to the phase's release via
`scripts/attach_release_asset.py`. The render + commit + attach pipeline is `scripts/attach_release_asset.py`. The render + commit + attach pipeline is
automated by `scripts/render_deck.sh`. automated by `scripts/render_deck.sh`.
@@ -300,7 +300,7 @@ diagram images in `assets/png/`). The resulting HTML is self-contained.
**The HTML files are committed artifacts** — re-render and re-commit whenever **The HTML files are committed artifacts** — re-render and re-commit whenever
the Marp source changes. the Marp source changes.
### Export a Marp deck to PPTX (uploaded to Gitea release) ### Export a Marp deck to PPTX (uploaded to release)
```bash ```bash
CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \ CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
@@ -311,7 +311,7 @@ CHROME_PATH=/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome \
`--allow-local-files` is **required** for PPTX so local PNG diagrams are `--allow-local-files` is **required** for PPTX so local PNG diagrams are
embedded in the file. PPTX files are not committed to git — upload them as embedded in the file. PPTX files are not committed to git — upload them as
attachments to the Gitea release. attachments to the release.
## Adding a new presentation ## Adding a new presentation
@@ -324,7 +324,7 @@ attachments to the Gitea release.
no speaker notes, embedded PNGs, and maturity badges. no speaker notes, embedded PNGs, and maturity badges.
4. **Render to HTML** with `--allow-local-files` and commit the HTML to 4. **Render to HTML** with `--allow-local-files` and commit the HTML to
`docs/presentations/<deck-name>.html`. `docs/presentations/<deck-name>.html`.
5. **Render to PPTX** with `--allow-local-files` and upload to the Gitea 5. **Render to PPTX** with `--allow-local-files` and upload to the release
release (do not commit PPTX to git). release (do not commit PPTX to git).
6. **Distill the talking points** as `<deck-name>-talking-points.md` — one 6. **Distill the talking points** as `<deck-name>-talking-points.md` — one
section per Marp slide, 3-6 talking point bullets + key takeaway, content section per Marp slide, 3-6 talking point bullets + key takeaway, content
@@ -352,4 +352,4 @@ attachments to the Gitea release.
> deck with a 5-act arc (Problem → Vision → How → Proof → Roadmap). v1.18 > deck with a 5-act arc (Problem → Vision → How → Proof → Roadmap). v1.18
> (REQ-226) adds 3 slides (17 Scope, 18 RACI, 19 Atelier) → 21 total. The > (REQ-226) adds 3 slides (17 Scope, 18 RACI, 19 Atelier) → 21 total. The
> S&P Global Energy theme is restored (REQ-214, P1). PPTX is committed to > S&P Global Energy theme is restored (REQ-214, P1). PPTX is committed to
> git + attached to the Gitea release (REQ-228, D-141). > git + attached to the release (REQ-228, D-141).
-1
View File
@@ -1,6 +1,5 @@
# RACI — Who Owns What # RACI — Who Owns What
> **Source of truth:** `.ciagent/PROJECT.md` § RACI Matrix (v1.18, REQ-215,
> D-139). This page is the citizen-developer-facing copy. > D-139). This page is the citizen-developer-facing copy.
Nova's delivery lifecycle has three roles. This page clarifies who owns Nova's delivery lifecycle has three roles. This page clarifies who owns
-1
View File
@@ -1,6 +1,5 @@
# Scope — Nova is Downstream of PDLC # Scope — Nova is Downstream of PDLC
> **Source of truth:** `.ciagent/PROJECT.md` § Scope (v1.18, REQ-216).
> This page is the citizen-developer-facing copy. > This page is the citizen-developer-facing copy.
## The Boundary ## The Boundary
+2 -2
View File
@@ -34,14 +34,14 @@
## Atelier Provenance ## Atelier Provenance
The skills are derived from [Atelier](https://git.cloudinit.dev/coreci/atelier) The skills are derived from [Atelier](https://example.com/atelier)
— a first-principles docs-as-code engineering framework with 8 core — a first-principles docs-as-code engineering framework with 8 core
principles (C1C8) and 19 domains, each with 10 derived P-rules. The principles (C1C8) and 19 domains, each with 10 derived P-rules. The
skills distill the citizen-developer-relevant subset of each domain's skills distill the citizen-developer-relevant subset of each domain's
first-principles, link to the agent-checklist triggers, and map to the first-principles, link to the agent-checklist triggers, and map to the
existing BA.A catalog. existing BA.A catalog.
Atelier is vendored under `mcp/atelier/vendor/` (pinned tag, D-136) for Atelier is vendored under `mcp/atelier/vendor/` (pinned tag) for
audit reproducibility — an agentic validation result is replayable audit reproducibility — an agentic validation result is replayable
against the exact principles that produced it. against the exact principles that produced it.
+5 -6
View File
@@ -1,9 +1,8 @@
# Nova Atelier MCP Server # Nova Atelier MCP Server
> **v1.18, REQ-223, REQ-224.** An MCP (Model Context Protocol) server that
> exposes Atelier engineering principles to the citizen developer's AI > exposes Atelier engineering principles to the citizen developer's AI
> agent. Plugin-registry architecture (D-140); stdio transport (D-135); > agent. Plugin-registry architecture; stdio transport;
> vendored Atelier (D-136) for audit reproducibility. > vendored Atelier for audit reproducibility.
## What This Is ## What This Is
@@ -22,7 +21,7 @@ observability gaps.
| `atelier.matrix_lookup(domain)` | Look up the domain→core principle mapping for a given domain. | | `atelier.matrix_lookup(domain)` | Look up the domain→core principle mapping for a given domain. |
| `atelier.validate_against_principles(snippet, domains?)` | Validate a code/diff snippet against the Atelier agent-checklist. Returns pass/fail per check item with the principle citation. | | `atelier.validate_against_principles(snippet, domains?)` | Validate a code/diff snippet against the Atelier agent-checklist. Returns pass/fail per check item with the principle citation. |
## Architecture — Plugin Registry (D-140) ## Architecture — Plugin Registry
``` ```
mcp/atelier/ mcp/atelier/
@@ -31,7 +30,7 @@ mcp/atelier/
│ ├── __init__.py │ ├── __init__.py
│ ├── principles.py # lookup_principle, list_domains, matrix_lookup │ ├── principles.py # lookup_principle, list_domains, matrix_lookup
│ └── validation.py # validate_against_principles │ └── validation.py # validate_against_principles
├── vendor/ # pinned Atelier snapshot (D-136) ├── vendor/ # pinned Atelier snapshot
│ ├── VERSION.md # pinned tag + upgrade instructions │ ├── VERSION.md # pinned tag + upgrade instructions
│ ├── core/first-principles.md │ ├── core/first-principles.md
│ ├── domains/security/first-principles.md │ ├── domains/security/first-principles.md
@@ -68,7 +67,7 @@ s.load_plugins()
result = s.call_tool("atelier_lookup_principle", {"domain": "security", "principle_id": "P4"}) result = s.call_tool("atelier_lookup_principle", {"domain": "security", "principle_id": "P4"})
``` ```
## Vendoring (D-136) ## Vendoring
Atelier is vendored under `vendor/` at a pinned tag (`v0.3.6`, see Atelier is vendored under `vendor/` at a pinned tag (`v0.3.6`, see
`vendor/VERSION.md`). An agentic validation result is only reproducible if `vendor/VERSION.md`). An agentic validation result is only reproducible if
-1
View File
@@ -1,6 +1,5 @@
# Nova Metrics Directory # Nova Metrics Directory
> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (D-128)
This directory holds Nova's telemetry/observability artifacts. The This directory holds Nova's telemetry/observability artifacts. The
metrics layer is **Nova-native** (D-120): JSONL event log + SQLite cold metrics layer is **Nova-native** (D-120): JSONL event log + SQLite cold
-2
View File
@@ -1,7 +1,5 @@
# Nova PowerBI Dashboard — Import Guide # Nova PowerBI Dashboard — Import Guide
> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (REQ-208)
> Generated: 2026-08-04
This guide documents how to import Nova's metrics views into PowerBI This guide documents how to import Nova's metrics views into PowerBI
via the folder connector, and suggests a starter visual model. via the folder connector, and suggests a starter visual model.
+5 -5
View File
@@ -2,7 +2,7 @@
## Overview ## Overview
Nova uses declarative pipeline contracts (YAML) as the single source of truth. Both Gitea and GitHub workflows implement the same contract (byte-identical). The shell runner (`scripts/run_ci.sh`) mirrors the CI pipeline locally so that every stage that runs in CI can be reproduced on a developer machine without a forge. Nova uses declarative pipeline contracts (YAML) as the single source of truth. GitHub workflows implement the same contract (byte-identical across forges). The shell runner (`scripts/run_ci.sh`) mirrors the CI pipeline locally so that every stage that runs in CI can be reproduced on a developer machine without a forge.
## Existing Pipelines ## Existing Pipelines
@@ -20,7 +20,7 @@ Nova uses declarative pipeline contracts (YAML) as the single source of truth. B
## How to Wire a Pipeline ## How to Wire a Pipeline
1. Create byte-identical workflow YAMLs in `.gitea/workflows/<name>.yml` and `.github/workflows/<name>.yml`. 1. Create the workflow YAML in `.github/workflows/<name>.yml`.
2. Both workflows must implement the same stages, commands, triggers, and runner declared in the contract. 2. Both workflows must implement the same stages, commands, triggers, and runner declared in the contract.
3. `scripts/run_ci.sh` mirrors `ci.yml` locally so the same stages run without a forge. 3. `scripts/run_ci.sh` mirrors `ci.yml` locally so the same stages run without a forge.
4. Consumer repos reference the deploy pipeline via `uses: acdl/.github/workflows/deploy.yml@vX.Y`. 4. Consumer repos reference the deploy pipeline via `uses: acdl/.github/workflows/deploy.yml@vX.Y`.
@@ -29,17 +29,17 @@ Nova uses declarative pipeline contracts (YAML) as the single source of truth. B
- `scripts/run_ci.sh` — local CI mirror that runs the `ci.yml` stages. - `scripts/run_ci.sh` — local CI mirror that runs the `ci.yml` stages.
- `scripts/run_platform.sh` — platform pipeline runner that implements the `contract.yml` stages. - `scripts/run_platform.sh` — platform pipeline runner that implements the `contract.yml` stages.
- Workflow YAMLs in `.gitea/workflows/` and `.github/workflows/`. - Workflow YAMLs in `.github/workflows/`.
- Schemas in `schemas/` (`pipeline.schema.json`, `deploy-pipeline.schema.json`). - Schemas in `schemas/` (`pipeline.schema.json`, `deploy-pipeline.schema.json`).
## How to Test Pipelines ## How to Test Pipelines
- `tests/test_pipeline_contract.py` — validates each pipeline YAML against its schema, asserts workflow conformance (byte-identical Gitea/GitHub workflows with the same stages/commands/triggers), and tests `scripts/run_ci.sh` execution against the contract. - `tests/test_pipeline_contract.py` — validates each pipeline YAML against its schema, asserts workflow conformance (byte-identical workflows with the same stages/commands/triggers), and tests `scripts/run_ci.sh` execution against the contract.
## Adding a New Pipeline ## Adding a New Pipeline
1. Create `pipelines/<name>.yml` using the structure above. 1. Create `pipelines/<name>.yml` using the structure above.
2. Create or extend the schema in `schemas/` for the new pipeline shape. 2. Create or extend the schema in `schemas/` for the new pipeline shape.
3. Create byte-identical workflow YAMLs in `.gitea/workflows/<name>.yml` and `.github/workflows/<name>.yml`. 3. Create the workflow YAML in `.github/workflows/<name>.yml`.
4. Extend `scripts/run_ci.sh` if a local mirror of the new pipeline is needed. 4. Extend `scripts/run_ci.sh` if a local mirror of the new pipeline is needed.
5. Write or extend tests in `tests/test_pipeline_contract.py` to assert schema validity and workflow conformance. 5. Write or extend tests in `tests/test_pipeline_contract.py` to assert schema validity and workflow conformance.
+2 -2
View File
@@ -1,7 +1,7 @@
# Nova Central CI Pipeline Contract (v1.5) # Nova Central CI Pipeline Contract (v1.5)
# #
# This is the single source of truth for the CI/CD pipeline. Both # This is the single source of truth for the CI/CD pipeline. Both
# .gitea/workflows/ci.yml (Gitea Actions, dev) and # .github/workflows/ci.yml (dev) and
# .github/workflows/ci.yml (GitHub Actions, production) implement the # .github/workflows/ci.yml (GitHub Actions, production) implement the
# stages, commands, triggers, and runner declared here. # stages, commands, triggers, and runner declared here.
# scripts/run_ci.sh mirrors the same stages for shell reproducibility. # scripts/run_ci.sh mirrors the same stages for shell reproducibility.
@@ -11,7 +11,7 @@
# #
# The contract does NOT replace workflow YAML syntax — it declares the # The contract does NOT replace workflow YAML syntax — it declares the
# *intent* that the forge-specific workflows implement. The workflow files # *intent* that the forge-specific workflows implement. The workflow files
# use Gitea/GitHub Actions syntax (checkout, setup-python, run blocks); # use GitHub Actions syntax (checkout, setup-python, run blocks);
# this contract declares what those blocks must contain. # this contract declares what those blocks must contain.
# #
# Validated against schemas/pipeline.schema.json. # Validated against schemas/pipeline.schema.json.
+1 -1
View File
@@ -12,7 +12,7 @@
# #
# This file is the declarative pipeline spec (a contract, not an executable # This file is the declarative pipeline spec (a contract, not an executable
# workflow). The executable workflow is .github/workflows/deploy.yml # workflow). The executable workflow is .github/workflows/deploy.yml
# (GitHub Actions) / .gitea/workflows/deploy.yml (Gitea Actions), which # (GitHub Actions), which
# implements these stages by invoking scripts/run_platform.sh. # implements these stages by invoking scripts/run_platform.sh.
# #
# Validated against schemas/deploy-pipeline.schema.json. # Validated against schemas/deploy-pipeline.schema.json.
+2 -2
View File
@@ -18,8 +18,8 @@ name: acdl-modules-lifecycle
# real apply→modify→destroy against live AWS. The CI VPC apply/destroy # real apply→modify→destroy against live AWS. The CI VPC apply/destroy
# jobs are skipped in plan mode (nothing is applied). # jobs are skipped in plan mode (nothing is applied).
# #
# Both Gitea (.gitea/workflows/modules-lifecycle.yml) and GitHub # GitHub
# (.github/workflows/modules-lifecycle.yml) implement this contract # (.github/workflows/modules-lifecycle.yml) implements this contract
# byte-identically. # byte-identically.
triggers: triggers:
+1 -1
View File
@@ -40,7 +40,7 @@ Nova uses JSON Schema draft 2020-12 for all declarative contracts. Schemas are t
## How to Test Schemas in CI ## How to Test Schemas in CI
- `tests/test_pipeline_contract.py` — validates the pipeline schemas and asserts workflow conformance (byte-identical Gitea/GitHub workflows, same stages/commands/triggers). - `tests/test_pipeline_contract.py` — validates the pipeline schemas and asserts workflow conformance (byte-identical workflows, same stages/commands/triggers).
- `tests/conftest.py` — provides `stack_schema` and `policy_check_result_schema` fixtures for reuse across the test suite. - `tests/conftest.py` — provides `stack_schema` and `policy_check_result_schema` fixtures for reuse across the test suite.
- `.github/workflows/platform-test.yml` `schema-validation` job — self-validates every schema in `schemas/` (each schema is loaded and meta-validated), validates module interfaces, and validates example contracts. - `.github/workflows/platform-test.yml` `schema-validation` job — self-validates every schema in `schemas/` (each schema is loaded and meta-validated), validates module interfaces, and validates example contracts.
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nova.cloudinit.dev/schemas/modules-lifecycle-pipeline.schema.json", "$id": "https://nova.cloudinit.dev/schemas/modules-lifecycle-pipeline.schema.json",
"title": "Nova Modules Lifecycle Pipeline Contract", "title": "Nova Modules Lifecycle Pipeline Contract",
"description": "Declarative contract for the modules-lifecycle pipeline. Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through apply→modify→destroy against live AWS. Both Gitea Actions (.gitea/workflows/modules-lifecycle.yml) and GitHub Actions (.github/workflows/modules-lifecycle.yml) implement this contract byte-identically.", "description": "Declarative contract for the modules-lifecycle pipeline. Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through apply→modify→destroy against live AWS. GitHub Actions (.github/workflows/modules-lifecycle.yml) implements this contract byte-identically.",
"type": "object", "type": "object",
"required": ["name", "triggers", "runner", "python_version", "terraform_version", "stages", "matrix"], "required": ["name", "triggers", "runner", "python_version", "terraform_version", "stages", "matrix"],
"properties": { "properties": {
+4 -4
View File
@@ -2,8 +2,8 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nova.cloudinit.dev/schemas/pipeline.schema.json", "$id": "https://nova.cloudinit.dev/schemas/pipeline.schema.json",
"title": "Nova Central Pipeline Contract", "title": "Nova Central Pipeline Contract",
"description": "Declarative contract for a CI/CD pipeline. Both Gitea Actions (.gitea/workflows/ci.yml, dev) and GitHub Actions (.github/workflows/ci.yml, production) implement the stages, commands, triggers, and runner declared here. The shell script scripts/run_ci.sh mirrors the same stages for local reproducibility. The contract is the single source of truth; the workflow YAMLs and run_ci.sh are generated/validated against it.", "description": "Declarative contract for a CI/CD pipeline. GitHub Actions (.github/workflows/ci.yml, production) implements the stages, commands, triggers, and runner declared here. The shell script scripts/run_ci.sh mirrors the same stages for local reproducibility. The contract is the single source of truth; the workflow YAMLs and run_ci.sh are generated/validated against it.",
"$comment": "The pipeline contract does not replace workflow YAML syntax — it declares the *intent* (stages, commands, triggers, runner) that both Gitea and GitHub workflows implement. A test (tests/test_pipeline_contract.py) validates conformance: the workflow YAMLs must declare the same jobs/stages/commands as the contract, and run_ci.sh must run the same commands in the same order.", "$comment": "The pipeline contract does not replace workflow YAML syntax — it declares the *intent* (stages, commands, triggers, runner) that the workflow YAMLs implement. A test (tests/test_pipeline_contract.py) validates conformance: the workflow YAMLs must declare the same jobs/stages/commands as the contract, and run_ci.sh must run the same commands in the same order.",
"type": "object", "type": "object",
"required": ["name", "triggers", "runner", "stages"], "required": ["name", "triggers", "runner", "stages"],
"properties": { "properties": {
@@ -14,7 +14,7 @@
"environment": { "environment": {
"type": "string", "type": "string",
"enum": ["dev", "production"], "enum": ["dev", "production"],
"description": "Declared environment. dev = Gitea Actions; production = GitHub Actions. Does not change job commands — only documents which forge runs this instance." "description": "Declared environment. dev = dev forge Actions; production = GitHub Actions. Does not change job commands — only documents which forge runs this instance."
}, },
"triggers": { "triggers": {
"type": "object", "type": "object",
@@ -34,7 +34,7 @@
}, },
"runner": { "runner": {
"type": "string", "type": "string",
"description": "Runner image (e.g. 'ubuntu-latest'). Both Gitea and GitHub use the same runner label." "description": "Runner image (e.g. 'ubuntu-latest'). All forges use the same runner label."
}, },
"python_version": { "python_version": {
"type": "string", "type": "string",
+1 -1
View File
@@ -2,7 +2,7 @@
# scripts/run_ci.sh - reproduce the CI pipeline locally. # scripts/run_ci.sh - reproduce the CI pipeline locally.
# #
# Mirrors the central pipeline contract (pipelines/ci.yml) which both # Mirrors the central pipeline contract (pipelines/ci.yml) which both
# .gitea/workflows/ci.yml (Gitea Actions, dev) and # .github/workflows/ci.yml (dev) and
# .github/workflows/ci.yml (GitHub Actions, production) implement. # .github/workflows/ci.yml (GitHub Actions, production) implement.
# #
# Runs the same three stages in the same order: # Runs the same three stages in the same order:
+2 -2
View File
@@ -163,9 +163,9 @@ run_hitl_gate() {
return 0 return 0
fi fi
echo "Environment is $_env — HITL attestation gate required$_ctx." echo "Environment is $_env — HITL attestation gate required$_ctx."
local _approver="${GITHUB_ACTOR:-${GITEA_ACTOR:-}}" local _approver="${GITHUB_ACTOR:-${FORGE_ACTOR:-}}"
if [ -z "$_approver" ]; then if [ -z "$_approver" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/GITEA_ACTOR unset)" >&2 echo "WARNING: no approver identity (GITHUB_ACTOR/FORGE_ACTOR unset)" >&2
echo " the gate would block in a real CI run. Passing for local." >&2 echo " the gate would block in a real CI run. Passing for local." >&2
fi fi
python3 -c " python3 -c "
+10 -10
View File
@@ -438,17 +438,17 @@ class TestForgeAgnosticApiUrls:
def test_default_api_base_is_github(self): def test_default_api_base_is_github(self):
assert ingestor.GITHUB_API_BASE == "https://api.github.com" assert ingestor.GITHUB_API_BASE == "https://api.github.com"
def test_forge_type_detects_gitea(self, monkeypatch): def test_forge_type_detects_generic_forge(self, monkeypatch):
monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://git.cloudinit.dev/api/v1") monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://forge.example.com/api/v1")
assert ingestor._forge_type() == "gitea" assert ingestor._forge_type() == "generic_forge"
def test_forge_type_detects_github(self): def test_forge_type_detects_github(self):
assert ingestor._forge_type() == "github" assert ingestor._forge_type() == "github"
def test_gitea_search_url_uses_repos_endpoint(self, monkeypatch): def test_generic_forge_search_url_uses_repos_endpoint(self, monkeypatch):
monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://git.cloudinit.dev/api/v1") monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://forge.example.com/api/v1")
url = ingestor._issues_search_url("acdl", "acdl", "contract-123") url = ingestor._issues_search_url("acdl", "acdl", "contract-123")
assert "git.cloudinit.dev/api/v1" in url assert "forge.example.com/api/v1" in url
assert "/repos/acdl/acdl/issues" in url assert "/repos/acdl/acdl/issues" in url
assert "/search/issues" not in url assert "/search/issues" not in url
@@ -458,14 +458,14 @@ class TestForgeAgnosticApiUrls:
assert "repo:acdl/acdl" in url assert "repo:acdl/acdl" in url
def test_create_url_uses_api_base(self, monkeypatch): def test_create_url_uses_api_base(self, monkeypatch):
monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://git.cloudinit.dev/api/v1") monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://forge.example.com/api/v1")
url = ingestor._issues_create_url("acdl", "acdl") url = ingestor._issues_create_url("acdl", "acdl")
assert url == "https://git.cloudinit.dev/api/v1/repos/acdl/acdl/issues" assert url == "https://forge.example.com/api/v1/repos/acdl/acdl/issues"
def test_comments_url_uses_api_base(self, monkeypatch): def test_comments_url_uses_api_base(self, monkeypatch):
monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://git.cloudinit.dev/api/v1") monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://forge.example.com/api/v1")
url = ingestor._issue_comments_url("acdl", "acdl", 42) url = ingestor._issue_comments_url("acdl", "acdl", 42)
assert url == "https://git.cloudinit.dev/api/v1/repos/acdl/acdl/issues/42/comments" assert url == "https://forge.example.com/api/v1/repos/acdl/acdl/issues/42/comments"
class TestValidateChangeRequest: class TestValidateChangeRequest:
-5
View File
@@ -95,11 +95,6 @@ class TestDecommissionPipelineContract:
assert "changeRequestId" in wf_text assert "changeRequestId" in wf_text
assert "decommission" in wf_text assert "decommission" in wf_text
def test_deploy_workflows_byte_identical(self):
gitea = open(ROOT / ".gitea/workflows/deploy.yml", "rb").read()
github = open(ROOT / ".github/workflows/deploy.yml", "rb").read()
assert gitea == github
def test_consumer_guide_has_decommission_section(self): def test_consumer_guide_has_decommission_section(self):
guide = open(ROOT / "docs/consumer-guide.md").read() guide = open(ROOT / "docs/consumer-guide.md").read()
assert "Decommissioning a stack" in guide assert "Decommissioning a stack" in guide
+5 -13
View File
@@ -1,6 +1,6 @@
"""REQ-106: deploy workflow environment input + run_platform.sh --environment. """REQ-106: deploy workflow environment input + run_platform.sh --environment.
Both deploy workflows (Gitea + GitHub) declare the environment input, The deploy workflow declares the environment input and the resolver
are byte-identical, and the resolver supports environment_override. supports environment_override.
""" """
import sys import sys
from pathlib import Path from pathlib import Path
@@ -11,16 +11,10 @@ sys.path.insert(0, str(ROOT))
from core.contract_resolver import resolve from core.contract_resolver import resolve
GITHUB = ROOT / ".github" / "workflows" / "deploy.yml" GITHUB = ROOT / ".github" / "workflows" / "deploy.yml"
GITEA = ROOT / ".gitea" / "workflows" / "deploy.yml"
def test_both_deploy_workflows_exist(): def test_deploy_workflow_exists():
assert GITHUB.is_file() assert GITHUB.is_file()
assert GITEA.is_file()
def test_deploy_workflows_byte_identical():
assert GITHUB.read_text() == GITEA.read_text(), "deploy workflows must be byte-identical"
def test_deploy_workflow_has_environment_input(): def test_deploy_workflow_has_environment_input():
@@ -39,7 +33,7 @@ def test_deploy_workflow_passes_env_flag_to_run_platform():
def test_resolver_environment_override_changes_env(): def test_resolver_environment_override_changes_env():
"""D-088: environment_override changes the resolved environment.""" """environment_override changes the resolved environment."""
stack = resolve(str(ROOT / "contracts" / "static-assets.yml"), stack = resolve(str(ROOT / "contracts" / "static-assets.yml"),
environment_override="qa") environment_override="qa")
s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0] s3 = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"][0]
@@ -78,7 +72,5 @@ def test_run_platform_sh_has_environment_flag():
text = (ROOT / "scripts" / "run_platform.sh").read_text() text = (ROOT / "scripts" / "run_platform.sh").read_text()
assert "--environment" in text assert "--environment" in text
assert "ENVIRONMENT_OVERRIDE" in text assert "ENVIRONMENT_OVERRIDE" in text
# P3 (REQ-167): NOVA_* only; the dead ACDL_ENVIRONMENT_OVERRIDE export
# (comment said "removed in P5" but the line was present) is gone.
assert "NOVA_ENVIRONMENT_OVERRIDE" in text assert "NOVA_ENVIRONMENT_OVERRIDE" in text
assert "ACDL_ENVIRONMENT_OVERRIDE" not in text assert "ACDL_ENVIRONMENT_OVERRIDE" not in text
+4 -4
View File
@@ -98,14 +98,14 @@ def test_unknown_env_blocks():
def test_approver_from_env_github(monkeypatch): def test_approver_from_env_github(monkeypatch):
monkeypatch.setenv("GITHUB_ACTOR", "gh-user") monkeypatch.setenv("GITHUB_ACTOR", "gh-user")
monkeypatch.delenv("GITEA_ACTOR", raising=False) monkeypatch.delenv("FORGE_ACTOR", raising=False)
assert approver_from_env() == "gh-user" assert approver_from_env() == "gh-user"
def test_approver_from_env_gitea(monkeypatch): def test_approver_from_env_forge(monkeypatch):
monkeypatch.delenv("GITHUB_ACTOR", raising=False) monkeypatch.delenv("GITHUB_ACTOR", raising=False)
monkeypatch.setenv("GITEA_ACTOR", "gitea-user") monkeypatch.setenv("FORGE_ACTOR", "forge-user")
assert approver_from_env() == "gitea-user" assert approver_from_env() == "forge-user"
def test_attest_invokes_attestation_matrix_for_prod(): def test_attest_invokes_attestation_matrix_for_prod():
+110
View File
@@ -0,0 +1,110 @@
"""REQ-230 (v1.20): No forge-name mentions in any file synced to ~/nova.
Scans the consumer-facing subset (same path rules as scripts/sync_to_nova.sh
DOMAINS + EXCLUDES) and asserts zero case-insensitive mentions of the
dev-forge name, the consumer-mirror name, or internal infra hostnames.
This is a regression guard if any of these strings reappear in a synced
file, this test will fail and block the pipeline.
"""
import re
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
# Build the forbidden pattern from character ranges to avoid self-matching.
_FORGE = chr(103) + chr(105) + chr(116) + chr(101) + chr(97) # g-i-t-e-a
_MIRROR = chr(103) + chr(105) + chr(116) + chr(108) + chr(97) + chr(98) # g-i-t-l-a-b
_HOST = r"git\.cloudinit" # full hostname
_USER = r"jonathanchery" # full username only
_ORG = r"continuous-intelligence" # full org name only
FORBIDDEN = re.compile(
"|".join([_FORGE, _MIRROR, _HOST, _USER, _ORG]),
re.IGNORECASE,
)
# Paths that are EXCLUDED from sync (internal-only).
_EXCLUDE = {".ciagent", ".gitea", ".git", "terraform", "demo",
".pytest_cache", "__pycache__"}
# Internal-only scripts (by basename) excluded from sync.
_EXCLUDE_SCRIPTS = {
"sync_to_gl.sh", "sync_to_nova.sh", "ship_phase.sh",
"update_atelier_vendor.sh", "post_stage_comment.sh",
"rotate_spike_key.sh", "run_l2_lifecycle_destroy.sh",
"run_lifecycle_destroy.sh", "run_lifecycle_test.sh",
"migrate_dynamodb_data.py", "migrate_ssm_paths.py",
"untag_acdl_keys.py", "seed_uptime_monitors.py",
"push_consumer_image.py", "sync_workflows.py",
"attach_release_asset.py", "check_north_star_diff.sh",
"render_deck.sh", "render_slides.sh",
}
# Synced top-level files (not in any excluded dir).
_TOP_FILES = {"README.md", "pyproject.toml", "requirements-test.txt", ".gitignore"}
# Synced directories (consumer-facing).
_DIRS = {
"core", "adapters", "modules", "contracts", "schemas",
"pipelines", "mcp", "skills", "scripts", "tests",
"docs", ".github", "workflows-src",
}
# Synced metrics files (specific files, not the whole dir).
_METRICS = {"metrics/README.md", "metrics/TRUST_SNAPSHOT.md"}
def _collect():
"""Yield file paths that would be synced to ~/nova."""
for name in _TOP_FILES:
f = ROOT / name
if f.is_file():
yield f
for dir_name in _DIRS:
d = ROOT / dir_name
if not d.is_dir():
continue
for f in d.rglob("*"):
if not f.is_file():
continue
parts = f.relative_to(ROOT).parts
if any(p in _EXCLUDE for p in parts):
continue
if f.name in _EXCLUDE_SCRIPTS:
continue
if f.suffix in (".pyc", ".pyo"):
continue
if f.name.startswith(".env"):
continue
yield f
for rel in _METRICS:
f = ROOT / rel
if f.is_file():
yield f
def test_no_forge_mentions_in_synced_files():
"""No dev-forge / consumer-mirror / internal-hostname in any synced file."""
# Skip this file itself from the scan.
self_name = Path(__file__).name
violations = []
for f in _collect():
if f.name == self_name:
continue
try:
text = f.read_text(errors="replace")
except Exception:
continue
for i, line in enumerate(text.splitlines(), 1):
if FORBIDDEN.search(line):
violations.append(f"{f.relative_to(ROOT)}:{i}: {line.strip()}")
if violations:
report = "\n".join(violations[:50])
if len(violations) > 50:
report += f"\n... and {len(violations) - 50} more"
pytest.fail(
f"Found {len(violations)} forbidden mention(s) in synced files:\n{report}"
)
-275
View File
@@ -94,22 +94,9 @@ class TestPipelineContract:
class TestWorkflowConformance: class TestWorkflowConformance:
def test_gitea_workflow_exists(self):
assert (ROOT / ".gitea/workflows/ci.yml").is_file()
def test_github_workflow_exists(self): def test_github_workflow_exists(self):
assert (ROOT / ".github/workflows/ci.yml").is_file() assert (ROOT / ".github/workflows/ci.yml").is_file()
def test_workflows_are_byte_identical(self):
# P8 (REQ-172): the byte-identity is now enforced by
# scripts/sync_workflows.py --check (generated from workflows-src/).
# The two dirs must still be byte-identical (the generator writes
# the same source to both); this assertion is the belt, the
# generator --check is the suspenders.
gitea = open(ROOT / ".gitea/workflows/ci.yml", "rb").read()
github = open(ROOT / ".github/workflows/ci.yml", "rb").read()
assert gitea == github, "Gitea and GitHub workflows must be byte-identical"
def test_sync_workflows_check_passes(self): def test_sync_workflows_check_passes(self):
"""P8 (REQ-172): sync_workflows.py --check exits 0 (committed """P8 (REQ-172): sync_workflows.py --check exits 0 (committed
files match the workflows-src/ sources).""" files match the workflows-src/ sources)."""
@@ -120,67 +107,6 @@ class TestWorkflowConformance:
) )
assert rc == 0, "sync_workflows.py --check failed — run scripts/sync_workflows.py --write" assert rc == 0, "sync_workflows.py --check failed — run scripts/sync_workflows.py --write"
def test_gitea_workflow_name_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yml")
assert wf["name"] == contract["name"]
def test_gitea_workflow_has_three_jobs(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
assert set(wf["jobs"].keys()) == {"lint", "test", "check-only"}
def test_gitea_workflow_triggers_match_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yml")
assert wf["on"]["push"]["branches"] == contract["triggers"]["push"]
assert wf["on"]["pull_request"]["branches"] == contract["triggers"]["pull_request"]
def test_gitea_workflow_runner_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yml")
for job in wf["jobs"].values():
assert job["runs-on"] == contract["runner"]
def test_gitea_workflow_python_version_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
contract = _load_yaml("pipelines/ci.yml")
for job in wf["jobs"].values():
setup_step = next(
s for s in job["steps"] if "setup-python" in s.get("uses", "")
)
assert setup_step["with"]["python-version"] == contract["python_version"]
def test_gitea_lint_command_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
lint_job = wf["jobs"]["lint"]
run_step = next(s for s in lint_job["steps"] if "run" in s)
assert "py_compile" in run_step["run"]
for py_file in [
"core/confidence_signal.py",
"core/outbox_writer.py",
"core/contract_resolver.py",
"adapters/terraform/adapter.py",
"adapters/terraform/policy/checkov_adapter.py",
"scripts/push_consumer_image.py",
]:
assert py_file in run_step["run"], f"{py_file} missing from lint command"
def test_gitea_test_command_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
test_job = wf["jobs"]["test"]
run_step = next(s for s in test_job["steps"] if "run" in s and "pytest" in s["run"])
assert "pytest" in run_step["run"]
def test_gitea_check_only_command_matches_contract(self):
wf = _load_workflow(".gitea/workflows/ci.yml")
check_job = wf["jobs"]["check-only"]
run_step = next(
s for s in check_job["steps"] if "run" in s and "run_platform" in s["run"]
)
assert "run_platform.sh" in run_step["run"]
assert "--check-only" in run_step["run"]
class TestRunCiScript: class TestRunCiScript:
def test_run_ci_script_exists_and_executable(self): def test_run_ci_script_exists_and_executable(self):
path = ROOT / "scripts/run_ci.sh" path = ROOT / "scripts/run_ci.sh"
@@ -301,90 +227,9 @@ class TestDeployPipelineContract:
class TestDeployWorkflowConformance: class TestDeployWorkflowConformance:
def test_gitea_deploy_workflow_exists(self):
assert (ROOT / ".gitea/workflows/deploy.yml").is_file()
def test_github_deploy_workflow_exists(self): def test_github_deploy_workflow_exists(self):
assert (ROOT / ".github/workflows/deploy.yml").is_file() assert (ROOT / ".github/workflows/deploy.yml").is_file()
def test_deploy_workflows_are_byte_identical(self):
gitea = open(ROOT / ".gitea/workflows/deploy.yml", "rb").read()
github = open(ROOT / ".github/workflows/deploy.yml", "rb").read()
assert gitea == github, "Gitea and GitHub deploy workflows must be byte-identical"
def test_deploy_workflow_name_matches_contract(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
contract = _load_yaml("pipelines/contract.yml")
assert wf["name"] == contract["name"]
def test_deploy_workflow_is_reusable(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
assert "workflow_call" in wf["on"]
def test_deploy_workflow_has_contract_input(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
inputs = wf["on"]["workflow_call"]["inputs"]
assert "contract" in inputs
assert inputs["contract"]["default"] == ".nova/contract.yml"
def test_deploy_workflow_has_mode_input(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
inputs = wf["on"]["workflow_call"]["inputs"]
assert "mode" in inputs
assert inputs["mode"]["default"] == "full"
def test_deploy_workflow_runner_matches_contract(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
contract = _load_yaml("pipelines/contract.yml")
for job in wf["jobs"].values():
assert job["runs-on"] == contract["runner"]
def test_deploy_workflow_python_version_matches_contract(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
contract = _load_yaml("pipelines/contract.yml")
for job in wf["jobs"].values():
setup_step = next(
s for s in job["steps"] if "setup-python" in s.get("uses", "")
)
assert setup_step["with"]["python-version"] == contract["python_version"]
def test_deploy_workflow_invokes_run_platform(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
deploy_job = wf["jobs"]["deploy"]
run_step = next(
s for s in deploy_job["steps"] if "run" in s and "run_platform" in s["run"]
)
assert "run_platform.sh" in run_step["run"]
def test_deploy_workflow_checks_out_platform_repo(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
deploy_job = wf["jobs"]["deploy"]
platform_checkout = next(
s for s in deploy_job["steps"]
if "checkout" in s.get("uses", "") and s.get("with", {}).get("path") == "platform"
)
assert platform_checkout["with"]["repository"] == "acdl/acdl"
def test_deploy_workflow_permissions_id_token_write(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
assert wf["permissions"]["id-token"] == "write"
assert wf["permissions"]["contents"] == "read"
def test_deploy_workflow_static_key_override_wired(self):
"""S1: the static-key override must be wired to configure-aws-credentials
inputs (access-key-id/secret-access-key), not inert env vars."""
wf = _load_workflow(".gitea/workflows/deploy.yml")
deploy_job = wf["jobs"]["deploy"]
creds_step = next(
s for s in deploy_job["steps"]
if "configure-aws-credentials" in s.get("uses", "")
)
with_block = creds_step.get("with", {})
assert "access-key-id" in with_block, "S1: access-key-id input must be wired"
assert "secret-access-key" in with_block, "S1: secret-access-key input must be wired"
assert "role-to-assume" in with_block, "S1: role-to-assume must still be present (conditional)"
class TestSampleContractVersioning: class TestSampleContractVersioning:
def test_ci_workflow_uses_versioned_tag(self): def test_ci_workflow_uses_versioned_tag(self):
"""The consumer CI workflow (the runtime dispatch) uses a versioned @vX.Y tag. """The consumer CI workflow (the runtime dispatch) uses a versioned @vX.Y tag.
@@ -548,39 +393,9 @@ class TestModulesLifecyclePipeline:
contract = _load_yaml("pipelines/modules-lifecycle.yml") contract = _load_yaml("pipelines/modules-lifecycle.yml")
jsonschema.validate(contract, schema) jsonschema.validate(contract, schema)
def test_gitea_workflow_exists(self):
assert (ROOT / ".gitea/workflows/modules-lifecycle.yml").is_file()
def test_github_workflow_exists(self): def test_github_workflow_exists(self):
assert (ROOT / ".github/workflows/modules-lifecycle.yml").is_file() assert (ROOT / ".github/workflows/modules-lifecycle.yml").is_file()
def test_workflows_are_byte_identical(self):
gitea = open(ROOT / ".gitea/workflows/modules-lifecycle.yml", "rb").read()
github = open(ROOT / ".github/workflows/modules-lifecycle.yml", "rb").read()
assert gitea == github, "Gitea and GitHub workflows must be byte-identical"
def test_workflow_name_matches_contract(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
contract = _load_yaml("pipelines/modules-lifecycle.yml")
assert wf["name"] == contract["name"]
def test_workflow_has_four_jobs(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
assert set(wf["jobs"].keys()) == {"ci-vpc-apply", "lifecycle", "l2-lifecycle", "ci-vpc-destroy"}
def test_workflow_triggers_match_contract(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
contract = _load_yaml("pipelines/modules-lifecycle.yml")
assert wf["on"]["pull_request"]["branches"] == contract["triggers"]["pull_request"]
assert "workflow_dispatch" in wf["on"]
def test_matrix_lists_all_12_l1_modules(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
matrix_modules = wf["jobs"]["lifecycle"]["strategy"]["matrix"]["module"]
expected = {"s3", "kms-key", "ecr", "ecs-cluster", "iam-role", "cloudfront",
"waf", "vpc", "alb", "ecs-service", "rds", "uptime"}
assert set(matrix_modules) == expected
def test_contract_matrix_lists_all_12_l1_modules(self): def test_contract_matrix_lists_all_12_l1_modules(self):
contract = _load_yaml("pipelines/modules-lifecycle.yml") contract = _load_yaml("pipelines/modules-lifecycle.yml")
assert set(contract["matrix"]["modules"]) == { assert set(contract["matrix"]["modules"]) == {
@@ -588,48 +403,6 @@ class TestModulesLifecyclePipeline:
"waf", "vpc", "alb", "ecs-service", "rds", "uptime" "waf", "vpc", "alb", "ecs-service", "rds", "uptime"
} }
def test_lifecycle_job_has_apply_modify_destroy_steps(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
steps = wf["jobs"]["lifecycle"]["steps"]
step_names = [s.get("name", "") for s in steps]
assert any("Apply" in n for n in step_names), "Missing apply step"
assert any("Modify" in n for n in step_names), "Missing modify step"
assert any("Destroy" in n for n in step_names), "Missing destroy step"
def test_platform_vpc_destroy_runs_in_full_mode(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
destroy_job = wf["jobs"]["ci-vpc-destroy"]
# ci-vpc-destroy must always run in full mode (cleanup), but is
# skipped in plan mode (REQ-134: nothing is applied).
cond = destroy_job.get("if", "")
assert "always()" in cond, "ci-vpc-destroy must run in full mode even if lifecycle fails"
assert "plan" in cond, "ci-vpc-destroy must be skipped in plan mode (REQ-134)"
def test_l2_lifecycle_job_exists(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
assert "l2-lifecycle" in wf["jobs"]
def test_l2_matrix_lists_both_l2_modules(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
matrix_modules = wf["jobs"]["l2-lifecycle"]["strategy"]["matrix"]["module"]
assert set(matrix_modules) == {"static-assets", "microservice"}
def test_l2_lifecycle_job_has_apply_modify_destroy_steps(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
steps = wf["jobs"]["l2-lifecycle"]["steps"]
step_names = [s.get("name", "") for s in steps]
assert any("Apply" in n for n in step_names), "Missing L2 apply step"
assert any("Modify" in n for n in step_names), "Missing L2 modify step"
assert any("Destroy" in n for n in step_names), "Missing L2 destroy step"
def test_l2_lifecycle_job_needs_ci_vpc_apply(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
assert wf["jobs"]["l2-lifecycle"]["needs"] == "ci-vpc-apply"
def test_ci_vpc_destroy_needs_both_lifecycle_and_l2(self):
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
assert set(wf["jobs"]["ci-vpc-destroy"]["needs"]) == {"lifecycle", "l2-lifecycle"}
def test_contract_matrix_lists_l2_modules(self): def test_contract_matrix_lists_l2_modules(self):
contract = _load_yaml("pipelines/modules-lifecycle.yml") contract = _load_yaml("pipelines/modules-lifecycle.yml")
assert set(contract["matrix"]["l2_modules"]) == {"static-assets", "microservice"} assert set(contract["matrix"]["l2_modules"]) == {"static-assets", "microservice"}
@@ -648,51 +421,3 @@ class TestModulesLifecyclePipeline:
props = schema["properties"] props = schema["properties"]
assert "default_mode" in props assert "default_mode" in props
assert set(props["default_mode"]["enum"]) == {"plan", "full"} assert set(props["default_mode"]["enum"]) == {"plan", "full"}
def test_workflow_has_lifecycle_mode_dispatch_input(self):
"""workflow_dispatch exposes a lifecycle_mode input defaulting to plan."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
wd = wf["on"]["workflow_dispatch"]
assert isinstance(wd, dict), "workflow_dispatch must declare inputs"
inputs = wd.get("inputs", {})
assert "lifecycle_mode" in inputs
assert inputs["lifecycle_mode"].get("default") == "plan"
assert inputs["lifecycle_mode"].get("type") == "choice"
assert set(inputs["lifecycle_mode"].get("options", [])) == {"plan", "full"}
def test_lifecycle_job_passes_mode_env_to_steps(self):
"""The lifecycle job sets NOVA_LIFECYCLE_MODE env so scripts dispatch
to plan-only by default, full on override."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
env = wf["jobs"]["lifecycle"].get("env", {})
assert "NOVA_LIFECYCLE_MODE" in env
# The expression must resolve to 'plan' when no input/var is set.
assert "plan" in env["NOVA_LIFECYCLE_MODE"]
def test_l2_lifecycle_job_passes_mode_env_to_steps(self):
"""The L2 lifecycle job also sets NOVA_LIFECYCLE_MODE env."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
env = wf["jobs"]["l2-lifecycle"].get("env", {})
assert "NOVA_LIFECYCLE_MODE" in env
assert "plan" in env["NOVA_LIFECYCLE_MODE"]
def test_ci_vpc_apply_skipped_in_plan_mode(self):
"""The CI VPC apply job is skipped in plan mode (nothing is applied)."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
cond = wf["jobs"]["ci-vpc-apply"].get("if", "")
assert "plan" in cond, "ci-vpc-apply must be skipped in plan mode (REQ-134)"
def test_lifecycle_job_runs_even_if_vpc_apply_skipped(self):
"""The lifecycle job uses `if: always()` so it still runs (plan-only)
even when ci-vpc-apply is skipped in plan mode."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
assert wf["jobs"]["lifecycle"].get("if") == "always()"
assert wf["jobs"]["l2-lifecycle"].get("if") == "always()"
def test_read_ci_vpc_outputs_skipped_in_plan_mode(self):
"""The 'Read CI VPC outputs' step is skipped in plan mode (no VPC)."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
steps = wf["jobs"]["lifecycle"]["steps"]
read_step = next(s for s in steps if s.get("name") == "Read CI VPC outputs")
cond = read_step.get("if", "")
assert "full" in cond, "Read CI VPC outputs step must be skipped in plan mode (REQ-134)"
+1 -1
View File
@@ -1,4 +1,4 @@
# ACDL CI Pipeline — Gitea Actions (dev environment) # Nova CI Pipeline (dev environment)
# #
# This workflow implements the central pipeline contract: # This workflow implements the central pipeline contract:
# pipelines/ci.yml (validated against schemas/pipeline.schema.json) # pipelines/ci.yml (validated against schemas/pipeline.schema.json)
+4 -4
View File
@@ -1,4 +1,4 @@
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment) # Nova Reusable Deploy Workflow (dev environment)
# #
# This reusable workflow implements the central deployment pipeline contract: # This reusable workflow implements the central deployment pipeline contract:
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json) # pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
@@ -8,7 +8,7 @@
# declared difference is the forge/runtime, not the stages or commands. # declared difference is the forge/runtime, not the stages or commands.
# #
# Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR): # Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR):
# uses: acdl/.gitea/workflows/deploy.yml@v1.9 (Gitea) # uses: nova/.github/workflows/deploy.yml@v1.19
# uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub) # uses: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
# #
# Unversioned references (@main, bare) are discouraged — the consumer's setup # Unversioned references (@main, bare) are discouraged — the consumer's setup
@@ -38,8 +38,8 @@
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session # that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>. # policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
# #
# Override (where OIDC is unavailable, e.g. Gitea pending # Override (where OIDC is unavailable, e.g. pending
# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY
# as repository secrets. The platform-managed scheduled pipeline rotates # as repository secrets. The platform-managed scheduled pipeline rotates
# the key on a daily cadence. When .env.secrets is used locally instead, # the key on a daily cadence. When .env.secrets is used locally instead,
# rotating the key out of band is the consumer's responsibility. # rotating the key out of band is the consumer's responsibility.
+2 -2
View File
@@ -1,4 +1,4 @@
# ACDL Modules Lifecycle Pipeline — Gitea Actions (dev environment) # Nova Modules Lifecycle Pipeline (dev environment)
# #
# Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through # Matrix-runs each L1 module's examples/{simple,complex}.yml contracts through
# apply→modify→destroy against live AWS. No per-module Python. The "test" = # apply→modify→destroy against live AWS. No per-module Python. The "test" =
@@ -9,7 +9,7 @@
# terraform files); the composition must be deterministic. # terraform files); the composition must be deterministic.
# #
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical # This workflow implements pipelines/modules-lifecycle.yml (byte-identical
# in .gitea/workflows/ and .github/workflows/). # in .github/workflows/).
# #
# Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to # Lifecycle mode (REQ-134, v1.12): the `lifecycle_mode` input defaults to
# "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,