coding.sgit.ai / admin / how this site is built

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
JobRuns whenWhat it doesIf it fails
validatealwaysRe-runs every generator in --check mode, then node admin/build/validate.jsNothing is tagged and nothing is deployed. The live site is untouched.
tag-releasepush to dev onlyWorks out this release's tag, verifies it, pushes itNo deploy. The release is wrong, not the site — fix the version and push again.
deployany push or dispatch, never a PRAssembles the tree (minus .git, .github) and publishes it to GitHub PagesThe 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:

#CheckWhy it exists
1Version agreementadmin/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 twiceA 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.
2Internal 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 namesA 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.
3Canonical host — every page declares a rel="canonical", and every canonical and og:url is on the host named in CNAMEA 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.
4The 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 blockSee 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:

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:

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.

FileOwnsGenerated from
admin/build/version.txtThe version — single source of truthhand-edited, once per release
admin/build/chrome.pyThe nav and footer of every page, and the version badge in all of themone definition in the script
admin/build/pagelib.pyThe shared page shell and the write-or-check writer
admin/build/gen_documents.pydocuments/data/documents.json + briefs/*.md
admin/build/gen_llms_full.pyllms-full.txtllms.txt, index.md, briefs/*.md
admin/build/gen_rules.pyrules/index.htmldata/rules.json
admin/build/gen_inline.pyEvery number and every code example on every pagebriefs/conventions__machine-readable.json and briefs/*.md
admin/build/gen_sitemap.pysitemap.xmlthe tree, dated from the versions table
admin/build/validate.jsThe 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

Repository
Release branch
dev — the default branch, and what GitHub Pages serves
Host
coding.sgit.ai, from CNAME; the gate checks every canonical URL against it
Dependencies
Python 3 and Node, both stdlib-only. Nothing to install.
Licence
Site content CC BY 4.0 — Dinis Cruz, with AI co-authorship (Claude, Anthropic)

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.