Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8effef415 | |||
| 4ac68b7270 | |||
| 518bbe32a7 | |||
| ed36519223 | |||
| aa3e385606 |
@@ -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.
|
||||||
@@ -126,20 +126,46 @@ engine-specific code. `modules/`, `schemas/`, `contracts/`,
|
|||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
### Prerequisites
|
### Quick start (offline, no AWS required)
|
||||||
|
|
||||||
> These prerequisites are for running the **platform repo** locally. A
|
The fastest way to verify the platform works — no AWS credentials, no
|
||||||
> consumer does not need any of these — see the
|
bootstrap, no cost. See the [Consumer guide](docs/consumer-guide.md)
|
||||||
> [Consumer guide](docs/consumer-guide.md) for the consumer happy path.
|
for the consumer happy path (a consumer owns only a contract + app code).
|
||||||
|
|
||||||
- A platform-managed environment (see [docs/environments/](docs/environments/)).
|
```bash
|
||||||
For local testing, `core/environments/dev.json` is provided as the sample.
|
# Install test dependencies
|
||||||
- AWS credentials for the dev environment (in `.env.secrets`, gitignored;
|
pip install -r requirements-test.txt
|
||||||
see [Credentials & zero-trust](#credentials--zero-trust)).
|
|
||||||
- `terraform` (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3`
|
|
||||||
+ `jsonschema`.
|
|
||||||
|
|
||||||
### Run the platform pipeline end-to-end
|
# 1. Run the test suite (all offline — uses moto for DynamoDB mocking)
|
||||||
|
python3 -m pytest tests/ -v
|
||||||
|
|
||||||
|
# 2. Run the platform in check-only mode (offline — contract -> resolver ->
|
||||||
|
# adapter -> structure validation). Uses the default sample contract
|
||||||
|
# (contracts/static-assets.yaml) + sample dev environment.
|
||||||
|
bash scripts/run_platform.sh --check-only
|
||||||
|
# Expected: "=== PLATFORM CHECK OK ==="
|
||||||
|
|
||||||
|
# 3. Run the headline E2E against the local emulating tier (emulates ECS,
|
||||||
|
# outbox, S3 state, Lambda in-process; D-092).
|
||||||
|
bash scripts/run_platform.sh --local
|
||||||
|
# Expected: "=== LOCAL E2E OK ==="
|
||||||
|
|
||||||
|
# 4. 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:
|
||||||
|
bash scripts/run_platform.sh --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run against live AWS (requires credentials + bootstrap)
|
||||||
|
|
||||||
|
> Prerequisites: a platform-managed environment (see
|
||||||
|
> [docs/environments/](docs/environments/); `core/environments/dev.json`
|
||||||
|
> is the sample), AWS credentials for dev (in `.env.secrets`, gitignored;
|
||||||
|
> see [Credentials & zero-trust](#credentials--zero-trust)), `terraform`
|
||||||
|
> (pin `1.9.*`), `checkov` (pin `>=3.2,<4`), `python3` + `boto3` +
|
||||||
|
> `jsonschema`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
||||||
@@ -168,26 +194,6 @@ bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml
|
|||||||
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
|
bash scripts/run_platform.sh --quiet contracts/static-assets.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test the platform (offline, no AWS required)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install test dependencies
|
|
||||||
pip install -r requirements-test.txt
|
|
||||||
|
|
||||||
# Run the test suite (all offline — uses moto for DynamoDB mocking)
|
|
||||||
python3 -m pytest tests/ -v
|
|
||||||
|
|
||||||
# Run the platform in check-only mode (offline — no AWS, no policy checks,
|
|
||||||
# no outbox). Uses the default sample contract (contracts/static-assets.yaml)
|
|
||||||
# and the sample dev environment (core/environments/dev.json).
|
|
||||||
bash scripts/run_platform.sh --check-only
|
|
||||||
# Expected: "=== PLATFORM CHECK OK ==="
|
|
||||||
|
|
||||||
# Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
|
||||||
bash scripts/run_ci.sh
|
|
||||||
# Expected: "=== CI PIPELINE OK ==="
|
|
||||||
```
|
|
||||||
|
|
||||||
### CI/CD pipelines
|
### CI/CD pipelines
|
||||||
|
|
||||||
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
The CI/CD pipeline is defined by a **central pipeline contract** — a
|
||||||
|
|||||||
@@ -64,6 +64,21 @@ def _load_json(path):
|
|||||||
return json.load(fh)
|
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):
|
def _load_yaml(path):
|
||||||
with open(path, "r") as fh:
|
with open(path, "r") as fh:
|
||||||
return yaml.safe_load(fh)
|
return yaml.safe_load(fh)
|
||||||
@@ -468,7 +483,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
|
|||||||
contract["environment"] = environment_override
|
contract["environment"] = environment_override
|
||||||
|
|
||||||
# Load schemas
|
# 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
|
# Validate contract against schema
|
||||||
jsonschema.validate(contract, contract_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
|
stack_instance["outputs"] = merged_outputs
|
||||||
|
|
||||||
# Validate against stack schema
|
# 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)
|
jsonschema.validate(stack_instance, stack_schema)
|
||||||
|
|
||||||
return stack_instance
|
return stack_instance
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ from core import env as _envhelper
|
|||||||
SSM_PREFIX = "/nova"
|
SSM_PREFIX = "/nova"
|
||||||
KMS_KEY_ID_ENV = "NOVA_KMS_KEY_ID"
|
KMS_KEY_ID_ENV = "NOVA_KMS_KEY_ID"
|
||||||
|
|
||||||
# Outputs that are safe to display in a PR comment (no secrets).
|
# P14 (REQ-178): SAFE_OUTPUT_NAMES is schema-driven (derived from
|
||||||
SAFE_OUTPUT_NAMES = {
|
# 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",
|
"distribution_domain_name",
|
||||||
"bucket_arn",
|
"bucket_arn",
|
||||||
"bucket_name",
|
"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():
|
def _ssm_client():
|
||||||
if boto3 is None:
|
if boto3 is None:
|
||||||
raise RuntimeError("boto3 is required for SSM publishing")
|
raise RuntimeError("boto3 is required for SSM publishing")
|
||||||
|
|||||||
@@ -668,17 +668,9 @@ def write_report(report: RegressionReport,
|
|||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
milestone = _envhelper.get_env("REGRESSION_MILESTONE", "v1.10") or "v1.10"
|
"""P13 (REQ-177): re-export from core.regression_verify_cli."""
|
||||||
phase = int(_envhelper.get_env("REGRESSION_PHASE", "52") or "52")
|
from core.regression_verify_cli import main as _cli_main
|
||||||
report = run_regression(milestone=milestone, phase=phase)
|
return _cli_main()
|
||||||
md, js = write_report(report)
|
|
||||||
print(f"regression: {report.summary} -> {md}")
|
|
||||||
if not report.passed:
|
|
||||||
print("FAIL: regression surfaced non-Verified/non-Skipped capabilities "
|
|
||||||
"(milestone gate blocks)", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
print(f"regression: gate passes (summary={report.summary})")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Nova Regression Verify CLI — command-line entry point.
|
||||||
|
|
||||||
|
Extracted from core/regression_verify.py (P13, REQ-177).
|
||||||
|
|
||||||
|
G-113 import direction: this module imports core.regression_verify (the
|
||||||
|
library) for run_regression + write_report. The library does not import
|
||||||
|
this CLI module. Nothing imports this CLI except direct invocation.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from core import env as _envhelper
|
||||||
|
from core.regression_verify import run_regression, write_report
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
"""CLI: run the regression gate and write the report."""
|
||||||
|
milestone = _envhelper.get_env("REGRESSION_MILESTONE", "v1.10") or "v1.10"
|
||||||
|
phase = int(_envhelper.get_env("REGRESSION_PHASE", "52") or "52")
|
||||||
|
report = run_regression(milestone=milestone, phase=phase)
|
||||||
|
md, js = write_report(report)
|
||||||
|
print(f"regression: {report.summary} -> {md}")
|
||||||
|
if not report.passed:
|
||||||
|
print("FAIL: regression surfaced non-Verified/non-Skipped capabilities "
|
||||||
|
"(milestone gate blocks)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"regression: gate passes (summary={report.summary})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+42
-2
@@ -7,6 +7,9 @@
|
|||||||
# run_platform.sh --plan-only <contract.yml> (AWS plan only, no Checkov/outbox)
|
# 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 --apply <contract.yml> (AWS apply: init/validate/plan/apply)
|
||||||
# run_platform.sh --destroy <contract.yml> (AWS destroy: init/validate/destroy)
|
# 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:
|
# Modes:
|
||||||
# --check-only (offline, no AWS/Checkov/DynamoDB — for CI)
|
# --check-only (offline, no AWS/Checkov/DynamoDB — for CI)
|
||||||
@@ -17,13 +20,19 @@
|
|||||||
# contract -> resolver -> stack -> adapter -> terraform init/validate/plan/apply -> exit 0
|
# contract -> resolver -> stack -> adapter -> terraform init/validate/plan/apply -> exit 0
|
||||||
# --destroy (requires AWS creds; use --decommission <CR> for gated production teardown)
|
# --destroy (requires AWS creds; use --decommission <CR> for gated production teardown)
|
||||||
# contract -> resolver -> stack -> adapter -> terraform init/validate/destroy -> exit 0
|
# 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)
|
# (default) (requires AWS creds + Checkov + DynamoDB)
|
||||||
# contract -> resolver -> stack -> adapter -> terraform plan -> Checkov ->
|
# contract -> resolver -> stack -> adapter -> terraform plan -> Checkov ->
|
||||||
# confidence -> outbox
|
# confidence -> outbox
|
||||||
#
|
#
|
||||||
# Flags:
|
# Flags:
|
||||||
# --quiet suppress terraform/checkov streaming (output to log only)
|
# --quiet suppress terraform/checkov streaming (output to log only)
|
||||||
# --decommission gate --destroy with D-070 two-step CR validation (requires <CR>)
|
# --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 contract file is a YAML file validated against schemas/contract.schema.json.
|
||||||
# The resolver (core/contract_resolver.py) resolves it to a Target Stack
|
# The resolver (core/contract_resolver.py) resolves it to a Target Stack
|
||||||
@@ -59,6 +68,36 @@ CHANGE_REQUEST_ID=""
|
|||||||
ENVIRONMENT_OVERRIDE=""
|
ENVIRONMENT_OVERRIDE=""
|
||||||
CONTRACT=""
|
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
|
# Parse args; --environment takes a value (either --environment=VALUE or
|
||||||
# --environment VALUE). The contract / changeRequestId are the remaining
|
# --environment VALUE). The contract / changeRequestId are the remaining
|
||||||
# positional args.
|
# positional args.
|
||||||
@@ -69,6 +108,7 @@ for arg in "$@"; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
|
--help|-h) _print_help; exit 0 ;;
|
||||||
--check-only) CHECK_ONLY=1 ;;
|
--check-only) CHECK_ONLY=1 ;;
|
||||||
--plan-only) PLAN_ONLY=1 ;;
|
--plan-only) PLAN_ONLY=1 ;;
|
||||||
--apply) APPLY_ONLY=1 ;;
|
--apply) APPLY_ONLY=1 ;;
|
||||||
|
|||||||
@@ -34,4 +34,15 @@ class TestDocsCoverage:
|
|||||||
assert "How to Write an Adapter" in content
|
assert "How to Write an Adapter" in content
|
||||||
assert "How to Wire" in content
|
assert "How to Wire" in content
|
||||||
assert "How to Test" 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