From a95c11527c2a0fd3bbd4d0791e61ad06378c831c Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Thu, 10 Sep 2026 20:11:20 -0400 Subject: [PATCH] ci(terraform-isolation): re-evaluate the gate on label changes --- .github/workflows/ci.yaml | 19 +++++++++++++-- QUALITY_GATES.md | 9 +++++--- scripts/check-terraform-isolation.mjs | 4 +++- scripts/check-terraform-isolation.test.mjs | 27 ++++++++++++++++++++++ terraform/README.md | 13 +++++++---- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53262ad1..21a0130a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,6 +3,16 @@ name: Frontend checks on: pull_request: branches: [main, dev, staging] + # labeled/unlabeled re-evaluate terraform-isolation when the override + # label is added or removed. Without them a mixed PR can keep a stale + # green isolation check after the label is taken off. Other jobs skip + # on those events so a label change does not re-run the suite. + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled push: branches: [main, dev, staging] workflow_dispatch: {} @@ -13,12 +23,15 @@ permissions: jobs: build-and-test: name: Build and test + # Skipped jobs satisfy the required checks on the unchanged SHA. + if: github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled') # Org reusable workflow (Node 24): format check, lint, build, unit tests. uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@af0f002e14a08cdbfd879c1183bfe7eb2604bce9 # v1.0.8 with: node-version: "24" governance: + if: github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled') # Repo-owned guarantee that every frontend quality gate runs from this # repository, independent of (and in addition to) the reusable workflow. # `npm run verify` is the single command that chains: format check, lint @@ -76,8 +89,9 @@ jobs: # 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. + # `terraform-isolation-override` label is the reviewed exception. This + # job runs on labeled/unlabeled as well as the default PR types so the + # check matches the labels currently on the PR. name: Terraform and application changes are isolated if: github.event_name == 'pull_request' runs-on: ubuntu-latest @@ -97,6 +111,7 @@ jobs: visual-regression: name: Visual regression + if: github.event_name != 'pull_request' || (github.event.action != 'labeled' && github.event.action != 'unlabeled') runs-on: ubuntu-latest container: mcr.microsoft.com/playwright:v1.61.1-noble steps: diff --git a/QUALITY_GATES.md b/QUALITY_GATES.md index 60ed9970..17ea3de5 100644 --- a/QUALITY_GATES.md +++ b/QUALITY_GATES.md @@ -49,10 +49,13 @@ 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 reads the override when it runs** — the +- **The isolation gate re-evaluates on label changes** — the `terraform-isolation-override` label is the only way to merge a mixed - 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. + Terraform/application PR. `ci.yaml` runs `terraform-isolation` on + `labeled` and `unlabeled` as well as the default pull-request types, so + adding or removing the label re-checks the current labels. The other CI + jobs skip on those events. Removing the label fails a mixed PR that had + previously passed with the override. ## Where the gates run diff --git a/scripts/check-terraform-isolation.mjs b/scripts/check-terraform-isolation.mjs index 7b28c149..d289f9a7 100644 --- a/scripts/check-terraform-isolation.mjs +++ b/scripts/check-terraform-isolation.mjs @@ -17,7 +17,9 @@ // TERRAFORM_ISOLATION_OVERRIDE=true downgrades a failure to a warning. CI sets // it only when the PR carries the `terraform-isolation-override` label, which // reviewers grant to the rare change that must introduce Terraform variables -// together with the workflow that consumes them. +// together with the workflow that consumes them. The checker has no memory of +// a previous pass: the same mixed diff fails again as soon as the override +// env is unset (label removal). import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; diff --git a/scripts/check-terraform-isolation.test.mjs b/scripts/check-terraform-isolation.test.mjs index 24c6c87d..aa3ae4d7 100644 --- a/scripts/check-terraform-isolation.test.mjs +++ b/scripts/check-terraform-isolation.test.mjs @@ -109,6 +109,33 @@ test("CLI override downgrades a mixed change to a warning that names the label", assert.equal(notTrue.status, 1); }); +test("removing the override fails a mixed change that was previously green", () => { + const files = ["terraform/live/dev/main.tf", ".github/workflows/deploy.yml"]; + const previouslyGreen = runGate(files, { + TERRAFORM_ISOLATION_OVERRIDE: "true", + }); + assert.equal( + previouslyGreen.status, + 0, + previouslyGreen.stdout + previouslyGreen.stderr, + ); + assert.match(previouslyGreen.stdout, /WARNING/); + + // CI sets TERRAFORM_ISOLATION_OVERRIDE from contains(...labels), which is + // the string "false" after the label is removed. A stale green check must + // not survive that. + const afterLabelRemoved = runGate(files, { + TERRAFORM_ISOLATION_OVERRIDE: "false", + }); + assert.equal( + afterLabelRemoved.status, + 1, + afterLabelRemoved.stdout + afterLabelRemoved.stderr, + ); + assert.match(afterLabelRemoved.stdout, /FAIL/); + assert.match(afterLabelRemoved.stdout, /deploy\.yml/); +}); + test("CLI refuses to run without a base ref or --stdin", () => { const result = spawnSync(process.execPath, [SCRIPT], { encoding: "utf8" }); assert.notEqual(result.status, 0); diff --git a/terraform/README.md b/terraform/README.md index a20e97cc..8522c080 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -221,11 +221,14 @@ the labels. Push-to-`dev` releases return behind the repository variable `.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. + Terraform variables together with the workflow that consumes them (PR C). + Adding or removing that label re-runs `terraform-isolation` against the + labels currently on the PR; the other CI jobs skip on those events so a + label change does not re-run the suite. Removing the label fails a mixed + PR that had previously passed with the override, so a stale green check + cannot merge. 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. - **Every Terraform merge produces a VCS run.** A human confirms or discards it before the next content release. Do not leave a pending run on the workspace.