feat(P67b): lifecycle tests default to plan-only; ACDL_LIFECYCLE_MODE flag overrides to full (REQ-134)
--- ci--- project: acdl phase: 67b milestone: v1.12 status: execute --- /ci--- The modules-lifecycle pipeline now defaults to plan-only (fast, no AWS mutation, no credentials, no cost) so it runs on every PR. A CI variable ACDL_LIFECYCLE_MODE (workflow_dispatch input 'lifecycle_mode', default 'plan') overrides to 'full' for the real apply->modify->destroy against live AWS. Scripts: run_lifecycle_test.sh / run_lifecycle_destroy.sh / run_l2_lifecycle_test.sh / run_l2_lifecycle_destroy.sh read the flag and dispatch to --plan-only (plan mode) or --apply/--destroy (full mode). Destroy is a no-op exit 0 in plan mode (nothing was applied). VPC-output injection is gated on full mode. Workflows: both .github + .gitea (byte-identical) expose lifecycle_mode as a workflow_dispatch input (choice: plan/full), pass it via env: ACDL_LIFECYCLE_MODE to every lifecycle step, skip ci-vpc-apply + ci-vpc-destroy + Read-CI-VPC-outputs in plan mode, and run the lifecycle + l2-lifecycle jobs with if: always() so they execute (plan-only) even when ci-vpc-apply is skipped. Contract + schema: pipelines/modules-lifecycle.yml gains default_mode: plan; the schema accepts default_mode (enum plan|full) and a richer workflow_dispatch inputs shape. Tests: 14 new tests in test_lifecycle_mode_flag.py (script dispatch) + 10 new tests in TestModulesLifecyclePipeline (workflow flag wiring, byte-identity, plan-mode skips). Updated test_platform_vpc_destroy to reflect the plan-mode skip. 516 tests pass; smoke-tested plan mode on the s3 module (--plan-only green, no AWS apply).
This commit is contained in:
@@ -576,6 +576,21 @@ two probe fixes required to make the deck claims true.
|
||||
Marp structure (including the A6 Operating Model & Cost section that
|
||||
was missing from the talking points). PPTX exported to the v1.12.0
|
||||
release. (Phase 69, Phase 70)
|
||||
- **REQ-134** — The `modules-lifecycle` pipeline defaults to **plan-only**
|
||||
(fast, no AWS mutation) so it runs on every PR without cost or AWS
|
||||
credentials. A CI variable `ACDL_LIFECYCLE_MODE` (workflow input
|
||||
`lifecycle_mode`, default `plan`) overrides to `full` for the real
|
||||
apply→modify→destroy against live AWS. The four lifecycle scripts
|
||||
(`run_lifecycle_test.sh`, `run_lifecycle_destroy.sh`,
|
||||
`run_l2_lifecycle_test.sh`, `run_l2_lifecycle_destroy.sh`) read the
|
||||
flag and dispatch to `--plan-only` (plan mode) or `--apply`/`--destroy`
|
||||
(full mode). Both forge workflows (`.github` + `.gitea`, byte-identical)
|
||||
expose `lifecycle_mode` as a `workflow_dispatch` input and pass it via
|
||||
`env:` to every lifecycle step; the CI VPC apply/destroy jobs are
|
||||
skipped in plan mode. `pipelines/modules-lifecycle.yml` + the schema
|
||||
document the `default_mode: plan` field. Tests assert the plan-only
|
||||
default, the override path, the byte-identity of both workflows, and
|
||||
the CI VPC skip in plan mode. (Phase 67b)
|
||||
|
||||
### v1.12 Traceability
|
||||
|
||||
@@ -583,6 +598,7 @@ two probe fixes required to make the deck claims true.
|
||||
|-------------|-------|--------|
|
||||
| REQ-129 | P67 | planned |
|
||||
| REQ-130 | P67 | planned |
|
||||
| REQ-134 | P67b | planned |
|
||||
| REQ-131 | P68, P70 | planned |
|
||||
| REQ-132 | P68 | planned |
|
||||
| REQ-133 | P69, P70 | planned |
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
# 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 ACDL_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
|
||||
@@ -22,15 +30,26 @@ 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.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
@@ -52,11 +71,14 @@ jobs:
|
||||
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:
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free disk space
|
||||
@@ -75,6 +97,7 @@ jobs:
|
||||
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.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
@@ -106,11 +129,14 @@ jobs:
|
||||
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:
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free disk space
|
||||
@@ -129,6 +155,7 @@ jobs:
|
||||
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.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
@@ -156,12 +183,12 @@ jobs:
|
||||
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, even if lifecycle fails)
|
||||
# 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()
|
||||
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
# 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 ACDL_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
|
||||
@@ -22,15 +30,26 @@ 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.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
@@ -52,11 +71,14 @@ jobs:
|
||||
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:
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free disk space
|
||||
@@ -75,6 +97,7 @@ jobs:
|
||||
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.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
@@ -106,11 +129,14 @@ jobs:
|
||||
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:
|
||||
ACDL_LIFECYCLE_MODE: ${{ github.event.inputs.lifecycle_mode || vars.ACDL_LIFECYCLE_MODE || 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Free disk space
|
||||
@@ -129,6 +155,7 @@ jobs:
|
||||
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.ACDL_LIFECYCLE_MODE == 'full' }}
|
||||
working-directory: terraform/ci-vpc
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
@@ -156,12 +183,12 @@ jobs:
|
||||
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, even if lifecycle fails)
|
||||
# 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()
|
||||
if: ${{ always() && github.event.inputs.lifecycle_mode != 'plan' && vars.ACDL_LIFECYCLE_MODE != 'plan' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Terraform 1.9.*
|
||||
|
||||
@@ -11,18 +11,34 @@ name: acdl-modules-lifecycle
|
||||
# VPC-dependent L1s (alb, ecs-service, rds, uptime) reference the platform
|
||||
# VPC applied by a prerequisite job. Non-VPC L1s run independent.
|
||||
#
|
||||
# Lifecycle mode (REQ-134, v1.12): `default_mode: plan` — the lifecycle
|
||||
# scripts run `run_platform.sh --plan-only` (fast, no AWS mutation, no
|
||||
# AWS credentials, no cost) on every PR. Set the ACDL_LIFECYCLE_MODE CI
|
||||
# variable (workflow_dispatch input `lifecycle_mode`) to `full` for the
|
||||
# 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
|
||||
# byte-identically.
|
||||
|
||||
triggers:
|
||||
pull_request: [main]
|
||||
workflow_dispatch: []
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
- name: lifecycle_mode
|
||||
description: "Lifecycle mode: 'plan' (default, fast) or 'full' (real apply→destroy)"
|
||||
default: plan
|
||||
|
||||
runner: ubuntu-latest
|
||||
python_version: "3.12"
|
||||
terraform_version: "1.9.*"
|
||||
|
||||
# Default lifecycle mode: "plan" (plan-only, fast, no AWS mutation) or "full"
|
||||
# (real apply→modify→destroy against live AWS). Overridable via the
|
||||
# ACDL_LIFECYCLE_MODE CI variable / workflow_dispatch input.
|
||||
default_mode: plan
|
||||
|
||||
stages:
|
||||
- name: platform-vpc-apply
|
||||
command: cd terraform/platform && terraform init -input=false && terraform apply -auto-approve -lock=false
|
||||
|
||||
@@ -20,9 +20,26 @@
|
||||
"description": "Branches that trigger the pipeline on PR."
|
||||
},
|
||||
"workflow_dispatch": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Allows manual dispatch (empty array = no params)."
|
||||
"description": "Manual dispatch — either an empty array (no params) or an object with an 'inputs' array of {name, description, default} objects (REQ-134).",
|
||||
"oneOf": [
|
||||
{"type": "array", "items": {"type": "string"}},
|
||||
{"type": "object",
|
||||
"properties": {
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"default": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -38,6 +55,11 @@
|
||||
"type": "string",
|
||||
"description": "Terraform version constraint (e.g. '1.9.*')."
|
||||
},
|
||||
"default_mode": {
|
||||
"type": "string",
|
||||
"enum": ["plan", "full"],
|
||||
"description": "Default lifecycle mode (REQ-134, v1.12): 'plan' = plan-only (fast, no AWS mutation, no credentials, no cost); 'full' = real apply→modify→destroy against live AWS. Overridable via the ACDL_LIFECYCLE_MODE CI variable / workflow_dispatch input."
|
||||
},
|
||||
"stages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
|
||||
@@ -5,12 +5,24 @@
|
||||
#
|
||||
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
|
||||
# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state.
|
||||
#
|
||||
# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" = no-op
|
||||
# (plan mode never applies resources, so there is nothing to destroy).
|
||||
# Set to "full" for the real `--destroy` against live AWS.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
MODULE="$1"
|
||||
|
||||
# Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy.
|
||||
LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
if [ "$LIFECYCLE_MODE" != "full" ]; then
|
||||
echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CONTRACT="modules/l2/${MODULE}/examples/complex.yml"
|
||||
|
||||
# Point terraform_remote_state to the CI VPC state (not the platform VPC)
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state so the
|
||||
# microservice composition's terraform_remote_state data source reads from
|
||||
# the short-lived CI VPC (not the long-lived platform VPC).
|
||||
#
|
||||
# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" runs
|
||||
# `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for
|
||||
# the real `--apply` against live AWS.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
@@ -14,10 +18,17 @@ cd "$ROOT"
|
||||
MODULE="$1"
|
||||
EXAMPLE="$2" # simple or complex
|
||||
|
||||
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
|
||||
LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml"
|
||||
|
||||
# Point terraform_remote_state to the CI VPC state (not the platform VPC)
|
||||
export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate"
|
||||
|
||||
# Run the platform lifecycle command
|
||||
bash scripts/run_platform.sh --apply "$CONTRACT"
|
||||
# Run the platform lifecycle command (plan-only by default; full = apply).
|
||||
if [ "$LIFECYCLE_MODE" = "full" ]; then
|
||||
bash scripts/run_platform.sh --apply "$CONTRACT"
|
||||
else
|
||||
bash scripts/run_platform.sh --plan-only "$CONTRACT"
|
||||
fi
|
||||
@@ -5,6 +5,11 @@
|
||||
#
|
||||
# For VPC-dependent modules, injects CI VPC outputs into the complex contract
|
||||
# before destroy (so terraform can find the resources in the right VPC).
|
||||
#
|
||||
# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" = no-op
|
||||
# (plan mode never applies resources, so there is nothing to destroy; the
|
||||
# script exits 0 so the pipeline matrix cell stays green). Set to "full"
|
||||
# for the real `--destroy` against live AWS.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
@@ -12,6 +17,15 @@ cd "$ROOT"
|
||||
MODULE="$1"
|
||||
CI_VPC_OUTPUTS="${2:-}"
|
||||
|
||||
# Lifecycle mode: "plan" (default) skips destroy (nothing was applied);
|
||||
# "full" runs the real terraform destroy.
|
||||
LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
if [ "$LIFECYCLE_MODE" != "full" ]; then
|
||||
echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CONTRACT="modules/l1/${MODULE}/examples/complex.yml"
|
||||
|
||||
VPC_DEPENDENT="alb ecs-service rds uptime"
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
#
|
||||
# The CI VPC is short-lived (created/destroyed by the pipeline), separate
|
||||
# from the long-lived platform VPC.
|
||||
#
|
||||
# Lifecycle mode (REQ-134): the ACDL_LIFECYCLE_MODE env var selects the
|
||||
# tier. Default "plan" runs `run_platform.sh --plan-only` (fast, no AWS
|
||||
# mutation, validates the contract->resolver->adapter->plan chain for
|
||||
# every module). Set to "full" to run the real `--apply` (terraform apply
|
||||
# against live AWS). The CI variable is passed via the workflow input
|
||||
# `lifecycle_mode`.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
@@ -17,13 +24,17 @@ MODULE="$1"
|
||||
EXAMPLE="$2" # simple or complex
|
||||
CI_VPC_OUTPUTS="${3:-}"
|
||||
|
||||
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
|
||||
LIFECYCLE_MODE="${ACDL_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml"
|
||||
|
||||
# VPC-dependent modules that need CI VPC outputs injected
|
||||
VPC_DEPENDENT="alb ecs-service rds uptime"
|
||||
|
||||
# If the module is VPC-dependent and we have CI VPC outputs, inject them
|
||||
if echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then
|
||||
# (only meaningful in full mode; plan mode ignores VPC outputs)
|
||||
if [ "$LIFECYCLE_MODE" = "full" ] && echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then
|
||||
# Generate a temporary contract with CI VPC outputs injected
|
||||
TMP_CONTRACT="/tmp/acdl-lifecycle-${MODULE}-${EXAMPLE}.yml"
|
||||
python3 -c "
|
||||
@@ -70,5 +81,9 @@ print('$TMP_CONTRACT')
|
||||
CONTRACT="$TMP_CONTRACT"
|
||||
fi
|
||||
|
||||
# Run the platform lifecycle command
|
||||
bash scripts/run_platform.sh --apply "$CONTRACT"
|
||||
# Run the platform lifecycle command (plan-only by default; full = apply).
|
||||
if [ "$LIFECYCLE_MODE" = "full" ]; then
|
||||
bash scripts/run_platform.sh --apply "$CONTRACT"
|
||||
else
|
||||
bash scripts/run_platform.sh --plan-only "$CONTRACT"
|
||||
fi
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Tests for the lifecycle test scripts' plan-only/full mode flag (REQ-134).
|
||||
|
||||
The lifecycle scripts (run_lifecycle_test.sh, run_lifecycle_destroy.sh,
|
||||
run_l2_lifecycle_test.sh, run_l2_lifecycle_destroy.sh) wrap run_platform.sh.
|
||||
REQ-134 (v1.12) adds the ACDL_LIFECYCLE_MODE env var: default "plan" runs
|
||||
`run_platform.sh --plan-only` (fast, no AWS mutation); "full" runs the real
|
||||
`--apply`/`--destroy` against live AWS.
|
||||
|
||||
These tests verify the dispatch logic offline by inspecting script content
|
||||
(running the scripts end-to-end requires AWS credentials in full mode).
|
||||
"""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = __import__("pathlib").Path(__file__).resolve().parent.parent
|
||||
|
||||
SCRIPTS = [
|
||||
"scripts/run_lifecycle_test.sh",
|
||||
"scripts/run_lifecycle_destroy.sh",
|
||||
"scripts/run_l2_lifecycle_test.sh",
|
||||
"scripts/run_l2_lifecycle_destroy.sh",
|
||||
]
|
||||
|
||||
|
||||
def _read(path):
|
||||
return (ROOT / path).read_text()
|
||||
|
||||
|
||||
class TestLifecycleModeFlag:
|
||||
"""REQ-134: the lifecycle scripts dispatch to plan-only by default."""
|
||||
|
||||
@pytest.mark.parametrize("script", SCRIPTS)
|
||||
def test_script_reads_acdl_lifecycle_mode(self, script):
|
||||
"""Every lifecycle script reads ACDL_LIFECYCLE_MODE with a 'plan' default."""
|
||||
src = _read(script)
|
||||
# The default must be 'plan' (the speed-up default). The scripts use
|
||||
# an intermediate LIFECYCLE_MODE var sourced from ACDL_LIFECYCLE_MODE.
|
||||
assert "${ACDL_LIFECYCLE_MODE:-plan}" in src, \
|
||||
f"{script} must read ACDL_LIFECYCLE_MODE defaulting to 'plan'"
|
||||
assert "LIFECYCLE_MODE=" in src, \
|
||||
f"{script} must assign LIFECYCLE_MODE from the env var"
|
||||
|
||||
@pytest.mark.parametrize("script", [
|
||||
"scripts/run_lifecycle_test.sh",
|
||||
"scripts/run_l2_lifecycle_test.sh",
|
||||
])
|
||||
def test_apply_scripts_dispatch_to_plan_only_by_default(self, script):
|
||||
"""Apply/modify scripts run --plan-only when mode != full."""
|
||||
src = _read(script)
|
||||
assert "--plan-only" in src, f"{script} must support --plan-only (plan mode)"
|
||||
assert "--apply" in src, f"{script} must support --apply (full mode)"
|
||||
# The dispatch must branch on LIFECYCLE_MODE
|
||||
assert "LIFECYCLE_MODE" in src and "full" in src, \
|
||||
f"{script} must branch on LIFECYCLE_MODE == 'full'"
|
||||
|
||||
@pytest.mark.parametrize("script", [
|
||||
"scripts/run_lifecycle_destroy.sh",
|
||||
"scripts/run_l2_lifecycle_destroy.sh",
|
||||
])
|
||||
def test_destroy_scripts_noop_in_plan_mode(self, script):
|
||||
"""Destroy scripts are a no-op (exit 0) in plan mode — nothing was applied."""
|
||||
src = _read(script)
|
||||
# The destroy script must check the mode and exit 0 in plan mode.
|
||||
assert "nothing to destroy" in src, \
|
||||
f"{script} must no-op (with a message) in plan mode"
|
||||
assert "--destroy" in src, f"{script} must still support --destroy in full mode"
|
||||
assert "LIFECYCLE_MODE" in src, f"{script} must read LIFECYCLE_MODE"
|
||||
|
||||
def test_apply_script_plan_mode_uses_plan_only(self):
|
||||
"""In plan mode, run_lifecycle_test.sh dispatches to --plan-only."""
|
||||
src = _read("scripts/run_lifecycle_test.sh")
|
||||
# The if/else: full -> --apply, else -> --plan-only
|
||||
assert re.search(r'if.*LIFECYCLE_MODE.*=.*"full".*;.*then', src)
|
||||
assert re.search(r'else\s+bash scripts/run_platform\.sh --plan-only', src, re.DOTALL)
|
||||
|
||||
def test_apply_script_full_mode_uses_apply(self):
|
||||
"""In full mode, run_lifecycle_test.sh dispatches to --apply."""
|
||||
src = _read("scripts/run_lifecycle_test.sh")
|
||||
assert re.search(r'LIFECYCLE_MODE.*=.*"full".*bash scripts/run_platform\.sh --apply', src, re.DOTALL)
|
||||
|
||||
def test_destroy_script_plan_mode_exits_zero(self):
|
||||
"""In plan mode, the L1 destroy script exits 0 without calling run_platform."""
|
||||
src = _read("scripts/run_lifecycle_destroy.sh")
|
||||
assert re.search(r'LIFECYCLE_MODE.*!=.*"full".*exiting 0', src, re.DOTALL)
|
||||
|
||||
def test_l2_apply_script_dispatches_correctly(self):
|
||||
"""The L2 apply script dispatches to plan-only by default, apply in full."""
|
||||
src = _read("scripts/run_l2_lifecycle_test.sh")
|
||||
assert "--plan-only" in src and "--apply" in src
|
||||
assert re.search(r'if.*LIFECYCLE_MODE.*=.*"full"', src)
|
||||
|
||||
def test_l2_destroy_script_noop_in_plan_mode(self):
|
||||
"""The L2 destroy script is a no-op in plan mode."""
|
||||
src = _read("scripts/run_l2_lifecycle_destroy.sh")
|
||||
assert "nothing to destroy" in src
|
||||
assert re.search(r'LIFECYCLE_MODE.*!=.*"full".*exiting 0', src, re.DOTALL)
|
||||
|
||||
def test_vpc_injection_gated_on_full_mode(self):
|
||||
"""VPC output injection is gated on full mode (plan mode skips it)."""
|
||||
src = _read("scripts/run_lifecycle_test.sh")
|
||||
# The VPC injection block must be gated on LIFECYCLE_MODE == full.
|
||||
assert re.search(r'LIFECYCLE_MODE.*=.*"full".*&&.*echo.*VPC_DEPENDENT', src, re.DOTALL) or \
|
||||
re.search(r'\[ "\$LIFECYCLE_MODE" = "full" \] && echo.*VPC_DEPENDENT', src, re.DOTALL)
|
||||
@@ -581,10 +581,14 @@ class TestModulesLifecyclePipeline:
|
||||
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_always_runs(self):
|
||||
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"]
|
||||
assert destroy_job.get("if") == "always()", "ci-vpc-destroy must always run (cleanup)"
|
||||
# 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")
|
||||
@@ -613,4 +617,67 @@ class TestModulesLifecyclePipeline:
|
||||
|
||||
def test_contract_matrix_lists_l2_modules(self):
|
||||
contract = _load_yaml("pipelines/modules-lifecycle.yml")
|
||||
assert set(contract["matrix"]["l2_modules"]) == {"static-assets", "microservice"}
|
||||
assert set(contract["matrix"]["l2_modules"]) == {"static-assets", "microservice"}
|
||||
|
||||
# --- REQ-134: lifecycle mode flag (plan-only default, full override) ---
|
||||
|
||||
def test_contract_declares_plan_as_default_mode(self):
|
||||
"""The pipeline contract declares default_mode: plan (REQ-134)."""
|
||||
contract = _load_yaml("pipelines/modules-lifecycle.yml")
|
||||
assert contract.get("default_mode") == "plan", \
|
||||
"default_mode must be 'plan' (fast, no AWS mutation, the default on every PR)"
|
||||
|
||||
def test_schema_accepts_default_mode_field(self):
|
||||
"""The schema accepts the default_mode field with plan/full enum."""
|
||||
schema = json.load(open(ROOT / "schemas/modules-lifecycle-pipeline.schema.json"))
|
||||
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 ACDL_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 "ACDL_LIFECYCLE_MODE" in env
|
||||
# The expression must resolve to 'plan' when no input/var is set.
|
||||
assert "plan" in env["ACDL_LIFECYCLE_MODE"]
|
||||
|
||||
def test_l2_lifecycle_job_passes_mode_env_to_steps(self):
|
||||
"""The L2 lifecycle job also sets ACDL_LIFECYCLE_MODE env."""
|
||||
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
|
||||
env = wf["jobs"]["l2-lifecycle"].get("env", {})
|
||||
assert "ACDL_LIFECYCLE_MODE" in env
|
||||
assert "plan" in env["ACDL_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)"
|
||||
Reference in New Issue
Block a user