Merge pull request #8 from pip-install-python/sync/1.6.22-1.6.33 #12
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. | |
| # | |
| # ======================= 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. | |
| # ========================================================================== | |
| # | |
| # 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 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. | |
| outputs: | |
| deployed: ${{ steps.hook.outputs.deployed }} | |
| steps: | |
| - 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: Trigger the Render deploy hook | |
| id: hook | |
| env: | |
| HOOK: ${{ secrets.RENDER_DEPLOY_HOOK_URL }} | |
| run: | | |
| if [ -z "$HOOK" ]; then | |
| # ::warning, not ::notice — dash-email's finding (2026-08-23): | |
| # with no hook AND a push Render's autoDeploy happened to skip, | |
| # nothing deployed at all and the build-match wait below timed | |
| # out on a deploy nobody had started. The wait refusing was | |
| # honest; the quiet notice was why the cause took a run to see. | |
| echo "::warning::RENDER_DEPLOY_HOOK_URL is not set — this push deploys only if render.yaml autoDeploy fires. The build-match wait below still holds for THIS commit either way." | |
| 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 | |
| # 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 != '' | |
| # 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 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)) | |
| 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] | |
| # 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. | |
| # 'skipped' as well as 'cancelled' (muicharts' guard, 2026-08-23): a | |
| # deploy job that never ran leaves no build-match wait behind it, so a | |
| # verify that fires anyway certifies whatever build happens to be | |
| # serving and turns one cause into two red jobs. | |
| if: >- | |
| always() && needs.deploy.result != 'cancelled' | |
| && needs.deploy.result != 'skipped' | |
| && (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" | |
| # 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." |