diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9a39ad3b..53262ad1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,6 +71,30 @@ jobs: env: 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: name: Visual regression runs-on: ubuntu-latest diff --git a/.github/workflows/terraform-isolation.yaml b/.github/workflows/terraform-isolation.yaml deleted file mode 100644 index 14c53730..00000000 --- a/.github/workflows/terraform-isolation.yaml +++ /dev/null @@ -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}" diff --git a/QUALITY_GATES.md b/QUALITY_GATES.md index 78b5dcec..60ed9970 100644 --- a/QUALITY_GATES.md +++ b/QUALITY_GATES.md @@ -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 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 | -| 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 @@ -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 are migration evidence reviewed by a human before an approved apply (`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/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 @@ -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 `npm run verify` (with Terraform 1.16.0 installed) so the maintainability ratchets and repository gates are guaranteed from this repository regardless - of the reusable workflow. -- **CI ([`.github/workflows/terraform-isolation.yaml`](.github/workflows/terraform-isolation.yaml)):** - on every PR (including label events), fails when `terraform/**` and - application code change together. + of the reusable workflow. On pull requests the same workflow's + `terraform-isolation` job fails when `terraform/**` and application code + change together. ## Toolchain pin diff --git a/README.md b/README.md index 1fc10a6c..9a4bc04a 100644 --- a/README.md +++ b/README.md @@ -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 (`@Sea-Haven-Industries/internal-dev`); new pushes dismiss stale approvals. Merged branches are deleted automatically. -- A PR that changes `terraform/**` may not also change application code - (`.github/workflows/terraform-isolation.yaml`); ship Terraform in its own PR. +- A PR that changes `terraform/**` may not also change application code (the + `terraform-isolation` CI job); ship Terraform in its own PR. - Promotion flow: `feature/* → dev` (deployed to `dev.seahaven.com` through the **Deploy dev content** workflow while the Terraform adoption is in progress) `→ 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), [`ARCHITECTURE_AND_CODE_QUALITY.md`](ARCHITECTURE_AND_CODE_QUALITY.md), and [`REVIEW_AND_PR_FRAMEWORK.md`](REVIEW_AND_PR_FRAMEWORK.md). -- **Terraform isolation** - ([`.github/workflows/terraform-isolation.yaml`](.github/workflows/terraform-isolation.yaml)) - — fails a PR that mixes `terraform/**` with application code, so a Terraform - merge never races a content release for the HCP workspace. +- **Terraform isolation** (the `terraform-isolation` job in + [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)) — fails a PR that + mixes `terraform/**` with application code, so a Terraform merge never races + a content release for the HCP workspace. - **Dev content** ([`.github/workflows/deploy.yml`](.github/workflows/deploy.yml)) — `workflow_dispatch` on `dev` only while the Terraform adoption is in progress. Runs `npm run verify`, assumes `githubdeploy-shoc-frontend-new-dev`, diff --git a/infra/cdk/README.md b/infra/cdk/README.md index ba46bf4b..cf7d466f 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -36,8 +36,7 @@ infra/cdk/ test/frontend-stack.test.mjs template assertions for both modes scripts/deploy-web.sh build SPA -> s3 sync -> CloudFront invalidation .github/workflows/ - ci.yaml quality gates (lint / build / test / governance) - terraform-isolation.yaml PRs may not mix terraform/** with app code + ci.yaml quality gates (lint / build / test / governance / terraform isolation) deploy.yml dev content publish (workflow_dispatch on dev) deploy-staging.yml standalone staging deploy (push to staging) ``` diff --git a/terraform/README.md b/terraform/README.md index 1f436b17..a20e97cc 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -217,11 +217,12 @@ the labels. Push-to-`dev` releases return behind the repository variable ## Operational rules - **Terraform-only PRs.** A PR that changes `terraform/**` may not change - deployable application code. `.github/workflows/terraform-isolation.yaml` - enforces this; documentation and the `scripts/*terraform*` tooling are - allowed alongside. A reviewer may add the `terraform-isolation-override` - label for the rare change that must introduce Terraform variables together - with the workflow that consumes them (PR A and PR C). The label is the + deployable application code. The `terraform-isolation` job in + `.github/workflows/ci.yaml` enforces this; documentation and the + `scripts/*terraform*` tooling are allowed alongside. A reviewer may add the + `terraform-isolation-override` label for the rare change that must introduce + 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 removes the label path from the checker and workflow so the gate has no exception.