Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"Bash(wget:*)",
"Bash(nc:*)",
"Bash(ssh:*)",
"Bash(git commit --no-verify:*)",
"Bash(git commit -n:*)",
"Bash(git push --no-verify:*)",
"Read(**/.env*)",
"Read(**/secrets/**)"
],
Expand Down
17 changes: 17 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh
# Fast checks before every commit; the heavier ones run in pre-push, and CI runs
# all of them again. Enabled by `pnpm install` (the root `prepare` script) or
# `git config core.hooksPath .githooks`. Never bypass with --no-verify: fix the
# failure instead.
#
# format:check and lint read the working tree, not the staged snapshot, so
# unstaged edits can make them pass or fail. Stage everything you mean to commit.
set -eu

pnpm format:check
pnpm lint

# Scan only what is staged. This fails closed: when neither gitleaks nor Docker
# is available, secrets:scan exits 2 and the commit is refused, since the point
# of this hook is to stop a secret before it reaches the remote.
pnpm secrets:scan --staged
9 changes: 9 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh
# Slower checks before every push; pre-commit already ran format, lint and the
# staged secrets scan. Turbo's cache replays unchanged packages. Build is left
# to CI. Never bypass with --no-verify: fix the failure instead.
set -eu

pnpm typecheck
pnpm test
pnpm knip
17 changes: 17 additions & 0 deletions .github/scripts/secrets-scan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
"$repo_root"/*) ;;
*) set -- -v "$git_common_dir:$git_common_dir:ro" "$@" ;;
esac
# Inside a commit hook, `git commit -a` and `git commit <path>` stage the
# commit in a temporary index named by GIT_INDEX_FILE. A local gitleaks
# inherits it but the container does not, so forward it as an absolute path,
# or `--staged` scans the ordinary index instead. It normally lives in the git
# dir, which the mounts above cover; mount its directory when it does not.
if [ -n "${GIT_INDEX_FILE:-}" ]; then
case "$GIT_INDEX_FILE" in
/*) index_file=$GIT_INDEX_FILE ;;
*) index_file=$PWD/$GIT_INDEX_FILE ;;
esac
index_dir=$(dirname "$index_file")
case "$index_dir" in
"$repo_root" | "$repo_root"/* | "$git_common_dir" | "$git_common_dir"/*) ;;
*) set -- -v "$index_dir:$index_dir:ro" "$@" ;;
esac
set -- -e "GIT_INDEX_FILE=$index_file" "$@"
fi
exec docker run --rm "$@"
fi

Expand Down
77 changes: 17 additions & 60 deletions .github/workflows/ci.yml → .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
name: CI
name: Checks

# Every check, defined once. `feature.yml` (PRs) and `main.yml` (push to main)
# call it; a called workflow cannot raise the caller's token, so each caller job
# grants the permissions the jobs below ask for.
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_call:

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# PR runs are superseded by a newer push. A *running* main run (which may later
# deploy) is never cancelled; a *pending* one is still replaced by a newer push.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
check:
name: ${{ matrix.task }}
Expand All @@ -32,8 +26,10 @@ jobs:
persist-credentials: false
fetch-depth: 1

# zizmor prefers the `$/` self-repository syntax, but actionlint 1.7.12
# rejects it; switch once actionlint accepts `$/`.
- name: Setup
uses: ./.github/actions/setup
uses: ./.github/actions/setup # zizmor: ignore[self-repository]

# Turbo's local cache, restored from the latest run of the same task so an
# unchanged package replays instead of rerunning. Only typecheck, test and
Expand Down Expand Up @@ -81,17 +77,16 @@ jobs:
strategy:
fail-fast: false
matrix:
# `actions` scans the workflow files themselves (injection, unpinned
# third-party actions, over-broad permissions).
language: [javascript-typescript, actions]
# The workflow files are checked by the `workflow-lint` job instead.
language: [javascript-typescript]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 1

# Advanced setup: turn OFF "default setup" under Settings → Code security,
# or GitHub rejects these uploads. Neither language needs a build step.
# or GitHub rejects these uploads. No build step is needed.
- name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
Expand All @@ -104,47 +99,9 @@ jobs:
with:
category: "/language:${{ matrix.language }}"

# The one check to require in branch protection. `if: always()` makes it run
# even when a needed job failed, so it reports a failure instead of skipping.
all-passed:
name: all-passed
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [check, secrets-scan, codeql]
permissions: {}
steps:
- name: Check results
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
for r in $RESULTS; do
[ "$r" = success ] || { echo "a required job ended with: $r"; exit 1; }
done

# Example deploy job. The `if:` only skips PR runs; the real security boundary
# is the `production` environment's deployment-branch rule (main only), set
# under Settings → Environments. Hand build output over from `build` with
# actions/upload-artifact there and actions/download-artifact here.
#
# deploy:
# name: deploy
# runs-on: ubuntu-latest
# timeout-minutes: 10
# needs: all-passed
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# environment: production
# permissions:
# contents: read
# id-token: write
# steps:
# - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# with:
# persist-credentials: false
# fetch-depth: 1
#
# - name: Setup
# uses: ./.github/actions/setup
#
# - name: Deploy
# run: echo "deploy here"
# actionlint (with shellcheck on `run:` scripts) and zizmor over the workflow
# and action files.
workflow-lint:
permissions:
contents: read
uses: valuecodes/automation/.github/workflows/workflow-lint.yml@c207ce5866c0f7893ecc481f596edd7fe7cfae62 # v0.4.0
36 changes: 36 additions & 0 deletions .github/workflows/feature.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Feature branch CI

on:
pull_request:
branches: [main]

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# A newer push to the PR supersedes the running checks.
cancel-in-progress: true

jobs:
checks:
permissions:
actions: read
contents: read
security-events: write
uses: ./.github/workflows/checks.yml # zizmor: ignore[self-repository]

# The one check to require in branch protection. `if: always()` makes it run
# even when a check failed, so it reports a failure instead of skipping.
all-passed:
name: all-passed
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [checks]
permissions: {}
steps:
- name: Check results
env:
RESULT: ${{ needs.checks.result }}
run: |
[ "$RESULT" = success ] || { echo "checks ended with: $RESULT"; exit 1; }
45 changes: 45 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Main branch CI

on:
push:
branches: [main]

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# A *running* main run (which may be deploying) is never cancelled; a
# *pending* one is still replaced by a newer push.
cancel-in-progress: false

jobs:
checks:
permissions:
actions: read
contents: read
security-events: write
uses: ./.github/workflows/checks.yml # zizmor: ignore[self-repository]

# Example deploy job, run once every check passed. The real security boundary
# is the `production` environment's deployment-branch rule (main only), set
# under Settings → Environments.
#
# deploy:
# name: deploy
# runs-on: ubuntu-latest
# timeout-minutes: 10
# needs: [checks]
# environment: production
# permissions:
# contents: read
# steps:
# - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# with:
# persist-credentials: false
# fetch-depth: 1
#
# - name: Setup
# uses: ./.github/actions/setup # zizmor: ignore[self-repository]
#
# - name: Deploy
# run: echo "deploy here"
5 changes: 2 additions & 3 deletions .github/workflows/maintenance-dashboard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ name: Maintenance dashboard

on:
schedule:
# Daily, after the Node.js update run. An off-peak minute avoids the
# top-of-the-hour queue.
# Daily. An off-peak minute avoids the top-of-the-hour queue.
- cron: "41 5 * * *"
workflow_dispatch:

Expand All @@ -17,4 +16,4 @@ jobs:
permissions:
contents: read
issues: write
uses: valuecodes/automation/.github/workflows/maintenance-dashboard.yml@53132eebb05a206f7ecf60ab0add5dcf5470f4b6 # v0.2.2
uses: valuecodes/automation/.github/workflows/maintenance-dashboard.yml@c207ce5866c0f7893ecc481f596edd7fe7cfae62 # v0.4.0
19 changes: 0 additions & 19 deletions .github/workflows/node-update.yml

This file was deleted.

50 changes: 30 additions & 20 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,47 +66,57 @@ To add a package, copy an existing one's `package.json`, `tsconfig.json` and
`src/<name>.ts` directly through `exports` (no barrel `index.ts`), run
`pnpm install` and add its row to the Packages table above.

There is no post-edit formatting hook: run `pnpm format` yourself before committing.
Nothing formats on edit, and the pre-commit hook only checks formatting: run
`pnpm format` yourself before committing.

Git hooks live in `.githooks/` and are plain shell scripts, with no hook manager.
`pnpm install` enables them through the root `prepare` script. The setting goes in
the clone's shared git config, so it covers every worktree, and a branch without
`.githooks/` simply runs no hooks. If you cloned before the hooks existed and have
not run `pnpm install` since, run `git config core.hooksPath .githooks` once. `pre-commit` runs `format:check`, `lint` and `secrets:scan --staged`. The
secrets scan fails closed, so the commit is refused when neither gitleaks nor
Docker is available. `pre-push` runs `typecheck`, `test` and `knip`. CI still
runs every check. `format:check` and `lint` read the working tree, not the staged
snapshot, so stage everything you mean to commit.

`secrets:scan` runs gitleaks (`.github/scripts/secrets-scan.sh`, version pinned
there) using a local `gitleaks` v8.19+ if one is on PATH, otherwise the pinned Docker image.
It exits 0 when clean and 1 when it finds a leak.

CI (`.github/workflows/ci.yml`) runs typecheck, lint, knip, format:check, test, build,
secrets-scan and CodeQL code scanning (`javascript-typescript` and `actions`) on PRs and
on push to `main`. `all-passed` fans those in and is the one check to require in branch
protection. The typecheck, test and build jobs restore Turbo's `.turbo` cache from the
previous run of the same task, so unchanged packages replay instead of rerunning.
CI checks are defined once in the reusable `.github/workflows/checks.yml`: typecheck,
lint, knip, format:check, test, build, secrets-scan, CodeQL code scanning
(`javascript-typescript`) and workflow-lint. `feature.yml` calls it on PRs and
`main.yml` on push to `main` (deploy jobs go there). workflow-lint calls the reusable
workflow from `valuecodes/automation`: actionlint (with shellcheck) and zizmor over the
workflow and action files. Each tool reads `.github/actionlint.yaml` / `zizmor.yml` if
added. `all-passed` in `feature.yml` fans the checks in and is the one check to require
in branch protection. The typecheck, test and build
jobs restore Turbo's `.turbo` cache from the previous run of the same task, so
unchanged packages replay instead of rerunning.

Dependabot (`.github/dependabot.yml`) bumps the SHA-pinned actions and reusable workflows
monthly in one grouped PR; npm dependencies are updated by hand.

`node-update.yml` calls the reusable workflow from `valuecodes/automation` (pinned to a
commit SHA) daily: when a newer Node.js release is out on the `.nvmrc` major, it opens a
`chore: bump Node.js to …` PR. Security releases open one right away; others wait 14
days. Its PRs start CI only after someone clicks **Approve workflows to run** (if no
runs show up, close and reopen the PR). Keep the exact Node version only in `.nvmrc`,
so that PR is the whole bump; the Cloudflare `NODE_VERSION` build variable of the
`apps/web` deploy is the one pin to update by hand, before the PR merges. A newer
release closes the older PR and deletes its branch, so push any follow-up commits just
before merging.

`maintenance-dashboard.yml` calls the maintenance dashboard workflow from
`valuecodes/automation` daily.
It keeps one issue, **Maintenance dashboard** (label `maintenance-dashboard`), listing
what is due: `catalog:` updates, pnpm (`packageManager`), gitleaks (`GITLEAKS_VERSION`
in the secrets-scan script), Node.js (`.nvmrc`) and `pnpm audit` findings. Like
`minimumReleaseAge`, a release shows up only once it is 14 days old. It never opens
PRs or commits; doing the updates stays manual. If `GITLEAKS_VERSION` moves out of
`.github/scripts/secrets-scan.sh`, pass the new path as the `gitleaks-script` input.
Both callers pin the same `valuecodes/automation` commit, so bump them together.
`minimumReleaseAge`, a release shows up only once it is 14 days old; a Node.js security
release shows up right away and is flagged. It never opens PRs or commits; doing the
updates stays manual. For a Node.js bump, change only `.nvmrc` (the one place that
holds the exact version) and the Cloudflare `NODE_VERSION` build variable of the
`apps/web` deploy. If `GITLEAKS_VERSION` moves out of `.github/scripts/secrets-scan.sh`,
pass the new path as the `gitleaks-script` input. The dashboard and workflow-lint
callers pin the same `valuecodes/automation` commit, so bump them together.

---

## Rules

- Keep diffs tight and focused; no drive-by refactors or new tooling without discussion.
- Never commit secrets, credentials, or `.env` files. All code must be public-safe.
- Never bypass the git hooks with `--no-verify`; fix the failure instead.
- Add dependencies to the correct workspace with `pnpm --filter <package> add <dep>`.
Every external version lives in the `catalog:` block of `pnpm-workspace.yaml`
and manifests reference it as `catalog:`; `@repo/*` deps stay `workspace:*`.
Expand Down
Loading
Loading