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
90 changes: 0 additions & 90 deletions .githooks/pre-push

This file was deleted.

60 changes: 60 additions & 0 deletions .github/workflows/hooks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Run Git Hooks

# Runs the pre-push hooks from lefthook.yml on the files this PR changes.
# A local hook can be skipped with --no-verify or never installed, so this is
# the check that cannot be bypassed. Same config as local, nothing to sync.

on:
pull_request:
branches:
- main
paths:
- "**.md"
- "docs/**"
- "lefthook.yml"
- "scripts/check-rules.mjs"
- "rules.config.yml"
- "package.json"
- "package-lock.json"
types:
- opened
- synchronize
- reopened
- ready_for_review

jobs:
hooks:
# Drafts are work in progress. The hooks start mattering when the PR is
# ready for review; the ready_for_review event runs them the moment it is.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# lefthook diffs against the base branch, so it needs its commits.
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

- name: Run the pre-push hooks
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
# lefthook reads the base branch from origin/HEAD, which checkout
# does not set, then diffs against the LOCAL branch of that name,
# which checkout does not create. The PR checkout is the merge of
# this PR into the base, so that diff is exactly this PR's changes.
git remote set-head origin "$BASE_REF"
git branch "$BASE_REF" "origin/$BASE_REF"
node_modules/.bin/lefthook run pre-push

- name: Audit the rules system
# lefthook skips every command when a PR only deletes files, and a
# deleted rule is what breaks dependencies and the index. This runs the
# cross-file checks on every PR, with no file list to get wrong.
run: node scripts/check-rules.mjs --rules
6 changes: 4 additions & 2 deletions .github/workflows/markdown_lint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Validate Markdown and PR naming
name: Validate PR naming

on:
pull_request:
Expand Down Expand Up @@ -30,6 +30,8 @@ jobs:
- uses: ./.holdex-actions/.github/actions/composed/pr-checks
with:
run-prettier: false
run-markdown: true
# Markdown lint runs from lefthook.yml in hooks.yml, with the same
# pinned rumdl as the local pre-push hook.
run-markdown: false
run-commits: true
package-manager: bun
55 changes: 0 additions & 55 deletions .github/workflows/rules_audit.yml

This file was deleted.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ improvements.
### Local

After cloning, run `npm install` once to install the pinned `rumdl` version and
enable the markdown lint and rules-audit checks on push (`postinstall` sets
`core.hooksPath` to `.githooks` automatically).
enable the markdown lint and rules-audit checks on push (`postinstall` runs
`lefthook install`). The hooks live in `lefthook.yml`, and CI runs the same
file.

### Stage / Preview

Expand Down
21 changes: 14 additions & 7 deletions docs/rules/DEV-380.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ Make lint automatic and version-stable, from the npm-pinned `rumdl` only.

1. Pin `rumdl` as a devDependency, so everyone runs the same version and avoids
the version drift that blocks pushes with unrelated reformatting.
1. In `postinstall`, set `git config core.hooksPath .githooks`, so the hook
installs on `npm install`.
1. Add a `.githooks/pre-push` that runs the pinned binary
(`node_modules/.bin/rumdl`), never a global one, over the markdown changed in
the push, and blocks the push if it reformats anything.
1. Install the hooks with [lefthook](https://lefthook.dev), pinned as a
devDependency, and run `lefthook install` in `postinstall`, so the hooks
install on `npm install`. A hand-written hook script re-implements which
files a push changes, and that logic drifts and breaks on new branches.
1. In `lefthook.yml`, add a `pre-push` command that runs the pinned binary
(`node_modules/.bin/rumdl check --fix`), never a global one, over
`{push_files}`, and set `fail_on_changes: always` so the push is blocked if
it reformats anything.
1. Run the same `lefthook.yml` in CI, so a push that skipped the hook is still
checked, and the check lives in one place.

### Acceptance Criteria

- [ ] `rumdl` is pinned in `devDependencies`
- [ ] `postinstall` sets `core.hooksPath` to `.githooks`
- [ ] `.githooks/pre-push` runs `node_modules/.bin/rumdl`, not a global binary
- [ ] `postinstall` runs `lefthook install`
- [ ] `lefthook.yml` runs `node_modules/.bin/rumdl` on `{push_files}`, not a
global binary
- [ ] CI runs the same `lefthook.yml`
- [ ] A push containing a lint violation is blocked until it is fixed
32 changes: 32 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Git hooks for this repo, managed by lefthook (https://lefthook.dev).
# `npm install` installs them. This file is the one place to edit hook logic:
# git runs it on every push, and CI runs the same commands on every PR.

pre-push:
# rumdl --fix rewrites files in place. Fail the push when it did, so the
# fixes get committed instead of pushed around.
fail_on_changes: always
fail_on_changes_diff: true
commands:
markdown:
# {push_files} is what this push adds. On a branch with no upstream yet
# lefthook diffs against the default branch, not the whole repo.
glob: "*.md"
# The npm-pinned rumdl only, never a global one: versions format
# differently and would block pushes with unrelated reformatting.
run: node_modules/.bin/rumdl check --fix {push_files}
priority: 1
rules:
# Authoring standard for the rules this push touches, so a push is
# blocked by its own rules, never by pre-existing ones.
glob: "docs/rules/*.md"
run: node scripts/check-rules.mjs --rules {push_files}
priority: 2
rules-system:
# Cross-file invariants (dependencies, index, reachability) over the whole
# tree, on any push. `--rules` with no names keeps the authoring standard
# out of scope. lefthook skips every pre-push command when a push only
# deletes files, so hooks.yml also runs this outside lefthook: deleting a
# rule is exactly what breaks these invariants.
run: node scripts/check-rules.mjs --rules
priority: 3
Loading
Loading