diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index 0586d9a2..692b1010 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -29,8 +29,10 @@ on: types: [completed] workflow_dispatch: {} -# Only one integ run at a time against the shared account — overlapping deploys -# would collide on the single hardcoded `backgroundagent-integ` stack name. +# Only one integ run at a time against the shared account. Stack names are now +# run-unique (`int-`, see the integ job) so this is no longer a correctness +# requirement — it bounds shared-account cost/parallelism rather than preventing +# a name collision. concurrency: group: cdk-integ cancel-in-progress: false @@ -217,30 +219,36 @@ jobs: - name: Install dependencies run: yarn install --immutable - - name: Run integ tests (deploy → assert → destroy) - run: mise //cdk:integ - - # Safety net: integ-runner forces teardown on success and failure, but if - # the run is cancelled or crashes mid-deploy the stack can be stranded in - # the shared account. Delete it directly via CloudFormation so we never - # leak billable resources. + # Teardown: integ-runner forces `cdk destroy` on success and failure (via + # the CDK bootstrap roles, which hold the CloudFormation permissions). No + # raw-CLI safety net here: the GitHub OIDC role has no + # cloudformation:DeleteStack/DescribeStacks, so such a step can only fail + # (#566). If a cancelled/crashed run strands the stack, reclaim is the + # stranded-stack cleanup owned by #400/#72 — and the run-unique name below + # means a stranded stack never blocks a later run. # - # NOTE: `cdk destroy backgroundagent-integ` would NOT work here — it - # synthesizes the main app (src/main.ts), which does not contain the integ - # stack, so it exits 0 having deleted nothing. Target the stack by its - # literal CloudFormation name instead. delete-stack is idempotent (no-op if - # already gone), so `|| true` only guards transient API errors. - - name: Ensure stack torn down - if: always() + # INTEG_STACK_NAME maps the stack to the commit under test plus the run + # number (`int--`), replacing the single fixed + # `backgroundagent-integ` name. The run number makes same-commit re-runs + # distinct; the `int-` prefix marks it ephemeral for #72's sweep. HEAD_SHA + # is the resolved PR head (or dispatched ref) and RUN_NUMBER is a positive + # integer, so both are already lowercase-alphanumeric — no sanitizing + # needed to stay CloudFormation-valid. + - name: Run integ tests (deploy → assert → destroy) env: - AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }} - AWS_DEFAULT_REGION: ${{ vars.AWS_REGION || 'us-east-1' }} + HEAD_SHA: ${{ needs.resolve.outputs.head_sha }} + RUN_NUMBER: ${{ github.run_number }} run: | set -euo pipefail - aws cloudformation delete-stack --stack-name backgroundagent-integ || true - # No `|| true` on the wait: a DELETE_FAILED must surface loudly so we - # never silently leak billable resources in the shared account. - aws cloudformation wait stack-delete-complete --stack-name backgroundagent-integ + export INTEG_STACK_NAME="int-${HEAD_SHA:0:12}-${RUN_NUMBER}" + echo "Integ stack name: $INTEG_STACK_NAME" + mise //cdk:integ -- --all --require-approval never \ + -c stackName="integ-${{ github.run_id }}-${{ github.run_attempt }}" \ + -c github:sha="$(git rev-parse HEAD)" \ + -c github:ref="$(git branch --show-current || echo "detached")" \ + -c github:actor="${{ github.triggering_actor }}" \ + -c github:repository="$(gh repo view --json nameWithOwner --jq .nameWithOwner)" \ + -c github:clean="$([ -z "$(git status --porcelain)" ] && echo true || echo false)" # Post the final integ-smoke status back to the PR head so the check flips from # pending to success/failure. Skipped for workflow_dispatch (no PR to gate). diff --git a/cdk/test/integ/integ.task-api-smoke.ts b/cdk/test/integ/integ.task-api-smoke.ts index 5775de59..c5e78bc3 100644 --- a/cdk/test/integ/integ.task-api-smoke.ts +++ b/cdk/test/integ/integ.task-api-smoke.ts @@ -89,8 +89,15 @@ class TaskApiSmokeStack extends Stack { } } +// Stack name is run-unique in CI (`int--`, set by +// integ.yml) so a stranded stack from a cancelled/crashed run never collides +// with — or blocks — a later run under one fixed name. Falls back to a stable +// local name so the `mise //cdk:integ` dev path is unchanged. The `int-` prefix +// keeps it letter-led and the rest is lowercase-alphanumeric + hyphens, so the +// name is CloudFormation-valid. const app = new App(); -const stack = new TaskApiSmokeStack(app, 'backgroundagent-integ'); +const stackName = process.env.INTEG_STACK_NAME || 'backgroundagent-integ-local'; +const stack = new TaskApiSmokeStack(app, stackName); const integ = new IntegTest(app, 'TaskApiSmoke', { testCases: [stack],