Skip to content

Commit 6ed6d7b

Browse files
ten9876claude
andcommitted
Make the deploy smoke test survive a propagating deployment
The first run failed 12 checks against a site that was serving all of them correctly. Two causes, both in the checker: - It made a separate request per assertion — ~50 against a deployment that had existed for ten seconds. Cloudflare had not finished handing it to every edge, so some returned 404 or dropped the connection, and each of those read as a missing header. Now each URL is fetched once and asserted against many times: 19 requests, each retried with backoff. - curl's stderr went to /dev/null, so a connection failure was indistinguishable from a header that was genuinely absent. Failures now report the curl error. Also waits for the deployment to answer before starting, rather than sleeping ten seconds and hoping, and reads headers from the final response only — with -L the dump holds every hop. Verified: 60/60 against production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 28039b2 commit 6ed6d7b

2 files changed

Lines changed: 148 additions & 62 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,4 @@ jobs:
8282
# check it on every deploy. Runs against the fresh deployment URL.
8383
- name: Smoke-test the agent-discovery surface
8484
if: ${{ env.CF_TOKEN != '' && steps.deploy.outputs.deployment-url != '' }}
85-
run: |
86-
sleep 10
87-
bash scripts/check-agent-discovery.sh "${{ steps.deploy.outputs.deployment-url }}"
85+
run: bash scripts/check-agent-discovery.sh "${{ steps.deploy.outputs.deployment-url }}"

scripts/check-agent-discovery.sh

Lines changed: 147 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,108 +7,196 @@
77
# Every one of these is read by someone else's software, so a wrong status code
88
# or content type is a silent outage — nothing on the human site would break.
99
# CI runs this after each deploy.
10+
#
11+
# Each URL is fetched once and asserted against many times. Fetching per
12+
# assertion instead made ~50 requests at a deployment that was still
13+
# propagating, and read every transient failure as a missing header.
1014

1115
set -uo pipefail
1216

1317
BASE="${1:-https://www.aethersdr.com}"
1418
BASE="${BASE%/}"
1519
FAILURES=0
1620

21+
CACHE=$(mktemp -d)
22+
trap 'rm -rf "$CACHE"' EXIT
23+
1724
fail() { printf ' FAIL %s\n' "$*"; FAILURES=$((FAILURES + 1)); }
1825
pass() { printf ' ok %s\n' "$*"; }
19-
2026
lower() { tr '[:upper:]' '[:lower:]'; }
2127

22-
# expect_header <path> <header-name> <substring> [extra curl args...]
23-
# An empty substring means "present and non-empty".
28+
# Headers of the final response only — with -L the dump holds every hop.
29+
last_headers() {
30+
awk '/^HTTP\//{buf=""} {buf = buf $0 "\n"} END{printf "%s", buf}' "$1" | tr -d '\r'
31+
}
32+
33+
# fetch <key> <path> [curl args...]
34+
# Retries transient failures: a deployment that has just gone out is not
35+
# uniformly available yet, and a one-off connection error is not a finding.
36+
fetch() {
37+
local key="$1" path="$2"; shift 2
38+
local body="$CACHE/$key.body" hdr="$CACHE/$key.hdr" code
39+
local attempt
40+
41+
for attempt in 1 2 3 4 5; do
42+
code=$(curl -sS -L --max-time 25 -o "$body" -D "$hdr" -w '%{http_code}' \
43+
"$@" "$BASE$path" 2>"$CACHE/$key.err")
44+
if [[ "$code" == 2* ]]; then
45+
printf '%s' "$code" > "$CACHE/$key.code"
46+
printf '%s' "$path" > "$CACHE/$key.path"
47+
return 0
48+
fi
49+
sleep $((attempt * 3))
50+
done
51+
52+
printf '%s' "${code:-000}" > "$CACHE/$key.code"
53+
printf '%s' "$path" > "$CACHE/$key.path"
54+
}
55+
56+
label() { cat "$CACHE/$1.path" 2>/dev/null || printf '%s' "$1"; }
57+
58+
# expect_ok <key>
59+
expect_ok() {
60+
local code; code=$(cat "$CACHE/$1.code")
61+
if [[ "$code" == 2* ]]; then
62+
pass "$(label "$1") — HTTP $code"
63+
else
64+
fail "$(label "$1") — HTTP $code$( [[ -s "$CACHE/$1.err" ]] && printf ' (%s)' "$(head -1 "$CACHE/$1.err")" )"
65+
fi
66+
}
67+
68+
# expect_header <key> <header-name> <substring> ("" = present and non-empty)
2469
expect_header() {
25-
local path="$1" header="$2" want="$3"; shift 3
26-
local got value
27-
got=$(curl -sSL --max-time 20 -o /dev/null -D - "$@" "$BASE$path" 2>/dev/null \
28-
| tr -d '\r' | grep -i "^$header:" | tail -1)
29-
value=$(printf '%s' "${got#*:}" | sed 's/^ *//')
70+
local key="$1" header="$2" want="$3"
71+
local value
72+
value=$(last_headers "$CACHE/$key.hdr" | grep -i "^$header:" \
73+
| sed 's/^[^:]*: *//' | paste -sd, - )
3074

3175
if [[ -z "$want" ]]; then
32-
if [[ -n "$value" ]]; then pass "$path $header present ($value)"
33-
else fail "$path$header missing"; fi
76+
if [[ -n "$value" ]]; then pass "$(label "$key") $header: $value"
77+
else fail "$(label "$key")$header missing"; fi
3478
return
3579
fi
3680

3781
if [[ "$(printf '%s' "$value" | lower)" == *"$(printf '%s' "$want" | lower)"* ]]; then
38-
pass "$path$header contains '$want'"
82+
pass "$(label "$key")$header contains '$want'"
3983
else
40-
fail "$path$header expected '$want', got '${value:-<none>}'"
84+
fail "$(label "$key")$header expected '$want', got '${value:-<none>}'"
4185
fi
4286
}
4387

44-
# expect_body <path> <substring> [extra curl args...]
88+
# expect_body <key> <substring>
4589
expect_body() {
46-
local path="$1" want="$2"; shift 2
47-
local code body
48-
body=$(curl -sSL --max-time 20 -w '\n%{http_code}' "$@" "$BASE$path" 2>/dev/null)
49-
code="${body##*$'\n'}"
50-
body="${body%$'\n'*}"
51-
if [[ "$code" != "200" ]]; then
52-
fail "$path — HTTP $code"
53-
elif [[ "$body" == *"$want"* ]]; then
54-
pass "$path — 200, contains '$want'"
90+
local key="$1" want="$2"
91+
if grep -qF -- "$want" "$CACHE/$key.body" 2>/dev/null; then
92+
pass "$(label "$key") — body contains '$want'"
5593
else
56-
fail "$path — 200 but missing '$want'"
94+
fail "$(label "$key") — body missing '$want'"
5795
fi
5896
}
5997

98+
MD='Accept: text/markdown'
99+
HTML='Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
100+
60101
echo "Checking $BASE"
61102

103+
# A deployment that has just gone out needs a moment before every edge has it.
104+
printf 'Waiting for the deployment to answer'
105+
for attempt in $(seq 30); do
106+
if curl -sS -o /dev/null --max-time 10 -f "$BASE/" 2>/dev/null; then
107+
printf ' ready\n'
108+
break
109+
fi
110+
printf '.'
111+
sleep 4
112+
done
113+
114+
echo
115+
echo "Fetching"
116+
fetch home_html / -H "$HTML"
117+
fetch home_md / -H "$MD"
118+
fetch blog_html /blog -H "$HTML"
119+
fetch blog_md /blog -H "$MD"
120+
fetch lineage_html /lineage -H "$HTML"
121+
fetch lineage_md /lineage -H "$MD"
122+
fetch index_md /index.md
123+
fetch robots /robots.txt
124+
fetch sitemap /sitemap.xml
125+
fetch catalog /.well-known/api-catalog
126+
fetch openapi /api/v1/openapi.json
127+
fetch site /api/v1/site.json
128+
fetch posts /api/v1/posts.json
129+
fetch status /api/v1/status.json
130+
fetch apidocs /api/v1/docs.md
131+
fetch skills /.well-known/agent-skills/index.json
132+
fetch skill_dl /.well-known/agent-skills/aethersdr-downloads/SKILL.md
133+
fetch skill_facts /.well-known/agent-skills/aethersdr-project-facts/SKILL.md
134+
fetch skill_rel /.well-known/agent-skills/aethersdr-release-notes/SKILL.md
135+
62136
echo
63137
echo "Discoverability"
64-
expect_body /robots.txt "Sitemap: https://www.aethersdr.com/sitemap.xml"
65-
expect_body /sitemap.xml "<urlset"
66-
expect_body /sitemap.xml "<loc>https://www.aethersdr.com/</loc>"
67-
expect_header /sitemap.xml content-type xml
68-
expect_header / link 'rel="api-catalog"'
69-
expect_header / link 'rel="service-desc"'
70-
expect_header / link 'rel="service-doc"'
71-
expect_header / link 'rel="describedby"'
72-
expect_header / link 'rel="sitemap"'
138+
expect_ok robots
139+
expect_body robots "Sitemap: https://www.aethersdr.com/sitemap.xml"
140+
expect_ok sitemap
141+
expect_body sitemap "<urlset"
142+
expect_body sitemap "<loc>https://www.aethersdr.com/</loc>"
143+
expect_header sitemap content-type xml
144+
for rel in api-catalog service-desc service-doc describedby sitemap; do
145+
expect_header home_html link "rel=\"$rel\""
146+
done
73147

74148
echo
75149
echo "API catalog + content API"
76-
expect_body /.well-known/api-catalog '"linkset"'
77-
expect_header /.well-known/api-catalog content-type application/linkset+json
78-
expect_body /api/v1/openapi.json '"openapi"'
79-
expect_body /api/v1/site.json '"name": "AetherSDR"'
80-
expect_body /api/v1/posts.json '"posts"'
81-
expect_body /api/v1/status.json '"status"'
82-
expect_body /api/v1/docs.md '# AetherSDR site content API'
83-
expect_header /api/v1/site.json content-type application/json
84-
expect_header /api/v1/site.json access-control-allow-origin '*'
150+
expect_ok catalog
151+
expect_body catalog '"linkset"'
152+
expect_header catalog content-type application/linkset+json
153+
expect_ok openapi
154+
expect_body openapi '"openapi"'
155+
expect_body site '"name": "AetherSDR"'
156+
expect_body posts '"posts"'
157+
expect_body status '"status"'
158+
expect_body apidocs '# AetherSDR site content API'
159+
expect_header site content-type application/json
160+
expect_header site access-control-allow-origin '*'
85161

86162
echo
87163
echo "Agent skills"
88-
expect_body /.well-known/agent-skills/index.json 'schemas.agentskills.io'
89-
expect_header /.well-known/agent-skills/index.json content-type application/json
90-
for skill in aethersdr-downloads aethersdr-project-facts aethersdr-release-notes; do
91-
expect_body "/.well-known/agent-skills/$skill/SKILL.md" "name: $skill"
92-
done
164+
expect_ok skills
165+
expect_body skills 'schemas.agentskills.io'
166+
expect_header skills content-type application/json
167+
expect_body skill_dl 'name: aethersdr-downloads'
168+
expect_body skill_facts 'name: aethersdr-project-facts'
169+
expect_body skill_rel 'name: aethersdr-release-notes'
93170

94171
echo
95172
echo "Markdown negotiation"
96-
for path in / /blog /lineage; do
97-
expect_header "$path" content-type text/markdown -H 'Accept: text/markdown'
98-
expect_header "$path" x-markdown-tokens '' -H 'Accept: text/markdown'
99-
expect_header "$path" vary Accept -H 'Accept: text/markdown'
100-
# The human site must be untouched by any of this.
101-
expect_header "$path" content-type text/html \
102-
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
173+
for key in home_md blog_md lineage_md; do
174+
expect_ok "$key"
175+
expect_header "$key" content-type text/markdown
176+
expect_header "$key" x-markdown-tokens ''
177+
expect_header "$key" vary Accept
178+
done
179+
expect_body home_md '# AetherSDR'
180+
expect_body blog_md '## v26'
181+
expect_body lineage_md '# Lineage'
182+
183+
echo
184+
echo "The human site is unaffected"
185+
for key in home_html blog_html lineage_html; do
186+
expect_ok "$key"
187+
expect_header "$key" content-type text/html
103188
done
104-
expect_body / '# AetherSDR' -H 'Accept: text/markdown'
105-
expect_body /blog '## v26' -H 'Accept: text/markdown'
189+
expect_body home_html '<!doctype html'
190+
expect_header home_html link 'rel="alternate"'
191+
expect_header index_md content-type text/markdown
106192

107193
echo
108-
echo "Security headers still applied on negotiated paths"
109-
expect_header / content-security-policy "default-src 'self'" -H 'Accept: text/markdown'
110-
expect_header / x-frame-options DENY -H 'Accept: text/markdown'
111-
expect_header / x-content-type-options nosniff
194+
echo "Security headers survive the Pages Function"
195+
for key in home_html home_md; do
196+
expect_header "$key" content-security-policy "default-src 'self'"
197+
expect_header "$key" x-frame-options DENY
198+
expect_header "$key" x-content-type-options nosniff
199+
done
112200

113201
echo
114202
if [[ $FAILURES -eq 0 ]]; then

0 commit comments

Comments
 (0)