From 2e356920c7d4fd43044dbf27b594935e4bdcddbe Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 24 Jun 2026 16:42:54 -0400 Subject: [PATCH] Add reusable CI workflow for TypeScript front-end apps Adds ci-typescript-frontend.yaml, a workflow_call reusable CI for bundled TypeScript SPAs (Vite / React / Vue with vitest + Playwright). Existing reusable CIs do not fit this shape: ci-static is for plain HTML sites and ci-typescript-cdk targets CDK infra repos. The workflow runs as a single `ci` job so callers emit the `ci / ci` status context the org branch-protection rulesets require. Steps: a Sea Haven standards gate (required npm scripts present, plus a changed-line guard for AI-tool footers, hook bypasses, and hardcoded secrets), then format:check, lint, build, unit tests, and an optional Playwright browser smoke. Every step past the standards gate is individually toggleable, and string inputs are passed through env to avoid expression injection. Documents the workflow in the README reusable-workflows list. --- .github/workflows/ci-typescript-frontend.yaml | 203 ++++++++++++++++++ README.md | 2 + 2 files changed, 205 insertions(+) create mode 100644 .github/workflows/ci-typescript-frontend.yaml diff --git a/.github/workflows/ci-typescript-frontend.yaml b/.github/workflows/ci-typescript-frontend.yaml new file mode 100644 index 0000000..a5031d4 --- /dev/null +++ b/.github/workflows/ci-typescript-frontend.yaml @@ -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." diff --git a/README.md b/README.md index 13331c8..096beb9 100644 --- a/README.md +++ b/README.md @@ -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.