check-heading-titles.yml
Diff-scoped check that flags a clickbait heading. It fails by default, since agents ignore warnings (Ezra, 2026-11-02). A heading is a plain noun phrase that names its content (Ezra, 2026-11-03): it is not a question, and neither it nor a phrase after a colon begins with who, whom, whose, what, when, where, why, how or which. The rule text is in psw, “Subsection headings”.
The check reports two mistakes:
- Question. The heading text ends with
?. - Interrogative start. The heading, or any phrase after a colon in it, begins with who, whom, whose, what, when, where, why, how or which. A word that only begins with one of these, such as “Whatever” or “Howell’s”, does not count.
The check also reads the title of a fenced div or callout: the #### heading inside a definition, example or exercise div, or a title="..." attribute on the div’s opening fence.
The check does not count:
- a heading inside a code fence, an HTML comment or the YAML front matter;
- a heading on a line carrying
<!-- check-heading-titles: allow -->.
Scope
A finding is reported only when the heading line was added in the pull request, so existing headings are not swept. With no base-ref, or when the diff cannot be computed, the check skips with a warning instead of scanning the whole tree. Set whole-tree: true to scan every tracked page in full on purpose, which measures how many headings a corpus carries that break the rule.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
globs |
string | '*.qmd' |
Space-separated git pathspecs of tracked files to check (recursive by default). |
paths-ignore |
string | '' |
Comma- or newline-separated glob patterns (relative paths) to skip. Supports *, ? and recursive **. |
fail |
boolean | true |
Fail the workflow when an added heading is clickbait. Set false for warning annotations only. |
whole-tree |
boolean | false |
Scan every tracked page in full instead of only a PR’s added lines. |
python-version |
string | '3.x' |
Python version to set up. |
runs-on |
string | empty (CI_RUNNER variable, else ubuntu-latest) |
Runner label for the job. Use a self-hosted label only for a private repository; never for a public one, whose pull requests can run arbitrary code on it (#1167). |
Permissions
Only contents: read (the default), so no permissions: block is needed.
Example
# Copy to .github/workflows/check-heading-titles.yml in your repo.
# Diff-scoped check: flags a clickbait heading when a PR adds it: a question, or a
# heading (or a phrase after a colon) that starts with who, whom, whose, what, when,
# where, why, how or which.
# Fails by default.
# Only contents:read is needed (the default), so no permissions block is required.
name: Check heading titles
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
check:
uses: Morrison-Lab/gha/.github/workflows/check-heading-titles.yml@v3
# with:
# globs: '*.qmd' # pathspecs to check (recursive)
# paths-ignore: 'chapters/outtakes/**' # skip known-noisy paths
# fail: false # warn instead of block (defaults to true)
# whole-tree: true # scan every page, not just a PR's additionsSee examples/check-heading-titles.yml for the full caller stub.