← Files okrdevARCHIVED FILE

templates/github/workflows/ci.yml

4.65 KB · Oct 4, 2026 · 12:30 UTC

↓ Download file

# ci — lint, typecheck, unit tests, build (okrdev stack module)
#
# Installs to: .github/workflows/ci.yml
#
# This file is part of the OPTIONAL stack module (Next.js + Vercel + Neon + Drizzle;
# see docs/stack.md). The okrdev method needs none of it — if your repo already has
# CI, keep yours. okr-gate.yml works alongside any CI.
#
# Scope: exactly the checks that should block a merge — lint, typecheck, Vitest,
# build. Playwright smoke tests are deliberately NOT here: they run against the
# Vercel preview URL after it deploys, and Drizzle migrations run against the
# preview's Neon branch in that same flow (see templates/stack/README.md).
#
# okrdev short-circuit: weekly okrdev state PRs (check-ins, triage ledgers)
# touch only okrdev/**, and burning a full CI run on them would be pure tax.
# The first step in the job detects an okrdev-only diff and lets every later
# step skip while "ci" still reports green. Read that step's comment before
# "simplifying" this with paths-ignore — you'd brick the required check.
#
# Expects these package.json scripts (templates/stack/README.md sets them up):
#   lint, typecheck, test (as `vitest run`, not watch mode), build
#
# The job id is "ci" on purpose: templates/stack/branch-protection.sh registers
# "ci" as the required status check on main. Rename both together or neither.

name: ci

on:
  pull_request:
  push:
    branches: [main]
    # Why run on main too? Deploy = merge, so main must always be green. This run
    # is the tripwire that says so — or says exactly when it stopped being true.

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true # a new push obsoletes the old run; don't pay for both

permissions:
  contents: read

jobs:
  ci:
    name: ci
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2 # the okrdev-only step below diffs HEAD against its parent

      - name: okrdev-only short-circuit
        id: okrdev-only
        # okrdev state PRs change only okrdev/** — no code, so lint/typecheck/
        # tests/build have nothing to say. Detect that here and let the steps
        # below skip; the job still finishes and "ci" still reports green.
        #
        # NEVER use workflow-level `paths-ignore` on a required check instead:
        # a skipped workflow never reports its status check, GitHub waits on
        # "Expected" forever, and the PR is unmergeable. The short-circuit must
        # live INSIDE the required job — exactly like this step.
        run: |
          # On pull_request, HEAD is the PR merge commit, so HEAD^1 is the base
          # tip and the diff is exactly what the PR changes. On push it's the
          # pushed commit against its parent. Can't compute the diff (first
          # commit, force push)? Fail open: run the full job.
          if changed=$(git diff --name-only HEAD^1 HEAD 2>/dev/null) &&
             [ -n "$changed" ] &&
             ! grep -qv '^okrdev/' <<<"$changed"; then
            echo "okrdev/** only — skipping lint, typecheck, tests, build."
            echo "skip=true" >>"$GITHUB_OUTPUT"
          else
            echo "skip=false" >>"$GITHUB_OUTPUT"
          fi

      - uses: pnpm/action-setup@v4
        if: steps.okrdev-only.outputs.skip != 'true'
        # Reads the pnpm version from the "packageManager" field in package.json.
        # create-next-app sets it when you pick pnpm; if it's missing, add e.g.
        #   "packageManager": "pnpm@9.15.0"

      - uses: actions/setup-node@v4
        if: steps.okrdev-only.outputs.skip != 'true'
        with:
          node-version: 24 # match your local Node major
          cache: pnpm

      - name: Install dependencies
        if: steps.okrdev-only.outputs.skip != 'true'
        run: pnpm install --frozen-lockfile
        # --frozen-lockfile: CI installs exactly what the lockfile says or fails
        # loudly. A drifted lockfile should break here, not in production.

      - name: Lint
        if: steps.okrdev-only.outputs.skip != 'true'
        run: pnpm lint

      - name: Typecheck
        if: steps.okrdev-only.outputs.skip != 'true'
        run: pnpm typecheck

      - name: Unit tests (Vitest)
        if: steps.okrdev-only.outputs.skip != 'true'
        run: pnpm test

      - name: Build
        if: steps.okrdev-only.outputs.skip != 'true'
        run: pnpm build
        # The build should not need real secrets — previews and production get
        # theirs from Vercel. If Next.js insists on an env var at build time, give
        # it a harmless placeholder here rather than a secret:
        # env:
        #   DATABASE_URL: postgres://user:placeholder@localhost:5432/placeholder

SHA-256: e5eab608aba46cbd41fd05cfdedad05eb1ab4aa222cc2e7377a46b53562dcb92