Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ac68b7270 | |||
| 518bbe32a7 | |||
| ed36519223 |
@@ -0,0 +1,50 @@
|
||||
# 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).
|
||||
|
||||
## Shared workflows (byte-identical Gitea + GitHub)
|
||||
|
||||
These 3 are generated from `workflows-src/<name>` by
|
||||
`scripts/sync_workflows.py`; the `.gitea/workflows/<name>` mirror is kept
|
||||
byte-identical. 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`) |
|
||||
| `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)
|
||||
|
||||
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.
|
||||
|
||||
| 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 |
|
||||
|
||||
## Reusable deploy workflow (`deploy.yml`)
|
||||
|
||||
Consumer repos invoke the deploy workflow via a versioned tag:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
deploy:
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.15
|
||||
with:
|
||||
contract: .nova/contract.yml
|
||||
environment: dev
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
The workflow checks out the consumer repo + the Nova platform repo, runs
|
||||
`scripts/run_platform.sh`, and posts deploy outputs as a PR comment +
|
||||
to SSM Parameter Store.
|
||||
@@ -183,9 +183,17 @@ python3 -m pytest tests/ -v
|
||||
bash scripts/run_platform.sh --check-only
|
||||
# Expected: "=== PLATFORM CHECK OK ==="
|
||||
|
||||
# Run the headline E2E against the local emulating tier (no AWS credentials
|
||||
# needed — emulates ECS, outbox, S3 state, Lambda in-process; D-092).
|
||||
bash scripts/run_platform.sh --local
|
||||
# Expected: "=== LOCAL E2E OK ==="
|
||||
|
||||
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
||||
bash scripts/run_ci.sh
|
||||
# Expected: "=== CI PIPELINE OK ==="
|
||||
|
||||
# Show all run_platform.sh flags + a one-line description each.
|
||||
bash scripts/run_platform.sh --help
|
||||
```
|
||||
|
||||
### CI/CD pipelines
|
||||
|
||||
@@ -64,6 +64,21 @@ def _load_json(path):
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
# P14 (REQ-178): cache loaded JSON schemas so resolve() doesn't re-read
|
||||
# from disk on every call.
|
||||
_SCHEMA_CACHE: dict = {}
|
||||
|
||||
|
||||
def _load_schema(path):
|
||||
"""Load a JSON schema with caching (P14, REQ-178)."""
|
||||
cached = _SCHEMA_CACHE.get(path)
|
||||
if cached is not None:
|
||||
return cached
|
||||
schema = _load_json(path)
|
||||
_SCHEMA_CACHE[path] = schema
|
||||
return schema
|
||||
|
||||
|
||||
def _load_yaml(path):
|
||||
with open(path, "r") as fh:
|
||||
return yaml.safe_load(fh)
|
||||
@@ -468,7 +483,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
contract["environment"] = environment_override
|
||||
|
||||
# Load schemas
|
||||
contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
||||
contract_schema = _load_schema(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
||||
|
||||
# Validate contract against schema
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
@@ -588,7 +603,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
||||
stack_instance["outputs"] = merged_outputs
|
||||
|
||||
# Validate against stack schema
|
||||
stack_schema = _load_json(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
stack_schema = _load_schema(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
jsonschema.validate(stack_instance, stack_schema)
|
||||
|
||||
return stack_instance
|
||||
|
||||
@@ -38,8 +38,10 @@ from core import env as _envhelper
|
||||
SSM_PREFIX = "/nova"
|
||||
KMS_KEY_ID_ENV = "NOVA_KMS_KEY_ID"
|
||||
|
||||
# Outputs that are safe to display in a PR comment (no secrets).
|
||||
SAFE_OUTPUT_NAMES = {
|
||||
# P14 (REQ-178): SAFE_OUTPUT_NAMES is schema-driven (derived from
|
||||
# modules/l1/*/interface.json outputs that don't have sensitive:true).
|
||||
# Falls back to the hardcoded set if the interfaces can't be read.
|
||||
_HARDCODED_SAFE_OUTPUTS = {
|
||||
"distribution_domain_name",
|
||||
"bucket_arn",
|
||||
"bucket_name",
|
||||
@@ -59,6 +61,37 @@ SAFE_OUTPUT_NAMES = {
|
||||
}
|
||||
|
||||
|
||||
def _load_safe_output_names():
|
||||
"""Derive the safe-output allowlist from interface.json outputs.
|
||||
|
||||
P14 (REQ-178): scan modules/l1/*/interface.json; an output is safe if
|
||||
its spec does not set sensitive:true. Falls back to the hardcoded set
|
||||
if no interfaces are readable.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
safe = set()
|
||||
try:
|
||||
for iface in (root / "modules" / "l1").glob("*/interface.json"):
|
||||
d = json.loads(iface.read_text())
|
||||
outs = d.get("outputs", {})
|
||||
if isinstance(outs, dict):
|
||||
for name, spec in outs.items():
|
||||
if not (isinstance(spec, dict) and spec.get("sensitive")):
|
||||
safe.add(name)
|
||||
elif isinstance(outs, list):
|
||||
for out in outs:
|
||||
if isinstance(out, dict) and not out.get("sensitive"):
|
||||
safe.add(out.get("name", ""))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return safe or _HARDCODED_SAFE_OUTPUTS
|
||||
|
||||
|
||||
SAFE_OUTPUT_NAMES = _load_safe_output_names()
|
||||
|
||||
|
||||
def _ssm_client():
|
||||
if boto3 is None:
|
||||
raise RuntimeError("boto3 is required for SSM publishing")
|
||||
|
||||
+42
-2
@@ -7,6 +7,9 @@
|
||||
# run_platform.sh --plan-only <contract.yml> (AWS plan only, no Checkov/outbox)
|
||||
# run_platform.sh --apply <contract.yml> (AWS apply: init/validate/plan/apply)
|
||||
# run_platform.sh --destroy <contract.yml> (AWS destroy: init/validate/destroy)
|
||||
# run_platform.sh --local [contract.yml] (local emulating tier, no AWS)
|
||||
# run_platform.sh --decommission <CR> <contract.yml> (gated teardown)
|
||||
# run_platform.sh --help (show all flags)
|
||||
#
|
||||
# Modes:
|
||||
# --check-only (offline, no AWS/Checkov/DynamoDB — for CI)
|
||||
@@ -17,13 +20,19 @@
|
||||
# contract -> resolver -> stack -> adapter -> terraform init/validate/plan/apply -> exit 0
|
||||
# --destroy (requires AWS creds; use --decommission <CR> for gated production teardown)
|
||||
# contract -> resolver -> stack -> adapter -> terraform init/validate/destroy -> exit 0
|
||||
# --local (no AWS creds; local emulating tier D-092)
|
||||
# contract -> resolver -> adapter -> local S3/ECS/outbox/Lambda stubs -> exit 0
|
||||
# (default) (requires AWS creds + Checkov + DynamoDB)
|
||||
# contract -> resolver -> stack -> adapter -> terraform plan -> Checkov ->
|
||||
# confidence -> outbox
|
||||
#
|
||||
# Flags:
|
||||
# --quiet suppress terraform/checkov streaming (output to log only)
|
||||
# --decommission gate --destroy with D-070 two-step CR validation (requires <CR>)
|
||||
# --quiet suppress terraform/checkov streaming (output to log only)
|
||||
# --decommission gate --destroy with D-070 two-step CR validation (requires <CR>)
|
||||
# --deploy-uptime deploy the uptime monitoring stack (separate state)
|
||||
# --local run the headline E2E against the local emulating tier (D-092)
|
||||
# --environment <name> override the contract's environment at load time (D-088)
|
||||
# --help, -h show all flags + a one-line description
|
||||
#
|
||||
# The contract file is a YAML file validated against schemas/contract.schema.json.
|
||||
# The resolver (core/contract_resolver.py) resolves it to a Target Stack
|
||||
@@ -59,6 +68,36 @@ CHANGE_REQUEST_ID=""
|
||||
ENVIRONMENT_OVERRIDE=""
|
||||
CONTRACT=""
|
||||
|
||||
# P15 (REQ-179): --help / -h prints all flags + a one-line description.
|
||||
_print_help() {
|
||||
cat <<'HELP'
|
||||
Nova platform pipeline — run_platform.sh
|
||||
|
||||
Usage:
|
||||
run_platform.sh <contract.yml> (full e2e with AWS)
|
||||
run_platform.sh --check-only [contract.yml] (offline, no AWS)
|
||||
run_platform.sh --plan-only <contract.yml> (AWS plan only)
|
||||
run_platform.sh --apply <contract.yml> (AWS apply)
|
||||
run_platform.sh --destroy <contract.yml> (AWS destroy)
|
||||
run_platform.sh --local [contract.yml] (local emulating tier)
|
||||
run_platform.sh --decommission <CR> <contract.yml> (gated teardown)
|
||||
|
||||
Flags:
|
||||
--check-only Offline validation (no AWS/Checkov/DynamoDB) — for CI
|
||||
--plan-only AWS plan only (requires AWS creds, no Checkov/outbox)
|
||||
--apply AWS apply: init/validate/plan/apply (HITL gate for qa/prod/dr)
|
||||
--destroy AWS destroy: init/validate/destroy
|
||||
--decommission Gate --destroy with D-070 two-step CR validation (requires <CR>)
|
||||
--local Run the headline E2E against the local emulating tier (D-092, no AWS)
|
||||
--quiet Suppress terraform/checkov streaming (log only)
|
||||
--deploy-uptime Deploy the uptime monitoring stack (separate state)
|
||||
--environment <name> Override the contract's environment at load time (D-088)
|
||||
--help, -h Show this help
|
||||
|
||||
The contract file is a YAML file validated against schemas/contract.schema.json.
|
||||
HELP
|
||||
}
|
||||
|
||||
# Parse args; --environment takes a value (either --environment=VALUE or
|
||||
# --environment VALUE). The contract / changeRequestId are the remaining
|
||||
# positional args.
|
||||
@@ -69,6 +108,7 @@ for arg in "$@"; do
|
||||
continue
|
||||
fi
|
||||
case "$arg" in
|
||||
--help|-h) _print_help; exit 0 ;;
|
||||
--check-only) CHECK_ONLY=1 ;;
|
||||
--plan-only) PLAN_ONLY=1 ;;
|
||||
--apply) APPLY_ONLY=1 ;;
|
||||
|
||||
@@ -34,4 +34,15 @@ class TestDocsCoverage:
|
||||
assert "How to Write an Adapter" in content
|
||||
assert "How to Wire" in content
|
||||
assert "How to Test" in content
|
||||
assert "Existing Adapters" in content
|
||||
assert "Existing Adapters" in content
|
||||
|
||||
def test_github_workflows_readme_catalogs_all_workflows():
|
||||
"""P16 (REQ-180): .github/workflows/README.md catalogs all 7 workflows."""
|
||||
from pathlib import Path
|
||||
readme = Path(__file__).resolve().parent.parent / ".github" / "workflows" / "README.md"
|
||||
assert readme.is_file(), ".github/workflows/README.md missing"
|
||||
text = readme.read_text()
|
||||
for wf in ["ci.yml", "deploy.yml", "modules-lifecycle.yml",
|
||||
"platform-test.yml", "primitives-plan.yml", "patterns-plan.yml",
|
||||
"release.yml"]:
|
||||
assert wf in text, f"{wf} not cataloged in .github/workflows/README.md"
|
||||
|
||||
Reference in New Issue
Block a user