sync: geo.resolved reads THIS request's headers, on every backend #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CD | |
| # Deploys llms.2plot.dev, then checks the live site. | |
| # | |
| # ============================ B3 TODO ===================================== | |
| # THE LIVE HALF OF THIS WORKFLOW IS DORMANT UNTIL A RENDER SERVICE EXISTS. | |
| # | |
| # 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 for fifteen minutes 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.) Pointing it at llms.2plot.dev today is no | |
| # better: that domain still serves the OUTGOING site this migration replaces. | |
| # | |
| # TO UNGUARD, in two steps, no workflow edit either time: | |
| # B3 (staging): set the repo variable SITE_URL to the service's | |
| # https://<name>.onrender.com URL. The wait and the battery | |
| # come alive against staging. | |
| # B5 (cutover): change that same variable to https://llms.2plot.dev once | |
| # the domain moves. | |
| # ========================================================================== | |
| # | |
| # The deploy step POSTs to a Render deploy hook held in the | |
| # RENDER_DEPLOY_HOOK_URL secret. Without that secret the step is skipped and | |
| # the workflow goes straight to verification — useful when Render is already | |
| # auto-deploying from GitHub on its own, and it means this repo doesn't fail | |
| # CD on day one for a secret it was never given. | |
| 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 60 × 15s) 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. | |
| timeout-minutes: 20 | |
| environment: | |
| name: production | |
| # B3 TODO: 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, and the | |
| # first run in this repo's history does not hinge on how GitHub treats | |
| # a blank environment URL. | |
| outputs: | |
| deployed: ${{ steps.hook.outputs.deployed }} | |
| steps: | |
| - name: Say plainly that the live half is dormant | |
| if: env.SITE_URL == '' | |
| run: | | |
| echo "::notice::No Render service yet — SITE_URL repo variable is unset, so the build-match wait and the live batteries are skipped. Set SITE_URL to the .onrender.com URL at B3 to bring them up (see this workflow's header)." | |
| - name: Trigger the Render deploy hook | |
| id: hook | |
| env: | |
| HOOK: ${{ secrets.RENDER_DEPLOY_HOOK_URL }} | |
| run: | | |
| if [ -z "$HOOK" ]; then | |
| echo "::notice::RENDER_DEPLOY_HOOK_URL is not set. Skipping the deploy trigger and verifying whatever is currently live." | |
| echo "deployed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| curl -fsS -X POST "$HOOK" > /dev/null | |
| echo "deployed=true" >> "$GITHUB_OUTPUT" | |
| - name: Wait for THIS build to serve traffic | |
| # B3 TODO: skips while SITE_URL is unset — see the header. Without | |
| # this guard the loop below polls a host this repo does not own for | |
| # fifteen minutes and then fails the run. | |
| if: env.SITE_URL != '' | |
| # NOT gated on the hook step: with RENDER_DEPLOY_HOOK_URL unset, | |
| # Render's own autoDeploy ships the commit minutes after the push — | |
| # and the old shape skipped this wait and ran the battery three | |
| # seconds later against the PREVIOUS release. That defect was | |
| # 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. | |
| 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 | |
| for _ in $(seq 1 60); 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)) | |
| 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] | |
| # B3 TODO: the whole job is dormant until SITE_URL names a real service. | |
| # `vars.SITE_URL` rather than `env.SITE_URL` because job-level `if` is | |
| # evaluated before the job's env context exists. | |
| if: >- | |
| always() && needs.deploy.result != 'cancelled' | |
| && (inputs.target_url || vars.SITE_URL) != '' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| # 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." |