mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
ci: fan out quality gates from this repository
This commit is contained in:
parent
3e5fba4d2b
commit
8ad7438f26
8 changed files with 154 additions and 132 deletions
56
.github/workflows/ci-terraform.yaml
vendored
56
.github/workflows/ci-terraform.yaml
vendored
|
|
@ -1,56 +0,0 @@
|
|||
name: Terraform CI
|
||||
|
||||
# Static checks only. Plans run in HCP Terraform as speculative VCS runs on
|
||||
# the PR (shoc-frontend-new-dev and shoc-frontend-new-staging). Applies are
|
||||
# HCP auto-apply on merge to main (dev) and on a vX.Y.Z-staging tag (staging).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- "terraform/**"
|
||||
- "scripts/**"
|
||||
- ".github/workflows/ci-terraform.yaml"
|
||||
- ".github/workflows/deploy-web.yaml"
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "terraform/**"
|
||||
- "scripts/**"
|
||||
- ".github/workflows/ci-terraform.yaml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
terraform:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||
with:
|
||||
terraform_version: "1.16.0"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform fmt
|
||||
run: terraform fmt -check -recursive terraform
|
||||
|
||||
- name: Validate live/dev
|
||||
run: |
|
||||
terraform -chdir=terraform/live/dev init -backend=false -input=false -lockfile=readonly -no-color
|
||||
terraform -chdir=terraform/live/dev validate -no-color
|
||||
|
||||
- name: Validate live/staging
|
||||
run: |
|
||||
terraform -chdir=terraform/live/staging init -backend=false -input=false -lockfile=readonly -no-color
|
||||
terraform -chdir=terraform/live/staging validate -no-color
|
||||
|
||||
- name: Import plan guard tests
|
||||
run: python3 scripts/test-terraform-import-plan-check.py
|
||||
|
||||
- name: App/Terraform isolation tests
|
||||
run: python3 scripts/test_check_app_terraform_isolation.py
|
||||
130
.github/workflows/ci.yaml
vendored
130
.github/workflows/ci.yaml
vendored
|
|
@ -13,25 +13,81 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build and test
|
||||
# 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"
|
||||
static:
|
||||
name: static
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run format:check
|
||||
- run: npm run lint
|
||||
|
||||
build:
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
unit:
|
||||
name: unit
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm test -- --shard=${{ matrix.shard }}/2
|
||||
|
||||
visual:
|
||||
name: Visual regression
|
||||
runs-on: ubuntu-latest
|
||||
container: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:e2e:visual
|
||||
- name: Upload visual diff artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: visual-regression-diffs
|
||||
path: |
|
||||
test-results/visual
|
||||
playwright-report-visual
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
governance:
|
||||
# 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
|
||||
# (--max-warnings=0), type-check + build, unit tests, then the governance
|
||||
# checks in scripts/governance-check.mjs (godfile ratchet, changed-file
|
||||
# maintainability gate, Terraform fmt/validate, Terraform import-plan
|
||||
# guard, HCP run guard, CloudFront verify, GitHub workflow shell, and G13
|
||||
# app/Terraform isolation). Runs on PRs to main or dev; push is main only.
|
||||
# If the reusable workflow is later confirmed to run every gate, this job
|
||||
# can be slimmed to `npm run governance`.
|
||||
# Repo-owned gates: godfile ratchet, changed-file maintainability,
|
||||
# Terraform fmt/validate, import-plan guard, HCP run guard, CloudFront
|
||||
# verify, GitHub workflow shell, isolation classifier tests, and live G13
|
||||
# (pull_request and local only). Format, lint, build, and unit tests run
|
||||
# in the parallel jobs above, not here.
|
||||
#
|
||||
# GOVERNANCE_BASE points the changed-file gate at the right diff:
|
||||
# PR -> the PR target branch (origin/<base_ref>)
|
||||
|
|
@ -86,29 +142,29 @@ jobs:
|
|||
tar -xzf actionlint.tar.gz actionlint
|
||||
sudo mv actionlint /usr/local/bin/actionlint
|
||||
- run: npm ci
|
||||
- run: npm run verify
|
||||
- run: npm run governance
|
||||
env:
|
||||
GOVERNANCE_BASE: ${{ steps.governance-ref.outputs.base }}
|
||||
|
||||
visual-regression:
|
||||
name: Visual regression
|
||||
ci-complete:
|
||||
name: ci-complete
|
||||
if: always()
|
||||
needs: [static, build, unit, visual, governance]
|
||||
runs-on: ubuntu-latest
|
||||
container: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:e2e:visual
|
||||
- name: Upload visual diff artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: visual-regression-diffs
|
||||
path: |
|
||||
test-results/visual
|
||||
playwright-report-visual
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
- name: All required jobs passed
|
||||
shell: bash
|
||||
env:
|
||||
RESULTS: ${{ join(needs.*.result, ' ') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
failed=0
|
||||
for result in ${RESULTS}; do
|
||||
if [[ "${result}" != "success" ]]; then
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
if [[ "${failed}" -ne 0 ]]; then
|
||||
echo "Required jobs did not all succeed: ${RESULTS}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
1
.github/workflows/deploy-web.yaml
vendored
1
.github/workflows/deploy-web.yaml
vendored
|
|
@ -28,7 +28,6 @@ on:
|
|||
- "docs/**"
|
||||
- "**/*.md"
|
||||
- ".github/workflows/ci.yaml"
|
||||
- ".github/workflows/ci-terraform.yaml"
|
||||
- ".github/workflows/deploy-web.yaml"
|
||||
release:
|
||||
types: [published]
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ This chains the full set: Prettier check, ESLint (`--max-warnings=0`), TypeScrip
|
|||
build (`tsc -b && vite build`), unit tests (`vitest run`), and the governance
|
||||
checks (`npm run governance`), including G13 app/Terraform isolation. **Do not
|
||||
claim a task is done until `npm run verify` is green locally.** CI runs the same
|
||||
`npm run verify` in a repo-owned `governance` job, so a green local run mirrors
|
||||
CI.
|
||||
gates as parallel jobs in [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)
|
||||
(`static`, `build`, `unit`, `visual`, `governance`) with `ci-complete` failing if
|
||||
any of those jobs did not succeed.
|
||||
|
||||
## Non-negotiable rules (enforced; do not work around)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ isolation). A task is not done until this is green.
|
|||
| HCP run guard | `npm run test:hcp-run-guard` → `scripts/test-hcp-run-guard.py` | `governance` + CI | Workspace invariants + apply reconcile |
|
||||
| CloudFront release verify | `npm run test:cloudfront-release-verify` → `scripts/test-verify-cloudfront-release.sh` | `governance` + CI | Stubbed aws/curl |
|
||||
| GitHub workflow shell | `npm run test:github-workflows` → `scripts/check-github-workflows.sh` | `governance` + CI | `bash -n` + actionlint |
|
||||
| G13 App/Terraform isolation | `python3 scripts/check_app_terraform_isolation.py` vs `GOVERNANCE_BASE` | `governance` + CI | Deployable app files vs `terraform/` |
|
||||
| G13 App/Terraform isolation | `python3 scripts/check_app_terraform_isolation.py` vs merge-base of `GOVERNANCE_BASE` | `governance` + CI | Deployable app files vs `terraform/` (live classifier: PR + local) |
|
||||
|
||||
## No-false-pass guarantees
|
||||
|
||||
|
|
@ -60,15 +60,15 @@ isolation). A task is not done until this is green.
|
|||
|
||||
- **Locally:** `npm run verify`. `lint-staged` (via Husky) re-runs ESLint +
|
||||
Prettier on staged files at commit; commitlint enforces Conventional Commits.
|
||||
- **CI ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)):** the org
|
||||
reusable workflow (`ci-typescript-frontend.yaml`, Node 24) runs
|
||||
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.
|
||||
- **Terraform CI ([`.github/workflows/ci-terraform.yaml`](.github/workflows/ci-terraform.yaml)):**
|
||||
fmt, `init -backend=false`, validate, import-plan unit tests, and G13
|
||||
classifier unit tests on `terraform/**` changes for PRs to `main` or `dev`.
|
||||
- **CI ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)):** this
|
||||
repository owns every job. `static` (`format:check` + `lint`), `build`
|
||||
(`tsc -b && vite build`), `unit` (`vitest run` in two shards), `visual`
|
||||
(Playwright visual), and `governance` (`npm run governance`, with Terraform
|
||||
1.16.0) run in parallel. `ci-complete` fails unless all of those jobs
|
||||
succeeded and is the required merge-queue check. Live G13 runs on
|
||||
`pull_request` and locally; it skips `merge_group` and `push`. Terraform
|
||||
fmt/validate and the related unit tests run inside `governance` on every
|
||||
event.
|
||||
|
||||
## Toolchain pin
|
||||
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -131,8 +131,8 @@ commitlint enforces conventional commit messages. Run `npx tsc --noEmit` (or
|
|||
rejects anything else at commit time.
|
||||
- Open PRs against `main`. The PR body uses the three-section layout the
|
||||
template pre-fills: Summary, Changes and value, Ticket. `main` needs the
|
||||
`governance` and `Build and test / ci` checks and an approving review from a
|
||||
code owner (`@Sea-Haven-Industries/internal-dev`); new pushes dismiss stale
|
||||
`ci-complete` check and an approving review from a code owner
|
||||
(`@Sea-Haven-Industries/internal-dev`); new pushes dismiss stale
|
||||
approvals. PRs merge through the merge queue, so a branch does not need to
|
||||
be updated with `main` before it merges. Merged branches are deleted
|
||||
automatically.
|
||||
|
|
@ -148,19 +148,12 @@ commitlint enforces conventional commit messages. Run `npx tsc --noEmit` (or
|
|||
No stored AWS keys — OIDC only. Infrastructure and content deploy separately:
|
||||
|
||||
- **CI** ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)) — on push
|
||||
and PRs to `main`, calls
|
||||
`Sea-Haven-Industries/.github` → `ci-typescript-frontend.yaml` (Node 24):
|
||||
format check, lint, build, tests; **and** runs a repo-owned `governance` job
|
||||
that calls `npm run verify` so every gate (including the maintainability
|
||||
ratchets in [`scripts/governance-check.mjs`](scripts/governance-check.mjs)
|
||||
and the Terraform gates) is guaranteed from this repository. Conventions
|
||||
and gates are documented under
|
||||
and PRs to `main`, runs format, lint, build, sharded unit tests, visual
|
||||
regression, and `npm run governance` as parallel jobs, then `ci-complete`.
|
||||
Conventions and gates are documented under
|
||||
[`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 CI**
|
||||
([`.github/workflows/ci-terraform.yaml`](.github/workflows/ci-terraform.yaml))
|
||||
— fmt, `init -backend=false`, validate, and import-plan tests.
|
||||
- **SPA content** ([`.github/workflows/deploy-web.yaml`](.github/workflows/deploy-web.yaml))
|
||||
— push to `main` deploys `dev`; a published `vX.Y.Z-staging` release deploys
|
||||
`staging`. Syncs `dist/` to the bucket root and invalidates `/*`.
|
||||
|
|
|
|||
|
|
@ -218,13 +218,23 @@ function runRepositoryGate(label, script) {
|
|||
return { label, status: result.status, error: result.error };
|
||||
}
|
||||
|
||||
function shouldRunLiveIsolation() {
|
||||
const eventName = process.env.GITHUB_EVENT_NAME;
|
||||
return !eventName || eventName === "pull_request";
|
||||
}
|
||||
|
||||
function runIsolationGate(baseRef) {
|
||||
const files = gitLines(["diff", "--name-only", "--diff-filter=ACMR", baseRef, "HEAD"]);
|
||||
return spawnSync("python3", ["scripts/check_app_terraform_isolation.py"], {
|
||||
// Diff from the merge base, not the moving base tip. A two-dot diff against
|
||||
// a branch that has advanced reports everything the base gained after the
|
||||
// branch point as if this change reverted it.
|
||||
const mergeBase = gitText(["merge-base", baseRef, "HEAD"]);
|
||||
const files = gitLines(["diff", "--name-only", "--diff-filter=ACMR", mergeBase, "HEAD"]);
|
||||
const result = spawnSync("python3", ["scripts/check_app_terraform_isolation.py"], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
input: `${files.join("\n")}\n`,
|
||||
});
|
||||
return { ...result, mergeBase };
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
|
@ -326,22 +336,40 @@ function main() {
|
|||
}
|
||||
|
||||
console.log("─".repeat(64));
|
||||
console.log(`G13: application and Terraform isolation (${baseRef ?? "no base"}..HEAD)`);
|
||||
if (!baseRef) {
|
||||
if (!shouldRunLiveIsolation()) {
|
||||
console.log(
|
||||
`G13: skipped — live isolation is a pull-request property (event: ${process.env.GITHUB_EVENT_NAME})`,
|
||||
);
|
||||
} else if (!baseRef) {
|
||||
console.log("G13: application and Terraform isolation (no base...HEAD)");
|
||||
console.log(" FAIL (no valid base ref)");
|
||||
failures.push(
|
||||
"G13: base ref is required but was not found. Set GOVERNANCE_BASE to a valid commit or fetch origin/dev.",
|
||||
);
|
||||
} else {
|
||||
const isolation = runIsolationGate(baseRef);
|
||||
if (isolation.error) {
|
||||
console.log(" FAIL (could not start)");
|
||||
failures.push(`G13: could not start: ${isolation.error.message}`);
|
||||
} else {
|
||||
const output = `${isolation.stdout ?? ""}${isolation.stderr ?? ""}`.trim();
|
||||
if (output) console.log(` ${output.replaceAll("\n", "\n ")}`);
|
||||
if (isolation.status !== 0) {
|
||||
failures.push("G13: do not mix deployable application files with terraform/");
|
||||
let isolation;
|
||||
try {
|
||||
isolation = runIsolationGate(baseRef);
|
||||
} catch (error) {
|
||||
console.log(`G13: application and Terraform isolation (${baseRef}...HEAD)`);
|
||||
console.log(" FAIL (could not resolve merge base)");
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
failures.push(`G13: could not resolve merge base against HEAD: ${message}`);
|
||||
}
|
||||
if (isolation) {
|
||||
const mergeBase = isolation.mergeBase ?? "unresolvable";
|
||||
console.log(
|
||||
`G13: application and Terraform isolation (${baseRef}...HEAD, merge base ${mergeBase.slice(0, 7)})`,
|
||||
);
|
||||
if (isolation.error) {
|
||||
console.log(" FAIL (could not start)");
|
||||
failures.push(`G13: could not start: ${isolation.error.message}`);
|
||||
} else {
|
||||
const output = `${isolation.stdout ?? ""}${isolation.stderr ?? ""}`.trim();
|
||||
if (output) console.log(` ${output.replaceAll("\n", "\n ")}`);
|
||||
if (isolation.status !== 0) {
|
||||
failures.push("G13: do not mix deployable application files with terraform/");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ bash scripts/test-verify-cloudfront-release.sh
|
|||
|
||||
PRs cannot mix `terraform/` with deployable application files. Workflow, docs,
|
||||
and gate-script changes may travel with either side. G13 is
|
||||
`python3 scripts/check_app_terraform_isolation.py` against the PR base.
|
||||
`python3 scripts/check_app_terraform_isolation.py` against the merge base of
|
||||
the PR.
|
||||
|
||||
`npm run test:terraform` and `npm run verify` wrap the same gates. They never
|
||||
create an HCP run or touch AWS.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue