monorepo-template / ops
louisbrulenaudet/monorepo-template/.cursor/rules/ops/ci.mdc
CI parity with pnpm ci, expression-context rules for env blocks, affected vs full graph by event, frozen lockfile, SHA pins, deny-by-default permissions
Cursor rule19 starsChanged 6 days ago
- Reads credentials
- Installs packages
What's in it
- Continuous Integration
- Expression contexts - read this before touching env:
- Repo invariants
---
description: "CI parity with pnpm ci, expression-context rules for env blocks, affected vs full graph by event, frozen lockfile, SHA pins, deny-by-default permissions"
alwaysApply: false
globs: .github/workflows/**,.github/actions/**,.github/CODEOWNERS
---
# Continuous Integration
The gate is [`.github/workflows/ci.yml`](../../../.github/workflows/ci.yml). Read it for the step list. It is **both** the PR check and a reusable workflow: `release.yml`'s `gate` job calls it so every push to `main` is validated before a tag is cut - see [`ops/release.mdc`](release.mdc). Deploy: [`ops/cd.mdc`](cd.mdc). Weakening a CI gate to force green is covered by [`guardrails.mdc`](../core/guardrails.mdc).
## Expression contexts - read this before touching `env:`
**Workflow-level `env:` may only reference `github`, `secrets`, `inputs`, `vars`.** Job-level `env:` adds `needs`, `strategy`, `matrix`. **`runner`, `job`, `steps`, and `env` are step-level only.** Referencing an unavailable context is a *validation* error: the file stops parsing, GitHub reports the run under the file path instead of the workflow name, and **zero jobs execute**. It does not degrade to an empty string.
This is not hypothetical. `d3e741b` added `NODE_COMPILE_CACHE: ${{ runner.temp }}/…` to workflow-level `env:` in `ci.yml` and `cd.yml`; both files were unparseable and **14 consecutive pushes ran no CI at all** before it was caught. Anything needing `$RUNNER_TEMP` goes through a step:
```yaml
- run: echo "NODE_COMPILE_CACHE=$RUNNER_TEMP/node-compile-cache" >> "$GITHUB_ENV"
```
## Repo invariants
- **Triggers:** `pull_request`, `workflow_call`, `workflow_dispatch`. **No `push:`** - pushes to `main` are covered by `release.yml`'s `gate`, so a commit is never checked twice.
- **Parity is about the *kinds* of checks, not the invocation mechanism.** Change both when adding or renaming a gate - the `deps:format:check` script exists because both call it by name. The mechanisms differ on purpose: locally `pnpm ci` is `pnpm boundaries`, then **one** `turbo run` of the `//#` root tasks plus `types:check check-types test build` with `--continue=dependencies-successful`, then `pnpm audit`; here the same checks are separate `parallel:` steps calling bare `pnpm run` scripts. The local `&&` chain is deliberately serial: `boundaries` is a fail-fast pre-gate (failures are rare and cheap to fix) and `audit` a success-gated network tail - a parallel-sections rewrite was evaluated and rejected to keep the gate zero-dependency. Do not route the workflow through the `//#` tasks - a root task's default inputs span the whole repo, so it would be permanently "affected" under `futureFlags.affectedUsingTaskInputs` and defeat the `--affected` step below. Scope differs by event on purpose: `--affected` on `pull_request` only, full graph on `workflow_call` / `workflow_dispatch` (a release must validate everything). Two `if:`-gated steps express this - never interpolate the flag into a `run:` body. The dependency audit is `pull_request`-only for the inverse reason: the advisory DB changes daily, so an auditing release gate is not a pure function of the commit; local `pnpm run ci` still audits.
- **No `TURBO_SCM_BASE` in CI.** `--affected` runs on `pull_request` only, where turbo infers the base branch from GitHub Actions and `futureFlags.githubActionsRemoteBaseRefFallback` resolves it to `origin/<base_ref>` (checkout creates only remote-tracking refs). Do not set it off PRs: `github.event.before` is all-zeros on a new branch, which silently degrades `--affected` to "everything changed". Keep `fetch-depth: 0` + `filter: blob:none`.
- **Parallelization:** one job, using the GitHub Actions `parallel:` step keyword (GA 2026-06-25, alongside `background` / `wait` / `wait-all` / `cancel`) for OXC (`lint:ci`, pinned to `--format=github` for PR annotations, and `format`), `boundaries`, `types:check`, `knip`, and `syncpack`. A `parallel:` list item takes **no sibling keys** - `parallel:` is its only property. A **single** `turbo run check-types test build --continue=dependencies-successful` invocation, not two turbo CLIs; turbo schedules against default concurrency (no override in `turbo.json`), and the `--continue` flag makes one CI run report every package's failures instead of stopping at the first. Wrangler deploy dry-run runs `--filter='./apps/*'` after the join, so every app is covered the moment it exists. It is redundant for `worker-api` (whose `build` script *is* `wrangler deploy --dry-run`) and `deploy` is `cache: false`, so that redundancy costs a few seconds - the price of not maintaining an app list here.
- **The release PR is skipped** via a job-level `if` bound to `head_ref == 'changeset-release/main'` **and** author `github-actions[bot]` - a prefix match would let any PR skip the required check by naming its branch. A `pull_request` `branches-ignore` filters the *base* branch and cannot do this. The release commit is validated by `gate` on the merge commit, so no branch-protection exemption is needed.
- **Step bodies longer than a few lines live as scripts, not inline YAML:** workflow-invoked ones under `.github/actions/<workflow>/` (`cd/`, `release/`), composite-action ones beside their `action.yml`. Invoke with `bash <path>` and wire inputs through step-level `env:`; each script opens with `set -euo pipefail` and fails fast on missing input (`:?` guards or an explicit check). Short steps stay inline.
- **Install:** `pnpm install --frozen-lockfile` after `pnpm/setup` with `install: false`. Node `runtime: node@24` matching root engines.
- **Remote cache:** job env wires `TURBO_TOKEN`, `TURBO_TEAM`, and `TURBO_REMOTE_CACHE_SIGNATURE_KEY` (repo secret, >= 32 bytes, same value as local machines - `turbo.json` enables `remoteCache.signature` + `longerSignatureKey`). `workflow_call` declares those two secrets **explicitly** so the release gate never receives Cloudflare credentials; never switch that call to `secrets: inherit`. Job env also sets `TURBO_CACHE`: `local:rw,remote:rw` on `pull_request`, `local:rw,remote:w` everywhere else. Signing checks integrity, not trust, and same-repo PR runs hold both the token and the key, so a PR could upload an entry that the release gate would otherwise replay as a passing `check-types`/`test`/`build`. Write-only makes the gate execute the full graph fresh and publish trusted artifacts. CD is covered separately, because it runs with no token at all.
- **Pins:** every `uses:` is a full-length commit SHA with a `# vX.Y.Z` comment. `actions/checkout` sets `persist-credentials: false`. Workflow `permissions: {}` with per-job re-grants - `contents: read` only, since artifact upload and the setup caches use the runner's runtime token, not `GITHUB_TOKEN`. Runners: `ubuntu-24.04` (not `ubuntu-latest`).
- **`cancel-in-progress` only on pull_request.** Never interpolate `github.ref_name` into script bodies. No production/Cloudflare secrets in this workflow (CD only).
- Do not add scanners "for completeness" without a concrete threat and owner. Telemetry stays off at workflow `env:`.
More agent context in louisbrulenaudet/monorepo-template
68 other files this repository gives its agents, the first 60 shown.
CLAUDE.md
Cursor rule
- .cursor/rules/backend/hono-gateway.mdc
- .cursor/rules/backend/ports.mdc
- .cursor/rules/backend/workers-cache.mdc
- .cursor/rules/backend/workers-config.mdc
- .cursor/rules/contracts/contracts.mdc
- .cursor/rules/contracts/type-inference.mdc
- .cursor/rules/core/boundaries.mdc
- .cursor/rules/core/guardrails.mdc
- .cursor/rules/core/turborepo.mdc
- .cursor/rules/core/worktrees.mdc
- .cursor/rules/frontend/frontend-architecture.mdc
- .cursor/rules/frontend/react-doctor.mdc
- .cursor/rules/frontend/react.mdc
- .cursor/rules/frontend/tailwind.mdc
- .cursor/rules/frontend/tanstack-query.mdc
- .cursor/rules/frontend/tanstack-router.mdc
- .cursor/rules/frontend/vite-config.mdc
- .cursor/rules/ops/cd.mdc
- .cursor/rules/ops/previews.mdc
- .cursor/rules/ops/release.mdc
- .cursor/rules/quality/code-style.mdc
- .cursor/rules/quality/comments.mdc
- .cursor/rules/quality/knip.mdc
- .cursor/rules/quality/markdown-style.mdc
- .cursor/rules/quality/naming.mdc
- .cursor/rules/quality/testing.mdc
- .cursor/rules/quality/typescript-config.mdc
- .cursor/rules/quality/vitest-config.mdc
- .cursor/rules/tests/front-react.mdc
- .cursor/rules/tests/hono-workers.mdc
- .cursor/rules/tests/vitest.mdc
Skill
- front-vitest.agents/skills/front-vitest/SKILL.md
- git-commit.agents/skills/git-commit/SKILL.md
- hono.agents/skills/hono/SKILL.md
- monorepo-agent-setup.agents/skills/monorepo-agent-setup/SKILL.md
- playwright-cli.agents/skills/playwright-cli/SKILL.md
- pnpm.agents/skills/pnpm/SKILL.md
- privileged-legal-data.agents/skills/privileged-legal-data/SKILL.md
- react-doctor.agents/skills/react-doctor/SKILL.md
- review-architecture.agents/skills/review-architecture/SKILL.md
- review-ci.agents/skills/review-ci/SKILL.md
- review-code-quality.agents/skills/review-code-quality/SKILL.md
- review-configuration.agents/skills/review-configuration/SKILL.md
- review-performance.agents/skills/review-performance/SKILL.md
- review-security.agents/skills/review-security/SKILL.md
- review-seo.agents/skills/review-seo/SKILL.md
- review.agents/skills/review/SKILL.md
- review-stack.agents/skills/review-stack/SKILL.md
- review-tests.agents/skills/review-tests/SKILL.md
- review-ui.agents/skills/review-ui/SKILL.md
- run-app.agents/skills/run-app/SKILL.md
- security-audit.agents/skills/security-audit/SKILL.md
- skills-update.agents/skills/skills-update/SKILL.md
- tanstack-config.agents/skills/tanstack-config/SKILL.md
- tanstack-devtools.agents/skills/tanstack-devtools/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

