From b65b2f5b5964b6dfd1516171fb3e230454c333d8 Mon Sep 17 00:00:00 2001 From: Hannes Stiebitzhofer Date: Mon, 14 Sep 2026 00:43:16 +0200 Subject: [PATCH] Split the post-deploy smoke probe by what each half can prove (#112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The step added in #111 turned every deploy on main red. From the runner, the two /.well-known/ paths returned 200 — the fix it was written to guard works — but the / and /feed.xml canaries 403'd through ten retries each. Cloudflare bot protection blocks the runner's datacenter IP on ordinary paths and exempts /.well-known/ by design, so ACME and security.txt keep working. Exactly the paths we care about got through; the canaries could not. Not a site fault: the origin serves all four, and both canary paths return 200 from a residential IP. So the probe splits: - the origin, all paths, for artifact completeness — no bot protection in front of it, and this is the #110 class (a directory silently missing from the artifact) generalized past the two files that happened to expose it - the canonical host, /.well-known/ only, for the Worker's rewrite — the reachable subset, now asserting content types rather than status, since text/plain and application/pgp-keys ARE the requirement from #107 and #109 and a 200 with the wrong type is still a regression Dropping the canaries is not a weakening: they existed to show the site was up, and a 200 from /.well-known/security.txt on that same host already shows it. Rejected a Cloudflare WAF skip rule for GitHub's IP ranges — a manual console change that widens the site's exposure to make a test convenient, covering ground the origin probe already covers. Closes #112 --- .github/workflows/deploy.yml | 37 ++++++++++++++++++++++++++++-------- CLAUDE.md | 5 ++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2ddd5b8..80bceb6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,16 +46,37 @@ jobs: # A green deploy is not proof the site serves what we built: #110 shipped a "successful" # deploy whose artifact silently omitted .well-known/, and only a manual curl caught it. # Fail the run instead, on the canonical host the Worker actually fronts. - - name: smoke — the canonical URLs answer + # Split by what each half can prove (#112). Cloudflare bot protection 403s the runner's + # datacenter IP on ordinary paths but exempts /.well-known/ by design (so ACME and + # security.txt keep working), so the canonical host is only probeable there. + - name: smoke — the deployed artifact is complete (origin) run: | set -uo pipefail fail=0 - for path in / /feed.xml /.well-known/security.txt /.well-known/pgp-security.asc; do - # -f makes a 4xx an error so --retry-all-errors keeps trying: the Worker caches its - # own 404 for 60s, so the first probe after a deploy can legitimately still miss. - code=$(curl -fsS -L -o /dev/null -w '%{http_code}' \ - --retry 10 --retry-delay 10 --retry-all-errors \ - "https://material-identity.eu${path}") || { code="${code:-000} FAILED"; fail=1; } - printf '%-34s %s\n' "$path" "$code" + # No bot protection here, so this half covers every path — it is the #110 class + # (a directory silently missing from the artifact), generalized beyond two files. + for path in / /feed.xml /well-known/security.txt /well-known/pgp-security.asc; do + code=$(curl -fsS -L -o /dev/null -w '%{http_code}' --retry 5 --retry-delay 10 \ + --retry-all-errors "https://material-identity.github.io/dictionary${path}") \ + || { code="${code:-000} FAILED"; fail=1; } + printf '%-32s %s\n' "$path" "$code" done exit $fail + + - name: smoke — the canonical URLs answer with the right types (worker) + run: | + set -uo pipefail + fail=0 + # A 200 with the wrong type is still a regression: the content types ARE the + # requirement from #107/#109. -f makes a 4xx an error so --retry-all-errors keeps + # trying; the Worker caches its own 404 for 60s, so the first probe can legitimately miss. + check() { + local path=$1 want=$2 got + got=$(curl -fsS -L -o /dev/null -w '%{content_type}' --retry 10 --retry-delay 10 \ + --retry-all-errors "https://material-identity.eu${path}") || got="unreachable" + printf '%-32s %s\n' "$path" "$got" + [ "$got" = "$want" ] || { echo " expected: $want"; fail=1; } + } + check /.well-known/security.txt 'text/plain; charset=utf-8' + check /.well-known/pgp-security.asc 'application/pgp-keys' + exit $fail diff --git a/CLAUDE.md b/CLAUDE.md index dd53d9b..a50e2e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,7 +119,10 @@ section for the full explanation). not under `published/`. **Nothing emitted into `site/` may start with a dot**: `upload-pages-artifact` tars with `--exclude=.[^/]*`, so the build writes `site/well-known/` and the Worker rewrites `/.well-known/` onto it (#110). A build test asserts the site is - dot-free, and `deploy.yml` smoke-tests the live URLs — a green deploy alone proved nothing + dot-free, and `deploy.yml` smoke-tests after deploying — a green deploy alone proved nothing. + Two probes, because Cloudflare 403s the runner on ordinary paths but exempts `/.well-known/` + (#112): the origin for artifact completeness, the canonical host for the Worker's rewrite and + content types. Probe the origin, not `material-identity.eu`, for anything outside `.well-known` - `REVIEW.md` — what reviewers check beyond CI; read it before reviewing any publish PR - `standards/` — local-only licensed docs; only its README is committed