From ce125c1961a4ec3ab2ec3db4f11448ce3bc76b3d Mon Sep 17 00:00:00 2001 From: Chad Miller Date: Thu, 13 Aug 2026 21:38:39 -0700 Subject: [PATCH] feat(release): a directory page on the artifacts site Each publish renders index.html from releases.json, which accumulates what the record cannot say: when each release was published, and how large its module is. The page lists releases newest first with links to each module and manifest, so the artifacts site answers a browser as well as the wizard. Co-Authored-By: Claude Fable 5 --- scripts/publish-worker-artifact.mjs | 96 +++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/scripts/publish-worker-artifact.mjs b/scripts/publish-worker-artifact.mjs index b858e37..b3458e8 100644 --- a/scripts/publish-worker-artifact.mjs +++ b/scripts/publish-worker-artifact.mjs @@ -92,6 +92,100 @@ try { // No record yet: the first publish creates it. } +// releases.json accumulates what the record cannot say: when each release +// was published. The directory page renders from it. +/** @type {{version: string, publishedAt: string, moduleSize: number}[]} */ +let releases = []; +const releasesEntry = existingFiles.find((f) => f.path === 'releases.json'); +const releasesCid = releasesEntry?.blob?.ref?.$link; +if (releasesCid) { + try { + const response = await fetch( + `${pdsUrl}/xrpc/com.atproto.sync.getBlob` + + `?did=${encodeURIComponent(session.did)}&cid=${encodeURIComponent(releasesCid)}`, + ); + if (response.ok) releases = await response.json(); + } catch { + // A fresh list rebuilds from this release forward. + } +} +releases = [ + ...releases.filter((r) => r.version !== version), + { + version, + publishedAt: new Date().toISOString(), + moduleSize: moduleBytes.length, + }, +]; +// Newest first, numerically by dotted parts. +releases.sort((a, b) => { + const pa = a.version.split('.').map(Number); + const pb = b.version.split('.').map(Number); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0); + } + return 0; +}); + +/** @param {number} bytes */ +function formatSize(bytes) { + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +const indexHtml = ` + + + + +pds.js worker artifacts + + + +pds +

Cloudflare worker artifacts

+ + +${releases + .map( + (r) => + `` + + `` + + `` + + `` + + ``, + ) + .join('\n')} +
versionpublishedsize
${r.version}${r.publishedAt.slice(0, 10)}${formatSize(r.moduleSize)}modulemanifest
+

latest/ points at ${releases[0].version}. +Each release is a self-contained Cloudflare worker module and a manifest +naming its compatibility settings and bindings. +start.pdsjs.dev installs it into your +own account. Built by the release pipeline from +pds.js.

+ + +`; + +const indexBlob = await uploadBlob( + new TextEncoder().encode(indexHtml), + 'text/html; charset=utf-8', +); +const releasesBlob = await uploadBlob( + new TextEncoder().encode(JSON.stringify(releases, null, 2)), + 'application/json', +); + const IMMUTABLE = 'public, max-age=31536000, immutable'; const SHORT = 'public, max-age=300'; const entries = [ @@ -103,6 +197,8 @@ const entries = [ }, { path: 'latest/index.js', blob: moduleBlob, cacheControl: SHORT }, { path: 'latest/manifest.json', blob: manifestBlob, cacheControl: SHORT }, + { path: 'index.html', blob: indexBlob, cacheControl: SHORT }, + { path: 'releases.json', blob: releasesBlob, cacheControl: SHORT }, ]; const replaced = new Set(entries.map((e) => e.path)); const files = [ -- 2.51.2