Every push to main of machinebehavior.io starts the workflow .github/workflows/conformity.yml; since ADR-0029 that push is a merged pull request or a bot commit. The workflow also runs every Monday at 06:17 UTC, by hand, and on every pull request to main. It has five jobs. Job 2 runs only if job 1 passes; jobs 3 and 5 run beside them and the deploy does not wait for them; job 4 runs after each successful deploy from main. A pull request run is the required check before the merge: it runs job 1 on the merge result of the pull request, commits nothing and skips job 2. The workflow grants no permissions itself; each job asks for its own, listed under its heading.
Job 1: conformity checks
Permissions: contents: write, for the bot commit when the deploy key is missing.
- Check out
mainwith full history (a pull request run checks out the merge result). On every run except a pull request run, checkout uses the deploy key in the secretCONFORMITY_PUSH_KEYwhen it exists, so the bot commit in step 3 passes the branch rules onmain. - Run the conformity action against every published page (see Conformity gate).
- Commit
conformity/latest.jsonandconformity/index.htmlasconformity-bot, with[skip ci]in the message, and push. Skipped on pull request runs. - Gate step: read the overall result; anything other than
passfails the job with "deploy blocked, previous build stays live".
Job 2: deploy to Pages
Permissions: contents: read, pages: write and id-token: write for the deploy, and actions: read and issues: read for the two snapshots in steps 4 and 5.
- Check out
mainagain, after the bot commit, without persisting the credentials. - Refresh the map: run
scripts/crawl_map.pyagainst the live sites. The newmap/graph.jsonreplaces the committed one only if it has at least as many nodes and links. A partial crawl (for example Substack answering 403 to the runner) keeps the committed snapshot. This step may fail without failing the job, and its result is never committed. - Rebuild the search index:
scripts/build_search.pymerges the refreshed map with the docs and services intoinside/search.json, so search matches the deployed map. It may fail without failing the job (the committed index is deployed) and is never committed. See Top bar and search. - Snapshot the deploy feed: read the last 15 Actions runs of machinebehavior.io and tychat.io and write
inside/deploys.jsonfor Inside. Like the map refresh, it may fail without failing the job and is never committed. - Snapshot the issues:
scripts/board_snapshot.pyreads the open and recently closed GitHub Issues and writesinside/board/issues.jsonfor the board. It may fail without failing the job and is never committed. See Ticket board. - Upload the repository as the Pages artifact and deploy it.
Job 3: workflow security (zizmor)
Permissions: contents: read.
zizmor checks every workflow and the conformity action under .github/ for template injection, broad permissions, persisted credentials, unpinned or impostor actions, and actions with known advisories. It runs on every trigger, pull requests included. A finding fails this job and never blocks the deploy; it is a required check only if the ruleset names it. Accepted findings have an inline # zizmor: ignore[<rule>] comment, and .github/zizmor.yml lists each one with its reason. Details in conformity/README.md.
Job 4: submit changed URLs to IndexNow
Permissions: contents: read, to check out the deployed commit with its history, and deployments: read, to find where the range starts.
Runs after job 2 succeeds on main (#85).
- Check out the commit job 2 deployed, recorded by its step "Record the deployed commit", with full history.
- Find the start of the range: the commit of the newest
github-pagesdeployment with a success status, other than this run's. Starting there, and not at the commit before the push, also covers pushes whose own run failed or was cancelled, and deploys from scheduled and manual runs. A deployment carries its run's trigger commit, which comes before that run's bot commit, so the range also holds the previous bot commit and/conformity/goes out with most submissions. When the API gives no such deployment, a push run starts at the commit before the push (github.event.before); any other run submits nothing. scripts/indexnow.pylists the files that changed between the start and the deployed commit. Each changedindex.htmlof a page insitemap.xmlbecomes its URL, a changedllms.txtis added, and the sitemap URL goes with every submission. Removed pages are left out. When nothing a search engine reads changed, the job stops here.python3 scripts/test_indexnow.pytests the script on a small repository built in a temporary folder; job 5 runs it.curlposts the host, the key, the key location and the URL list tohttps://api.indexnow.org/indexnowand logs the HTTP status: 200 means submitted, 202 received with the key check pending. Any other status, or no URL list, is a warning in the job log.
The key is public by design: the engines fetch /a738b040f4845f81e1a541ccd2b9e05d.txt from the site root to check that a submission comes from the site's owner. The job is continue-on-error and never fails the run; the deploy is already live, and a refused submission waits for the next crawl. To check a submission, read the job log of the run.
Job 5: script tests
Permissions: contents: read.
Runs the tests of the scripts this workflow runs, on every trigger, pull requests included: python3 scripts/test_indexnow.py (#85). A failure fails this job and never blocks the deploy; it is a required check only if the ruleset names it.
What happens around a deploy
- The deploy exporter polls the GitHub Actions API and writes each run, step and gate check to Loki and Prometheus. The Website deploys dashboard shows them.
- Inside reads its deploy feed from the snapshot written in step 4; the visitor's browser sends no request to GitHub.
Check before you push
git pull --rebase
python3 .github/actions/conformity/run.py --root . --config conformity/site-tier.json \
--requirements conformity/requirements.json --out conformity --dry-run
Push only when the dry run prints overall=pass. In scripts, compare the value; a grep for overall= succeeds on a failing run too.