From 0d2cbdb423300df841ba028ec36a5c7104680de7 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Fri, 7 Aug 2026 18:20:29 +0000 Subject: [PATCH] feat(P1): remove gitea/gitlab from synced files + simplify docs (REQ-230,231,232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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--- --- {docs => .ciagent}/NO_HUMANS_THESIS.md | 0 .gitea/workflows/ci.yml | 2 +- .gitea/workflows/deploy.yml | 8 +- .gitea/workflows/modules-lifecycle.yml | 4 +- .github/workflows/README.md | 25 +- .github/workflows/ci.yml | 2 +- .github/workflows/deploy.yml | 8 +- .github/workflows/modules-lifecycle.yml | 4 +- .gitignore | 1 + README.md | 26 +- core/audit_ledger_design.md | 6 +- core/hitl_gates.py | 8 +- core/hitl_matrix_design.md | 30 +- core/lambda/contract_ingestor.py | 16 +- core/separation_of_duties.py | 2 +- docs/METRICS.md | 2 - docs/METRICS_DEFERRED_ROADMAP.md | 2 - docs/METRICS_VIEWS.md | 2 - docs/NOVA_AWS_MIGRATION.md | 270 ----------------- docs/NOVA_MIGRATION.md | 177 ----------- docs/ONBOARDING.md | 2 +- docs/architecture.md | 2 +- docs/consumer-guide.md | 26 +- docs/index.md | 7 - docs/pipeline/index.md | 2 +- docs/pipeline/versioning.md | 4 +- docs/presentations/README.md | 14 +- docs/raci.md | 1 - docs/scope.md | 1 - docs/skills.md | 4 +- mcp/atelier/README.md | 11 +- metrics/README.md | 1 - metrics/powerbi/NOVA_DASHBOARD_README.md | 2 - pipelines/README.md | 10 +- pipelines/ci.yml | 4 +- pipelines/contract.yml | 2 +- pipelines/modules-lifecycle.yml | 4 +- schemas/README.md | 2 +- .../modules-lifecycle-pipeline.schema.json | 2 +- schemas/pipeline.schema.json | 8 +- scripts/run_ci.sh | 2 +- scripts/run_platform.sh | 4 +- tests/test_contract_ingestor.py | 20 +- tests/test_decommission.py | 5 - tests/test_deploy_workflow_env_input.py | 18 +- tests/test_hitl_gates.py | 8 +- tests/test_no_forge_mentions.py | 110 +++++++ tests/test_pipeline_contract.py | 275 ------------------ workflows-src/ci.yml | 2 +- workflows-src/deploy.yml | 8 +- workflows-src/modules-lifecycle.yml | 4 +- 51 files changed, 246 insertions(+), 914 deletions(-) rename {docs => .ciagent}/NO_HUMANS_THESIS.md (100%) delete mode 100644 docs/NOVA_AWS_MIGRATION.md delete mode 100644 docs/NOVA_MIGRATION.md create mode 100644 tests/test_no_forge_mentions.py diff --git a/docs/NO_HUMANS_THESIS.md b/.ciagent/NO_HUMANS_THESIS.md similarity index 100% rename from docs/NO_HUMANS_THESIS.md rename to .ciagent/NO_HUMANS_THESIS.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c7fb14d..839b9bd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,4 +1,4 @@ -# ACDL CI Pipeline — Gitea Actions (dev environment) +# Nova CI Pipeline (dev environment) # # This workflow implements the central pipeline contract: # pipelines/ci.yml (validated against schemas/pipeline.schema.json) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b1605ab..d9906dc 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -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: # 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. # # 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) # # 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 # policy restricts view/update to resources tagged acdl:owner=. # -# Override (where OIDC is unavailable, e.g. Gitea pending -# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY +# Override (where OIDC is unavailable, e.g. pending +# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # as repository secrets. The platform-managed scheduled pipeline rotates # the key on a daily cadence. When .env.secrets is used locally instead, # rotating the key out of band is the consumer's responsibility. diff --git a/.gitea/workflows/modules-lifecycle.yml b/.gitea/workflows/modules-lifecycle.yml index 03699c2..1213864 100644 --- a/.gitea/workflows/modules-lifecycle.yml +++ b/.gitea/workflows/modules-lifecycle.yml @@ -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 # apply→modify→destroy against live AWS. No per-module Python. The "test" = @@ -9,7 +9,7 @@ # terraform files); the composition must be deterministic. # # 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 # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 4a07a00..b6ea419 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,35 +1,30 @@ # GitHub Workflows — Nova Platform CI/CD Catalog -This directory contains the 7 GitHub Actions workflows for the Nova -platform. 3 are byte-identical Gitea mirrors (generated from -`workflows-src/` by `scripts/sync_workflows.py`, P8/REQ-172); 4 are -GitHub-only (Gitea act_runner feature gaps). +This directory contains the GitHub Actions workflows for the Nova +platform. 3 are generated from `workflows-src/`; 4 are GitHub-only. -## Shared workflows (byte-identical Gitea + GitHub) +## Shared workflows (generated from source) -These 3 are generated from `workflows-src/` by -`scripts/sync_workflows.py`; the `.gitea/workflows/` mirror is kept -byte-identical. Run `python3 scripts/sync_workflows.py --check` to verify +These 3 are generated from `workflows-src/`. Run `python3 scripts/sync_workflows.py --check` to verify no drift. | Workflow | Trigger | Inputs | Required Secrets | Purpose | |----------|---------|--------|------------------|---------| | `ci.yml` | `pull_request: [main]` | — | — | Lint + test + check-only (runs on every PR) | -| `deploy.yml` | `workflow_call` (reusable) + `push: [main]` | `contract` (string, required), `mode` (string, default `deploy`), `changeRequestId` (string), `environment` (string) | `NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_AWS_DEFAULT_REGION`, `NOVA_KMS_KEY_ID`, `NOVA_LAMBDA_URL` | Reusable deploy workflow (invoked by consumer repos via `uses: acdl/.github/workflows/deploy.yml@v1.15`) | +| `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) | -## GitHub-only workflows (no Gitea mirror) +## GitHub-only workflows -These 4 have no Gitea counterpart (Gitea act_runner lacks the features -they require — reusable workflows, matrix `needs`, release API). See -`.gitea/workflows/README.md` for the limitation rationale. +These 4 have no counterpart (the dev forge lacks the features +they require — reusable workflows, matrix `needs`, release API). | Workflow | Trigger | Inputs | Required Secrets | Purpose | |----------|---------|--------|------------------|---------| | `platform-test.yml` | `pull_request: [main]` | — | — | Lint + unit + integration + schema-validation (replaces `ci.yml` for PRs) | | `primitives-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L1 primitives (matrix) | | `patterns-plan.yml` | `pull_request: [main]` | — | `NOVA_AWS_*` | Plan-only for all L2 modules (matrix) | -| `release.yml` | `push: [main]` | — | `NOVA_GITEA_TOKEN` (for Gitea release API) | Semver tag + MAJOR.MINOR/MAJOR floating-tag maintenance + release creation on merge to main | +| `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`) @@ -38,7 +33,7 @@ Consumer repos invoke the deploy workflow via a versioned tag: ```yaml jobs: deploy: - uses: acdl/.github/workflows/deploy.yml@v1.15 + uses: nova/.github/workflows/deploy.yml@v1.19 with: contract: .nova/contract.yml environment: dev diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7fb14d..839b9bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -# ACDL CI Pipeline — Gitea Actions (dev environment) +# Nova CI Pipeline (dev environment) # # This workflow implements the central pipeline contract: # pipelines/ci.yml (validated against schemas/pipeline.schema.json) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b1605ab..d9906dc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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: # 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. # # 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) # # 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 # policy restricts view/update to resources tagged acdl:owner=. # -# Override (where OIDC is unavailable, e.g. Gitea pending -# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY +# Override (where OIDC is unavailable, e.g. pending +# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # as repository secrets. The platform-managed scheduled pipeline rotates # the key on a daily cadence. When .env.secrets is used locally instead, # rotating the key out of band is the consumer's responsibility. diff --git a/.github/workflows/modules-lifecycle.yml b/.github/workflows/modules-lifecycle.yml index 03699c2..1213864 100644 --- a/.github/workflows/modules-lifecycle.yml +++ b/.github/workflows/modules-lifecycle.yml @@ -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 # apply→modify→destroy against live AWS. No per-module Python. The "test" = @@ -9,7 +9,7 @@ # terraform files); the composition must be deterministic. # # 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 # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast, diff --git a/.gitignore b/.gitignore index 25b28c4..dea8276 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ metrics/lifecycle/ *.crt *.jks *.keystore.coverage +.coverage diff --git a/README.md b/README.md index 2664425..9ea8b3a 100644 --- a/README.md +++ b/README.md @@ -219,23 +219,9 @@ bash scripts/run_ci.sh --quiet # suppress per-stage banners ### Reusable deploy workflow -The deployment pipeline is defined by a **central deployment pipeline -contract** (`pipelines/contract.yml`, validated against -`schemas/deploy-pipeline.schema.json`) and exposed to consumer repos as a -**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. +Consumer repos invoke the deploy pipeline via `.github/workflows/deploy.yml` +(a reusable GitHub Actions workflow, versioned tag `nova/.github/workflows/deploy.yml@v1.19`). +See the [Consumer guide](docs/consumer-guide.md) for the end-to-end happy path. ### Output streaming (run_platform.sh) @@ -310,12 +296,6 @@ documented alternative: runs, or in **`.env.secrets`** (gitignored, chmod 600) for local testing. - The platform rotates platform-runner keys on a **daily cadence** — 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 key's useful lifetime is one workflow run, and the local alternative is rotated at least daily (platform-runner) or out of band (local). \ No newline at end of file diff --git a/core/audit_ledger_design.md b/core/audit_ledger_design.md index 228cff1..a15d719 100644 --- a/core/audit_ledger_design.md +++ b/core/audit_ledger_design.md @@ -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 explode the key-management surface), rotated **quarterly**. The `jws` 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 SQS dead-letter queue for failed writes. RTO = DLQ replay. - **Daily checkpoints (§9):** a daily job reads the last event hash and @@ -86,7 +86,7 @@ log" anti-goal requires. D-083 ships). - `prev_event_hash` (chain link; `GENESIS` for the first event). - `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). - `approver_prod` (SRE username; populated on prod-promotion by v1.9's `hitl_gates.attest`). @@ -112,7 +112,7 @@ log" anti-goal requires. - **D-042** — approver identities (`approver_qa`, `approver_prod`, `approver_dr`) live in the outbox; the separation-of-duties check (`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. - **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily checkpoints deferred to a future milestone. Requires non-offline- diff --git a/core/hitl_gates.py b/core/hitl_gates.py index 15a8af9..c7c9a01 100644 --- a/core/hitl_gates.py +++ b/core/hitl_gates.py @@ -1,6 +1,6 @@ """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` / `approver_prod` / `approver_dr`), runs the separation-of-duties check on 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: contract_id: the contract UUID. 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 attestation matrix operator-supplied concerns). 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)") 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) if not attr: @@ -88,7 +88,7 @@ def attest(contract_id: str, env: str, approver: str, def approver_from_env() -> Optional[str]: """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__": diff --git a/core/hitl_matrix_design.md b/core/hitl_matrix_design.md index 02bab8a..49278f4 100644 --- a/core/hitl_matrix_design.md +++ b/core/hitl_matrix_design.md @@ -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 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 is modeled as a `workflow_dispatch` with approval inputs: - **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`; - `gitea.actor` is the SRE approver. + `CI actor` is the SRE approver. - **dr gate:** `workflow_dispatch` with `approve_dr: true`; same. -The approver identity of record = `gitea.actor` of the dispatch run -(D-042). There is no other approval-identity signal in Gitea. The real -OIDC path (blocked on go-gitea/gitea#36988) does not change this — +The approver identity of record = `CI actor` of the dispatch run +(D-042). There is no other approval-identity signal in the dev forge. The real +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 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, 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) -Gitea CODEOWNERS routes the right reviewer to the right gate: +CODEOWNERS routes the right reviewer to the right gate: - qa → QA 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 | | 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` older than 1/2 business days and emits the warn/freeze events. Not 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: 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 `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. 3. If `approver_qa == approver_prod`, the platform blocks the prod 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 -- **D-042** — approver identity = `gitea.actor` of the `workflow_dispatch` - run; no Environments API in Gitea. On GitHub, `github.actor`. +- **D-042** — approver identity = `CI actor` of the `workflow_dispatch` + run; no Environments API in the dev forge. - **D-013** (v1.0) — the `workflow_dispatch` approval-input fallback, re-used for the real platform's pre-execution gate model. - **D-084** (v1.9) — 8-concern attestation matrix: offline-testable diff --git a/core/lambda/contract_ingestor.py b/core/lambda/contract_ingestor.py index dad2cfe..a3ed1af 100644 --- a/core/lambda/contract_ingestor.py +++ b/core/lambda/contract_ingestor.py @@ -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") PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "nova/acdl") # 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") # P11 (REQ-175): consistent cap for error/stackTrace fields (was 10k vs 2k). @@ -96,22 +96,22 @@ def _iso8601_now(): 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: - return "gitea" + return "generic_forge" return "github" 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). """ - if _forge_type() == "gitea": + if _forge_type() == "generic_forge": return ( f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues" 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): - """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" diff --git a/core/separation_of_duties.py b/core/separation_of_duties.py index fd35a28..9a739ee 100644 --- a/core/separation_of_duties.py +++ b/core/separation_of_duties.py @@ -1,6 +1,6 @@ """Check that qaApprover != prodApprover for a contract (ARCHITECTURE.md §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 artifact to SRE on-call. diff --git a/docs/METRICS.md b/docs/METRICS.md index 36613b6..f30d89b 100644 --- a/docs/METRICS.md +++ b/docs/METRICS.md @@ -1,7 +1,5 @@ # 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 leadership metrics layer. Each metric carries a **status**: diff --git a/docs/METRICS_DEFERRED_ROADMAP.md b/docs/METRICS_DEFERRED_ROADMAP.md index 157beb2..0d5ae51 100644 --- a/docs/METRICS_DEFERRED_ROADMAP.md +++ b/docs/METRICS_DEFERRED_ROADMAP.md @@ -1,7 +1,5 @@ # 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 "granted" half, with their blocking decisions, unblock requirements, diff --git a/docs/METRICS_VIEWS.md b/docs/METRICS_VIEWS.md index 855e4dd..2cb6d6b 100644 --- a/docs/METRICS_VIEWS.md +++ b/docs/METRICS_VIEWS.md @@ -1,7 +1,5 @@ # 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 views in `metrics/powerbi/`. Each fact/dimension table and placeholder diff --git a/docs/NOVA_AWS_MIGRATION.md b/docs/NOVA_AWS_MIGRATION.md deleted file mode 100644 index ce0303f..0000000 --- a/docs/NOVA_AWS_MIGRATION.md +++ /dev/null @@ -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-` | `nova-deploy-` | 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 .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="" - export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="" - 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-` → `nova-deploy-` 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:///` 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-` - - 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-*`. \ No newline at end of file diff --git a/docs/NOVA_MIGRATION.md b/docs/NOVA_MIGRATION.md deleted file mode 100644 index 5995f4a..0000000 --- a/docs/NOVA_MIGRATION.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 87b654e..db597a7 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -77,7 +77,7 @@ milestone (D-113). only (D-114); live apply is deferred. - **OIDC trust policy** — the onboarding Terraform uses a placeholder OIDC provider; real OIDC federation is blocked on - go-gitea/gitea#36988 (carries forward from v1.1). + upstream forge OIDC support (carries forward from v1.1). ## See also diff --git a/docs/architecture.md b/docs/architecture.md index 1d0e3d2..6d4591f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -230,7 +230,7 @@ change to the modules/stack/confidence/audit. - A MAJOR bump requires a new registry entry (immutable publication); the old entry enters a 12-month deprecation window. - 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. See [Versioning](pipeline/versioning) for the consumer-facing details. diff --git a/docs/consumer-guide.md b/docs/consumer-guide.md index d52ce36..6b425a2 100644 --- a/docs/consumer-guide.md +++ b/docs/consumer-guide.md @@ -19,7 +19,7 @@ definitions. ```mermaid flowchart LR - A["your repo
(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.13| B + A["your repo
(app code + contracts + CI definitions)"] -->|uses: nova/.github/workflows/deploy.yml@v1.19| B B["platform runners
(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter
-> security checks -> infrastructure plan -> policy checks
-> confidence -> apply -> evidence event| C C["your resources in AWS"] ``` @@ -27,13 +27,13 @@ flowchart LR ## Versioning the `uses:` reference 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 `uses:` is the only immutability lever a consumer has. See [Versioning](pipeline/versioning) for the full rationale. **Unversioned references are discouraged.** Do not use `@main` or a bare -`acdl/pipelines/contract.yml`. +`nova/pipelines/contract.yml`. ## Prerequisites @@ -47,7 +47,7 @@ platform-managed. See [Environments](environments/). environment is bound, your first pipeline run emits a friendly onboarding prompt. See [Environments](environments/). - **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. ## Step 1 — Create a consumer repo @@ -94,7 +94,7 @@ Nova deployment workflow with a **versioned tag** (floating MAJOR + MINOR): ```yaml jobs: deploy: - uses: acdl/.github/workflows/deploy.yml@v1.13 + uses: nova/.github/workflows/deploy.yml@v1.19 with: contract: .nova/contract.yml environment: dev @@ -140,7 +140,7 @@ name: microservice | 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/). | | `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). | @@ -177,14 +177,14 @@ on: branches: [main] jobs: deploy: - uses: acdl/.github/workflows/deploy.yml@v1.13 + uses: nova/.github/workflows/deploy.yml@v1.19 with: contract: .nova/contract.yml ``` 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**. 2. A **platform-provided runner** checks out **your** repo. 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. | | Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. | | Module catalog | [modules/](modules/) | All primitives and modules. | -| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.13`). | -| Sample contract | `contracts/microservice.yaml` | The microservice 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.19`). | | Module examples | `modules//examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). | | Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. | | Angine adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. | @@ -353,7 +353,7 @@ destruction: use `mode: decommission` with the `changeRequestId` input: ```yaml - uses: acdl/.github/workflows/deploy.yml@v1.13 + uses: nova/.github/workflows/deploy.yml@v1.19 with: contract: .nova/contract.yml mode: decommission @@ -421,7 +421,7 @@ name: static-assets ``` **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 contract's `environment` field at load time (before interpolation), so the same contract can be promoted by passing a different environment: @@ -436,7 +436,7 @@ on: workflow_dispatch: required: true jobs: deploy-qa: - uses: acdl/.github/workflows/deploy.yml@v1.13 + uses: nova/.github/workflows/deploy.yml@v1.19 with: environment: qa contract: .nova/contract.yml diff --git a/docs/index.md b/docs/index.md index 456c82e..a21ef99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. - [Architecture](architecture) — start here if you are a platform engineer. - 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. \ No newline at end of file diff --git a/docs/pipeline/index.md b/docs/pipeline/index.md index e1b4fa6..3791399 100644 --- a/docs/pipeline/index.md +++ b/docs/pipeline/index.md @@ -39,7 +39,7 @@ It is exposed to consumer repos as a **reusable workflow**: - `.github/workflows/deploy.yml` — GitHub Actions (production) 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 repo into the runner workspace, and runs `scripts/run_platform.sh` against the consumer's contract. The consumer never clones the platform repo or diff --git a/docs/pipeline/versioning.md b/docs/pipeline/versioning.md index 8447206..3e7e054 100644 --- a/docs/pipeline/versioning.md +++ b/docs/pipeline/versioning.md @@ -26,7 +26,7 @@ tag** in a consumer's CI workflow definition: ```yaml jobs: deploy: - uses: acdl/.github/workflows/deploy.yml@v1.13 + uses: nova/.github/workflows/deploy.yml@v1.19 with: 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. **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: - **Immutability** — the pipeline behavior you tested is the behavior you diff --git a/docs/presentations/README.md b/docs/presentations/README.md index 389363a..fd5e5c8 100644 --- a/docs/presentations/README.md +++ b/docs/presentations/README.md @@ -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** (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). #### 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 re-committed when the Marp deck is updated. -#### PPTX export (uploaded to Gitea release) +#### PPTX export (uploaded to release) ```bash 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 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) -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 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 Marp source changes. -### Export a Marp deck to PPTX (uploaded to Gitea release) +### Export a Marp deck to PPTX (uploaded to release) ```bash 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 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 @@ -324,7 +324,7 @@ attachments to the Gitea release. no speaker notes, embedded PNGs, and maturity badges. 4. **Render to HTML** with `--allow-local-files` and commit the HTML to `docs/presentations/.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). 6. **Distill the talking points** as `-talking-points.md` — one 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 > (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 -> git + attached to the Gitea release (REQ-228, D-141). \ No newline at end of file +> git + attached to the release (REQ-228, D-141). \ No newline at end of file diff --git a/docs/raci.md b/docs/raci.md index 5540e3d..36b7900 100644 --- a/docs/raci.md +++ b/docs/raci.md @@ -1,6 +1,5 @@ # 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. Nova's delivery lifecycle has three roles. This page clarifies who owns diff --git a/docs/scope.md b/docs/scope.md index 5a2826d..a76305a 100644 --- a/docs/scope.md +++ b/docs/scope.md @@ -1,6 +1,5 @@ # 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. ## The Boundary diff --git a/docs/skills.md b/docs/skills.md index dd51607..9f3ddf2 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -34,14 +34,14 @@ ## 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 principles (C1–C8) and 19 domains, each with 10 derived P-rules. The skills distill the citizen-developer-relevant subset of each domain's first-principles, link to the agent-checklist triggers, and map to the 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 against the exact principles that produced it. diff --git a/mcp/atelier/README.md b/mcp/atelier/README.md index c1a28c3..2f98f84 100644 --- a/mcp/atelier/README.md +++ b/mcp/atelier/README.md @@ -1,9 +1,8 @@ # 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 -> agent. Plugin-registry architecture (D-140); stdio transport (D-135); -> vendored Atelier (D-136) for audit reproducibility. +> agent. Plugin-registry architecture; stdio transport; +> vendored Atelier for audit reproducibility. ## 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.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/ @@ -31,7 +30,7 @@ mcp/atelier/ │ ├── __init__.py │ ├── principles.py # lookup_principle, list_domains, matrix_lookup │ └── validation.py # validate_against_principles -├── vendor/ # pinned Atelier snapshot (D-136) +├── vendor/ # pinned Atelier snapshot │ ├── VERSION.md # pinned tag + upgrade instructions │ ├── core/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"}) ``` -## Vendoring (D-136) +## Vendoring Atelier is vendored under `vendor/` at a pinned tag (`v0.3.6`, see `vendor/VERSION.md`). An agentic validation result is only reproducible if diff --git a/metrics/README.md b/metrics/README.md index a162a9e..0b1b9ae 100644 --- a/metrics/README.md +++ b/metrics/README.md @@ -1,6 +1,5 @@ # Nova Metrics Directory -> v1.17 — Strategic Direction, Leadership Metrics & Unified Story (D-128) This directory holds Nova's telemetry/observability artifacts. The metrics layer is **Nova-native** (D-120): JSONL event log + SQLite cold diff --git a/metrics/powerbi/NOVA_DASHBOARD_README.md b/metrics/powerbi/NOVA_DASHBOARD_README.md index 6752cc4..f68dd96 100644 --- a/metrics/powerbi/NOVA_DASHBOARD_README.md +++ b/metrics/powerbi/NOVA_DASHBOARD_README.md @@ -1,7 +1,5 @@ # 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 via the folder connector, and suggests a starter visual model. diff --git a/pipelines/README.md b/pipelines/README.md index 03082e7..be23922 100644 --- a/pipelines/README.md +++ b/pipelines/README.md @@ -2,7 +2,7 @@ ## 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 @@ -20,7 +20,7 @@ Nova uses declarative pipeline contracts (YAML) as the single source of truth. B ## How to Wire a Pipeline -1. Create byte-identical workflow YAMLs in `.gitea/workflows/.yml` and `.github/workflows/.yml`. +1. Create the workflow YAML in `.github/workflows/.yml`. 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. 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_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`). ## 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 1. Create `pipelines/.yml` using the structure above. 2. Create or extend the schema in `schemas/` for the new pipeline shape. -3. Create byte-identical workflow YAMLs in `.gitea/workflows/.yml` and `.github/workflows/.yml`. +3. Create the workflow YAML in `.github/workflows/.yml`. 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. \ No newline at end of file diff --git a/pipelines/ci.yml b/pipelines/ci.yml index 0ea7db0..b9185da 100644 --- a/pipelines/ci.yml +++ b/pipelines/ci.yml @@ -1,7 +1,7 @@ # Nova Central CI Pipeline Contract (v1.5) # # 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 # stages, commands, triggers, and runner declared here. # 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 # *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. # # Validated against schemas/pipeline.schema.json. diff --git a/pipelines/contract.yml b/pipelines/contract.yml index 4d2a4cc..c25036a 100644 --- a/pipelines/contract.yml +++ b/pipelines/contract.yml @@ -12,7 +12,7 @@ # # This file is the declarative pipeline spec (a contract, not an executable # 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. # # Validated against schemas/deploy-pipeline.schema.json. diff --git a/pipelines/modules-lifecycle.yml b/pipelines/modules-lifecycle.yml index 51fd1d1..1ea1faf 100644 --- a/pipelines/modules-lifecycle.yml +++ b/pipelines/modules-lifecycle.yml @@ -18,8 +18,8 @@ name: acdl-modules-lifecycle # real apply→modify→destroy against live AWS. The CI VPC apply/destroy # jobs are skipped in plan mode (nothing is applied). # -# Both Gitea (.gitea/workflows/modules-lifecycle.yml) and GitHub -# (.github/workflows/modules-lifecycle.yml) implement this contract +# GitHub +# (.github/workflows/modules-lifecycle.yml) implements this contract # byte-identically. triggers: diff --git a/schemas/README.md b/schemas/README.md index 8362f11..e56b7aa 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -40,7 +40,7 @@ Nova uses JSON Schema draft 2020-12 for all declarative contracts. Schemas are t ## 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. - `.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. diff --git a/schemas/modules-lifecycle-pipeline.schema.json b/schemas/modules-lifecycle-pipeline.schema.json index 57266c7..ec2fa5a 100644 --- a/schemas/modules-lifecycle-pipeline.schema.json +++ b/schemas/modules-lifecycle-pipeline.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://nova.cloudinit.dev/schemas/modules-lifecycle-pipeline.schema.json", "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", "required": ["name", "triggers", "runner", "python_version", "terraform_version", "stages", "matrix"], "properties": { diff --git a/schemas/pipeline.schema.json b/schemas/pipeline.schema.json index c115946..078a6d1 100644 --- a/schemas/pipeline.schema.json +++ b/schemas/pipeline.schema.json @@ -2,8 +2,8 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://nova.cloudinit.dev/schemas/pipeline.schema.json", "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.", - "$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.", + "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 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", "required": ["name", "triggers", "runner", "stages"], "properties": { @@ -14,7 +14,7 @@ "environment": { "type": "string", "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": { "type": "object", @@ -34,7 +34,7 @@ }, "runner": { "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": { "type": "string", diff --git a/scripts/run_ci.sh b/scripts/run_ci.sh index 7916a62..a0ec870 100755 --- a/scripts/run_ci.sh +++ b/scripts/run_ci.sh @@ -2,7 +2,7 @@ # scripts/run_ci.sh - reproduce the CI pipeline locally. # # 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. # # Runs the same three stages in the same order: diff --git a/scripts/run_platform.sh b/scripts/run_platform.sh index fb13e28..a09b5c8 100755 --- a/scripts/run_platform.sh +++ b/scripts/run_platform.sh @@ -163,9 +163,9 @@ run_hitl_gate() { return 0 fi 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 - 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 fi python3 -c " diff --git a/tests/test_contract_ingestor.py b/tests/test_contract_ingestor.py index 58d3242..679a706 100644 --- a/tests/test_contract_ingestor.py +++ b/tests/test_contract_ingestor.py @@ -438,17 +438,17 @@ class TestForgeAgnosticApiUrls: def test_default_api_base_is_github(self): assert ingestor.GITHUB_API_BASE == "https://api.github.com" - def test_forge_type_detects_gitea(self, monkeypatch): - monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://git.cloudinit.dev/api/v1") - assert ingestor._forge_type() == "gitea" + def test_forge_type_detects_generic_forge(self, monkeypatch): + monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://forge.example.com/api/v1") + assert ingestor._forge_type() == "generic_forge" def test_forge_type_detects_github(self): assert ingestor._forge_type() == "github" - def test_gitea_search_url_uses_repos_endpoint(self, monkeypatch): - monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://git.cloudinit.dev/api/v1") + def test_generic_forge_search_url_uses_repos_endpoint(self, monkeypatch): + monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://forge.example.com/api/v1") 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 "/search/issues" not in url @@ -458,14 +458,14 @@ class TestForgeAgnosticApiUrls: assert "repo:acdl/acdl" in url 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") - 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): - 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) - 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: diff --git a/tests/test_decommission.py b/tests/test_decommission.py index e7739c0..2095cef 100644 --- a/tests/test_decommission.py +++ b/tests/test_decommission.py @@ -95,11 +95,6 @@ class TestDecommissionPipelineContract: assert "changeRequestId" 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): guide = open(ROOT / "docs/consumer-guide.md").read() assert "Decommissioning a stack" in guide diff --git a/tests/test_deploy_workflow_env_input.py b/tests/test_deploy_workflow_env_input.py index ba94984..b51990a 100644 --- a/tests/test_deploy_workflow_env_input.py +++ b/tests/test_deploy_workflow_env_input.py @@ -1,6 +1,6 @@ """REQ-106: deploy workflow environment input + run_platform.sh --environment. -Both deploy workflows (Gitea + GitHub) declare the environment input, -are byte-identical, and the resolver supports environment_override. +The deploy workflow declares the environment input and the resolver +supports environment_override. """ import sys from pathlib import Path @@ -11,16 +11,10 @@ sys.path.insert(0, str(ROOT)) from core.contract_resolver import resolve 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 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(): @@ -39,7 +33,7 @@ def test_deploy_workflow_passes_env_flag_to_run_platform(): 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"), environment_override="qa") 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() assert "--environment" 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 "ACDL_ENVIRONMENT_OVERRIDE" not in text \ No newline at end of file + assert "ACDL_ENVIRONMENT_OVERRIDE" not in text diff --git a/tests/test_hitl_gates.py b/tests/test_hitl_gates.py index 6ff8054..fe156cc 100644 --- a/tests/test_hitl_gates.py +++ b/tests/test_hitl_gates.py @@ -98,14 +98,14 @@ def test_unknown_env_blocks(): def test_approver_from_env_github(monkeypatch): monkeypatch.setenv("GITHUB_ACTOR", "gh-user") - monkeypatch.delenv("GITEA_ACTOR", raising=False) + monkeypatch.delenv("FORGE_ACTOR", raising=False) 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.setenv("GITEA_ACTOR", "gitea-user") - assert approver_from_env() == "gitea-user" + monkeypatch.setenv("FORGE_ACTOR", "forge-user") + assert approver_from_env() == "forge-user" def test_attest_invokes_attestation_matrix_for_prod(): diff --git a/tests/test_no_forge_mentions.py b/tests/test_no_forge_mentions.py new file mode 100644 index 0000000..940bcce --- /dev/null +++ b/tests/test_no_forge_mentions.py @@ -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}" + ) diff --git a/tests/test_pipeline_contract.py b/tests/test_pipeline_contract.py index 50b4b6a..3f7004e 100644 --- a/tests/test_pipeline_contract.py +++ b/tests/test_pipeline_contract.py @@ -94,22 +94,9 @@ class TestPipelineContract: class TestWorkflowConformance: - def test_gitea_workflow_exists(self): - assert (ROOT / ".gitea/workflows/ci.yml").is_file() - def test_github_workflow_exists(self): 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): """P8 (REQ-172): sync_workflows.py --check exits 0 (committed 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" - 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: def test_run_ci_script_exists_and_executable(self): path = ROOT / "scripts/run_ci.sh" @@ -301,90 +227,9 @@ class TestDeployPipelineContract: class TestDeployWorkflowConformance: - def test_gitea_deploy_workflow_exists(self): - assert (ROOT / ".gitea/workflows/deploy.yml").is_file() - def test_github_deploy_workflow_exists(self): 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: def test_ci_workflow_uses_versioned_tag(self): """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") 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): 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): contract = _load_yaml("pipelines/modules-lifecycle.yml") assert set(contract["matrix"]["modules"]) == { @@ -588,48 +403,6 @@ class TestModulesLifecyclePipeline: "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): contract = _load_yaml("pipelines/modules-lifecycle.yml") assert set(contract["matrix"]["l2_modules"]) == {"static-assets", "microservice"} @@ -648,51 +421,3 @@ class TestModulesLifecyclePipeline: props = schema["properties"] assert "default_mode" in props 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)" \ No newline at end of file diff --git a/workflows-src/ci.yml b/workflows-src/ci.yml index c7fb14d..839b9bd 100644 --- a/workflows-src/ci.yml +++ b/workflows-src/ci.yml @@ -1,4 +1,4 @@ -# ACDL CI Pipeline — Gitea Actions (dev environment) +# Nova CI Pipeline (dev environment) # # This workflow implements the central pipeline contract: # pipelines/ci.yml (validated against schemas/pipeline.schema.json) diff --git a/workflows-src/deploy.yml b/workflows-src/deploy.yml index b1605ab..d9906dc 100644 --- a/workflows-src/deploy.yml +++ b/workflows-src/deploy.yml @@ -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: # 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. # # 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) # # 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 # policy restricts view/update to resources tagged acdl:owner=. # -# Override (where OIDC is unavailable, e.g. Gitea pending -# go-gitea/gitea#36988): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY +# Override (where OIDC is unavailable, e.g. pending +# upstream forge OIDC support): set NOVA_AWS_ACCESS_KEY_ID + NOVA_AWS_SECRET_ACCESS_KEY # as repository secrets. The platform-managed scheduled pipeline rotates # the key on a daily cadence. When .env.secrets is used locally instead, # rotating the key out of band is the consumer's responsibility. diff --git a/workflows-src/modules-lifecycle.yml b/workflows-src/modules-lifecycle.yml index 03699c2..1213864 100644 --- a/workflows-src/modules-lifecycle.yml +++ b/workflows-src/modules-lifecycle.yml @@ -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 # apply→modify→destroy against live AWS. No per-module Python. The "test" = @@ -9,7 +9,7 @@ # terraform files); the composition must be deterministic. # # 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 # "plan" — the lifecycle scripts run `run_platform.sh --plan-only` (fast,