How this site is built
A static site, in one repository, deployed by GitHub Pages from the dev branch. There is no framework, no build step you have to install, and no server. What there is is a release pipeline that refuses to publish a site that fails its own checks — and that pipeline is the first thing this site shipped, before it had any content to publish.
Everything below runs identically on a laptop and in CI. That is the whole design: the commands in the release checklist are the commands the workflow runs, so a release that passes locally passes in CI, and a release that fails in CI fails the same way in front of you.
The pipeline: three jobs, each gating the next
One workflow, .github/workflows/deploy-pages.yml, on every push to dev and main, on every pull request against them, and on manual dispatch.
# push to dev validate ──▶ tag-release ──▶ deploy │ │ │ │ │ └─ upload-pages-artifact, deploy-pages │ └─ git push origin refs/tags/vX.Y.Z └─ gen_*.py --check ; node admin/build/validate.js # pull request → validate only (no tag, no deploy) # push to main → validate, deploy; tag-release skips # dispatch → validate, deploy; tag-release skips
| Job | Runs when | What it does | If it fails |
|---|---|---|---|
| validate | always | Re-runs every generator in --check mode, then node admin/build/validate.js | Nothing is tagged and nothing is deployed. The live site is untouched. |
| tag-release | push to dev only | Works out this release's tag, verifies it, pushes it | No deploy. The release is wrong, not the site — fix the version and push again. |
| deploy | any push or dispatch, never a PR | Assembles the tree (minus .git, .github) and publishes it to GitHub Pages | The previously deployed version stays live. |
main is deploy-only, on purpose. tag-release is gated on refs/heads/dev, so a push to main validates and publishes without writing a tag. That makes main usable as a deploy test or a fallback without polluting the tag history.
The release gate
admin/build/validate.js is plain Node with no dependencies, so it runs anywhere Node exists. Four checks, in order, and any failure exits non-zero:
| # | Check | Why it exists |
|---|---|---|
| 1 | Version agreement — admin/build/version.txt against every page's version badge, the row in the versions table, llms.txt, llms-full.txt and index.md, with no version listed twice | A version that means different things in different files means nothing. The duplicate check exists because a blanket version-bump sed across a sibling site once rewrote the history table and shipped two rows with the same number. |
| 2 | Internal links — every relative href and src in every HTML file resolves to a file that exists, and every #fragment resolves to an id on the page it names | A static site's most common defect, and the one nobody notices until a reader does. The fragment half was added in v0.2.0 with the content: this site cross-links by section — “the alignment argument”, “the broken guard”, “Q4” — and a renamed heading turns every one of those into a link that lands at the top of the right page and says nothing. It found four on its first run. External and mailto: links are skipped, and a fragment on a .md or .txt target is not checked, because it is not an anchor. |
| 3 | Canonical host — every page declares a rel="canonical", and every canonical and og:url is on the host named in CNAME | A page that claims a canonical URL on the wrong host is worse than one that claims none. CNAME is the single source of truth for the hostname, here and in the generators. |
| 4 | The leak tripwire — nothing in the tree may look like a vault key, an AWS access key id, a GitHub token, an API secret or a private key block | See below. This is the check this site cannot afford to skip. |
The leak tripwire
A site about how software gets written quotes real sessions, real configuration and real terminal output — that is the point of it. It is also the one way this repository could publish something it cannot take back. So the gate scans every file in the tree, not just the HTML, for a handful of credential shapes:
- an sgit vault key — a long passphrase joined by a colon to a UUID-shaped id
- an AWS access key id (
AKIA…,ASIA…) - a GitHub token (
ghp_,gho_,ghu_,ghs_,ghr_) - an API secret key (
sk-…,sk-ant-…) - a PEM private key block
- a Slack token (
xoxb-and friends) - a bare 12-digit number — the shape of an AWS account id. Added in v0.2.0
The fix for a tripped wire is always to redact the snippet, never to widen the pattern. The one file exempt from the scan is validate.js itself, which necessarily carries the patterns — that exemption is what makes them writable at all.
The 12-digit pattern was added because the content release needed it. Two of the thirteen source documents carry the brief pack's own do-not-publish list, and that list names an AWS account id in order to forbid it. Publishing the list verbatim would publish the value.
Note what the pattern deliberately is not: it does not contain the account id. A tripwire that hard-codes the secret it is looking for has published the secret in the tripwire. It matches the shape instead — as every other pattern here does — which is also why it will occasionally fire on a legitimate 12-digit number, and why the answer to that is a redaction or a deliberate line-level exemption rather than a looser regex. What was redacted, and what was kept →
How CI decides the tag
Every push to dev is a minor release. The version is owned by one file and stated twice, and CI refuses to tag unless both agree:
# 1. the file that owns it admin/build/version.txt v0.1.0 # 2. the release commit's subject git commit -m "site v0.1.0: the pipeline, before the site" # CI then checks, in this order: newest release commit's version == version.txt # or: error, the two disagree tag vX.Y.Z sits on THAT commit # or: error, version was not bumped vX.Y.Z is the next minor after the latest tag # or a deliberate major to .0 git push origin refs/tags/vX.Y.Z # the load-bearing push
Three details are worth knowing, because each of them is a bug that was fixed once already on a sibling site:
- The release commit is not always HEAD. When a pull request lands as a merge commit, HEAD is the merge and the release commit is its parent. Every check anchors to the newest commit whose subject carries a version, not to HEAD.
- Historical tags are backfilled. On its first run the job reads the whole history and tags any commit whose subject carries a version it cannot find a tag for. It is idempotent, so it is a no-op on every run afterwards, and it checks the remote before pushing so it does not repeat work it has already done.
- A backfilled tag can be legitimately rejected.
GITHUB_TOKENcannot push a ref onto a commit whose tree carries a different.github/workflowsblob. Those are reported as warnings and the job still succeeds — the release's own tag is the load-bearing one, and a human can complete the set withgit push origin --tags.
What is generated, and how drift is caught
Anything that restates something else is generated from it. A hand-maintained twin of a file is a stale artefact with a longer fuse — the sitemap that forgets a page, the "full text" file that is missing three documents.
| File | Owns | Generated from |
|---|---|---|
admin/build/version.txt | The version — single source of truth | hand-edited, once per release |
admin/build/chrome.py | The nav and footer of every page, and the version badge in all of them | one definition in the script |
admin/build/pagelib.py | The shared page shell and the write-or-check writer | — |
admin/build/gen_documents.py | documents/ | data/documents.json + briefs/*.md |
admin/build/gen_llms_full.py | llms-full.txt | llms.txt, index.md, briefs/*.md |
admin/build/gen_rules.py | rules/index.html | data/rules.json |
admin/build/gen_inline.py | Every number and every code example on every page | briefs/conventions__machine-readable.json and briefs/*.md |
admin/build/gen_sitemap.py | sitemap.xml | the tree, dated from the versions table |
admin/build/validate.js | The release gate | — |
Each generator takes --check, which regenerates in memory and compares. CI runs all of them that way before validation, so a published page that has drifted from its source fails the build instead of quietly shipping.
gen_inline.py is the one specific to this site's subject. A style guide
whose examples have drifted from the code is worse than no style guide, and the same is true of
a site whose numbers have drifted from the survey they came from. So no number and no snippet
on this site is typed by a person: a page carries
<span class="cnt" data-k="python.files"></span> and
<figure class="src" data-src="01__python.md" data-block="0"></figure>,
and the generator fills both in place the way chrome.py fills the nav — the figure
from the published survey, the
example from the published document it was quoted in, with its real path and its own
Apache-2.0 notice attached.
What that does not buy is freshness. This repository holds the website, not the 217,266 lines it describes, so the survey is a photograph and not a feed. Every number carries its survey date, and closing the gap is tracked as R2.
One subtlety: the --check comparison ignores the nav and footer blocks. chrome.py rewrites those in place after generation, so comparing them would always differ on chrome alone. Stripping both blocks from both sides compares exactly what the generator is responsible for and nothing it is not.
The release checklist
Seven steps, in this order, and the order is load-bearing twice.
gen_inline.py runs after the generators, because it writes into pages they
have just produced. And the files that read the tree run after chrome has stamped the
version, or they assemble a stale version line.
# 1. bump the version and add its row admin/build/version.txt # vX.Y.Z, exactly once per release admin/versions.html # a row saying what changed admin/comms.html # what is outstanding, if it changed # 2. regenerate the pages python3 admin/build/gen_documents.py python3 admin/build/gen_rules.py # 3. propagate nav, footer and the version badge everywhere python3 admin/build/chrome.py # 4. write every number and example in — AFTER the pages exist python3 admin/build/gen_inline.py # 5. regenerate what reads the tree — AFTER chrome python3 admin/build/gen_llms_full.py python3 admin/build/gen_sitemap.py # 6. run exactly what CI runs python3 admin/build/gen_documents.py --check python3 admin/build/gen_rules.py --check python3 admin/build/gen_inline.py --check python3 admin/build/gen_llms_full.py --check python3 admin/build/gen_sitemap.py --check node admin/build/validate.js # 7. release git commit -am "site vX.Y.Z: ..." && git push -u origin dev
The repository
One trap, inherited and already sprung on a sibling site. The Python .gitignore this repository starts from carries build/, which silently swallows admin/build/ — every generator and the release gate pass locally and are absent from the checkout, so the validate job dies on a missing file before it can check anything. This repository carries the !admin/build/ negation from its first commit.