ci: run the Terraform isolation gate as a job in the CI workflow

This commit is contained in:
Adam Moussa 2026-09-10 19:37:27 -04:00
parent 7ab6fa30e7
commit 8ec91f0dac
No known key found for this signature in database
6 changed files with 44 additions and 55 deletions

View file

@ -71,6 +71,30 @@ jobs:
env: env:
GOVERNANCE_BASE: ${{ steps.governance-ref.outputs.base }} GOVERNANCE_BASE: ${{ steps.governance-ref.outputs.base }}
terraform-isolation:
# Fails a pull request that changes `terraform/**` together with deployable
# application code (scripts/check-terraform-isolation.mjs). A merge that
# does both queues an HCP VCS run and a content release at the same time,
# and the two race for the workspace lock. The
# `terraform-isolation-override` label is the reviewed exception; it is
# read when the job runs, so re-run this workflow after labeling.
name: Terraform and application changes are isolated
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
- name: Check changed files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
TERRAFORM_ISOLATION_OVERRIDE: ${{ contains(github.event.pull_request.labels.*.name, 'terraform-isolation-override') }}
run: node scripts/check-terraform-isolation.mjs --base "${BASE_SHA}" --head "${HEAD_SHA}"
visual-regression: visual-regression:
name: Visual regression name: Visual regression
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -1,35 +0,0 @@
name: Terraform isolation
# Fails a pull request that changes `terraform/**` together with deployable
# application code (see scripts/check-terraform-isolation.mjs). A merge that
# does both queues an HCP VCS run and a content release at the same time, and
# the two race for the workspace lock.
#
# Runs on label events too, so adding or removing the
# `terraform-isolation-override` label re-evaluates the gate without a push.
on:
pull_request:
branches: [main, dev, staging]
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
jobs:
terraform-isolation:
name: Terraform and application changes are isolated
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
- name: Check changed files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
TERRAFORM_ISOLATION_OVERRIDE: ${{ contains(github.event.pull_request.labels.*.name, 'terraform-isolation-override') }}
run: node scripts/check-terraform-isolation.mjs --base "${BASE_SHA}" --head "${HEAD_SHA}"

View file

@ -30,7 +30,7 @@ and synthesis. A task is not done until this is green.
| Terraform isolation gate contract | `npm run test:terraform-isolation` → `scripts/check-terraform-isolation.test.mjs` | `governance` + CI | Changed-file classifier | | Terraform isolation gate contract | `npm run test:terraform-isolation` → `scripts/check-terraform-isolation.test.mjs` | `governance` + CI | Changed-file classifier |
| Terraform formatting/validation | `npm run test:terraform` → `scripts/terraform-validate.mjs` | `governance` + CI | `terraform/live/dev` | | Terraform formatting/validation | `npm run test:terraform` → `scripts/terraform-validate.mjs` | `governance` + CI | `terraform/live/dev` |
| CDK build, tests, synthesis | `npm run test:infra` | `governance` + CI | `infra/cdk/**`, both synth modes | | CDK build, tests, synthesis | `npm run test:infra` | `governance` + CI | `infra/cdk/**`, both synth modes |
| Terraform/app change isolation | `.github/workflows/terraform-isolation.yaml` → `scripts/check-terraform-isolation.mjs` | CI (PR) | Changed files of the PR | | Terraform/app change isolation | `ci.yaml` job `terraform-isolation` → `scripts/check-terraform-isolation.mjs` | CI (PR) | Changed files of the PR |
## No-false-pass guarantees ## No-false-pass guarantees
@ -49,9 +49,10 @@ and synthesis. A task is not done until this is green.
against synthetic plan JSON. Real import and controlled-update plans from HCP against synthetic plan JSON. Real import and controlled-update plans from HCP
are migration evidence reviewed by a human before an approved apply are migration evidence reviewed by a human before an approved apply
(`terraform/README.md`). (`terraform/README.md`).
- **The isolation gate re-evaluates on label changes** — the - **The isolation gate reads the override when it runs** — the
`terraform-isolation-override` label is the only way to merge a mixed `terraform-isolation-override` label is the only way to merge a mixed
Terraform/application PR, and the gate logs the override on the run. Terraform/application PR, the gate logs the override on the run, and the
workflow must be re-run after the label is added or removed.
## Where the gates run ## Where the gates run
@ -62,10 +63,9 @@ and synthesis. A task is not done until this is green.
format/lint/build/tests, **and** a repo-owned `governance` job runs format/lint/build/tests, **and** a repo-owned `governance` job runs
`npm run verify` (with Terraform 1.16.0 installed) so the maintainability `npm run verify` (with Terraform 1.16.0 installed) so the maintainability
ratchets and repository gates are guaranteed from this repository regardless ratchets and repository gates are guaranteed from this repository regardless
of the reusable workflow. of the reusable workflow. On pull requests the same workflow's
- **CI ([`.github/workflows/terraform-isolation.yaml`](.github/workflows/terraform-isolation.yaml)):** `terraform-isolation` job fails when `terraform/**` and application code
on every PR (including label events), fails when `terraform/**` and change together.
application code change together.
## Toolchain pin ## Toolchain pin

View file

@ -135,8 +135,8 @@ commitlint enforces conventional commit messages. Run `npx tsc --noEmit` (or
a green CI run and an approving review from a code owner a green CI run and an approving review from a code owner
(`@Sea-Haven-Industries/internal-dev`); new pushes dismiss stale approvals. (`@Sea-Haven-Industries/internal-dev`); new pushes dismiss stale approvals.
Merged branches are deleted automatically. Merged branches are deleted automatically.
- A PR that changes `terraform/**` may not also change application code - A PR that changes `terraform/**` may not also change application code (the
(`.github/workflows/terraform-isolation.yaml`); ship Terraform in its own PR. `terraform-isolation` CI job); ship Terraform in its own PR.
- Promotion flow: `feature/* → dev` (deployed to `dev.seahaven.com` through the - Promotion flow: `feature/* → dev` (deployed to `dev.seahaven.com` through the
**Deploy dev content** workflow while the Terraform adoption is in progress) **Deploy dev content** workflow while the Terraform adoption is in progress)
`→ main` (production promotion — no prod environment exists yet). `→ main` (production promotion — no prod environment exists yet).
@ -156,10 +156,10 @@ No stored AWS keys — OIDC only. Infrastructure and content deploy separately:
[`AGENTS.md`](AGENTS.md), [`QUALITY_GATES.md`](QUALITY_GATES.md), [`AGENTS.md`](AGENTS.md), [`QUALITY_GATES.md`](QUALITY_GATES.md),
[`ARCHITECTURE_AND_CODE_QUALITY.md`](ARCHITECTURE_AND_CODE_QUALITY.md), and [`ARCHITECTURE_AND_CODE_QUALITY.md`](ARCHITECTURE_AND_CODE_QUALITY.md), and
[`REVIEW_AND_PR_FRAMEWORK.md`](REVIEW_AND_PR_FRAMEWORK.md). [`REVIEW_AND_PR_FRAMEWORK.md`](REVIEW_AND_PR_FRAMEWORK.md).
- **Terraform isolation** - **Terraform isolation** (the `terraform-isolation` job in
([`.github/workflows/terraform-isolation.yaml`](.github/workflows/terraform-isolation.yaml)) [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)) — fails a PR that
— fails a PR that mixes `terraform/**` with application code, so a Terraform mixes `terraform/**` with application code, so a Terraform merge never races
merge never races a content release for the HCP workspace. a content release for the HCP workspace.
- **Dev content** ([`.github/workflows/deploy.yml`](.github/workflows/deploy.yml)) - **Dev content** ([`.github/workflows/deploy.yml`](.github/workflows/deploy.yml))
— `workflow_dispatch` on `dev` only while the Terraform adoption is in — `workflow_dispatch` on `dev` only while the Terraform adoption is in
progress. Runs `npm run verify`, assumes `githubdeploy-shoc-frontend-new-dev`, progress. Runs `npm run verify`, assumes `githubdeploy-shoc-frontend-new-dev`,

View file

@ -36,8 +36,7 @@ infra/cdk/
test/frontend-stack.test.mjs template assertions for both modes test/frontend-stack.test.mjs template assertions for both modes
scripts/deploy-web.sh build SPA -> s3 sync -> CloudFront invalidation scripts/deploy-web.sh build SPA -> s3 sync -> CloudFront invalidation
.github/workflows/ .github/workflows/
ci.yaml quality gates (lint / build / test / governance) ci.yaml quality gates (lint / build / test / governance / terraform isolation)
terraform-isolation.yaml PRs may not mix terraform/** with app code
deploy.yml dev content publish (workflow_dispatch on dev) deploy.yml dev content publish (workflow_dispatch on dev)
deploy-staging.yml standalone staging deploy (push to staging) deploy-staging.yml standalone staging deploy (push to staging)
``` ```

View file

@ -217,11 +217,12 @@ the labels. Push-to-`dev` releases return behind the repository variable
## Operational rules ## Operational rules
- **Terraform-only PRs.** A PR that changes `terraform/**` may not change - **Terraform-only PRs.** A PR that changes `terraform/**` may not change
deployable application code. `.github/workflows/terraform-isolation.yaml` deployable application code. The `terraform-isolation` job in
enforces this; documentation and the `scripts/*terraform*` tooling are `.github/workflows/ci.yaml` enforces this; documentation and the
allowed alongside. A reviewer may add the `terraform-isolation-override` `scripts/*terraform*` tooling are allowed alongside. A reviewer may add the
label for the rare change that must introduce Terraform variables together `terraform-isolation-override` label for the rare change that must introduce
with the workflow that consumes them (PR A and PR C). The label is the Terraform variables together with the workflow that consumes them (PR A and
PR C), then re-run the workflow so the job reads the label. The label is the
approval record. The override is temporary: a follow-up PR after PR C approval record. The override is temporary: a follow-up PR after PR C
removes the label path from the checker and workflow so the gate has no removes the label path from the checker and workflow so the gate has no
exception. exception.