Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 59 additions & 28 deletions .github/workflows/tiles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ on:
# this the workflow could only be exercised by merging it.
pull_request:

# Deploying through the Pages Actions integration rather than by force-pushing a
# branch, so this needs `pages: write` and the OIDC token `deploy-pages` uses to
# prove the deployment came from this workflow — and no longer needs write access
# to the repository's contents at all.
# `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
Expand All @@ -68,6 +67,11 @@ jobs:
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

Expand Down Expand Up @@ -145,11 +149,24 @@ jobs:
- 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
# Everything after this is the deploy job. Splitting them is what lets
# the build stop waiting: this job's work is done once dist/ is correct
# and uploaded, and whether Pages has finished serving it is a separate
# question with its own answer.
# 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:
Expand All @@ -170,28 +187,42 @@ jobs:

steps:
- name: Deploy to Pages
# This is the whole reason for the rewrite. Force-pushing a gh-pages
# branch tells you the push worked and nothing about the deployment, so
# the old workflow polled the live URL for a sha256 match and guessed at
# a timeout — a guess it got wrong twice, reporting three false failures
# over deployments that were fine. This action returns when GitHub says
# the deployment succeeded, which is the fact we actually wanted.
# 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 deployed site
# A short confirmation, not a wait. `deploy-pages` has already reported
# success, so this is not polling for the deployment to happen — it is
# checking that what Pages serves is what the app will actually fetch,
# which is a different property and the one that caught the
# stodevx.github.io redirect. Deliberately no `curl -L`: a redirect
# appearing here means the site is being served differently than the
# styles were built for, and should fail loudly.
- 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: |
base="https://stolaf.dev/campus-map-data"
for path in style.json style-pmtiles.json map.json map.geojson routing.json index.html; do
code=$(curl -sS -m 30 -o /dev/null -w '%{http_code}' "$base/$path" || echo 000)
echo "$path -> $code"
[ "$code" = "200" ] || { echo "::error::$base/$path returned $code"; exit 1; }
done
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 }}"
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,22 @@ to ignore the one time it is right.
nothing to poll and nothing to time out. The build job ends when `dist/` is
uploaded; the deploy job ends when GitHub says the deployment succeeded.

What survives from the old check is a short confirmation *after* that: a fetch of
each published entry point, with no `curl -L`, because a redirect appearing there
means the site is served differently than the styles were built for. That is the
check that caught the `stodevx.github.io` redirect, and it is worth keeping — it
just should not have been carrying the deployment wait on its back.
Nothing verifies the deployment afterwards, because GitHub has already said it
worked and re-deriving that over HTTP is how all of the above went wrong.

One thing *is* checked, and it is not GitHub's job — it is ours. The styles carry
absolute URLs, since 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, and the deployment is perfectly successful
throughout. That is not hypothetical: the first publish shipped
`stodevx.github.io` in the styles while the org's Pages site redirects to
`stolaf.dev`, and no deployment status would ever have shown it.

The check is a string comparison — the host read back out of the built
`style.json` against `deploy-pages`' own `page_url` — normalised for scheme and
trailing slash, because GitHub reports `http://host/path/` where the styles carry
`https://host/path`. It catches that bug at its source, instantly, with nothing
to race.

## Routing

Expand Down
30 changes: 14 additions & 16 deletions scripts/build_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,20 @@
## Adapted from course-data-visualization

The two-stage fit is [`StoDevX/course-data-visualization`][cdv]'s
`scripts/build_path_graph.py`, which did this first. Three things changed:

- **It reads this repo's own data.** That script took building centroids and
walkway lines from files in its own repo; this one reads `data/buildings.
geojson`, `data/walkways.geojson`, `data/campus-roads.geojson` and `map.json`,
which are the same ArcGIS layers one step closer to the source.
- **All 38 buildings, not 32.** Its building table was a dict keyed by the
college's `ABB`, and the seven buildings with no abbreviation collided on the
blank key `" "` — six of them overwrote each other, and the script carried a
`- {" "}` to work around the wreckage. Keying by this repo's ids instead
(which are the abbreviations where they exist and slugified names where they
do not) makes New Hall, the Townhouses and Tostrud Center routable
destinations rather than casualties.
- **Anchors, not vertex means.** Its centroid was the mean of a polygon's outer
ring, which lands outside anything L-shaped. This uses `map.json`'s label
anchors, which `geometry.py` guarantees are inside the building.
`scripts/build_path_graph.py`, which worked it out first. This differs from it
in three ways, each of which shows in the output:

- **It reads this repo's own data** — `data/buildings.geojson`,
`data/walkways.geojson`, `data/campus-roads.geojson` and `map.json` — rather
than copies of those layers kept elsewhere.
- **It covers all 38 buildings.** Keying buildings by this repo's ids (the
college's abbreviations where they exist, slugified names where they do not)
keeps New Hall, the Townhouses and Tostrud Center as routable destinations. A
table keyed on the `ABB` field alone collapses the seven buildings with no
abbreviation onto one blank key and reaches only 32.
- **It anchors on label anchors, not vertex means.** `geometry.py` guarantees
those sit inside the building; the mean of a polygon's outer ring does not,
for anything L-shaped.

[oc]: https://github.com/StoDevX/ole-compass
[cdv]: https://github.com/StoDevX/course-data-visualization
Expand Down
42 changes: 20 additions & 22 deletions scripts/make-style.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -158,29 +158,27 @@ const OSM_BUILDING_FILL = flatten(

// Why the OSM buildings are warm
//
// carls-app/map-tiles has to paint both of its building layers one colour:
// Carleton's hand-drawn footprints disagree with OSM's badly enough that any
// colour difference shows up as a doubled, misregistered outline.
// Two building layers overlap on campus, and where two datasets disagree about
// an outline the difference has to read as one building rather than as a
// registration error. carls-app/map-tiles solves that for Carleton by painting
// both layers a single colour, because Carleton's hand-drawn footprints and
// OSM's disagree substantially.
//
// St. Olaf does not have that problem, and it is worth saying how that was
// established rather than assumed — the layer being named `buildings_openstreet`
// is suggestive but proves nothing. Rendering the tileset with the OSM buildings
// layer forced to pure red shows the overhang directly: across the campus core
// it is a handful of narrow strips, the widest being two slivers along the west
// edge of Rolvaag Memorial Library. Almost every OSM footprint on campus sits
// entirely under a college polygon.
// St. Olaf's disagree very little. Rendering the tileset with this layer forced
// to pure red shows the overhang directly: across the campus core it is a
// handful of narrow strips, the widest being two slivers along the west edge of
// Rolvaag Memorial Library. Almost every OSM footprint on campus sits entirely
// under a college polygon, which is what lets the campus layer take a colour of
// its own.
//
// (That same diagnostic corrected a wrong guess. The pale shapes that look like
// duplicate buildings next to New Hall and Rolvaag are not buildings at all —
// they are OSM's `school` landuse, which blankets the whole campus, plus this
// repo's own `campus_grounds`. Neither has anything to do with the building
// layers.)
// This layer still shares that colour's hue family rather than taking a neutral
// grey, so a visible sliver reads as part of the same building, and so downtown
// Northfield — which has no campus data at all — sits in the same palette.
//
// So the campus can safely have its own colour. The OSM layer is nonetheless
// kept in the same warm hue family rather than a neutral grey, so that where a
// sliver does show it reads as part of the same building instead of a different
// kind of object — and so that downtown Northfield, which has no campus data at
// all, sits in the same palette instead of turning grey.
// Worth knowing when reading the map: the pale shapes beside New Hall and
// Rolvaag that look like duplicate buildings are OSM's `school` landuse, which
// blankets the campus, plus this repo's own `campus_grounds`. Neither is a
// building layer.
const campusLayers = [
{
id: "campus_grounds",
Expand Down Expand Up @@ -226,8 +224,8 @@ const campusLayers = [
// OSM has good footway coverage over the campus core, but the college's
// walkway layer is 138 lines and 10.8 km — comparable to the pedestrian graph
// in StoDevX/ole-compass — and its Natural Lands trails add another 12 km
// that OSM largely does not have. All of it was being scraped into data/ and
// then dropped, which is a strange thing for a campus wayfinding map to do.
// that OSM largely does not have. On a map people walk a campus with, that is
// worth drawing.
//
// Drawn over `campus_grounds` so a walk through a parking lot still reads,
// and under `campus_buildings` so nothing crosses a building.
Expand Down