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
22 changes: 22 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Check

on:
pull_request:
push:
branches:
- master

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run check
16 changes: 16 additions & 0 deletions baselines/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Public route baseline

`public-routes.json` records the public GitHub Pages surface that existed before the handbook work began. It is a non-breaking contract, not a permanent ban on editing a presentation.

The fixture contains:

- the source commit and GitHub Pages configuration;
- every route linked from the portfolio index;
- the live deck, speaker notes, standalone deck, carousel, PDF, and exported carousel images;
- the observed HTTP status, content type, response size, and final URL on the capture date;
- SHA-256 hashes for the root index and every presentation HTML entry point; and
- the current absence of `robots.txt`, `sitemap.xml`, and `llms.txt`.

`npm run check:baseline` confirms that every source file still exists and that hashed presentation files have not changed silently. An intentional presentation edit should update the affected hash and explain the change in the pull request. The live HTTP observations are historical evidence; they are not fetched during CI.

Captured on 2026-10-09 from commit `0f3b72ad28a41001604b74572368b505c7762378`.
49 changes: 49 additions & 0 deletions baselines/public-routes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"version": 1,
"capturedAt": "2026-10-09",
"sourceCommit": "0f3b72ad28a41001604b74572368b505c7762378",
"pages": {
"url": "https://last9.github.io/presentations/",
"status": "built",
"buildType": "legacy",
"sourceBranch": "master",
"sourcePath": "/",
"customDomain": null,
"httpsEnforced": true
},
"discoveryEndpoints": [
{ "path": "/presentations/robots.txt", "status": 404, "contentType": "text/html; charset=utf-8", "contentLength": 9379, "finalUrl": "https://last9.github.io/presentations/robots.txt" },
{ "path": "/presentations/sitemap.xml", "status": 404, "contentType": "text/html; charset=utf-8", "contentLength": 9379, "finalUrl": "https://last9.github.io/presentations/sitemap.xml" },
{ "path": "/presentations/llms.txt", "status": 404, "contentType": "text/html; charset=utf-8", "contentLength": 9379, "finalUrl": "https://last9.github.io/presentations/llms.txt" }
],
"routes": [
{ "path": "/presentations/", "sourcePath": "index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 7156, "finalUrl": "https://last9.github.io/presentations/", "sha256": "6811607e6a984f711978a2496f5899ce9a4b2e710d9811455b12c194679c9683" },
{ "path": "/presentations/talk-1/", "sourcePath": "talk-1/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 31405, "finalUrl": "https://last9.github.io/presentations/talk-1/", "sha256": "cfcda2f8dabc06c95a72e34d1027cb02d65dacccc18b2f3d9430c0ee3ad20787" },
{ "path": "/presentations/talk-2/", "sourcePath": "talk-2/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 12091, "finalUrl": "https://last9.github.io/presentations/talk-2/", "sha256": "194a32dad91d8b52124865e2e01f25df1a141595d29ec96937afd08b9c6f948f" },
{ "path": "/presentations/talk-3/", "sourcePath": "talk-3/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 11738, "finalUrl": "https://last9.github.io/presentations/talk-3/", "sha256": "b60009a4c5d00bb2458915c7c0c1424008c9f0d05b9abcacb73284d4bd8b7152" },
{ "path": "/presentations/talk-4/", "sourcePath": "talk-4/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 9310, "finalUrl": "https://last9.github.io/presentations/talk-4/", "sha256": "9d6abae2865d89e8670df2d39ee8690b29bab8152e5e1827455553e9a4fdfcf3" },
{ "path": "/presentations/talk-5/", "sourcePath": "talk-5/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 9131, "finalUrl": "https://last9.github.io/presentations/talk-5/", "sha256": "d1d7246fdb86b056ef65b56ffeb6100be0f5c6d2c4d348d0314f3a4e2920ce3b" },
{ "path": "/presentations/talk-6/", "sourcePath": "talk-6/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 11959, "finalUrl": "https://last9.github.io/presentations/talk-6/", "sha256": "15cee85eb534fb16fed12c1aeaecb1692abd39f1ce8dc12afcdf3a8ccb4055a5" },
{ "path": "/presentations/talk-7/", "sourcePath": "talk-7/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 10072, "finalUrl": "https://last9.github.io/presentations/talk-7/", "sha256": "d00f073b4de637a029562831fcdb44fc5edbb58eaa34dd2a62ad7948a5622b45" },
{ "path": "/presentations/talk-8/", "sourcePath": "talk-8/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 15524, "finalUrl": "https://last9.github.io/presentations/talk-8/", "sha256": "5246f0f3249d8fc5244675a604f405760f23d442fbec2e0c98e25665461696b5" },
{ "path": "/presentations/whose-cloud-is-it-anyway-sep-2-2026/", "sourcePath": "whose-cloud-is-it-anyway-sep-2-2026/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 31178, "finalUrl": "https://last9.github.io/presentations/whose-cloud-is-it-anyway-sep-2-2026/", "sha256": "61ab292eea0ce4d7de3fee3246c6102ca670217a2192cfc0f01f90f43f6213a6" },
{ "path": "/presentations/docs/", "sourcePath": "docs/README.md", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 14969, "finalUrl": "https://last9.github.io/presentations/docs/" },
{ "path": "/presentations/talk-live/", "sourcePath": "talk-live/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 59528, "finalUrl": "https://last9.github.io/presentations/talk-live/", "sha256": "cf507f84a043d2efbb1c6fe075d6bbbde76a702359f12f1fa84b83a8db4dd5b1" },
{ "path": "/presentations/talk-live/notes.html", "sourcePath": "talk-live/notes.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 21529, "finalUrl": "https://last9.github.io/presentations/talk-live/notes.html", "sha256": "bd36f0b50c39f99455d3aced2bafd93c0e197a16049430855d6fc1b0021a882f" },
{ "path": "/presentations/whose-cloud-is-it-anyway-sep-2-2026/whose-cloud-is-it-anyway.html", "sourcePath": "whose-cloud-is-it-anyway-sep-2-2026/whose-cloud-is-it-anyway.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 518853, "finalUrl": "https://last9.github.io/presentations/whose-cloud-is-it-anyway-sep-2-2026/whose-cloud-is-it-anyway.html", "sha256": "f26cb8f6c64675d49915d7f0eab906a51ebd790f670e77b51d4dcbc2f4f80d60" },
{ "path": "/presentations/carousel/", "sourcePath": "carousel/index.html", "status": 200, "contentType": "text/html; charset=utf-8", "contentLength": 30832, "finalUrl": "https://last9.github.io/presentations/carousel/", "sha256": "7dfece23f66636194594e890c3b93dd384cd4a02ef6a9d53ec575cfcbe286a1d" },
{ "path": "/presentations/carousel/untrusted-agents-carousel.pdf", "sourcePath": "carousel/untrusted-agents-carousel.pdf", "status": 200, "contentType": "application/pdf", "contentLength": 1332876, "finalUrl": "https://last9.github.io/presentations/carousel/untrusted-agents-carousel.pdf" },
{ "path": "/presentations/carousel/out/slide-01.png", "sourcePath": "carousel/out/slide-01.png", "status": 200, "contentType": "image/png", "contentLength": 141400, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-01.png" },
{ "path": "/presentations/carousel/out/slide-02.png", "sourcePath": "carousel/out/slide-02.png", "status": 200, "contentType": "image/png", "contentLength": 317308, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-02.png" },
{ "path": "/presentations/carousel/out/slide-03.png", "sourcePath": "carousel/out/slide-03.png", "status": 200, "contentType": "image/png", "contentLength": 176380, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-03.png" },
{ "path": "/presentations/carousel/out/slide-04.png", "sourcePath": "carousel/out/slide-04.png", "status": 200, "contentType": "image/png", "contentLength": 297670, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-04.png" },
{ "path": "/presentations/carousel/out/slide-05.png", "sourcePath": "carousel/out/slide-05.png", "status": 200, "contentType": "image/png", "contentLength": 153523, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-05.png" },
{ "path": "/presentations/carousel/out/slide-06.png", "sourcePath": "carousel/out/slide-06.png", "status": 200, "contentType": "image/png", "contentLength": 188250, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-06.png" },
{ "path": "/presentations/carousel/out/slide-07.png", "sourcePath": "carousel/out/slide-07.png", "status": 200, "contentType": "image/png", "contentLength": 186396, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-07.png" },
{ "path": "/presentations/carousel/out/slide-08.png", "sourcePath": "carousel/out/slide-08.png", "status": 200, "contentType": "image/png", "contentLength": 201202, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-08.png" },
{ "path": "/presentations/carousel/out/slide-09.png", "sourcePath": "carousel/out/slide-09.png", "status": 200, "contentType": "image/png", "contentLength": 204271, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-09.png" },
{ "path": "/presentations/carousel/out/slide-10.png", "sourcePath": "carousel/out/slide-10.png", "status": 200, "contentType": "image/png", "contentLength": 205389, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-10.png" },
{ "path": "/presentations/carousel/out/slide-11.png", "sourcePath": "carousel/out/slide-11.png", "status": 200, "contentType": "image/png", "contentLength": 176095, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-11.png" },
{ "path": "/presentations/carousel/out/slide-12.png", "sourcePath": "carousel/out/slide-12.png", "status": 200, "contentType": "image/png", "contentLength": 166771, "finalUrl": "https://last9.github.io/presentations/carousel/out/slide-12.png" }
]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"description": "Last9 conference talks and presentation assets",
"type": "module",
"scripts": {
"check": "node scripts/check-local-links.mjs",
"check": "node scripts/check-local-links.mjs && node scripts/check-public-route-baseline.mjs",
"check:baseline": "node scripts/check-public-route-baseline.mjs",
"dev": "serve -l 8000 .",
"start": "serve -l 8000 .",
"assets": "node scripts/svg-to-png.mjs",
Expand Down
76 changes: 76 additions & 0 deletions scripts/check-public-route-baseline.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node

import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "..");
const fixturePath = resolve(root, "baselines/public-routes.json");
const fixture = JSON.parse(readFileSync(fixturePath, "utf8"));
const failures = [];
const seenPaths = new Set();

if (fixture.version !== 1) failures.push("fixture version must be 1");
if (!/^\d{4}-\d{2}-\d{2}$/.test(fixture.capturedAt)) {
failures.push("capturedAt must use YYYY-MM-DD");
}
if (!/^[0-9a-f]{40}$/.test(fixture.sourceCommit)) {
failures.push("sourceCommit must be a full Git commit SHA");
}
if (!Array.isArray(fixture.routes) || fixture.routes.length === 0) {
failures.push("routes must be a non-empty array");
}

for (const endpoint of fixture.discoveryEndpoints ?? []) {
if (endpoint.status !== 404) {
failures.push(`${endpoint.path}: discovery baseline status must be 404`);
}
const expectedUrl = new URL(endpoint.path, "https://last9.github.io").href;
if (endpoint.finalUrl !== expectedUrl) {
failures.push(`${endpoint.path}: finalUrl must be ${expectedUrl}`);
}
}

for (const route of fixture.routes ?? []) {
if (seenPaths.has(route.path)) failures.push(`duplicate route: ${route.path}`);
seenPaths.add(route.path);

if (!route.path.startsWith("/presentations/")) {
failures.push(`${route.path}: route must remain under /presentations/`);
}
if (route.status !== 200) failures.push(`${route.path}: expected status must be 200`);
if (!route.contentType) failures.push(`${route.path}: contentType is required`);
if (!Number.isInteger(route.contentLength) || route.contentLength <= 0) {
failures.push(`${route.path}: contentLength must be a positive integer`);
}

const expectedUrl = new URL(route.path, "https://last9.github.io").href;
if (route.finalUrl !== expectedUrl) {
failures.push(`${route.path}: finalUrl must be ${expectedUrl}`);
}

const sourcePath = resolve(root, route.sourcePath);
if (!existsSync(sourcePath)) {
failures.push(`${route.path}: missing source ${route.sourcePath}`);
continue;
}

if (route.sha256) {
const actualHash = createHash("sha256").update(readFileSync(sourcePath)).digest("hex");
if (actualHash !== route.sha256) {
failures.push(
`${route.path}: ${route.sourcePath} changed (expected ${route.sha256}, got ${actualHash})`,
);
}
}
}

if (failures.length) {
console.error("Public route baseline failures:\n" + failures.map((item) => `- ${item}`).join("\n"));
process.exit(1);
}

const hashedRoutes = fixture.routes.filter((route) => route.sha256).length;
console.log(
`Checked ${fixture.routes.length} public routes, ${hashedRoutes} content hashes, and ${fixture.discoveryEndpoints?.length ?? 0} discovery endpoints against the ${fixture.capturedAt} baseline.`,
);
Loading