Checks, Repairs, and the Gates Before Deployment
I can finish an article, listen to it, fix the sentence that sounds wrong, and still ship a broken link. Jekyll might build the page perfectly well. That does not mean the short URL points where I expect, the image path has the right case, or the project data in the generated site is the version I meant to publish.
That is why this site has accumulated so many checks. Some came from a specific failure. Others came from being tired of finding the failure only after a push. At this point there are enough scripts in utils/bin that it is tempting to say “the pipeline checks everything.” It doesn’t. Some checks only warn, some repair files, and the GitHub Pages job runs a narrower set than my local machine. I need to know which gate I am actually standing at.
The Small Checks Catch Different Mistakes
The numbered scripts under utils/bin/checks cover environment dependencies, permalinks, Jekyll doctor, source links, generated-site links, external links, large files, a strict build, required files, Liquid syntax, front matter, navigation, image paths, and text cleanup. The runner, utils/bin/check_site.sh, calls the matching shell scripts in filename order and stops when one exits unsuccessfully. It accepts --list, --run, and --skip for a narrower pass.
I like having a quick way to ask one question while I am editing. A short-link check is a different question from whether the complete site builds. The Ruby short-link tools verify the front-matter identity and generated redirects; the taxonomy checker asks whether I used canonical tags. A production build is still needed to see what Liquid and the plugins actually produce. None of these checks decides whether the article is clear, whether a diagram tells the truth, or whether I meant to publish it today.
For a quick inventory or one focused gate, the runner accepts an extensionless check name:
./utils/bin/check_site.sh --list
./utils/bin/check_site.sh --run 03_jekyll_doctor
That second command invokes Jekyll doctor in the current checkout. On my case-insensitive macOS filesystem it reports URLs that differ only by case and exits 1; its wrapper treats that as a warning unless STRICT_JEKYLL_DOCTOR=1 is set. So “the runner passed” still requires reading what it printed.
Some names in the numbered directory sound stronger than their implementation. The Liquid syntax step searches files for a for without an endfor, or an if without an endif, prints warnings, and exits successfully. The navigation step also warns and returns success. I would not treat either as a parser or as a deployment blocker. The real Jekyll build does more useful work on Liquid, but a successful build still does not read the article for me.
Each gate answers a different question; review and the push decision remain mine.
The editable Graphviz source is available if you want to change the flow for your own site.
check-boundaries.dot text View source
digraph CheckBoundaries {
graph [bgcolor="#111923", pad="0.35", rankdir="TB", nodesep="0.45", ranksep="0.60", fontname="Helvetica", fontcolor="#edf2f5", label="A check is evidence, not a publishing decision", labelloc="t", fontsize=20];
node [shape=box, style="rounded,filled", color="#587080", fillcolor="#20303d", fontcolor="#f4f5f6", fontname="Helvetica", fontsize=12, margin="0.17,0.12"];
edge [color="#9db1bc", arrowsize=0.7, penwidth=1.5];
source [label="Article source\nmetadata, links, assets", fillcolor="#513d28", color="#d4a660"];
focused [label="Focused checks\nshort URLs, tags, redirects"];
suite [label="Local numbered suite\nbuilds plus repair steps"];
review [label="Review changed files\nand rendered site", fillcolor="#234243", color="#73b8b5"];
push [label="Human push decision", fillcolor="#513d28", color="#d4a660"];
ci [label="Pages workflow\nproduction build + verifiers"];
deploy [label="Pages deployment", fillcolor="#263958", color="#80a6d7"];
{ rank=same; source; focused; suite; review; }
{ rank=same; push; ci; deploy; }
source -> focused -> suite -> review [constraint=false];
review -> push;
push -> ci -> deploy [constraint=false];
focused -> ci [style=invis, weight=4];
suite -> deploy [style=invis, weight=4];
review:n -> source:n [style=dashed, color="#d4a660", constraint=false];
}
A Check That Changes the Thing It Checks
Here is the awkward part: the full numbered suite is not a read-only inspection. Its environment step may install missing Python packages. Its project-data step calls fetch_og.py and can stage generated YAML, pages, and images. The front-matter step inserts missing fields. The image-path step can rewrite paths and updates a local timestamp. The cleanup step can rewrite changed text files and re-stage those already in the index. The build and HTMLProofer steps also replace build output. That is useful work when I have chosen it, but it is not the command I run just to ask “what would change?”
The internal generated-site link check builds again before HTMLProofer examines _site; the external link check is normally skipped unless CHECK_EXTERNAL_LINKS=1 is set. That external pass depends on the network and can run into rate limits or temporary failures. jekyll-site -c is another explicit way to request the generated-site HTMLProofer pass during a build. These are separate entry points with overlapping work, not one magical validator hiding behind a single name.
There is a particularly good reminder in the installed pre-commit hook. Its comment says it skips the project-data refresh, and it calls:
./utils/bin/check_site.sh --skip 05_update_project_data.sh
The runner removes .sh before it compares check names. So that argument does not match 05_update_project_data in the current checkout. The refresh step is still selected. That is a real bug in the local hook contract, and it means I cannot honestly describe the hook as a non-mutating validation pass. I check the staged diff again after it runs, especially when a tool can stage something for me. A hook can stop a bad commit; it can also change the material I thought I was committing.
The repository has a separate .pre-commit-config.yaml with focused short-link and tag hooks. That file is configuration for the optional pre-commit framework. It does not prove those hooks are installed in a particular clone, and it is not the installed shell hook described above. The difference matters when somebody follows these instructions on their own checkout.
What Happens After I Push
The GitHub Pages workflow starts on a push to main or a manual dispatch. It checks out the repository, installs Ruby dependencies, clears the old output and Jekyll cache, and runs a production Jekyll build. Then it checks short-link front matter and generated redirects, verifies project permalink redirects, and runs the tag taxonomy checker before uploading the built site and deploying Pages.
A future date in a post is not a deployment timer. The production build excludes that post until its date arrives, and this workflow has no scheduled rebuild. I checked on October 11: Part 3 was dated October 8, but its live URL still returned 404. The last successful Pages run was October 6. Nothing had rebuilt the site after Part 3 became eligible. The source can be sitting on main while its page is still missing from Pages. I have to check the live route after the next deployment, not infer publication from Git history.
That is a useful deployment gate, but it is smaller than the local numbered suite. It does not run every utils/bin/checks script, and it does not crawl the live site or prove that an external page is still there. Those are different questions for the monitoring installment that follows this one. It also does not give GitHub Actions my editorial judgment. Pushing starts the workflow; it does not replace my decision to push.
For a small change, I start with the focused checks that answer the likely failure modes, build the site, and inspect the rendered page. Before committing, I look at the diff and the staged files again. If I choose the broader local suite, I treat its repairs and generated output as new changes to review, not as invisible housekeeping. After a push, I want the Pages job to succeed, and I want to know what it actually deployed. The handoff from “the command passed” to “this is the version I meant to publish” is still mine.
Try the Gate Without Touching This Site
The Hands-On companion gives you a small prepublication runner with invented pages and assets. You can make one gate fail, see what gets skipped, and inspect the fixture without running this repository’s mutating checks or contacting a service. It is a teaching lab, not a substitute for the actual site’s Ruby plugins, Jekyll build, or Pages workflow.
Current State
This site has useful, layered defenses: Ruby checks for URL and tag contracts, a substantial local script suite, a Jekyll build, and a narrower Pages deployment job. It also has overlap, warning-only steps, repair steps, and a hook argument that misses the check it intended to skip. I would rather describe those edges plainly than sell the pipeline as a green light that means everything is fine.
The next article looks at the other side of deployment. A successful upload does not tell me whether the live pages, redirects, images, and outside links keep working tomorrow.
Join the Discussion
Comments for this post live in GitHub Discussions. That keeps moderation in one place and gives the conversation a stable home.