verify(P8): workflow-generator-dedup — 4-layer verify PASS + ship
VERIFY: structural — generator + sources; behavioral — 98 tests + CI PASS; quality — ~20KB dedup, single source of truth. ---ci--- project: acdl phase: 8 milestone: v1.16 status: complete phase_role: execution requirements: covered: [REQ-172] partial: [] ---/ci---
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sync byte-identical workflows from workflows-src/ to .gitea/ + .github/ (P8, REQ-172).
|
||||||
|
|
||||||
|
Three workflow pairs are byte-identical Gitea + GitHub mirrors:
|
||||||
|
ci.yml, deploy.yml, modules-lifecycle.yml.
|
||||||
|
|
||||||
|
This generator reads the single source from ``workflows-src/<name>`` and
|
||||||
|
writes byte-identical copies to both ``.gitea/workflows/<name>`` and
|
||||||
|
``.github/workflows/<name>``. Use ``--check`` to verify the committed
|
||||||
|
files match the generated output (CI gate); use ``--write`` to regenerate
|
||||||
|
the committed files from the sources.
|
||||||
|
|
||||||
|
The 4 GitHub-only workflows (platform-test.yml, primitives-plan.yml,
|
||||||
|
patterns-plan.yml, release.yml) have no Gitea mirror (act_runner feature
|
||||||
|
gaps) and are NOT touched by this generator.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import filecmp
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SRC_DIR = ROOT / "workflows-src"
|
||||||
|
GITEA_DIR = ROOT / ".gitea" / "workflows"
|
||||||
|
GITHUB_DIR = ROOT / ".github" / "workflows"
|
||||||
|
|
||||||
|
PAIRS = ["ci.yml", "deploy.yml", "modules-lifecycle.yml"]
|
||||||
|
|
||||||
|
|
||||||
|
def _read_source(name: str) -> str:
|
||||||
|
src = SRC_DIR / name
|
||||||
|
if not src.is_file():
|
||||||
|
raise FileNotFoundError(f"source {src} missing")
|
||||||
|
return src.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def check() -> int:
|
||||||
|
"""Verify committed files match the sources. Exit 0 if clean, 1 if drift."""
|
||||||
|
drift = []
|
||||||
|
for name in PAIRS:
|
||||||
|
content = _read_source(name)
|
||||||
|
for dest_dir in (GITEA_DIR, GITHUB_DIR):
|
||||||
|
dest = dest_dir / name
|
||||||
|
if not dest.is_file():
|
||||||
|
drift.append(f"{dest} MISSING (expected from workflows-src/{name})")
|
||||||
|
continue
|
||||||
|
if dest.read_text() != content:
|
||||||
|
drift.append(f"{dest} DRIFTED from workflows-src/{name}")
|
||||||
|
if drift:
|
||||||
|
for d in drift:
|
||||||
|
print(f"DRIFT: {d}", file=sys.stderr)
|
||||||
|
print("\nRun: python3 scripts/sync_workflows.py --write", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"OK: {len(PAIRS)} workflow pairs match workflows-src/ sources")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def write() -> int:
|
||||||
|
"""Regenerate .gitea/ + .github/ from workflows-src/ sources."""
|
||||||
|
for name in PAIRS:
|
||||||
|
content = _read_source(name)
|
||||||
|
for dest_dir in (GITEA_DIR, GITHUB_DIR):
|
||||||
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(dest_dir / name).write_text(content)
|
||||||
|
print(f"wrote: .gitea/workflows/{name} + .github/workflows/{name}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Sync byte-identical workflow pairs.")
|
||||||
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("--check", action="store_true", help="verify committed files match sources (CI gate)")
|
||||||
|
group.add_argument("--write", action="store_true", help="regenerate committed files from sources")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
if args.check:
|
||||||
|
return check()
|
||||||
|
return write()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -101,10 +101,25 @@ class TestWorkflowConformance:
|
|||||||
assert (ROOT / ".github/workflows/ci.yml").is_file()
|
assert (ROOT / ".github/workflows/ci.yml").is_file()
|
||||||
|
|
||||||
def test_workflows_are_byte_identical(self):
|
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()
|
gitea = open(ROOT / ".gitea/workflows/ci.yml", "rb").read()
|
||||||
github = open(ROOT / ".github/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"
|
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)."""
|
||||||
|
import subprocess
|
||||||
|
rc = subprocess.call(
|
||||||
|
[sys.executable, "scripts/sync_workflows.py", "--check"],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
assert rc == 0, "sync_workflows.py --check failed — run scripts/sync_workflows.py --write"
|
||||||
|
|
||||||
def test_gitea_workflow_name_matches_contract(self):
|
def test_gitea_workflow_name_matches_contract(self):
|
||||||
wf = _load_workflow(".gitea/workflows/ci.yml")
|
wf = _load_workflow(".gitea/workflows/ci.yml")
|
||||||
contract = _load_yaml("pipelines/ci.yml")
|
contract = _load_yaml("pipelines/ci.yml")
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# ACDL CI Pipeline — Gitea Actions (dev environment)
|
||||||
|
#
|
||||||
|
# This workflow implements the central pipeline contract:
|
||||||
|
# pipelines/ci.yml (validated against schemas/pipeline.schema.json)
|
||||||
|
#
|
||||||
|
# The same contract is implemented by .github/workflows/ci.yml (GitHub
|
||||||
|
# Actions, production). Both files must be byte-identical — the only
|
||||||
|
# declared difference is the forge/runtime, not the stages or commands.
|
||||||
|
#
|
||||||
|
# Shell reproducibility: scripts/run_ci.sh runs the same 3 stages locally.
|
||||||
|
#
|
||||||
|
# Stages (from the contract):
|
||||||
|
# 1. lint — py_compile all Python files
|
||||||
|
# 2. test — pytest test suite (offline, no AWS)
|
||||||
|
# 3. check-only — run_platform.sh --check-only (offline, no AWS)
|
||||||
|
name: acdl-ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Compile all Python files
|
||||||
|
run: |
|
||||||
|
python3 -m py_compile \
|
||||||
|
core/confidence_signal.py \
|
||||||
|
core/outbox_writer.py \
|
||||||
|
core/output_publisher.py \
|
||||||
|
core/contract_resolver.py \
|
||||||
|
core/lambda/contract_ingestor.py \
|
||||||
|
adapters/terraform/adapter.py \
|
||||||
|
adapters/terraform/policy/checkov_adapter.py \
|
||||||
|
scripts/push_consumer_image.py
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install Terraform 1.9.*
|
||||||
|
run: |
|
||||||
|
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
|
||||||
|
- name: Install test dependencies
|
||||||
|
run: pip install -r requirements-test.txt
|
||||||
|
|
||||||
|
- name: Run pytest
|
||||||
|
run: python3 -m pytest tests/ -v --tb=short
|
||||||
|
|
||||||
|
check-only:
|
||||||
|
name: Platform check-only (offline)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install Terraform 1.9.*
|
||||||
|
run: |
|
||||||
|
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
|
||||||
|
- name: Install runtime dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
|
||||||
|
- name: Run platform check-only
|
||||||
|
run: bash scripts/run_platform.sh --check-only
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# ACDL Reusable Deploy Workflow — Gitea Actions (dev environment)
|
||||||
|
#
|
||||||
|
# This reusable workflow implements the central deployment pipeline contract:
|
||||||
|
# pipelines/contract.yml (validated against schemas/deploy-pipeline.schema.json)
|
||||||
|
#
|
||||||
|
# The same contract is implemented by .github/workflows/deploy.yml (GitHub
|
||||||
|
# Actions, production). Both files must be byte-identical — the only
|
||||||
|
# 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: acdl/.github/workflows/deploy.yml@v1.9 (GitHub)
|
||||||
|
#
|
||||||
|
# Unversioned references (@main, bare) are discouraged — the consumer's setup
|
||||||
|
# must be immutable + resilient. The versioned tag is the only immutability
|
||||||
|
# lever (version constraints cannot be expressed inside the contract).
|
||||||
|
#
|
||||||
|
# What this workflow does:
|
||||||
|
# 1. Checks out the consumer repo (the repo that invoked the workflow).
|
||||||
|
# 2. Checks out the ACDL platform repo into the workspace (platform/).
|
||||||
|
# This is the run-time fetch — consumers never clone the platform repo.
|
||||||
|
# 3. Installs runtime deps: Python 3.12, Terraform 1.9.*, Checkov.
|
||||||
|
# 4. Configures AWS auth (OIDC default; static-key override via secrets).
|
||||||
|
# 5. Runs scripts/run_platform.sh against the consumer's contract path.
|
||||||
|
# 6. Uploads artifacts (emitted Terraform, Checkov JSON, confidence JSON,
|
||||||
|
# platform log) for auditability.
|
||||||
|
#
|
||||||
|
# Inputs:
|
||||||
|
# contract — path to the consumer's contract YAML (default .nova/contract.yml)
|
||||||
|
# mode — full | plan-only | check-only (default full; dev = full apply,
|
||||||
|
# higher environments hold for HITL — the calling repo or the
|
||||||
|
# forge environment gate enforces that)
|
||||||
|
#
|
||||||
|
# Auth (zero-trust default — see README.md#credentials--zero-trust):
|
||||||
|
# OIDC federation is the default. permissions: id-token: write lets the
|
||||||
|
# forge mint a short-lived STS token. The role-to-assume is scoped by the
|
||||||
|
# consumer's repository identity (ABAC) — the workflow assumes the role
|
||||||
|
# that matches repo:org/consumer-repo:ref:refs/heads/main, and the session
|
||||||
|
# policy restricts view/update to resources tagged acdl:owner=<consumer-repo>.
|
||||||
|
#
|
||||||
|
# Override (where OIDC is unavailable, e.g. Gitea pending
|
||||||
|
# go-gitea/gitea#36988): 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.
|
||||||
|
name: nova-deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
contract:
|
||||||
|
description: Path to the consumer contract YAML (in the consumer repo)
|
||||||
|
type: string
|
||||||
|
default: .nova/contract.yml
|
||||||
|
mode:
|
||||||
|
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||||
|
type: string
|
||||||
|
default: full
|
||||||
|
changeRequestId:
|
||||||
|
description: Change request ID (required for decommission mode — validated against CMDB)
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
environment:
|
||||||
|
description: Target environment override (dev/qa/prod/dr); when empty, the contract's environment field is used
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out consumer repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check out ACDL platform repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: acdl/acdl
|
||||||
|
path: platform
|
||||||
|
ref: v1.9
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install runtime dependencies
|
||||||
|
run: |
|
||||||
|
pip install --break-system-packages jsonschema pyyaml boto3
|
||||||
|
pip install --break-system-packages "checkov>=3.2,<4"
|
||||||
|
|
||||||
|
- name: Install Terraform 1.9.*
|
||||||
|
run: |
|
||||||
|
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
|
||||||
|
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
# P4 (REQ-163): IAM role renamed acdl-deploy- → nova-deploy-.
|
||||||
|
role-to-assume: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/nova-deploy-{1}', secrets.NOVA_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
||||||
|
aws-region: us-east-1
|
||||||
|
access-key-id: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
secret-access-key: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
||||||
|
- name: Run the platform pipeline
|
||||||
|
working-directory: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
MODE_FLAG=""
|
||||||
|
case "${{ inputs.mode }}" in
|
||||||
|
full) MODE_FLAG="" ;;
|
||||||
|
plan-only) MODE_FLAG="--plan-only" ;;
|
||||||
|
check-only) MODE_FLAG="--check-only" ;;
|
||||||
|
decommission)
|
||||||
|
if [ -z "${{ inputs.changeRequestId }}" ]; then
|
||||||
|
echo "FAIL: changeRequestId is required for decommission mode"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
MODE_FLAG="--decommission ${{ inputs.changeRequestId }}"
|
||||||
|
;;
|
||||||
|
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
ENV_FLAG=""
|
||||||
|
if [ -n "${{ inputs.environment }}" ]; then
|
||||||
|
ENV_FLAG="--environment ${{ inputs.environment }}"
|
||||||
|
fi
|
||||||
|
bash platform/scripts/run_platform.sh $MODE_FLAG $ENV_FLAG "${{ inputs.contract }}"
|
||||||
|
|
||||||
|
- name: Post stage summary comment to PR
|
||||||
|
if: success() && github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
GITHUB_REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
bash platform/scripts/post_stage_comment.sh deploy pass '{"mode":"${{ inputs.mode }}","runId":"${{ github.run_id }}"}'
|
||||||
|
|
||||||
|
- name: Report error to platform team (on failure)
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: |
|
||||||
|
aws lambda invoke-function-url \
|
||||||
|
--function-url "${{ secrets.NOVA_LAMBDA_URL }}" \
|
||||||
|
--cli-binary-format raw-in-base64-out \
|
||||||
|
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
|
||||||
|
/dev/null || true
|
||||||
|
|
||||||
|
- name: Upload emitted Terraform
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nova-terraform
|
||||||
|
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
- name: Upload platform log
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nova-platform-log
|
||||||
|
path: platform/logs/
|
||||||
|
if-no-files-found: warn
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# ACDL Modules Lifecycle Pipeline — Gitea Actions (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" =
|
||||||
|
# the pipeline cell going green.
|
||||||
|
#
|
||||||
|
# Also matrix-runs L2 composition modules (static-assets, microservice) through
|
||||||
|
# the same apply→modify→destroy lifecycle. L2 = composition only (no L2
|
||||||
|
# terraform files); the composition must be deterministic.
|
||||||
|
#
|
||||||
|
# This workflow implements pipelines/modules-lifecycle.yml (byte-identical
|
||||||
|
# in .gitea/workflows/ and .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,
|
||||||
|
# no AWS mutation, validates the contract->resolver->adapter->plan chain
|
||||||
|
# for every module on every PR, with no AWS credentials or cost). Set to
|
||||||
|
# "full" via workflow_dispatch (or the NOVA_LIFECYCLE_MODE repo variable)
|
||||||
|
# to run the real apply→modify→destroy against live AWS. In plan mode the
|
||||||
|
# short-lived CI VPC apply/destroy jobs are skipped (nothing is applied).
|
||||||
|
#
|
||||||
|
# A short-lived CI VPC (terraform/ci-vpc/) is created before testing VPC-dependent
|
||||||
|
# modules (alb, ecs-service, rds, uptime, and L2 microservice) and destroyed
|
||||||
|
# after all tests complete. The CI VPC is separate from the long-lived platform
|
||||||
|
# VPC. Outputs are read from the S3 state by each lifecycle job (no artifact
|
||||||
|
# passing needed).
|
||||||
|
name: acdl-modules-lifecycle
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
lifecycle_mode:
|
||||||
|
description: "Lifecycle mode: 'plan' (default, fast, no AWS mutation) or 'full' (real apply→modify→destroy against live AWS)"
|
||||||
|
required: false
|
||||||
|
default: "plan"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- plan
|
||||||
|
- full
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Prerequisite: apply the short-lived CI VPC (needed by VPC-dependent L1s + L2 microservice)
|
||||||
|
# Skipped in plan mode (no resources are applied, so no VPC is needed).
|
||||||
|
ci-vpc-apply:
|
||||||
|
name: CI VPC apply
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Terraform 1.9.*
|
||||||
|
run: |
|
||||||
|
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
- name: Apply CI VPC
|
||||||
|
working-directory: terraform/ci-vpc
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: |
|
||||||
|
terraform init -input=false -lock=false
|
||||||
|
terraform apply -auto-approve -lock=false
|
||||||
|
|
||||||
|
# L1 lifecycle matrix: apply simple → apply complex (modify) → destroy
|
||||||
|
lifecycle:
|
||||||
|
name: L1 lifecycle (${{ matrix.module }})
|
||||||
|
needs: ci-vpc-apply
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
module: [s3, kms-key, ecr, ecs-cluster, iam-role, cloudfront, waf, vpc, alb, ecs-service, rds, uptime]
|
||||||
|
env:
|
||||||
|
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
|
||||||
|
sudo apt-get clean
|
||||||
|
df -h /
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Install Terraform 1.9.*
|
||||||
|
run: |
|
||||||
|
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
- name: Read CI VPC outputs
|
||||||
|
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||||
|
working-directory: terraform/ci-vpc
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: |
|
||||||
|
terraform init -input=false -lock=false
|
||||||
|
terraform output -json > /tmp/ci-vpc-outputs.json
|
||||||
|
- name: Apply (simple)
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
|
||||||
|
- name: Modify (complex)
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: bash scripts/run_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
|
||||||
|
- name: Destroy
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: bash scripts/run_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
|
||||||
|
|
||||||
|
# L2 lifecycle matrix: apply simple → apply complex (modify) → destroy
|
||||||
|
l2-lifecycle:
|
||||||
|
name: L2 lifecycle (${{ matrix.module }})
|
||||||
|
needs: ci-vpc-apply
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
module: [static-assets, microservice]
|
||||||
|
env:
|
||||||
|
NOVA_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.NOVA_LIFECYCLE_MODE || 'plan' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
|
||||||
|
sudo apt-get clean
|
||||||
|
df -h /
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Install Terraform 1.9.*
|
||||||
|
run: |
|
||||||
|
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
- name: Read CI VPC outputs
|
||||||
|
if: ${{ env.NOVA_LIFECYCLE_MODE == 'full' }}
|
||||||
|
working-directory: terraform/ci-vpc
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: |
|
||||||
|
terraform init -input=false -lock=false
|
||||||
|
terraform output -json > /tmp/ci-vpc-outputs.json
|
||||||
|
- name: Apply (simple)
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} simple /tmp/ci-vpc-outputs.json
|
||||||
|
- name: Modify (complex)
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: bash scripts/run_l2_lifecycle_test.sh ${{ matrix.module }} complex /tmp/ci-vpc-outputs.json
|
||||||
|
- name: Destroy
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: bash scripts/run_l2_lifecycle_destroy.sh ${{ matrix.module }} /tmp/ci-vpc-outputs.json
|
||||||
|
|
||||||
|
# Cleanup: destroy the CI VPC (always runs in full mode, even if lifecycle fails)
|
||||||
|
ci-vpc-destroy:
|
||||||
|
name: CI VPC destroy
|
||||||
|
needs: [lifecycle, l2-lifecycle]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.NOVA_LIFECYCLE_MODE != 'plan' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Terraform 1.9.*
|
||||||
|
run: |
|
||||||
|
wget -qO- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||||
|
- name: Destroy CI VPC
|
||||||
|
working-directory: terraform/ci-vpc
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.NOVA_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NOVA_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
run: |
|
||||||
|
terraform init -input=false -lock=false
|
||||||
|
terraform destroy -auto-approve -lock=false
|
||||||
Reference in New Issue
Block a user