Merge pull request #67 from Sea-Haven-Industries/infra/ci-typescript-frontend-reusable
Some checks are pending
ci / ci / ci (push) Waiting to run

Add reusable CI workflow for TypeScript front-end apps
This commit is contained in:
Adam Moussa 2026-06-24 16:45:50 -04:00 • committed by GitHub
commit 09fa684b37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 205 additions and 0 deletions

View file

@ -0,0 +1,203 @@
name: CI — TypeScript Frontend
# Reusable CI for bundled TypeScript front-end apps (Vite / React / Vue SPAs
# with vitest + Playwright). Emits the single `ci / ci` status context required
# by the org branch-protection rulesets — keep the caller job id `ci` so the
# context resolves to `ci / ci`.
#
# Runs, in order: a Sea Haven standards gate (required npm scripts present, no
# AI-tool footers / hook bypasses / hardcoded secrets in the added lines),
# then format:check, lint, build, unit tests, and an optional Playwright
# browser smoke. Every step past the standards gate is individually toggleable.
#
# Caller example:
# jobs:
# ci:
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@main
# with:
# node-version: "24"
on:
workflow_call:
inputs:
node-version:
description: "Node.js version to use"
type: string
default: "24"
working-directory:
description: "Directory to run npm/build/test commands from"
type: string
default: "."
cache-dependency-path:
description: "Path to package-lock.json for npm cache"
type: string
default: "package-lock.json"
required-scripts:
description: "Comma-separated npm scripts that must exist in package.json"
type: string
default: "format:check,lint,build,test,test:e2e"
run-standards:
description: "Verify required npm scripts exist"
type: boolean
default: true
run-guard:
description: "Guard added lines against AI-tool footers, hook bypasses, and hardcoded secrets"
type: boolean
default: true
run-format-check:
description: "Run the format:check script (Prettier)"
type: boolean
default: true
run-lint:
description: "Run the lint script (ESLint)"
type: boolean
default: true
run-build:
description: "Run the build script"
type: boolean
default: true
run-tests:
description: "Run the test script (vitest / unit tests)"
type: boolean
default: true
run-e2e:
description: "Run the test:e2e script (Playwright browser smoke)"
type: boolean
default: true
e2e-browser:
description: "Playwright browser to install for the e2e smoke"
type: string
default: "chromium"
run-conventions-check:
description: "Require README.md and a .gitignore that covers .env"
type: boolean
default: true
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: ci-typescript-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}
cancel-in-progress: true
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: npm
cache-dependency-path: ${{ inputs.cache-dependency-path }}
- name: Verify required npm scripts
if: ${{ inputs.run-standards }}
env:
REQUIRED_SCRIPTS: ${{ inputs.required-scripts }}
run: |
node <<'NODE'
const { readFileSync } = require("node:fs");
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
const required = (process.env.REQUIRED_SCRIPTS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const missing = required.filter((script) => !pkg.scripts?.[script]);
if (missing.length > 0) {
console.error(`Missing required scripts: ${missing.join(", ")}`);
process.exit(1);
}
console.log(`All required scripts present: ${required.join(", ")}`);
NODE
- name: Guard changed lines
if: ${{ inputs.run-guard }}
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE: ${{ github.event.before }}
run: |
set -euo pipefail
if [ "${EVENT_NAME}" = "pull_request" ]; then
BASE_REF="${PR_BASE_SHA}"
else
BASE_REF="${PUSH_BEFORE}"
fi
if [ -z "${BASE_REF}" ] || [ "${BASE_REF}" = "0000000000000000000000000000000000000000" ]; then
BASE_REF="$(git rev-parse HEAD~1 2>/dev/null || true)"
fi
if [ -z "${BASE_REF}" ]; then
echo "No base ref available; skipping changed-line guard."
exit 0
fi
ADDED_LINES="$(git diff --unified=0 "${BASE_REF}" "${GITHUB_SHA}" | grep -E '^\+' | grep -vE '^\+\+\+' || true)"
if printf '%s\n' "${ADDED_LINES}" | grep -E 'Generated with (Claude [C]ode|[C]odex|Chat[G]PT)|--no-[v]erify|HUSKY[=]0'; then
echo "Found generated-tool footer or hook bypass wording in added lines."
exit 1
fi
if printf '%s\n' "${ADDED_LINES}" | grep -E 'A[K]IA[0-9A-Z]{16}|A[S]IA[0-9A-Z]{16}|AWS[_]SECRET[_]ACCESS[_]KEY|aws[_-]?secret[_-]?access[_-]?key|J[W]TAuthenticationHIGHsecuredPassword|P[a]ssword=|S[e]ndGrid.*A[p]iKey'; then
echo "Found a likely secret in added lines. Move sensitive values to the environment or secret manager."
exit 1
fi
echo "Changed-line guard passed."
- name: Install dependencies
run: npm ci
- name: Format check
if: ${{ inputs.run-format-check }}
run: npm run format:check
- name: Lint
if: ${{ inputs.run-lint }}
run: npm run lint
- name: Build
if: ${{ inputs.run-build }}
run: npm run build
- name: Unit tests
if: ${{ inputs.run-tests }}
run: npm test
- name: Browser smoke
if: ${{ inputs.run-e2e }}
env:
CI: "true"
E2E_BROWSER: ${{ inputs.e2e-browser }}
run: |
npx playwright install --with-deps "${E2E_BROWSER}"
npm run test:e2e
- name: Conventions check
if: ${{ inputs.run-conventions-check }}
working-directory: ${{ github.workspace }}
run: |
errors=0
fail() { echo "::error::$1"; errors=$((errors + 1)); }
[[ -f README.md ]] || fail "Missing README.md"
if [[ -f .gitignore ]]; then
grep -qE '^\.env$|^\.env\b' .gitignore || fail ".gitignore does not include .env"
else
fail "Missing .gitignore"
fi
if [[ $errors -gt 0 ]]; then
echo "Conventions check failed with $errors error(s)."
exit 1
fi
echo "Conventions check passed."

View file

@ -10,6 +10,8 @@ Organization-level GitHub configuration for Sea Haven Industries.
**`.github/workflows/ci-typescript-cdk.yaml`** — Reusable CI workflow for TypeScript / CDK repos. Runs `npm ci` + optional `tsc --noEmit`, optional ESLint, optional Jest, and optional `cdk synth`. Also supports Node.js SAM repos via an optional `sam validate` step.
**`.github/workflows/ci-typescript-frontend.yaml`** — Reusable CI workflow for bundled TypeScript front-end apps (Vite / React / Vue SPAs). Runs a Sea Haven standards gate (required npm scripts present, no AI-tool footers / hook bypasses / hardcoded secrets in added lines), then `format:check`, `lint`, `build`, vitest unit tests, and an optional Playwright browser smoke. Emits the single `ci / ci` status context — keep the caller job id `ci`.
**`.github/workflows/cd-sam.yaml`** — Reusable CD workflow for SAM repos. Runs `sam build` + `sam deploy` with OIDC credentials and a CloudFormation execution role. Triggers via `workflow_call` from per-repo `deploy.yaml` on push to `main`.
**`.github/workflows/cd-cdk.yaml`** — Reusable CD workflow for CDK repos (TypeScript and Python). Runs `cdk deploy --all` with OIDC credentials. Supports optional Python setup for Python CDK repos and QEMU emulation for cross-platform Docker builds.