teams.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 a single role page 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.js Nothing is tagged and nothing is deployed. The live site is untouched.
tag-releasepush to dev onlyWorks out this release's tag, verifies it against the version file and the commit subject, 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. Six 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.
2Internal links — every relative href and src resolves to a file that exists, and every #fragment resolves to an id on the page it namesThis site cross-links between roster entries, the topologies page and the role-format page by section — a claim, an exclusion list, a learning — and a renamed heading turns every one of those into a link that lands at the top of the right page and says nothing.
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.
4The no-verbatim gate — every <blockquote> declares whose words it carries, and a third party's are capped at 40 wordsMost quotes on this site are the product team's own words (data-quote="founder"), which are theirs to publish. The cap still guards any future page quoting a third party — Wardley, Team Topologies, Cynefin.
5The roster is the data — every role in data/roster.json has a page, every roster page is in the data, and every tally written into a page is recomputed from the data and must agree06__ of the commissioning pack requires role counts to be generated, not claimed. See below.
6The leak tripwire — nothing in the tree may look like a vault key, an AWS access key id, a GitHub token, an API secret, a private key block, a Slack token or an AWS account id07__ of the pack requires this to be a build step, not a review step, precisely because this pack's own source material is a live product team's operational documents — the 8 April 2026 commit in the evolution page exists because this is a live risk, not a hypothetical one.

The roster is the data, in detail

Every count on the front page, the roster index and llms.txt is computed from data/roster.json — a page writes an empty slot, admin/build/chrome.py fills it after the generators run, and validate.js recomputes the same counts independently from the same file and fails on disagreement. Both halves have to be wrong in the same direction to ship a wrong number.

<!-- in the page source -->
<span class="tally" data-k="portable-core"></span> roles form the portable core

# chrome.py fills it, from data/roster.json
# validate.js recomputes the same count from the same file and fails on disagreement

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 roster"

# 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 worth knowing, each a bug that was already fixed once 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.

FileOwnsGenerated from
admin/build/version.txtThe version — single source of truthhand-edited, once per release
data/roster.jsonThe roster. Every role, its claim, its claim form, its exclusions, its teams and its commit countthe commissioning pack's teams__roster.json, unchanged
admin/build/chrome.pyThe nav and footer of every page, the version badge, and the roster talliesone definition in the script, plus data/roster.json
admin/build/pagelib.pyThe shared page shell, the write-or-check writer, and the tiny markdown this site's prose is authored in
admin/build/gen_roster.pyroster/ — every role, grouped by function, with its markdown twin data/roster.json
admin/build/gen_documents.pydocuments/ data/documents.json + briefs/*
admin/build/gen_llms_full.pyllms-full.txt llms.txt, index.md, roster/*/index.md, briefs/*.md
admin/build/gen_sitemap.pysitemap.xml the tree, dated from the versions table
admin/build/validate.jsThe release gate

Every generator has a --check mode, and CI runs all of them before the gate. A generated page that has drifted from its source is a build failure here, not a warning.

Why the markdown twins are generated too

Every roster entry publishes an HTML page and a markdown twin at the same URL stem, and the twin is not a second copy. The role's prose comes straight from data/roster.json and is rendered twice — once as HTML, once as markdown — through the same tiny inline-markdown renderer, so the two cannot drift out of sync with each other; either can still drift from the JSON, which is what --check catches.

The release checklist

These are the commands the workflow runs, in this order, for the reason each note gives:

# 1. bump the version — exactly once per release — and add a row to admin/versions.html
# 2. regenerate the pages that come from data
python3 admin/build/gen_roster.py
python3 admin/build/gen_documents.py

# 3. chrome AFTER the generators: it propagates the version badge, the nav and the
#    tallies into the pages they have just produced
python3 admin/build/chrome.py

# 4. the files that read the tree and the stamped twins — AFTER chrome, or they
#    assemble a stale version line
python3 admin/build/gen_llms_full.py
python3 admin/build/gen_sitemap.py

# 5. validate exactly what CI validates
python3 admin/build/gen_roster.py    --check
python3 admin/build/gen_documents.py --check
python3 admin/build/gen_llms_full.py --check
python3 admin/build/gen_sitemap.py   --check
node admin/build/validate.js

# 6. ship it
git commit -am "site vX.Y.Z: ..." && git push -u origin dev

One inherited trap, pre-empted

The Python .gitignore this repository starts from carries build/, which silently swallows admin/build/ — a sibling site shipped a first release whose validate job died on a missing file before it could check anything. The checks appeared to run and did not. The !admin/build/ negation is in this repository's first commit that adds it.