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:
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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,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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -41,3 +41,4 @@ metrics/lifecycle/
|
|||||||
*.crt
|
*.crt
|
||||||
*.jks
|
*.jks
|
||||||
*.keystore.coverage
|
*.keystore.coverage
|
||||||
|
.coverage
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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,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.
|
||||||
|
|
||||||
|
|||||||
@@ -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**:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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-*`.
|
|
||||||
@@ -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 P2–P4, 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 P2–P4 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 P2–P4 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 P2–P4 — 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 P3–P4 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 P3–P4 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 P2–P4.)*
|
|
||||||
2. **Before P5 — env vars:** rename `ACDL_*` CI secrets / workflow `env:`
|
|
||||||
blocks / local `.env.secrets` to `NOVA_*`. *(Incremental during P2–P4;
|
|
||||||
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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 -> resolver -> stack -> adapter<br/>-> security checks -> infrastructure plan -> policy checks<br/>-> confidence -> apply -> evidence event| C
|
B["platform runners<br/>(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter<br/>-> security checks -> infrastructure plan -> policy checks<br/>-> confidence -> apply -> 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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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,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
@@ -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 (C1–C8) and 19 domains, each with 10 derived P-rules. The
|
principles (C1–C8) 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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 "
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
)
|
||||||
@@ -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,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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user