Skip to content

Commit c01f0cc

Browse files
[DO NOT MERGE until #3630] Unify contributor pre-commit setup + docs (#3631)
> [!WARNING] > **Draft — merge last.** Merge order: **#3629#3630 → this PR.** > This branch is stacked on #3629, so #3629's commits appear in this diff until it merges (then the diff collapses to this PR's three commits). It must also land after #3630 because the docs describe the SCSS/stylelint hook that #3630 adds. Fixes a gap in the local contributor setup: following the documented steps left the pre-commit **framework** checks not running locally, so lint/secret/spell issues surfaced only in CI. ## The problem `setup-hooks.sh` runs `git config core.hooksPath .githooks`. Once `core.hooksPath` is set, git looks **only** there and ignores `.git/hooks` — which is where `pre-commit install` puts its hook. So the two documented setup steps conflict: `pre-commit install` even refuses to run (`Cowardly refusing to install hooks with core.hooksPath set`), and the framework checks never fired locally. `.githooks/pre-commit` only stamped dates. ## The fix **One hook runs everything.** `.githooks/pre-commit` now stamps content dates (as before), then invokes `pre-commit run` on the staged files. Contributors run a single setup — install the `pre-commit` binary and run `./setup-hooks.sh`; no `pre-commit install`. If the binary isn't installed, the hook stamps dates and prints a reminder rather than failing. Also in this PR: - **README + contributor hook guide rewritten** for the single setup path, with SCSS/stylelint added to the list of checks. - **Corrected tag validation from "blocking" to advisory** in both docs — `scripts/hooks/content_checks.py` tags mode always returns 0 (warns, never blocks). The old guide's "commit blocked due to tag errors" example was fiction. - **Fixed an invalid `margin-left: 0 em`** in `.docs-content` (`_custom.scss`) — the stray space made the declaration invalid, so the intended zero margin never applied. (The out-of-scope item spotted during DOCS-78.) ## Verification - Ran real commits with `core.hooksPath=.githooks`: the hook stamps dates **and** runs the full framework (previously it ran neither the framework nor blocked). Confirmed the framework's per-hook table appears and a clean tree passes. - markdownlint (via the hook) passes on the rewritten docs. - Empirically reproduced the original conflict: `pre-commit install` refuses when `core.hooksPath` is set. --- Created in collaboration with Claude Code running Opus 4.8 on 2026-07-23. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 212fd97 commit c01f0cc

5 files changed

Lines changed: 165 additions & 116 deletions

File tree

.githooks/pre-commit

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
#!/usr/bin/env python3
2-
"""Git pre-commit hook: stamp date/lastmod on Hugo content files.
2+
"""Git pre-commit hook: stamp content dates, then run the pre-commit framework.
33
4-
This is kept as a native git hook (enabled via ./setup-hooks.sh) rather than a
5-
pre-commit-framework hook because it stamps the current time and re-stages the
6-
file into the same commit -- pre-commit does not auto-restage, and a "now"
7-
timestamp can never pass a CI idempotency check.
4+
This is the single local entry point for commit checks. It runs in two steps:
85
9-
Tag validation and spell checking now live in the pre-commit framework
10-
(.pre-commit-config.yaml -> scripts/hooks/content_checks.py).
6+
1. Stamp date/lastmod on staged Hugo content files. This stays a native hook
7+
(rather than a pre-commit-framework hook) because it stamps the current time
8+
and re-stages the file into the same commit -- pre-commit does not
9+
auto-restage, and a "now" timestamp can never pass a CI idempotency check.
10+
2. Delegate to the pre-commit framework (.pre-commit-config.yaml) for linting,
11+
secret scanning, tag validation, spell checking, and SCSS/stylelint.
12+
13+
Both run from one hook on purpose. Enabling this hook sets
14+
core.hooksPath=.githooks (see ./setup-hooks.sh), which makes git ignore
15+
.git/hooks entirely -- so a separately `pre-commit install`ed hook would never
16+
fire. Chaining the framework from here keeps a single, conflict-free setup.
1117
"""
1218

1319
import re
20+
import shutil
1421
import subprocess
1522
import sys
1623
from datetime import datetime, timezone
@@ -80,6 +87,22 @@ def update_dates():
8087
return added, updated
8188

8289

90+
def run_framework_checks():
91+
"""Run the pre-commit framework on staged files (lint, secrets, tags, spell, SCSS).
92+
93+
Returns pre-commit's exit code so a failing check blocks the commit. If the
94+
`pre-commit` binary is not installed, warn and let the commit through --
95+
the same checks still run as a required check in CI.
96+
"""
97+
if shutil.which('pre-commit') is None:
98+
print("\n⚠️ 'pre-commit' is not installed, so lint/secret/tag/spell/SCSS "
99+
"checks did not run locally.")
100+
print(" Install it so they run before you push:")
101+
print(" brew install pre-commit # or: pipx install pre-commit")
102+
return 0
103+
return subprocess.run(['pre-commit', 'run']).returncode
104+
105+
83106
def main():
84107
added, updated = update_dates()
85108
if added:
@@ -90,7 +113,7 @@ def main():
90113
print(f"📅 Updated lastmod on {len(updated)} file(s):")
91114
for f in updated:
92115
print(f" - {f}")
93-
return 0
116+
return run_framework_checks()
94117

95118

96119
if __name__ == '__main__':

README.md

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -121,36 +121,48 @@ You can review our current list of [Tags](https://edu.chainguard.dev/tags).
121121
### Pre-commit
122122

123123
This repository uses the [pre-commit](https://pre-commit.com/) framework to check
124-
changes before they merge. The `pre-commit` check is **required** on pull requests,
125-
and it runs only on the files a PR changes (a pre-existing backlog is not gated).
126-
It checks:
124+
changes before they merge. The `pre-commit` check is **required** on pull
125+
requests and runs only on the files a pull request changes (a pre-existing
126+
backlog is not gated). Run the same checks locally to catch problems before you
127+
push. They cover:
127128

128-
- Secret scanning, private keys, large files, and file hygiene (whitespace, EOF, line endings)
129+
- Secret scanning, private keys, large files, and file hygiene such as trailing whitespace and end-of-file newlines
129130
- GitHub Actions security (`zizmor`) and linting (`actionlint`)
130-
- JavaScript (`eslint`), Markdown (`markdownlint`), and Python (`bandit`, `black`)
131-
- **Content tag validation** against the approved taxonomy (blocking)
132-
- **Spell checking** of prose with aspell — advisory and local-only (skipped in CI)
131+
- JavaScript (`eslint`), Markdown (`markdownlint`), SCSS (`stylelint`), and Python (`bandit`, `black`)
132+
- Content tag validation against the approved taxonomy (advisory: it warns but never blocks a commit)
133+
- Prose spell checking with aspell (advisory and local-only; skipped in CI)
134+
135+
**One-time setup:**
133136

134-
**Setup (one-time):**
135137
```sh
136-
# Install pre-commit
138+
# 1. Install the pre-commit framework
137139
brew install pre-commit # or: pipx install pre-commit
138140

139-
# Enable it in your clone
140-
pre-commit install
141+
# 2. Enable the repository's git hook
142+
./setup-hooks.sh
141143

142-
# Optional: enable local prose spell checking
144+
# 3. If you edit SCSS (assets/scss/), install the Node dependencies
145+
npm install
146+
147+
# 4. Optional: install aspell for local spell checking
143148
brew install aspell
144149
```
145150

146-
Separately, a native git hook stamps `date`/`lastmod` on content files (it
147-
re-stages into the same commit, so it stays a git hook rather than a pre-commit
148-
hook). Enable it once with:
149-
```sh
150-
./setup-hooks.sh
151-
```
151+
`setup-hooks.sh` points git at the `.githooks/` directory. On each commit, that
152+
hook stamps `date` and `lastmod` on changed content files, then runs the
153+
pre-commit framework, so one hook covers everything. You don't need to run
154+
`pre-commit install`: git ignores `.git/hooks` once `core.hooksPath` is set, so a
155+
separately installed hook wouldn't run. To skip the hook for a single commit, use
156+
`git commit --no-verify`.
157+
158+
The SCSS lint step runs the project's own `stylelint`, so it needs the Node
159+
dependencies from `npm install`. The other hooks use pre-commit-managed
160+
environments, so they need nothing beyond the framework itself. If you edit
161+
`assets/scss/` without installing the dependencies, that one hook fails; content
162+
and other contributors are unaffected.
152163

153164
**Resources:**
154-
- 📖 [Complete Pre-commit Hook Guide for Contributors](docs/pre-commit-hook-guide.md) - Detailed guide with examples
155-
- 📋 [Tag Guidelines](TAG_GUIDELINES.md) - Complete approved tag taxonomy
156-
- 📝 [Custom Dictionary](.aspell.en.pws) - Technical terms for spell checker
165+
166+
- [Complete pre-commit hook guide for contributors](docs/pre-commit-hook-guide.md) — detailed guide with examples
167+
- [Tag guidelines](TAG_GUIDELINES.md) — the approved tag taxonomy
168+
- [Custom dictionary](.aspell.en.pws) — technical terms for the spell checker

assets/scss/common/_custom.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ html[data-dark-mode] .logo-purple {
9393
}
9494

9595
.docs-content {
96-
margin-left: 0 em;
96+
margin-left: 0;
9797
padding: 0;
9898
}
9999

docs/pre-commit-hook-guide.md

Lines changed: 83 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,84 @@
22

33
This guide explains the automated checks that run on your content before it merges.
44

5-
## Two systems
5+
## How local checks run
66

7-
Content quality is enforced by two complementary systems:
7+
One git hook (`.githooks/pre-commit`, enabled by `./setup-hooks.sh`) runs on
8+
every commit and does two things:
89

9-
1. **The [pre-commit](https://pre-commit.com/) framework** (`.pre-commit-config.yaml`)
10-
runs on `git commit` and as a **required** CI check on pull requests. It handles
11-
tag validation and spell checking (plus repo-wide security/lint hooks). In CI it
12-
runs only on the files a PR changes.
13-
2. **A native git hook** (`.githooks/pre-commit`, enabled via `./setup-hooks.sh`)
14-
stamps content dates. It stays a git hook because it re-stages the stamped file
15-
into your commit, which the pre-commit framework does not do.
10+
1. **Stamps content dates.** It adds `date` and `lastmod` to new content files
11+
and refreshes `lastmod` on edited ones, then re-stages the file into your
12+
commit. This stays a native git hook because the pre-commit framework doesn't
13+
re-stage files, and a "now" timestamp would never pass a CI idempotency check.
14+
2. **Runs the [pre-commit](https://pre-commit.com/) framework**
15+
(`.pre-commit-config.yaml`) for linting, secret scanning, tag validation,
16+
spell checking, and SCSS/stylelint. The same framework runs as a **required**
17+
check on pull requests, where it lints only the files a pull request changes.
18+
19+
Because `./setup-hooks.sh` sets `core.hooksPath=.githooks`, git runs this hook
20+
and ignores anything in `.git/hooks`. You don't run `pre-commit install`: the
21+
hook calls the framework for you, so one setup covers everything.
1622

1723
## What gets checked
1824

19-
### 1. Automatic Date Management (git hook)
25+
### Content date stamping (git hook step)
26+
27+
- **New files**: Adds `date` and `lastmod` fields with the current timestamp
28+
- **Modified files**: Refreshes the `lastmod` field
29+
- **Format**: ISO 8601 with UTC timezone, for example `2025-01-16T10:30:45+00:00`
30+
- You don't add or update these fields yourself
31+
32+
### Linting and security (pre-commit framework)
33+
34+
These fail the commit, and the required CI check, when they find a problem:
35+
36+
- **Code and content linting**: JavaScript (`eslint`), Markdown (`markdownlint`), SCSS (`stylelint`), and Python (`bandit`, `black`)
37+
- **Security and hygiene**: secret scanning (`gitleaks`), private-key detection, large-file checks, and whitespace, end-of-file, and line-ending fixes
38+
- **GitHub Actions**: workflow security (`zizmor`) and linting (`actionlint`)
2039

21-
- **New files**: Automatically adds `date` and `lastmod` fields with the current timestamp
22-
- **Modified files**: Automatically updates the `lastmod` field to reflect when changes were made
23-
- **Format**: Uses ISO 8601 format with UTC timezone (e.g., `2025-01-16T10:30:45+00:00`)
24-
- **No manual work needed**: You no longer need to add or update these fields yourself!
40+
Each linter reads the repository's own configuration, for example
41+
`.stylelintrc.json` for SCSS and `.markdownlint-cli2.jsonc` for Markdown.
2542

26-
### 2. Tag Validation (pre-commit framework, blocking)
43+
The SCSS lint step runs the project's own `stylelint` from `node_modules`, so it
44+
needs `npm install` (see [Setting up](#setting-up)). The other linters use
45+
pre-commit-managed environments and need nothing more.
2746

28-
- Ensures all tags match our approved taxonomy
29-
- Checks that you haven't exceeded 5 tags per article
30-
- Verifies proper capitalization (Title Case vs. acronyms)
47+
### Tag validation (pre-commit framework, advisory)
3148

32-
### 3. Spelling (pre-commit framework, advisory + local-only)
49+
- Checks that tags match the approved taxonomy, stay within five per article, and use the expected capitalization
50+
- Reports problems as warnings but never blocks a commit
51+
52+
### Spelling (pre-commit framework, advisory and local-only)
3353

3454
- Catches typos and misspellings; reports but never blocks a commit
35-
- Runs locally only the CI check skips it (`SKIP=content-spellcheck`)
36-
- Ignores code blocks (between ```), Hugo shortcodes (e.g. `{{< youtube >}}`), inline code (between `), and URLs
55+
- Runs locally only; the CI check skips it (`SKIP=content-spellcheck`)
56+
- Ignores code blocks, Hugo shortcodes (for example `{{< youtube >}}`), inline code, and URLs
3757
- Uses a custom dictionary for technical terms (`.aspell.en.pws`)
3858

3959
## Setting up
4060

41-
### One-time setup
61+
Do this once per clone:
4262

43-
1. **Install and enable pre-commit** (tags, spell check, security/lint):
63+
1. **Install the pre-commit framework:**
4464

4565
```bash
4666
brew install pre-commit # or: pipx install pre-commit
47-
pre-commit install
4867
```
4968

50-
2. **Enable the date-stamping git hook**:
69+
2. **Enable the git hook:**
5170

5271
```bash
5372
./setup-hooks.sh
5473
```
5574

56-
3. **(Optional) Install aspell** so the local spell check runs:
75+
3. **Install Node dependencies if you edit SCSS** (`assets/scss/`), so the
76+
stylelint hook can run:
77+
78+
```bash
79+
npm install
80+
```
81+
82+
4. **Optional: install aspell** so the local spell check runs:
5783

5884
```bash
5985
# macOS
@@ -72,69 +98,51 @@ Content quality is enforced by two complementary systems:
7298
sudo pacman -S aspell
7399
```
74100

75-
That's it! Both run automatically when you commit.
101+
Every commit now stamps content dates and runs the framework checks. If you
102+
commit before installing the `pre-commit` framework, the hook still stamps dates
103+
and prints a reminder to install it.
76104

77-
## Understanding the Output
105+
## Understanding the output
78106

79-
When you run `git commit`, you'll see output like this:
107+
When you run `git commit`, the hook prints its date-stamp result first, then the
108+
pre-commit framework's per-hook results.
80109

81-
### ✅ Success Case
110+
### A passing commit
82111

83112
```text
84-
🔍 Running pre-commit checks...
85-
86-
📅 Updated lastmod dates for 1 file(s)
113+
📅 Updated lastmod on 1 file(s):
87114
- content/chainguard/chainguard-images/getting-started.md
88-
89-
✅ All checks passed!
115+
trim trailing whitespace.................................................Passed
116+
fix end of files.........................................................Passed
117+
Detect hardcoded secrets.................................................Passed
118+
markdownlint-cli2........................................................Passed
119+
Validate content tags....................................................Passed
90120
```
91121

92-
### ⚠️ With Warnings
122+
### A blocked commit
93123

94-
```text
95-
🔍 Running pre-commit checks...
124+
A blocking hook (a linter, a secret scan, or a file-hygiene check) prints the
125+
failure and stops the commit. Fix the reported issue, stage the change, and
126+
commit again:
96127

97-
📅 Added date fields to 1 new file(s)
98-
- content/chainguard/new-tutorial.md
128+
```text
129+
markdownlint-cli2........................................................Failed
130+
- hook id: markdownlint-cli2
131+
- exit code: 1
99132
100-
📄 content/chainguard/getting-started.md
101-
Tags: ["Chainguard Containers", "Overview", "NewTag"]
102-
⚠️ Tag not in approved list: 'NewTag'
103-
📝 Spelling errors found:
104-
- 'recieve' on line(s): 23
105-
- 'configuation' on line(s): 45
106-
107-
============================================================
108-
Pre-commit Check Summary:
109-
Tag Warnings: 1
110-
Tag Errors: 0
111-
Files with spelling issues: 1
112-
113-
💡 Consider reviewing TAG_GUIDELINES.md for approved tags
114-
115-
📝 Spelling issues found. Consider:
116-
- Fixing typos
117-
- Adding technical terms to your personal dictionary
118-
- Using 'git commit --no-verify' to skip this check
133+
content/chainguard/tutorial.md:14 MD012/no-multiple-blanks Multiple consecutive blank lines
119134
```
120135

121-
### ❌ With Errors (Blocks Commit)
136+
Some hooks fix the file for you (for example, trailing-whitespace and
137+
end-of-file-fixer) and then fail so you can review and re-stage the change.
122138

123-
```text
124-
🔍 Running pre-commit checks...
125-
126-
📄 content/chainguard/tutorial.md
127-
Tags: ["CHAINGUARD", "TUTORIAL"]
128-
❌ Tag should use Title Case: 'CHAINGUARD'
129-
❌ Tag should use Title Case: 'TUTORIAL'
139+
### Advisory warnings
130140

131-
============================================================
132-
Pre-commit Check Summary:
133-
Tag Warnings: 0
134-
Tag Errors: 2
135-
Files with spelling issues: 0
141+
Tag and spelling checks print warnings but don't stop the commit:
136142

137-
❌ Commit blocked due to tag errors. Please fix and try again.
143+
```text
144+
Validate content tags....................................................Passed
145+
⚠️ Tag not in approved list: 'NewTag'
138146
```
139147

140148
## Common Scenarios
@@ -189,9 +197,9 @@ lastmod: 2025-01-16T10:45:30+00:00
189197
---
190198
```
191199

192-
### Fixing Tag Errors
200+
### Fixing tag warnings
193201

194-
If you see a tag error, update your frontmatter:
202+
If you see a tag warning, update your frontmatter so the tag matches the approved taxonomy:
195203

196204
```yaml
197205
# ❌ Wrong

0 commit comments

Comments
 (0)