forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
213 lines (195 loc) · 8.75 KB
/
Copy pathsmoke-published.yml
File metadata and controls
213 lines (195 loc) · 8.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# Fetches what a hiker's phone fetches, from the bucket it fetches it from.
#
# pipeline/DATA_RELEASES.md §3b is the design; this closes #94. Everything else
# in this repository verifies against local files and mocks - nothing had ever
# pulled a real PMTiles archive over real HTTP range requests from R2 and read
# a tile out of it.
#
# WHY THIS IS WEEKLY AND check-deployment.yml IS DAILY. They ask different
# questions and cost different amounts:
#
# check-deployment.yml daily, no downloads "can a browser REACH it"
# this weekly, ~18 MB "is what is there CORRECT"
#
# A hash cannot be checked without reading the bytes, so this one downloads -
# but only artifacts under a size budget, which is ~18 MB of the ~2.9 GB
# published. The archives above that budget are covered structurally instead:
# their header, root directory and first tile are read over range requests,
# which costs 3-4 requests and under 103 KB even for the 1.18 GB tier.
#
# Daily would be the wrong trade against a rate-limited r2.dev subdomain, and
# #395 is about putting a ceiling on the bill. Never is how #94 stayed open for
# five weeks after there was something to check.
#
# IT HOLDS NO CREDENTIALS. Same posture as check-upstream-freshness.yml and
# check-deployment.yml: its whole input is public HTTPS reads of the same
# objects a phone reads, so it is structurally incapable of changing anyone's
# map.
#
# IT DOES NOT FAIL THE RUN, for the reason check-deployment.yml records at
# length: GitHub emails on every scheduled failure, so a persistent fault would
# send a weekly identical email until it was filtered. The tracking issue is
# the signal.
name: Smoke test published data
on:
schedule:
# 09:40 UTC on Mondays. Off the hour, and after check-deployment.yml at
# 09:15 - if CORS is broken, that is the job that should say so, and this
# one reporting the same outage differently would be noise.
- cron: "40 9 * * 1"
workflow_dispatch:
inputs:
base:
description: "Bucket base to check. Defaults to the DATA_BASE_URL variable."
type: string
required: false
max_hash_bytes:
description: "Largest artifact to download and hash. Raise to cover the archives; expect GBs."
type: string
required: false
permissions:
contents: read
issues: write
concurrency:
group: smoke-published
cancel-in-progress: false
env:
ISSUE_TITLE: "Published data smoke test"
ISSUE_LABEL: deployment-health
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: pipeline
steps:
- name: Is there a bucket to check
id: can
working-directory: .
env:
OVERRIDE: ${{ inputs.base }}
CONFIGURED: ${{ vars.DATA_BASE_URL }}
run: |
BASE="${OVERRIDE:-$CONFIGURED}"
if [ -z "$BASE" ]; then
echo "::warning::No DATA_BASE_URL is set, so there is no published data to smoke test. See LAUNCH_CHECKLIST.md step 2."
echo "go=false" >> "$GITHUB_OUTPUT"
else
echo "go=true" >> "$GITHUB_OUTPUT"
echo "base=$BASE" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
if: steps.can.outputs.go == 'true'
- uses: actions/setup-python@v5
if: steps.can.outputs.go == 'true'
with:
python-version: "3.14"
cache: pip
cache-dependency-path: pipeline/requirements.txt
- name: Install dependencies
if: steps.can.outputs.go == 'true'
run: pip install -r requirements.txt
# BASE and MAX arrive via env rather than ${{ }} inside the script: both
# can come verbatim from a workflow_dispatch input, and an expression
# expansion is pasted into the shell as literal text before bash parses
# it. pages.yml documents the same rule.
- name: Fetch what a phone fetches
id: smoke
if: steps.can.outputs.go == 'true'
env:
BASE: ${{ steps.can.outputs.base }}
MAX: ${{ inputs.max_hash_bytes }}
# set +e / PIPESTATUS rather than trusting the pipe: the step's exit
# status is tee's under the default shell, so both codes --exit-zero
# reserves - 2 for "nothing published to test", anything else for a
# crash - were invisible here (#655, same fix as check-deployment.yml).
run: |
set +e
python smoke_published.py \
--base "$BASE" \
${MAX:+--max-hash-bytes "$MAX"} \
--json smoke.json \
--exit-zero | tee smoke.txt
status=${PIPESTATUS[0]}
set -e
case "$status" in
0) ;;
2)
echo "::error::Nothing is published at $BASE to smoke test - smoke_published.py exited 2."
exit 2
;;
*)
echo "::error::smoke_published.py exited $status, which --exit-zero reserves for a crash."
exit "$status"
;;
esac
- name: Write the job summary
if: always() && steps.can.outputs.go == 'true'
run: |
{
echo "## Published data smoke test"
echo
if [ -f smoke.txt ]; then
echo '```'
cat smoke.txt
echo '```'
else
echo "The check did not produce a report. See the step log above."
fi
} >> "$GITHUB_STEP_SUMMARY"
# Shares ISSUE_LABEL with check-deployment.yml but not ISSUE_TITLE, so
# the two never fight over one issue: they can be red for unrelated
# reasons, and a single issue would have each run overwrite the other's
# findings. The shared label is what makes both findable at once.
- name: Open, update, or close the tracking issue
if: steps.can.outputs.go == 'true'
uses: actions/github-script@v7
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
with:
script: |
const fs = require('fs')
const trackingIssue = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/tracking-issue.js`)
const verdict = JSON.parse(fs.readFileSync('pipeline/smoke.json', 'utf8'))
const failed = verdict.failed ?? []
const unreachable = verdict.unreachable ?? []
const skipped = verdict.skipped ?? []
const keyOf = r => `${r.check}:${r.key}`
await trackingIssue({github, context, core}, {
label: process.env.ISSUE_LABEL,
title: process.env.ISSUE_TITLE,
checkedAt: verdict.checked_at,
keys: failed.map(keyOf),
// The all-clear needs both halves (#651): nothing failed, AND
// nothing was unreachable. An artifact that stops answering
// entirely is UNREACHABLE, not failed - closing on failed alone
// let the total outage close the alarm its corruption opened.
//
// Note this is NOT check-deployment.yml's condition, though both
// were wrong in the same round. That is why the shared module
// takes `healthy` rather than computing it.
healthy: failed.length === 0 && unreachable.length === 0,
allClear: `What the bucket serves matches what the manifest promises again, as of ${verdict.checked_at}. Closing.`,
render: (firstSeen, cell) => [
`${failed.length} check(s) failed against \`${verdict.base}\`.`,
'',
'**What this means that a reachability check cannot say:** these artifacts are being *served*, and what is served is not what `latest.json` promises. A phone downloading one holds it to the published SHA-256 and discards it on a mismatch, so this is a map that will not install rather than a map that is merely slow to reach.',
'',
'| check | kind | first seen | detail |',
'|---|---|---|---|',
...failed.map(r => `| \`${keyOf(r)}\` | ${r.check} | ${firstSeen[keyOf(r)]} | ${cell(r.detail)} |`),
'',
unreachable.length
? `${unreachable.length} check(s) could not be made at all and are not counted as failures.`
: '',
skipped.length
? `${skipped.length} check(s) were skipped — artifacts over the hash budget, and objects too small for a mid-file range probe. Skipped is not passed.`
: '',
'',
`Last checked ${verdict.checked_at} · [run](${process.env.RUN_URL})`,
'',
'---',
'_Generated by [Claude Code](https://claude.ai/code)_',
].join('\n'),
})