ADR-0029: Timestamped Build IDs for deployment identification and cache versioning¶
Context¶
Omeka S Playground ships a rolling release: main deploys continuously to GitHub
Pages and Cloudflare Pages, so there is no release train to attach a semantic
version to. Two things still need an identifier for the deployed artifact: a bug
report has to name the exact build it came from, and a redeploy must not leave a
returning browser serving a previous build's assets.
A partial mechanism already existed under a different name. scripts/esbuild.worker.mjs
hashed the freshly built dist/php-worker.bundle.js and wrote the first 12 hex
characters into src/generated/build-version.js, which sw.js imports for its
omeka-static-… cache name and the shell uses for the sw.js?v=… registration,
the versioned worker URL and the Sentry release.
That identifier has three problems:
- It cannot distinguish two builds of the same source. The hash only covers the worker bundle, so rebuilding an unchanged commit — or changing anything outside the bundle, such as the shell, an Omeka core bundle or a blueprint — yields the same value.
- It was committed to git. The generated file was tracked, so every worker change produced a churn commit and the checked-in value was routinely stale.
- It carries no provenance. There is no build time, no commit, no dirty flag, and nothing machine-readable is published with the deployed site.
Decision¶
Replace the content hash with a Build ID in the canonical format shared by all the sibling playgrounds:
YYYYMMDDTHHMMSSZ-<sha8>[-dirty]
for example 20260816T065012Z-9e39f37d. The timestamp is the UTC build time,
never the commit time, so a rebuild of unchanged source still yields a new ID.
scripts/lib/build-version.mjsowns the format (compose, parse, validate, resolve) as the single source of truth.scripts/write-build-version.mjs(npm run build:version) is a thin CLI over it that writessrc/generated/build-version.jsandassets/build-version.json;--print-versionprints the ID without writing.- Remove the content-hash generation from
scripts/esbuild.worker.mjs— one mechanism, not two competing ones — and untracksrc/generated/build-version.js. Both generated files are now git-ignored, so no identifier is maintained by hand. BUILD_VERSIONoverrides the ID outright;BUILD_SHAoverrides only the revision. TheDeploy Pagesworkflow computes the ID once and exports it via$GITHUB_ENV, so every later step in that job reuses the exact value — including the secondmake preparetriggered bymake bundle-all. Because that single job assembles one_sitethat is both uploaded as the Pages artifact and deployed to Cloudflare, both targets necessarily report the same Build ID.BUILD_SHAis set togithub.event.pull_request.head.sha || github.shaso pull request builds stay traceable to the source commit rather than the merge commitactions/checkoutresolves.- Local builds derive the SHA from git and append
-dirtyfor an unclean tree. - Existing cache wiring is kept as-is and simply carries the new value: the
omeka-static-…Service Worker cache and its activation cleanup, thesw.js?v=…registration, and the versioned worker URL. No query-string versioning is added to individual CSS/JS URLs — the shell loads native ES modules, so versioning an entry point would leave its import graph unversioned; the Service Worker cache namespace covers the whole graph instead. - The Build ID is surfaced as a copyable "Playground build" row in the Runtime
info panel, logged once at startup, and remains the Sentry
release(ADR-0028).
The Build ID names the Playground artifact only. The Omeka S version and PHP version running inside it stay separate, independently displayed values.
Consequences¶
- A bug report that quotes a Build ID pins the exact artifact, its build time and
its source commit;
assets/build-version.jsonexposes the same data to tooling. - Redeploys reliably invalidate the Service Worker cache, which the content hash did only when the worker bundle itself changed.
src/generated/build-version.jsno longer appears in diffs.- Every deploy now creates a new cache generation, so a returning visitor
re-downloads the cached
/dist/assets once per deploy — previously they were reused across deploys that did not touch the worker bundle. This is the intended trade: correctness over a saved download. make prepareandmake testgained abuild-versionprerequisite, since the generated module is no longer committed.- Persistent user data is deliberately not keyed by the Build ID: the IndexedDB journal stays keyed by scope, so deploying does not wipe a visitor's site.
Validation¶
tests/build-version.test.mjspins the format, the 8-character SHA, the metadata fields, theBUILD_VERSION/BUILD_SHAoverrides, dirty handling and that two build times for one commit produce different IDs. Time and git are injected, so the suite never depends on the wall clock.tests/e2e/shell.spec.mjsasserts the servedassets/build-version.jsonis canonical and matches the ID rendered in the info panel.