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.