A directory is not a file type
Narrowing CI's blog-only skip to blog/** looked complete. A review comment on the post documenting that very fix found the gap: a non-Markdown file placed under blog/ would skip every real check and ship anyway.
The previous fix made blog-only pull requests skip four expensive CI jobs by matching everything outside blog/ against !blog/**. That worked for its stated purpose: registry rows and post files stopped paying for a full type-check, test run, build, and end-to-end pass every time they filed paperwork. But blog/** excludes the whole subtree, not just the Markdown in it, and a review comment on the post that documented the original fix pointed out what that actually meant: drop a .ts file, a script, anything non-Markdown under blog/, and it would skip type-check, tests, build, and end-to-end entirely, then ship on the next deploy from main since nothing else gates that path.
The fix narrows the pattern from blog/** to blog/**/*.md across all three workflows that reference it. dorny/paths-filter’s matcher (picomatch) treats ** as zero-or-more directories, so the narrower pattern still matches root-level files like blog/registry.md, not only ones nested a level or two down:
filters: |
non_blog:
- '**'
- '!blog/**/*.md'
That alone would have been enough, except the pattern had already been wrong once in the same day: the earlier fix needed predicate-quantifier: every after the filter’s default matched everything regardless of the negation. Trusting a single pattern to keep being right felt like exactly the kind of assumption that had just failed, so the second layer doesn’t rely on the exclusion holding by itself. An always-running job now checks the same change list independent of whatever the (possibly-skipped) main jobs decide, and fails outright if anything under blog/ isn’t Markdown:
- name: Fail if a non-Markdown file changed under blog/
if: steps.filter.outputs.blog_non_md == 'true'
run: |
echo "::error::Non-Markdown file(s) changed under blog/"
exit 1
A failing job only blocks a merge if the ruleset knows its name, which is the same lesson the original fix already carried: a required check has to be registered by exact string, not assumed to apply. Detect changed paths now sits in the ruleset alongside the four jobs it gates, so a stray script under blog/ fails a check GitHub is actually waiting to hear from, rather than one it silently ignores.