Skip to content

ci(hooks): Lint the staged Markdown, not the working copy - #170

Merged
mpaulosky merged 3 commits into
mainfrom
squad/144-lint-staged-markdown
Sep 30, 2026
Merged

mpaulosky merged 3 commits into
mainfrom
squad/144-lint-staged-markdown

Conversation

@mpaulosky

@mpaulosky mpaulosky commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Item 2 of #144. .github/hooks/pre-commit ran markdownlint on the working-tree paths, so with a partly staged Markdown file the working copy could pass while the staged version (the one committed) failed, and only CI caught it.

  • The hook now writes each staged .md file's staged content, plus the staged .markdownlint-cli2.jsonc, into a temporary tree with the same layout (git checkout-index --prefix), and lints that tree. The config's ignores (e.g. docs/blogs/**) still apply.
  • Staged paths are read NUL-separated, so paths with spaces work, and renamed files (R) are linted under their new name.
  • Only the staged config is used: a config that exists only in the working copy isn't in the commit, so it isn't applied.
  • New .github/hooks/tests/pre-commit.test.sh (same style as the pre-push tests, with a stub linter), run by the CI Hook tests job.

Testing

  • pre-commit.test.sh: 12 cases pass, including "violation staged but fixed only in the working copy → refused", "violation only in the unstaged working copy → allowed", a renamed file, and a staged config that differs from the working copy's. The stub linter applies the config it finds (a forbidden word and an ignored path), so the suite fails if the hook lints under the working-copy config.
  • Checked with the real markdownlint-cli2 in a scratch repo using this repo's config: a staged violation is caught even when the working copy is fixed, and docs/blogs/ stays ignored.
  • shellcheck, yamllint, actionlint clean; scripts/gate.sh passed.

Refs #144 (items 1 and 3 follow separately; atelier-store later).

🤖 Generated with Claude Code

The pre-commit hook passed working-tree paths to markdownlint, so with a
partly staged file the working copy could pass while the staged version,
the one actually committed, failed and only CI caught it. The hook now
writes the staged content of each file, and the staged lint config, to
a temporary tree and lints that. Paths are read NUL-separated so spaces
survive. A new hook test covers staged-only and working-copy-only
violations, and CI runs it next to the pre-push tests.

Refs #144

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 30, 2026 00:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Renamed Markdown files can bypass linting, and the config fallback can still use unstaged working-tree content.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Updates the pre-commit hook to lint staged Markdown content instead of working-tree content.

Changes:

  • Creates an index-based temporary lint tree.
  • Adds seven hook regression tests.
  • Runs the new tests in CI.
File Description
.github/​hooks/​pre-commit Lints staged Markdown snapshots.
.github/​hooks/​tests/​pre-commit.test.sh Tests staged-content behavior and paths with spaces.
.github/​workflows/​ci.yml Adds pre-commit tests to CI.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/hooks/pre-commit Outdated
Comment thread .github/hooks/pre-commit
Comment thread .github/workflows/ci.yml
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Test Results Summary

483 tests  ±0   483 ✅ ±0   17s ⏱️ ±0s
  9 suites ±0     0 💤 ±0 
  9 files   ±0     0 ❌ ±0 

Results for commit 92c6d2b. ± Comparison against base commit c66bfd4.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.40%. Comparing base (c66bfd4) to head (92c6d2b).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #170   +/-   ##
=======================================
  Coverage   85.40%   85.40%           
=======================================
  Files          77       77           
  Lines        1596     1596           
  Branches      150      150           
=======================================
  Hits         1363     1363           
  Misses        189      189           
  Partials       44       44           

The hook's --diff-filter=ACM left out renames, so a renamed Markdown
file, even with edits, wasn't linted; R is now included. When the lint
config wasn't staged, the hook fell back to the working copy's, which
might not be in the commit; it now uses only the staged config. Adds a
test case for each, and the CI comment names the pre-commit tests.
Addresses Copilot review on #170.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 30, 2026 01:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The tests do not reliably validate rename setup or staged configuration behavior.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (3)

Comment thread .github/hooks/tests/pre-commit.test.sh Outdated
The stub linter only checked that a config existed, so the suite would
still pass if the hook linted under the working or HEAD config, or lost
the paths the config's ignores rely on. The stub now reads a forbidden
word and an ignored prefix from the config it finds, and new cases
cover a staged config that differs from the working one and a staged
file under an ignored path. Swapping the hook to the working config
fails two of them. Addresses Copilot review on #170.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 30, 2026 01:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The implementation addresses the staged-content mismatch and includes focused regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@mpaulosky
mpaulosky merged commit fe03c23 into main Sep 30, 2026
27 checks passed
@mpaulosky
mpaulosky deleted the squad/144-lint-staged-markdown branch September 30, 2026 01:12
mpaulosky added a commit that referenced this pull request Sep 30, 2026
* docs: add release blog for PR #170 [skip-release]

* docs: match the PR #170 post to the merged test suite [skip-release]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
mpaulosky added a commit to mpaulosky/IssueManager that referenced this pull request Sep 30, 2026
## Summary

Part 1 of #227, porting mpaulosky/IssueTracker#170 and #180.

`.github/hooks/pre-commit` linted the working-tree files, so a partly
staged Markdown file could pass locally and fail in CI. It now lints
what the commit will contain.

- **Staged snapshot:** each staged Markdown file's index content is
written into a temporary tree with the same layout (`git checkout-index
--prefix`), and markdownlint-cli2 runs from there. Paths are read
NUL-separated, so spaces survive, and renamed files (`R`) are linted
under their new name.
- **Staged configs only, both of them:** `.markdownlint-cli2.jsonc` and
`.markdownlint.json` are copied into the snapshot when they're staged.
markdownlint-cli2 reads both, and this repo's `.markdownlint.json`
(`"default": false`) turns off every rule the jsonc file doesn't set, so
the hook reaches the same result as `scripts/gate.sh`. A config that
exists only in the working copy isn't used.
- **Hook tests:** `.github/hooks/tests/pre-commit.test.sh` (14 cases)
and `pre-push.test.sh` (35 cases) run each hook in a throwaway repo with
stub tools. A new `Hook tests` job in `ci.yml` runs both on every PR.
It's not a required check.

## Testing

- `pre-commit.test.sh`: 14 passed. The two `.markdownlint.json` cases
were written first; the staged one failed until the hook snapshotted
that file.
- `pre-push.test.sh`: 35 passed, against this repo's `scripts/gate.sh`.
- `shellcheck`, `actionlint`, `zizmor` and `yamllint` are clean, and
this commit went through the new hook and the pre-push gate.

Refs #227

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants