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
4 changes: 2 additions & 2 deletions .devtools/check-branch-name.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# Usage: check-branch-name.sh [branch]
#
# The branch is the argument, then $BRANCH_NAME, then the current branch. The
# variable exists for the `conventional-branch` pre-commit hook: pre-commit cannot
# pass a per-run argument, and a guard checking `git switch -c <name>` has to
# variable exists for the `branch-name` lefthook job: a hook cannot be given a
# per-run argument, and a guard checking `git switch -c <name>` has to
# validate a branch that does not exist yet.
set -euo pipefail

Expand Down
61 changes: 0 additions & 61 deletions .devtools/check-conventional-commit.sh

This file was deleted.

2 changes: 1 addition & 1 deletion .devtools/check-pr-size.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Usage: check-pr-size.sh [--base <ref>] [--head <ref>] [--max <n>]
#
# Two callers, one script. ci-pr passes both ends of the pull request. The
# `pr-size` pre-commit hook passes nothing, so the base is worked out here:
# `pr-size` lefthook job passes nothing, so the base is worked out here:
# $PR_BASE, then the branch below this one in a gh stack, then the remote's
# default branch. A default branch that was worked out is read from origin, since
# the local copy only moves on a pull.
Expand Down
10 changes: 0 additions & 10 deletions .devtools/test-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,7 @@ expect pass "falls back to the current branch" "${branch}"
expect fail "reads BRANCH_NAME" env BRANCH_NAME=Bad_Name "${branch}"
expect pass "prefers the argument over BRANCH_NAME" env BRANCH_NAME=Bad_Name "${branch}" fix/login-timeout

echo "check-conventional-commit"
commit="${here}/check-conventional-commit.sh"
expect pass "accepts a scoped subject" "${commit}" 'feat(api): add cursor pagination'
expect pass "accepts a breaking change" "${commit}" 'fix!: drop support for Node 18'
expect pass "accepts a generated merge" "${commit}" "Merge branch 'main' into feat/x"
expect fail "rejects an unknown type" "${commit}" 'feature: add pagination'
expect fail "rejects a missing description" "${commit}" 'feat:'
printf 'fix(devtools): handle an empty diff\n\nA body may say anything at all.\n' >"${work}/MSG_OK"
printf 'handled an empty diff\n' >"${work}/MSG_BAD"
expect pass "reads a good message with --file" "${commit}" --file "${work}/MSG_OK"
expect fail "reads a bad message with --file" "${commit}" --file "${work}/MSG_BAD"

# The repo fixture has no .commitlintrc.yaml, so these run on config-conventional alone.
echo "pr-checks/lint.mjs"
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/ci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ permissions: {}

jobs:
lint:
# Both lint workflows name this job `hooks`, so the check a ruleset requires is
# `<caller job> / hooks` whichever one a repository calls, and moving from
# ci-pre-commit to ci-lint does not strand a pull request waiting on the old name.
# The rulesets require `lint / hooks`: the caller's job, then this name. Renaming
# it strands every open pull request waiting on a check that never reports.
name: hooks
runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout-minutes }}
Expand Down
137 changes: 0 additions & 137 deletions .github/workflows/ci-pre-commit.yml

This file was deleted.

40 changes: 0 additions & 40 deletions .pre-commit-hooks.yaml

This file was deleted.

3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ Tooling: mise pins every tool (`mise.toml`, `mise.lock`), lefthook runs the hook
`@commitlint/config-conventional` plus this repo's scopes (`.commitlintrc.yaml`). pnpm and `package.json` exist
only for semantic-release. This repo releases with semantic-release, not release-please like
the projects: it has no version file or changelog to maintain, and `release.yml` has to move the
floating `v1` tag. `.pre-commit-hooks.yaml` and `ci-pre-commit.yml` are deprecated and stay
until no consumer uses them.
floating `v1` tag.

## Non-negotiables

Expand Down
51 changes: 14 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@ everywhere at once.
| `.github/workflows/ci-go.yml` | Go modules, formatting, golangci-lint, race tests, coverage, govulncheck and build |
| `.github/workflows/ci-workflows.yml` | actionlint, zizmor and shellcheck over a repo's own workflows |
| `.github/workflows/ci-lint.yml` | A repo's own lefthook hooks, over the whole tree, with the tools mise pins |
| `.github/workflows/ci-pre-commit.yml` | Deprecated, use `ci-lint.yml`. A repo's own pre-commit hooks |
| `.github/workflows/ci-gitleaks.yml` | Secret scan of what a pull request adds |
| `.github/workflows/ci-template-drift.yml` | Compares a repo with the project template and tracks the difference in one issue |
| `.github/workflows/release-please.yml` | Grooms a release pull request from Conventional Commits, then tags and releases |
| `.github/workflows/publish-mod.yml` | Builds, attests and publishes a Minecraft mod to Modrinth, CurseForge and GitHub |

A repo with a `.pre-commit-config.yaml` should call `ci-pre-commit.yml` **instead of**
`ci-workflows.yml` and `ci-gitleaks.yml`, not alongside them. Those two download actionlint,
zizmor, shellcheck and gitleaks for repos that have no pre-commit config; a repo that has one
already pins the same four by frozen `rev:`, so calling both runs them twice at two sets of
version numbers that drift. One source of truth per repo.
A repo with a `lefthook.yml` should call `ci-lint.yml` **instead of** `ci-workflows.yml` and
`ci-gitleaks.yml`, not alongside them. Those two download actionlint, zizmor, shellcheck and
gitleaks for repos that have no hooks of their own; a repo that has them already pins the same
four in `mise.toml`, so calling both runs them twice at two sets of version numbers that drift.
One source of truth per repo.

Every `ci-*` file is `on: workflow_call` — library code, not something that runs on this repo's
own pull requests. The `self-*` files are this repo's thin callers of them, which is how the
Expand Down Expand Up @@ -55,18 +54,23 @@ tool with [mise](https://mise.jdx.dev/). A repository's own `lefthook.yml` holds

The org's own rules come from here. [`lefthook/org.yml`](lefthook/org.yml) checks the branch name
and the pull request size on push, with the scripts `ci-pr.yml` runs, so what passes locally
passes on the pull request. A repository pulls it in by tag:
passes on the pull request. A repository pulls it in at the floating `v1` tag, the one the
workflows are called at:

```yaml
# lefthook.yml
remotes:
- git_url: https://github.com/studiobimo/.github
ref: v1.9.0
ref: v1
refetch_frequency: 24h
configs:
- lefthook/org.yml
```

lefthook fetches a remote once per `ref`, so pin a release tag there, never the floating `v1`.
lefthook fetches a remote once per `ref` unless it is told to look again, so `refetch_frequency`
is what lets a moved `v1` reach the hooks: at most once a day. A fix here then lands in the hooks
and in CI together. `remotes` is only read from a repository's root `lefthook.yml`, not from a file
it `extends`.

Two environment variables make the checks usable from a script, such as an agent guard that has
to judge a command before it runs:
Expand All @@ -84,38 +88,11 @@ PR_BASE=main lefthook run pre-push --job pr-size
whose remote is gone and whose work is in: the tip is an ancestor of the default branch, or, for a
squash merge, `gh` reports a merged pull request with exactly that head. A branch checked out in
any worktree, or one it cannot show to be merged, is kept and named. Run it with `--dry-run` to
see what it would do. A repository gets the hook once it bumps `ref` and runs `lefthook install`.
see what it would do. A repository gets the hook the next time it runs `lefthook install`.

Commit messages are not part of `org.yml`: each repository runs commitlint against its own
`.commitlintrc.yaml` in a `commit-msg` job, and `ci-pr.yml` reads the same file.

## Pre-commit hooks (deprecated)

This repo is still a [pre-commit](https://pre-commit.com/) hook repo for the repositories that
have not moved to lefthook yet. It goes away when the last one has.

| Hook id | Stage | What it checks |
| --------------------- | -------------------- | -------------------------------------------------------------- |
| `conventional-commit` | `commit-msg` | The commit subject is a Conventional Commit |
| `conventional-branch` | `pre-push`, `manual` | The branch name follows Conventional Branch |
| `pr-size` | `pre-push`, `manual` | The branch changes at most 20 files against the layer below it |

```yaml
# .pre-commit-config.yaml
default_install_hook_types: [pre-commit, commit-msg, pre-push]
repos:
- repo: https://github.com/studiobimo/.github
rev: <full-sha> # frozen: v1.5.0
hooks:
- id: conventional-commit
- id: conventional-branch
- id: pr-size
```

Pin `rev:` to a release SHA with the version in a `frozen:` comment; Dependabot's `pre-commit`
ecosystem bumps both. The floating `v1` tag is for workflows only: pre-commit caches a hook repo by
`rev`, so a moving tag would never be re-fetched.

## Calling them

Call a workflow at `@v1`. The tag floats: a release moves it, so a fix lands everywhere at once.
Expand Down
Loading
Loading