feat(P26): 3 platform pipelines + release job with semver/tag updates
Phase 26 — platform-pipelines-and-release-automation: - platform-test.yml: PR pipeline (lint + unit-test + integration-test + schema-validation) replacing ci.yml for PRs; integration-test runs run_platform.sh --check-only for every contracts/*.yaml - primitives-plan.yml: PR pipeline with matrix over all 9 L1 primitives (s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf) - patterns-plan.yml: PR pipeline with matrix over all 2 L2 modules (static-assets, microservice) - release.yml: push-to-main pipeline computing next semver tag (PATCH for regular phases, MINOR for milestone completions), updating floating MAJOR.MINOR + MAJOR tags, and creating GitHub releases - run_primitive_plan.sh: plan-only/check-only runner for a single L1 primitive (adapter compile + structure validation offline) - run_pattern_plan.sh: plan-only/check-only runner for a single L2 pattern (environment check + contract validate + resolve + adapter + structure validation offline) - contracts/microservice.yaml: sample consumer contract for the microservice L2 module (schema-compliant scalar inputs) - instance.json for 8 L1 primitives (vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf) so the primitives-plan matrix can run the adapter offline; s3 already had one - tests/test_release_logic.py: unit test for semver computation (PATCH bump, MINOR bump on milestone, floating tag format) - tests/test_pipeline_contract.py: 19 new tests validating the 4 platform workflows exist and conform (stages, matrices, triggers, permissions) DEVIATION: The microservice pattern (run_pattern_plan.sh --check-only microservice + run_platform.sh --check-only contracts/microservice.yaml) fails at the adapter stage due to a pre-existing resolver ref-id mismatch for multi-resource L1s (resolver emits ref:vpc.subnet_ids but the expanded resource id is vpc-subnet). This predates Phase 26 and is out of scope for pipeline automation; the static-assets pattern passes end-to-end. The microservice contract is schema-valid and resolves correctly (11 resources); only the adapter compilation of multi-resource L1 refs fails. VERIFICATION: - bash scripts/run_ci.sh: PASS (lint + test + check-only) - python3 -m pytest tests/ -v: 266 passed - bash scripts/run_primitive_plan.sh --check-only s3: PASS - bash scripts/run_pattern_plan.sh --check-only static-assets: PASS - All 9 primitives pass run_primitive_plan.sh --check-only - All instance.json validate against stack.schema.json ---ci--- project: acdl phase: 26 milestone: v1.7 status: execute ---/ci---
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# ACDL Patterns Plan Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on PRs to main. For each L2 module, runs a plan-only (offline
|
||||||
|
# --check-only mode: resolves the sample contract for the module, runs the
|
||||||
|
# adapter, validates the emitted Terraform structure).
|
||||||
|
name: acdl-patterns-plan
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pattern-plan:
|
||||||
|
name: Pattern plan (${{ matrix.module }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
module: [static-assets, microservice]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Pattern plan check (${{ matrix.module }})
|
||||||
|
run: bash scripts/run_pattern_plan.sh --check-only ${{ matrix.module }}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# ACDL Platform Test Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on PRs to main. Replaces ci.yml for PRs (ci.yml stays for push-to-main).
|
||||||
|
# Four stages: lint, unit-test, integration-test, schema-validation.
|
||||||
|
#
|
||||||
|
# Shell reproducibility: scripts/run_ci.sh runs lint + test + check-only locally.
|
||||||
|
# The integration-test stage runs run_platform.sh --check-only for every
|
||||||
|
# contracts/*.yaml file. The schema-validation stage validates schemas, module
|
||||||
|
# interfaces, compositions, and example contracts.
|
||||||
|
name: acdl-platform-test
|
||||||
|
|
||||||
|
on:
|
||||||
|
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/contract_resolver.py \
|
||||||
|
core/environment_check.py \
|
||||||
|
core/output_publisher.py \
|
||||||
|
core/lambda/contract_ingestor.py \
|
||||||
|
adapters/terraform/adapter.py \
|
||||||
|
adapters/terraform/policy/checkov_adapter.py \
|
||||||
|
adapters/wiz/wiz_adapter.py \
|
||||||
|
adapters/kyverno/kyverno_adapter.py \
|
||||||
|
scripts/push_consumer_image.py
|
||||||
|
|
||||||
|
unit-test:
|
||||||
|
name: Unit tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install test dependencies
|
||||||
|
run: pip install -r requirements-test.txt
|
||||||
|
- name: Run pytest
|
||||||
|
run: python3 -m pytest tests/ -v --tb=short
|
||||||
|
|
||||||
|
integration-test:
|
||||||
|
name: Integration test (all sample contracts)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install runtime dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Run platform check-only for every sample contract
|
||||||
|
run: |
|
||||||
|
for contract in contracts/*.yaml; do
|
||||||
|
echo "--- Testing $contract ---"
|
||||||
|
bash scripts/run_platform.sh --check-only "$contract"
|
||||||
|
done
|
||||||
|
|
||||||
|
schema-validation:
|
||||||
|
name: Schema + module validation
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml
|
||||||
|
- name: Validate all schemas
|
||||||
|
run: |
|
||||||
|
python3 -c "
|
||||||
|
import json, glob, jsonschema
|
||||||
|
for schema_file in glob.glob('schemas/*.json'):
|
||||||
|
if 'contract.schema' in schema_file:
|
||||||
|
continue # has no self-validation
|
||||||
|
schema = json.load(open(schema_file))
|
||||||
|
# self-validate if it has a \$id
|
||||||
|
try:
|
||||||
|
jsonschema.Draft202012Validator.check_schema(schema)
|
||||||
|
except jsonschema.SchemaError as e:
|
||||||
|
raise SystemExit(f'{schema_file}: {e}')
|
||||||
|
print(f'{schema_file}: valid')
|
||||||
|
"
|
||||||
|
- name: Validate all module interfaces against stack.schema.json
|
||||||
|
run: |
|
||||||
|
python3 -c "
|
||||||
|
import json, glob, jsonschema, os
|
||||||
|
stack_schema = json.load(open('schemas/stack.schema.json'))
|
||||||
|
for iface_file in glob.glob('modules/l1/*/interface.json'):
|
||||||
|
try:
|
||||||
|
iface = json.load(open(iface_file))
|
||||||
|
# Validate basic structure (name, version, kind, type, inputs, outputs)
|
||||||
|
assert 'name' in iface, f'{iface_file}: missing name'
|
||||||
|
assert 'version' in iface, f'{iface_file}: missing version'
|
||||||
|
assert 'kind' in iface, f'{iface_file}: missing kind'
|
||||||
|
assert iface['kind'] == 'l1', f'{iface_file}: expected kind=l1'
|
||||||
|
assert 'type' in iface, f'{iface_file}: missing type'
|
||||||
|
assert 'inputs' in iface, f'{iface_file}: missing inputs'
|
||||||
|
assert 'outputs' in iface, f'{iface_file}: missing outputs'
|
||||||
|
print(f'{iface_file}: valid L1')
|
||||||
|
except Exception as e:
|
||||||
|
raise SystemExit(f'{iface_file}: {e}')
|
||||||
|
for comp_file in glob.glob('modules/l2/*/composition.json'):
|
||||||
|
try:
|
||||||
|
comp = json.load(open(comp_file))
|
||||||
|
assert 'name' in comp, f'{comp_file}: missing name'
|
||||||
|
assert 'version' in comp, f'{comp_file}: missing version'
|
||||||
|
assert 'kind' in comp, f'{comp_file}: missing kind'
|
||||||
|
assert comp['kind'] == 'l2', f'{comp_file}: expected kind=l2'
|
||||||
|
assert 'children' in comp, f'{comp_file}: missing children'
|
||||||
|
assert 'wires' in comp, f'{comp_file}: missing wires'
|
||||||
|
assert 'outputs' in comp, f'{comp_file}: missing outputs'
|
||||||
|
print(f'{comp_file}: valid L2')
|
||||||
|
except Exception as e:
|
||||||
|
raise SystemExit(f'{comp_file}: {e}')
|
||||||
|
"
|
||||||
|
- name: Validate module example contracts
|
||||||
|
run: |
|
||||||
|
python3 -c "
|
||||||
|
import json, yaml, glob, jsonschema
|
||||||
|
schema = json.load(open('schemas/contract.schema.json'))
|
||||||
|
# Validate example contracts if they exist
|
||||||
|
for example in glob.glob('modules/*/examples/*.yaml'):
|
||||||
|
try:
|
||||||
|
contract = yaml.safe_load(open(example))
|
||||||
|
jsonschema.validate(contract, schema)
|
||||||
|
print(f'{example}: valid contract')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'{example}: SKIP (not a contract or invalid: {e})')
|
||||||
|
# Also validate all sample contracts in contracts/
|
||||||
|
for contract_file in glob.glob('contracts/*.yaml'):
|
||||||
|
contract = yaml.safe_load(open(contract_file))
|
||||||
|
jsonschema.validate(contract, schema)
|
||||||
|
print(f'{contract_file}: valid contract')
|
||||||
|
"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ACDL Primitives Plan Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on PRs to main. For each L1 primitive, runs a plan-only (offline
|
||||||
|
# --check-only mode: resolves the primitive's instance.json, runs the adapter,
|
||||||
|
# validates the emitted Terraform structure).
|
||||||
|
name: acdl-primitives-plan
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
primitive-plan:
|
||||||
|
name: Primitive plan (${{ matrix.primitive }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
primitive: [s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install jsonschema pyyaml boto3
|
||||||
|
- name: Primitive plan check (${{ matrix.primitive }})
|
||||||
|
run: bash scripts/run_primitive_plan.sh --check-only ${{ matrix.primitive }}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# ACDL Release Pipeline — GitHub Actions (production)
|
||||||
|
#
|
||||||
|
# Runs on push to main. Computes the next semver tag from the latest tag +
|
||||||
|
# commit history, creates the tag, updates floating MAJOR.MINOR and MAJOR tags,
|
||||||
|
# and creates a GitHub release with auto-generated notes.
|
||||||
|
#
|
||||||
|
# Semver policy:
|
||||||
|
# - Regular phase commit -> bump PATCH (v1.6.0 -> v1.6.1)
|
||||||
|
# - Milestone completion ("docs(milestone): complete") -> bump MINOR (v1.6.1 -> v1.7.0)
|
||||||
|
# - Major bumps are manual (not implemented here).
|
||||||
|
name: acdl-release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Compute semver + update tags
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # need full history for tag computation
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Compute next version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
# Get the latest tag
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||||
|
echo "Latest tag: $LATEST_TAG"
|
||||||
|
|
||||||
|
# Parse the version
|
||||||
|
MAJOR=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\1/p')
|
||||||
|
MINOR=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\2/p')
|
||||||
|
PATCH=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\3/p')
|
||||||
|
|
||||||
|
# Check if this is a milestone completion (look for "docs(milestone): complete" in the latest commits)
|
||||||
|
if git log --format='%s' -5 | grep -q 'docs(milestone): complete'; then
|
||||||
|
# Milestone completion -> bump minor
|
||||||
|
MINOR=$((MINOR + 1))
|
||||||
|
PATCH=0
|
||||||
|
else
|
||||||
|
# Regular phase -> bump patch
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
MAJOR_MINOR_TAG="v${MAJOR}.${MINOR}"
|
||||||
|
MAJOR_TAG="v${MAJOR}"
|
||||||
|
|
||||||
|
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "major_minor_tag=$MAJOR_MINOR_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "major_tag=$MAJOR_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Next version: $NEW_TAG"
|
||||||
|
|
||||||
|
- name: Create version tag
|
||||||
|
run: |
|
||||||
|
git tag ${{ steps.version.outputs.new_tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.new_tag }}
|
||||||
|
|
||||||
|
- name: Update floating MAJOR.MINOR tag
|
||||||
|
run: |
|
||||||
|
git tag -f ${{ steps.version.outputs.major_minor_tag }} ${{ steps.version.outputs.new_tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.major_minor_tag }} --force
|
||||||
|
|
||||||
|
- name: Update floating MAJOR tag
|
||||||
|
run: |
|
||||||
|
git tag -f ${{ steps.version.outputs.major_tag }} ${{ steps.version.outputs.new_tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.major_tag }} --force
|
||||||
|
|
||||||
|
- name: Create GitHub release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# Generate release body from commit history since last tag
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
BODY=$(git log --format='- %s' "$PREV_TAG"..HEAD)
|
||||||
|
else
|
||||||
|
BODY=$(git log --format='- %s' HEAD)
|
||||||
|
fi
|
||||||
|
gh release create ${{ steps.version.outputs.new_tag }} \
|
||||||
|
--title "ACDL ${{ steps.version.outputs.new_tag }}" \
|
||||||
|
--notes "$BODY" \
|
||||||
|
--generate-notes || true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ACDL sample consumer contract — microservice module (dev)
|
||||||
|
#
|
||||||
|
# Reference example for an ECS Fargate microservice deployment.
|
||||||
|
#
|
||||||
|
# NOTE: The contract schema (schemas/contract.schema.json) restricts
|
||||||
|
# inputs to scalar types (string/number/boolean). Nested objects like
|
||||||
|
# `env: { LOG_LEVEL: info }` are not permitted; use a flat string
|
||||||
|
# (e.g. env_vars: "LOG_LEVEL=info") if environment variables are needed.
|
||||||
|
# This contract declares only the inputs the composition wires reference
|
||||||
|
# (bucket_name, region) plus a representative image/port.
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||||
|
module: microservice
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
bucket_name: acdl-microservice-demo
|
||||||
|
region: us-east-1
|
||||||
|
image: public.ecr.aws/docker/library/nginx:latest
|
||||||
|
port: 80
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "alb",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "alb-loadbalancer",
|
||||||
|
"type": "aws:elbv2:loadbalancer",
|
||||||
|
"module": "alb@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-alb",
|
||||||
|
"subnets": "subnet-12345",
|
||||||
|
"security_group": "sg-12345",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"lb_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alb-targetgroup",
|
||||||
|
"type": "aws:elbv2:targetgroup",
|
||||||
|
"module": "alb@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-alb",
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "HTTP",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"target_group_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alb-listener",
|
||||||
|
"type": "aws:elbv2:listener",
|
||||||
|
"module": "alb@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"port": 80,
|
||||||
|
"protocol": "HTTP",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"listener_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "cloudfront",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "cloudfront-originaccesscontrol",
|
||||||
|
"type": "aws:cloudfront:originaccesscontrol",
|
||||||
|
"module": "cloudfront@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-oac",
|
||||||
|
"origin_type": "s3",
|
||||||
|
"signing_behavior": "always",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"oac_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloudfront-distribution",
|
||||||
|
"type": "aws:cloudfront:distribution",
|
||||||
|
"module": "cloudfront@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"bucket_regional_domain_name": "acdl-spike-bucket.s3.us-east-1.amazonaws.com",
|
||||||
|
"price_class": "PriceClass_100",
|
||||||
|
"viewer_protocol_policy": "redirect-to-https",
|
||||||
|
"default_ttl": 3600,
|
||||||
|
"max_ttl": 86400,
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"distribution_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
},
|
||||||
|
"distribution_domain_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"oac_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "ecr",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "ecr",
|
||||||
|
"type": "aws:ecr:repository",
|
||||||
|
"module": "ecr@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-demo",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"repository_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"repository_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "ecs-cluster",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "ecs-cluster",
|
||||||
|
"type": "aws:ecs:cluster",
|
||||||
|
"module": "ecs-cluster@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-cluster",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"cluster_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
},
|
||||||
|
"cluster_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "ecs-service",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "service-taskdefinition",
|
||||||
|
"type": "aws:ecs:task_definition",
|
||||||
|
"module": "ecs-service@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"image": "public.ecr.aws/docker/library/nginx:latest",
|
||||||
|
"port": 80,
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"task_def_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "service-service",
|
||||||
|
"type": "aws:ecs:service",
|
||||||
|
"module": "ecs-service@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"cluster_arn": "arn:aws:ecs:us-east-1:123456789012:cluster/acdl-cluster",
|
||||||
|
"subnets": "subnet-12345",
|
||||||
|
"security_group": "sg-12345",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"service_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "iam-role",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "iam-role",
|
||||||
|
"type": "aws:iam:role",
|
||||||
|
"module": "iam-role@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"role_name": "acdl-task-role",
|
||||||
|
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ecs-tasks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"role_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
},
|
||||||
|
"role_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "vpc",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "vpc-vpc",
|
||||||
|
"type": "aws:ec2:vpc",
|
||||||
|
"module": "vpc@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"cidr": "10.0.0.0/16",
|
||||||
|
"name": "acdl-vpc",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"vpc_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vpc-subnet",
|
||||||
|
"type": "aws:ec2:subnet",
|
||||||
|
"module": "vpc@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"cidr": "10.0.1.0/24",
|
||||||
|
"az": "us-east-1a",
|
||||||
|
"name": "acdl-vpc",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"subnet_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vpc-routetable",
|
||||||
|
"type": "aws:ec2:routetable",
|
||||||
|
"module": "vpc@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "waf",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "waf-webacl",
|
||||||
|
"type": "aws:wafv2:webacl",
|
||||||
|
"module": "waf@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"name": "acdl-waf",
|
||||||
|
"scope": "cloudfront",
|
||||||
|
"default_action": "allow",
|
||||||
|
"region": "us-east-1"
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"web_acl_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run the platform pipeline for a single pattern (plan-only or check-only).
|
||||||
|
#
|
||||||
|
# Usage: run_pattern_plan.sh [--check-only] <module-name>
|
||||||
|
#
|
||||||
|
# --check-only: offline mode (no AWS) — resolves the sample contract for the
|
||||||
|
# module, runs the adapter, validates the emitted Terraform structure.
|
||||||
|
# (default): requires AWS — runs terraform plan on the emitted Terraform.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
CHECK_ONLY=0
|
||||||
|
MODULE=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--check-only) CHECK_ONLY=1 ;;
|
||||||
|
--*) echo "FAIL: unknown flag: $arg" >&2; exit 1 ;;
|
||||||
|
*) MODULE="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$MODULE" ] || { echo "FAIL: module name required" >&2; exit 1; }
|
||||||
|
|
||||||
|
CONTRACT="contracts/$MODULE.yaml"
|
||||||
|
[ -f "$CONTRACT" ] || { echo "FAIL: no sample contract at $CONTRACT for module '$MODULE'" >&2; exit 1; }
|
||||||
|
|
||||||
|
WORK="/tmp/acdl_pattern_plan_$MODULE"
|
||||||
|
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||||
|
|
||||||
|
echo "=== Pattern plan: $MODULE ==="
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 1: environment check ---"
|
||||||
|
python3 core/environment_check.py "$CONTRACT" || { echo "FAIL: environment not bound" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 2: validate contract ---"
|
||||||
|
python3 -c "
|
||||||
|
import json, yaml, jsonschema
|
||||||
|
schema = json.load(open('schemas/contract.schema.json'))
|
||||||
|
contract = yaml.safe_load(open('$CONTRACT'))
|
||||||
|
jsonschema.validate(contract, schema)
|
||||||
|
print(f'contract: module={contract[\"module\"]} env={contract[\"environment\"]}')
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 3: resolve contract -> stack ---"
|
||||||
|
python3 core/contract_resolver.py "$CONTRACT" "$WORK/stack.json" || { echo "FAIL: resolver failed" >&2; exit 1; }
|
||||||
|
python3 -c "import json; d=json.load(open('$WORK/stack.json')); print(f'stack: {d[\"stack\"][\"name\"]} {len(d[\"resources\"])} resource(s)')"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 4: adapter compiles stack -> terraform ---"
|
||||||
|
OUT_DIR="$WORK/terraform"
|
||||||
|
python3 adapters/terraform/adapter.py "$WORK/stack.json" "$OUT_DIR" || { echo "FAIL: adapter failed" >&2; exit 1; }
|
||||||
|
echo "adapter: emitted $OUT_DIR/{main.tf,terraform.tf,providers.tf}"
|
||||||
|
|
||||||
|
if [ "$CHECK_ONLY" = "1" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 5: validate adapter output structure (offline) ---"
|
||||||
|
python3 -c "
|
||||||
|
import json, os
|
||||||
|
d = json.load(open('$WORK/stack.json'))
|
||||||
|
assert d['stack']['kind'] == 'l2', f\"expected l2, got {d['stack']['kind']}\"
|
||||||
|
assert len(d['resources']) >= 1
|
||||||
|
tf_dir = '$OUT_DIR'
|
||||||
|
for f in ('main.tf', 'terraform.tf', 'providers.tf'):
|
||||||
|
assert os.path.isfile(os.path.join(tf_dir, f)), f'{f} missing'
|
||||||
|
main = open(os.path.join(tf_dir, 'main.tf')).read()
|
||||||
|
assert len(main) > 0, 'main.tf is empty'
|
||||||
|
print(f'pattern $MODULE: adapter output OK ({len(d[\"resources\"])} resource(s))')
|
||||||
|
"
|
||||||
|
echo ""
|
||||||
|
echo "=== PATTERN CHECK OK ($MODULE) ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 5: terraform init + validate + plan ---"
|
||||||
|
cd "$OUT_DIR"
|
||||||
|
terraform init -backend=false -input=false
|
||||||
|
terraform validate
|
||||||
|
terraform plan -lock=false -input=false -out=tfplan
|
||||||
|
echo "=== PATTERN PLAN OK ($MODULE) ==="
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run the platform pipeline for a single primitive (plan-only or check-only).
|
||||||
|
#
|
||||||
|
# Usage: run_primitive_plan.sh [--check-only] <primitive-name>
|
||||||
|
#
|
||||||
|
# --check-only: offline mode (no AWS) — resolves the primitive's instance.json,
|
||||||
|
# runs the adapter, validates the emitted Terraform structure.
|
||||||
|
# (default): requires AWS — runs terraform plan on the emitted Terraform.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
CHECK_ONLY=0
|
||||||
|
PRIMITIVE=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--check-only) CHECK_ONLY=1 ;;
|
||||||
|
--*) echo "FAIL: unknown flag: $arg" >&2; exit 1 ;;
|
||||||
|
*) PRIMITIVE="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$PRIMITIVE" ] || { echo "FAIL: primitive name required" >&2; exit 1; }
|
||||||
|
|
||||||
|
INSTANCE="modules/l1/$PRIMITIVE/instance.json"
|
||||||
|
[ -f "$INSTANCE" ] || { echo "FAIL: no instance.json for primitive '$PRIMITIVE'" >&2; exit 1; }
|
||||||
|
|
||||||
|
WORK="/tmp/acdl_primitive_plan_$PRIMITIVE"
|
||||||
|
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||||
|
|
||||||
|
echo "=== Primitive plan: $PRIMITIVE ==="
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 1: adapter compiles instance -> terraform ---"
|
||||||
|
OUT_DIR="$WORK/terraform"
|
||||||
|
python3 adapters/terraform/adapter.py "$INSTANCE" "$OUT_DIR" || { echo "FAIL: adapter failed" >&2; exit 1; }
|
||||||
|
echo "adapter: emitted $OUT_DIR/{main.tf,terraform.tf,providers.tf}"
|
||||||
|
|
||||||
|
if [ "$CHECK_ONLY" = "1" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 2: validate adapter output structure (offline) ---"
|
||||||
|
python3 -c "
|
||||||
|
import json, os
|
||||||
|
d = json.load(open('$INSTANCE'))
|
||||||
|
assert d['stack']['kind'] == 'l1', f\"expected l1, got {d['stack']['kind']}\"
|
||||||
|
assert len(d['resources']) >= 1
|
||||||
|
tf_dir = '$OUT_DIR'
|
||||||
|
for f in ('main.tf', 'terraform.tf', 'providers.tf'):
|
||||||
|
assert os.path.isfile(os.path.join(tf_dir, f)), f'{f} missing'
|
||||||
|
main = open(os.path.join(tf_dir, 'main.tf')).read()
|
||||||
|
assert len(main) > 0, 'main.tf is empty'
|
||||||
|
print(f'primitive $PRIMITIVE: adapter output OK ({len(d[\"resources\"])} resource(s))')
|
||||||
|
"
|
||||||
|
echo ""
|
||||||
|
echo "=== PRIMITIVE CHECK OK ($PRIMITIVE) ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 2: terraform init + validate + plan ---"
|
||||||
|
cd "$OUT_DIR"
|
||||||
|
terraform init -backend=false -input=false
|
||||||
|
terraform validate
|
||||||
|
terraform plan -lock=false -input=false -out=tfplan
|
||||||
|
echo "=== PRIMITIVE PLAN OK ($PRIMITIVE) ==="
|
||||||
@@ -360,4 +360,134 @@ class TestSampleContractVersioning:
|
|||||||
uses = contract["uses"]
|
uses = contract["uses"]
|
||||||
assert "@v" in uses, "sample contract must use a versioned @vX.Y tag"
|
assert "@v" in uses, "sample contract must use a versioned @vX.Y tag"
|
||||||
assert "@main" not in uses, "sample contract must not use @main"
|
assert "@main" not in uses, "sample contract must not use @main"
|
||||||
assert uses == "acdl/pipelines/deploy.yaml@v1.6"
|
assert uses == "acdl/pipelines/deploy.yaml@v1.6"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlatformWorkflows:
|
||||||
|
"""Validate the Phase 26 platform pipelines exist and conform."""
|
||||||
|
|
||||||
|
def test_platform_test_workflow_exists(self):
|
||||||
|
assert (ROOT / ".github/workflows/platform-test.yml").is_file()
|
||||||
|
|
||||||
|
def test_primitives_plan_workflow_exists(self):
|
||||||
|
assert (ROOT / ".github/workflows/primitives-plan.yml").is_file()
|
||||||
|
|
||||||
|
def test_patterns_plan_workflow_exists(self):
|
||||||
|
assert (ROOT / ".github/workflows/patterns-plan.yml").is_file()
|
||||||
|
|
||||||
|
def test_release_workflow_exists(self):
|
||||||
|
assert (ROOT / ".github/workflows/release.yml").is_file()
|
||||||
|
|
||||||
|
def test_platform_test_has_four_stages(self):
|
||||||
|
wf = _load_workflow(".github/workflows/platform-test.yml")
|
||||||
|
job_names = set(wf["jobs"].keys())
|
||||||
|
assert job_names == {"lint", "unit-test", "integration-test", "schema-validation"}
|
||||||
|
|
||||||
|
def test_platform_test_lint_compiles_python(self):
|
||||||
|
wf = _load_workflow(".github/workflows/platform-test.yml")
|
||||||
|
lint_job = wf["jobs"]["lint"]
|
||||||
|
run_step = next(s for s in lint_job["steps"] if "run" in s)
|
||||||
|
assert "py_compile" in run_step["run"]
|
||||||
|
for py_file in [
|
||||||
|
"core/confidence_signal.py",
|
||||||
|
"core/outbox_writer.py",
|
||||||
|
"core/contract_resolver.py",
|
||||||
|
"core/environment_check.py",
|
||||||
|
"core/output_publisher.py",
|
||||||
|
"core/lambda/contract_ingestor.py",
|
||||||
|
"adapters/terraform/adapter.py",
|
||||||
|
"adapters/terraform/policy/checkov_adapter.py",
|
||||||
|
"adapters/wiz/wiz_adapter.py",
|
||||||
|
"adapters/kyverno/kyverno_adapter.py",
|
||||||
|
"scripts/push_consumer_image.py",
|
||||||
|
]:
|
||||||
|
assert py_file in run_step["run"], f"{py_file} missing from platform-test lint"
|
||||||
|
|
||||||
|
def test_platform_test_unit_test_runs_pytest(self):
|
||||||
|
wf = _load_workflow(".github/workflows/platform-test.yml")
|
||||||
|
test_job = wf["jobs"]["unit-test"]
|
||||||
|
run_step = next(s for s in test_job["steps"] if "run" in s and "pytest" in s["run"])
|
||||||
|
assert "pytest" in run_step["run"]
|
||||||
|
|
||||||
|
def test_platform_test_integration_runs_all_contracts(self):
|
||||||
|
wf = _load_workflow(".github/workflows/platform-test.yml")
|
||||||
|
integ_job = wf["jobs"]["integration-test"]
|
||||||
|
run_step = next(
|
||||||
|
s for s in integ_job["steps"] if "run" in s and "run_platform" in s["run"]
|
||||||
|
)
|
||||||
|
assert "run_platform.sh" in run_step["run"]
|
||||||
|
assert "--check-only" in run_step["run"]
|
||||||
|
assert "contracts/*.yaml" in run_step["run"]
|
||||||
|
|
||||||
|
def test_platform_test_schema_validation_validates_schemas(self):
|
||||||
|
wf = _load_workflow(".github/workflows/platform-test.yml")
|
||||||
|
schema_job = wf["jobs"]["schema-validation"]
|
||||||
|
steps_text = " ".join(s.get("run", "") for s in schema_job["steps"])
|
||||||
|
assert "jsonschema" in steps_text
|
||||||
|
assert "stack.schema.json" in steps_text
|
||||||
|
|
||||||
|
def test_platform_test_triggers_pr_only(self):
|
||||||
|
wf = _load_workflow(".github/workflows/platform-test.yml")
|
||||||
|
assert "pull_request" in wf["on"]
|
||||||
|
assert "main" in wf["on"]["pull_request"]["branches"]
|
||||||
|
# platform-test should NOT trigger on push (ci.yml handles push-to-main)
|
||||||
|
assert "push" not in wf["on"]
|
||||||
|
|
||||||
|
def test_primitives_plan_has_matrix_with_all_l1_primitives(self):
|
||||||
|
wf = _load_workflow(".github/workflows/primitives-plan.yml")
|
||||||
|
job = wf["jobs"]["primitive-plan"]
|
||||||
|
matrix = job["strategy"]["matrix"]
|
||||||
|
expected = ["s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf"]
|
||||||
|
assert sorted(matrix["primitive"]) == sorted(expected)
|
||||||
|
|
||||||
|
def test_primitives_plan_runs_run_primitive_plan(self):
|
||||||
|
wf = _load_workflow(".github/workflows/primitives-plan.yml")
|
||||||
|
job = wf["jobs"]["primitive-plan"]
|
||||||
|
run_step = next(s for s in job["steps"] if "run" in s and "run_primitive_plan" in s["run"])
|
||||||
|
assert "run_primitive_plan.sh" in run_step["run"]
|
||||||
|
assert "--check-only" in run_step["run"]
|
||||||
|
|
||||||
|
def test_primitives_plan_triggers_pr_only(self):
|
||||||
|
wf = _load_workflow(".github/workflows/primitives-plan.yml")
|
||||||
|
assert "pull_request" in wf["on"]
|
||||||
|
assert "main" in wf["on"]["pull_request"]["branches"]
|
||||||
|
assert "push" not in wf["on"]
|
||||||
|
|
||||||
|
def test_patterns_plan_has_matrix_with_all_l2_modules(self):
|
||||||
|
wf = _load_workflow(".github/workflows/patterns-plan.yml")
|
||||||
|
job = wf["jobs"]["pattern-plan"]
|
||||||
|
matrix = job["strategy"]["matrix"]
|
||||||
|
expected = ["static-assets", "microservice"]
|
||||||
|
assert sorted(matrix["module"]) == sorted(expected)
|
||||||
|
|
||||||
|
def test_patterns_plan_runs_run_pattern_plan(self):
|
||||||
|
wf = _load_workflow(".github/workflows/patterns-plan.yml")
|
||||||
|
job = wf["jobs"]["pattern-plan"]
|
||||||
|
run_step = next(s for s in job["steps"] if "run" in s and "run_pattern_plan" in s["run"])
|
||||||
|
assert "run_pattern_plan.sh" in run_step["run"]
|
||||||
|
assert "--check-only" in run_step["run"]
|
||||||
|
|
||||||
|
def test_patterns_plan_triggers_pr_only(self):
|
||||||
|
wf = _load_workflow(".github/workflows/patterns-plan.yml")
|
||||||
|
assert "pull_request" in wf["on"]
|
||||||
|
assert "main" in wf["on"]["pull_request"]["branches"]
|
||||||
|
assert "push" not in wf["on"]
|
||||||
|
|
||||||
|
def test_release_workflow_triggers_push_main(self):
|
||||||
|
wf = _load_workflow(".github/workflows/release.yml")
|
||||||
|
assert "push" in wf["on"]
|
||||||
|
assert "main" in wf["on"]["push"]["branches"]
|
||||||
|
|
||||||
|
def test_release_workflow_has_contents_write_permission(self):
|
||||||
|
wf = _load_workflow(".github/workflows/release.yml")
|
||||||
|
# permissions are declared at the job level (the release job)
|
||||||
|
release_job = wf["jobs"]["release"]
|
||||||
|
assert release_job["permissions"]["contents"] == "write"
|
||||||
|
|
||||||
|
def test_release_workflow_fetch_depth_zero(self):
|
||||||
|
wf = _load_workflow(".github/workflows/release.yml")
|
||||||
|
release_job = wf["jobs"]["release"]
|
||||||
|
checkout = next(
|
||||||
|
s for s in release_job["steps"] if "checkout" in s.get("uses", "")
|
||||||
|
)
|
||||||
|
assert checkout["with"]["fetch-depth"] == 0
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Test the release semver computation logic (D-057)."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def compute_next_version(latest_tag, is_milestone_complete):
|
||||||
|
"""Compute the next semver tag from the latest tag + milestone flag."""
|
||||||
|
parts = latest_tag.lstrip("v").split(".")
|
||||||
|
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
|
||||||
|
if is_milestone_complete:
|
||||||
|
minor += 1
|
||||||
|
patch = 0
|
||||||
|
else:
|
||||||
|
patch += 1
|
||||||
|
new_tag = f"v{major}.{minor}.{patch}"
|
||||||
|
major_minor_tag = f"v{major}.{minor}"
|
||||||
|
major_tag = f"v{major}"
|
||||||
|
return new_tag, major_minor_tag, major_tag
|
||||||
|
|
||||||
|
|
||||||
|
class TestComputeNextVersion:
|
||||||
|
def test_regular_phase_bumps_patch(self):
|
||||||
|
assert compute_next_version("v1.6.0", False) == ("v1.6.1", "v1.6", "v1")
|
||||||
|
|
||||||
|
def test_milestone_complete_bumps_minor(self):
|
||||||
|
assert compute_next_version("v1.6.5", True) == ("v1.7.0", "v1.7", "v1")
|
||||||
|
|
||||||
|
def test_milestone_complete_resets_patch(self):
|
||||||
|
assert compute_next_version("v1.6.0", True) == ("v1.7.0", "v1.7", "v1")
|
||||||
|
|
||||||
|
def test_major_bump(self):
|
||||||
|
# Not implemented yet (major milestones are manual), but test the tag format
|
||||||
|
tag, mj, m = compute_next_version("v2.3.4", True)
|
||||||
|
assert tag == "v2.4.0"
|
||||||
|
assert mj == "v2.4"
|
||||||
|
assert m == "v2"
|
||||||
|
|
||||||
|
def test_floating_tags_match_major_minor(self):
|
||||||
|
tag, mj, m = compute_next_version("v1.7.3", False)
|
||||||
|
assert mj == "v1.7"
|
||||||
|
assert m == "v1"
|
||||||
|
assert tag == "v1.7.4"
|
||||||
|
|
||||||
|
def test_v1_tag_advances_with_minor(self):
|
||||||
|
# When minor bumps, v1 tag still points at the latest (force-moved)
|
||||||
|
tag, mj, m = compute_next_version("v1.6.5", True)
|
||||||
|
assert m == "v1"
|
||||||
|
assert tag == "v1.7.0"
|
||||||
Reference in New Issue
Block a user