Repository navigation
231 lines (205 loc) · 9.77 KB
/
Copy pathtiles.yml
File metadata and controls
231 lines (205 loc) · 9.77 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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
name: Build and publish tiles
on:
# Whenever the data changes. A person's push to main lands here directly. A
# scrape's data arrives through release.yml, which dispatches this workflow
# (workflow_dispatch below) after publishing its change report: a push made
# with GITHUB_TOKEN starts no workflow, so the scrape's own commit never
# triggers this. Either way a campus edit reaches the tileset without waiting
# for the daily cron below.
push:
branches: [main]
paths:
- map.geojson
- map.json
- data/**
- overrides.yaml
- build-tiles.sh
- site/**
- scripts/campus-layers.py
- scripts/explode.py
- scripts/make-style.mjs
- scripts/verify-tiles.py
- package.json
- package-lock.json
- mise.toml
- mise.lock
- .github/workflows/tiles.yml
workflow_dispatch:
# Daily at 07:00 UTC, matching carls-app/map-tiles.
#
# This tracks OpenStreetMap, not the college, and the two move at completely
# different speeds — which is why this schedule is not the scrape's. Protomaps
# rebuilds the planet every day from OSM minutely replication, and the build
# this pulls from carries OSM data to about 04:00 UTC the same day, so a daily
# run means an OSM edit reaches the app roughly a day later. 07:00 leaves the
# upstream build a few hours to publish.
#
# Rebuilding nightly is not the waste it looks like: a run costs well under a
# minute, and the force-push keeps gh-pages at a single commit however often it
# happens, so nothing accumulates.
schedule:
- cron: "0 7 * * *"
# Dry run on every PR: build and check the output, publish nothing. Without
# this the workflow could only be exercised by merging it.
pull_request:
# `pages: write` to deploy, and the OIDC token `deploy-pages` uses to prove the
# deployment came from this workflow. Nothing here writes to the repository, so
# `contents` stays read-only.
permissions:
contents: read
pages: write
id-token: write
concurrency:
# PRs get their own group so a dry run can never queue behind, or cancel, a
# scheduled publish. Publishes are never cancelled mid-flight: a half-finished
# Pages deployment is worse than a queued one.
group: >-
${{ github.event_name == 'pull_request'
&& format('tiles-dryrun-{0}', github.ref)
|| 'publish-tiles' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
# The host the styles were built for, read back out of the artifact rather
# than restated. See "Check the styles point where Pages serves" below.
site_url: ${{ steps.site.outputs.url }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Installs node, python, uv and the pmtiles CLI from mise.toml/mise.lock.
# build-tiles.sh calls `mise install` itself too, so this mostly serves to
# cache them across runs.
- uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
- run: uv sync --locked
- name: Install tippecanoe
# Tiles the campus layers and joins them into the basemap. build-tiles.sh
# fetches the rest of its toolchain itself, but tippecanoe is a C++ build
# with no prebuilt releases and the distro package is both current and
# instant.
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq tippecanoe
tippecanoe --version
- name: Build the routing graph
# Reads the scraped walkways, so it has to run after the data exists and
# before build-tiles.sh copies the publishable files into dist/.
run: uv run scripts/build_routing.py
- name: Build
run: ./build-tiles.sh
- name: Check the tiles are not gzipped
# Pages will not set Content-Encoding on a .pbf, so a gzipped tile would
# reach the app as garbage. This is the failure that is hardest to spot
# on device, so it is worth asserting in CI rather than trusting.
run: |
bad=0
while IFS= read -r f; do
if [ "$(head -c 2 "$f" | od -An -tx1 | tr -d ' \n')" = "1f8b" ]; then
echo "gzipped tile: $f"
bad=1
fi
done < <(find dist/tiles -name '*.pbf')
[ "$bad" = 0 ] || exit 1
echo "OK: $(find dist/tiles -name '*.pbf' | wc -l) tiles stored uncompressed"
- name: Check the published data matches the committed data
# build-tiles.sh copies map.json, map.geojson and data/ into dist so that
# ccc-server has a URL to fetch. If that copy ever diverged, the app
# would read one revision of the campus data while the tiles were built
# from another — which renders fine and is wrong.
run: |
diff -q map.json dist/map.json
diff -q map.geojson dist/map.geojson
diff -r -q data dist/data
[ ! -f routing.json ] || diff -q routing.json dist/routing.json
echo "OK: published data is byte-identical to the committed data"
- name: Summarise the build
# On a PR this is the payoff: the numbers that would have been
# published, visible in the run summary without anything being published.
run: |
{
echo "| | |"
echo "| --- | ---: |"
echo "| campus.pmtiles | $(numfmt --to=iec --suffix=B "$(wc -c < dist/campus.pmtiles)") |"
echo "| tiles | $(find dist/tiles -name '*.pbf' | wc -l) files |"
echo "| glyph ranges | $(find dist/fonts -name '*.pbf' | wc -l) |"
echo "| style layers | $(python3 -c 'import json;print(len(json.load(open("dist/style.json"))["layers"]))') |"
echo "| source-layers | $(python3 -c 'import json;print(len({l["source-layer"] for l in json.load(open("dist/style.json"))["layers"] if "source-layer" in l}))') |"
echo "| places in map.json | $(python3 -c 'import json;print(len(json.load(open("dist/map.json"))))') |"
echo
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "Dry run — nothing published."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Verify the routing graph
run: uv run scripts/verify.py
- name: Record the host the styles were built for
id: site
# Read from the built style.json rather than from SITE_URL, so this is
# the host the app will genuinely fetch — whatever make-style.mjs
# actually wrote — and not a restatement of it that could disagree.
run: |
url=$(python3 -c "import json;print(json.load(open('dist/style.json'))['sources']['basemap']['tiles'][0])")
case "$url" in
*"/tiles/"*) ;;
*) echo "::error::style.json tile URL has no /tiles/ segment: $url"; exit 1 ;;
esac
base="${url%%/tiles/*}"
echo "url=$base" >> "$GITHUB_OUTPUT"
echo "styles were built for $base"
- name: Upload the site
# This job's work ends here: dist/ is built, checked and handed off.
# Deploying it is the next job's problem.
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: dist
deploy:
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
# Declaring the environment is what makes the deployment show up on the
# repository's Pages settings and in the environment history, and it is what
# `deploy-pages` reports its status back to.
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to Pages
# Returns when GitHub reports the deployment succeeded. That status is
# the fact worth having and the only reliable source of it: pushing a
# branch tells you the push worked, and polling the live URL measures a
# deployment time that ranges from seconds to tens of minutes.
id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5
- name: Check the styles point where Pages serves
# Whether the deployment worked is GitHub's problem, and the step above
# answers it. This checks the part that is ours.
#
# The styles carry absolute URLs, because MapLibre Native resolves
# relative ones inconsistently. So if the host baked into them is not the
# host Pages serves from, every one of ~985 tile requests takes a
# redirect while the deployment reports success throughout — a failure no
# deployment status can see.
#
# Scheme and trailing slash are normalised because GitHub reports
# "http://host/path/" where the styles carry "https://host/path".
run: |
norm() { printf '%s' "$1" | sed -E 's#^https?://##; s#/+$##'; }
baked=$(norm "${{ needs.build.outputs.site_url }}")
served=$(norm "${{ steps.deployment.outputs.page_url }}")
echo "styles built for : $baked"
echo "Pages serves from: $served"
if [ -z "$baked" ] || [ -z "$served" ]; then
echo "::error::could not determine one of the two URLs — refusing to pass blind"
exit 1
fi
if [ "$baked" != "$served" ]; then
echo "::error::the styles point at $baked but Pages serves from $served"
echo "Every tile, glyph and sprite URL in style.json is absolute, so this" >&2
echo "means the app takes a redirect on each of them. Fix SITE_URL in" >&2
echo "build-tiles.sh to match, then rebuild." >&2
exit 1
fi
echo "Deployed to ${{ steps.deployment.outputs.page_url }}"