diff --git a/.github/workflows/tiles.yml b/.github/workflows/tiles.yml index b818665..b5eab35 100644 --- a/.github/workflows/tiles.yml +++ b/.github/workflows/tiles.yml @@ -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 @@ -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 @@ -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: @@ -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 }}" diff --git a/README.md b/README.md index e30578f..735f9b1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/scripts/build_routing.py b/scripts/build_routing.py index 6d2649e..5115a74 100755 --- a/scripts/build_routing.py +++ b/scripts/build_routing.py @@ -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 diff --git a/scripts/make-style.mjs b/scripts/make-style.mjs index 36c6d65..9e18901 100644 --- a/scripts/make-style.mjs +++ b/scripts/make-style.mjs @@ -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", @@ -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.