Skip to content

docs: amend DIVERGENCES 10 from my own re-measurement; take the dedup… #20

docs: amend DIVERGENCES 10 from my own re-measurement; take the dedup…

docs: amend DIVERGENCES 10 from my own re-measurement; take the dedup… #20

Workflow file for this run

name: CD
# Deploys llms.2plot.dev, then checks the live site.
#
# ======================= THE LIVE HALF IS DORMANT =========================
# Everything that touches a running host — the build-match wait and the whole
# `verify` job — is gated on the repository variable `SITE_URL`. While it is
# unset those steps skip with a notice and CD is effectively "CI plus an
# optional deploy trigger".
#
# This is not caution for its own sake. The template shipped
# SITE_URL=https://boilerplate.2plot.dev hard-coded, so an unguarded run here
# would poll ANOTHER SITE's /healthz waiting for a commit it will never
# serve, then battery it and report red — a fork failing CD on day one over a
# host it does not own. (Same class as the excalidraw finding: inherited
# workflows reference things that do not exist in the fork and deploy hosts
# that are not yours.) The guard is a recorded divergence — see
# DIVERGENCES.md — and it stays.
#
# STATUS 2026-08-26: the cutover HAPPENED. https://llms.2plot.dev serves this
# app (healthz `app: "llms"`, build == HEAD, verified by hand every round),
# so the remaining condition is met and the variable is simply not set yet.
#
# TO UNGUARD — one repository setting, no workflow edit:
# Settings → Secrets and variables → Actions → Variables →
# SITE_URL = https://llms.2plot.dev
# The build-match wait and the whole verify job come alive on the next push,
# and hand-verification stops being the only proof this repo has.
# ==========================================================================
#
# HOW THIS REPO DEPLOYS (1.6.35): Render watches the `release` branch, and
# `release` is written by exactly one thing — the `deploy` job below, which
# fast-forwards it to the run's own sha after `needs: [test]` is green. A
# push to main is therefore NOT a deploy; it is a candidate. The old shape
# POSTed to a Render deploy hook, which shipped whatever main happened to
# hold whether or not the matrix agreed. Nothing here holds a deploy secret
# any more: the mechanism is a ref and the platform's own autoDeploy.
#
# `build == HEAD` in the traps now means HEAD of `release`. `main` ahead of
# `release` is an uncertified push pending, never drift and never a hand
# deploy.
on:
push:
branches: [main]
workflow_dispatch:
inputs:
target_url:
description: Site to verify (skips the deploy when set to another host)
required: false
type: string
permissions:
contents: read
concurrency:
group: cd-production
cancel-in-progress: false
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
# No hard-coded fallback, deliberately: an empty SITE_URL is the signal
# that no service exists yet, and every live step keys off it. A default
# here would silently re-point this workflow at somebody else's host.
SITE_URL: ${{ inputs.target_url || vars.SITE_URL }}
jobs:
test:
name: ci
uses: ./.github/workflows/ci.yml
deploy:
name: deploy to render
needs: [test]
runs-on: ubuntu-latest
# Long enough for the build-match wait below (up to 100 × 15s = 25
# minutes) and no longer. Without it the job inherits GitHub's six-hour
# default, which is how a platform that never comes back healthy holds
# the `cd-production` concurrency group all day. Sized for the WORST
# build, not the median: a floor bump busts the Docker dependency cache
# by design, so this pipeline's most important deploy is also Render's
# slowest — dash-email's wait timed out on exactly that build class
# (2026-08-23) with the old 60 × 15s window.
timeout-minutes: 30
environment:
name: production
# Restore `url: ${{ inputs.target_url || vars.SITE_URL }}` when
# SITE_URL is set. Omitted while dormant rather than evaluated to an
# empty string — the deployment panel simply shows no link rather than
# this repo's runs hinging on how GitHub treats a blank environment
# URL.
# `contents: write` on THIS job only — it is the one thing in the
# workflow that writes to the repository (the `release` ref). The
# workflow-level grant above stays `read`.
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
# FULL history, not the default depth-1 shallow clone. A shallow
# HEAD pushed onto an EXISTING `release` is rejected as
# non-fast-forward ("fetch first") because the clone cannot show
# the remote that release's tip is an ancestor. The template's
# FIRST promote (ea4e104, 2026-08-29) succeeded only because
# `release` did not exist yet; its second (747d8b3) failed in one
# second. tests/test_cd_promotes_release.py pins the depth.
fetch-depth: 0
- name: Say plainly that the live half is dormant
if: env.SITE_URL == ''
run: |
echo "::warning::The SITE_URL repo variable is unset, so the build-match wait and the live batteries are SKIPPED — this run certifies nothing about production. The site is live at https://llms.2plot.dev; set SITE_URL to it (Settings → Variables) to bring the live half up. See this workflow's header."
- name: Promote to release
# Only a push to main promotes. A workflow_dispatch that names
# another host (target_url) is a verify-only run against that host
# and must not move this repo's release ref. The hook step this
# replaced had no such guard.
if: github.event_name == 'push' || inputs.target_url == ''
run: |
# Fast-forward push of the run's own sha. `needs: [test]` is the
# gate: a red matrix never reaches this step, so `release` cannot
# receive an uncertified commit by construction. On the FIRST run
# `release` does not exist yet and this push creates it.
#
# NOT --force, on purpose: a non-fast-forward means somebody wrote
# `release` by hand (a rollback, a hotfix). This job then FAILS
# and says so; a force here would silently undo that person.
#
# GITHUB_TOKEN pushes trigger no workflows — correct: `on:` is
# main-only, and it is Render, not Actions, that reacts to
# `release` (render.yaml `branch: release`, autoDeploy on).
# Refs are a filesystem: a `release/<anything>` branch (any depth
# — muischeduler carried a 2026-08-01 `release/v0.1.0`) makes
# `release` a DIRECTORY and this push fails with "cannot lock
# ref". Owner step: delete or rename every release/* branch —
# never --force (sync item 13).
if git ls-remote --heads origin 'release/*' | grep -q .; then
echo "::error::a release/* branch exists — 'release' cannot also be a ref. Delete or rename every release/* branch (owner step, sync item 13), then re-run failed jobs."
git ls-remote --heads origin 'release/*'
exit 1
fi
git push origin "HEAD:refs/heads/release"
echo "::notice::release → ${GITHUB_SHA} (fast-forward)"
- name: Wait for THIS build to serve traffic
# Skips while SITE_URL is unset — see the header. Without this guard
# the loop below polls a host this repo does not own for twenty-five
# minutes and then fails the run.
if: env.SITE_URL != ''
# Render deploys `release`, which the step above just set to this
# run's sha, so the wait is unchanged: hold until /healthz reports
# THIS build. The old shape skipped this wait and ran the battery
# three seconds later against the PREVIOUS release — invisible for
# the workflow's whole life (the old build always already passed
# the old battery) until a new surface made the race lose; found on
# muicharts, 2026-08-21, fleet-wide class.
env:
GH_TOKEN: ${{ github.token }}
run: |
# A bare 200 proves nothing about WHICH build answered: /healthz
# now reports the running instance's commit (RENDER_GIT_COMMIT),
# and this loop holds until it equals the SHA that triggered this
# run. Fallback: a build that predates the field gets the old
# sustained-health wait, with a warning naming what it can't tell
# — that fallback fires exactly once, on the deploy carrying the
# field for the first time.
want="${GITHUB_SHA}"
matched=0
ok=0
checked=""
for _ in $(seq 1 100); do
body="$(curl -fsS "$SITE_URL/healthz" 2>/dev/null || true)"
if [ -n "$body" ]; then
build="$(printf '%s' "$body" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("build",""))' 2>/dev/null || true)"
if [ "$build" = "$want" ]; then
matched=$((matched + 1))
[ "$matched" -ge 3 ] && break
elif [ -z "$build" ]; then
ok=$((ok + 1))
elif [ "$build" != "$checked" ]; then
# Fail FAST when a LATER commit already serves (1.6.25's
# trap, implemented rather than described): a bot-merged PR
# or a following push can ship past this run, and waiting
# the full 25 minutes to go red hides which of the two
# things happened. `ahead` means the live build is a
# descendant of ours — our artifact is superseded and that
# commit's own run must certify it.
checked="$build"
rel="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${want}...${build}" --jq .status 2>/dev/null || true)"
if [ "$rel" = "ahead" ]; then
echo "::error::superseded — $build already serves and is a descendant of $want; that commit's own run must certify"
exit 1
fi
echo "::notice::live build $build is ${rel:-unknown} relative to $want — still waiting for this run's build"
fi
fi
sleep 15
done
if [ "$matched" -ge 3 ]; then
echo "::notice::live /healthz reports build $want — verifying the artifact this run shipped."
elif [ "$ok" -ge 5 ]; then
echo "::warning::live /healthz predates the build field — cannot prove WHICH build is serving; verified sustained health only. This warning should appear exactly once."
else
echo "::error::$SITE_URL never served this run's build ($want) and never became reliably healthy"
exit 1
fi
verify:
name: verify the live site
needs: [deploy]
# ONLY after a SUCCESSFUL deploy (1.6.35 fix-forward, ops finding on the
# template's run 33262495272): the old `!= 'cancelled' && != 'skipped'`
# admitted 'failure', so when the promote step failed this job ran
# anyway and reported GREEN — it had smoke-tested the PREVIOUS build. A
# verify that passes when nothing deployed must not exist. (muicharts'
# 2026-08-23 `!= 'skipped'` half is subsumed: skipped is not success.)
#
# The SITE_URL clause is this fork's own (DIVERGENCES.md 4) and STAYS:
# the whole job is dormant until the variable names a real service, and
# `vars.SITE_URL` rather than `env.SITE_URL` because a job-level `if` is
# evaluated before the job's env context exists.
if: >-
needs.deploy.result == 'success'
&& (inputs.target_url || vars.SITE_URL) != ''
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
# The fleet Python — tests/test_python_version.py pins this literal
# against the Dockerfile's FROM tag.
python-version: "3.14"
- name: The live build IS this run's sha
# Belt to the `if:` above's braces, and stronger: it also catches a
# promote that succeeded while Render's build did not, or a later
# run that promoted past us between deploy and verify. Skipped on a
# workflow_dispatch against another host (its shas are not ours).
if: github.event_name == 'push' || inputs.target_url == ''
run: |
build="$(curl -fsS "$SITE_URL/healthz" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("build",""))')"
if [ "$build" != "$GITHUB_SHA" ]; then
echo "::error::verify refuses: $SITE_URL serves build '${build:-<none>}', this run is $GITHUB_SHA — nothing this run shipped is on the wire to verify"
exit 1
fi
echo "::notice::verifying build $build (this run's sha)"
# The network battery first: it is the same script, with the same check
# names, that CI ran against the container this deploy shipped. A name
# that passed in CI and fails here isolates the fault to the deploy.
- name: Network smoke battery
run: python scripts/network_smoke.py --base-url "$SITE_URL"
# Then the satellite-specific checks the battery does not make: every
# canonical, every crawler body, and every peer llms.txt in the
# directory actually resolving.
- name: Smoke-test the deployment
run: python scripts/smoke_live.py "$SITE_URL"
- name: Report
if: failure()
run: |
echo "::error::Live verification failed for $SITE_URL. Every failure these check for is silent in production: a site identity that fell back to a framework default, a stale dash-improve-my-llms artifact, a canonical on the wrong host, a page serving the JavaScript stub, a missing network directory, and dead peer llms.txt links."