fix(frontend): pin sveltekit version.name so builds are reproducible across architectures (#10315)

* fix(docker): pin frontend build stage to linux/amd64

Rollup selects platform-specific native binaries that can emit different
content-hashed chunk filenames for identical sources. The frontend assets are
embedded into the Rust binary via rust_embed, so building the stage once per
target architecture produced amd64 and arm64 images whose HTML references
`_app/immutable/chunks/<hash>.js` files that only exist in that architecture's
image. In a mixed-architecture cluster, a page served by a pod of one arch 404s
on JS/CSS fetched from a pod of the other.

Pinning the stage makes both image variants embed byte-identical assets. The
stage output is JS/CSS/HTML/WASM only, so the build platform does not leak into
the artifacts.

Fixes WIN-2242

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: tighten frontend platform-pin comment

Vite 8 bundles with rolldown, not rollup; name the right bindings and keep the
constraint to four lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): make the build reproducible so mixed-arch clusters agree on asset names

SvelteKit defaults `kit.version.name` to `Date.now().toString()`, so every build
of the same commit gets a different version string. It is embedded in the client
chunk (and in the `__sveltekit_<hash>` global derived from it), which changes
that chunk's content hash and cascades into new filenames for roughly a quarter
of `_app/immutable`. The assets are baked into the binary via rust_embed, so the
amd64 and arm64 images of one release ship different `chunks/<hash>.js` names:
in a mixed-architecture cluster, HTML served by a pod of one architecture 404s
on assets requested from a pod of the other.

Measured on the published windmill:1.770.0 images: 224 of 863 asset filenames
differ between the two architecture variants, yet 854 of 855 chunks are
byte-identical once chunk-name references are normalized. The single genuinely
differing chunk is the one carrying the timestamp. The bundler is deterministic
across architectures; the timestamp is the whole divergence.

Pinning the version to the package version (overridable via WM_BUILD_VERSION)
makes repeat builds byte-identical. `version.pollInterval` is 0 and nothing
reads the `updated` store, so this has no runtime behavior change.

This supersedes pinning the Docker frontend stage to linux/amd64, which fixed
the symptom by building the stage under emulation on the arm64 builder — that
cost 32 minutes of QEMU time per build and left the underlying non-determinism
in place.

Fixes WIN-2242

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): key the sveltekit version on the commit sha

The package version only moves on releases, but `:dev` and RHEL images are
published on every main push. Two such deployments would then advertise the same
SvelteKit version, and SvelteKit only recovers from a chunk that 404s after a
redeploy (client.js: "Referenced node could have been removed due to redeploy")
when the deployed version differs from the baked-in one, so an open tab would
render an error page instead of reloading.

Pass the commit sha through WM_BUILD_VERSION from every workflow that builds the
root Dockerfile, so the value is identical across the per-architecture builds of
one commit and distinct between commits. The package version stays the fallback,
which keeps unwired builds architecture-consistent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(docker): declare WM_BUILD_VERSION in the RHEL frontend stages

The RHEL workflows copy docker/RHEL{8,9}/Dockerfile over the root one before
building, so the build-arg was unconsumed there and those images fell back to
the package version: two RHEL builds between releases would share a SvelteKit
version across different manifests.

Also switch the root declaration to the `ARG name=""` form used by `features`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: keep the version-arg rationale in one place

The root Dockerfile comment restated what frontend/svelte.config.js already
documents; point at it instead, matching the RHEL Dockerfiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-24 23:55:47 +02:00
committed by GitHub
parent 71b7135cf2
commit 65db58bfda
8 changed files with 24 additions and 0 deletions
@@ -63,6 +63,7 @@ jobs:
push: true
build-args: |
features=ee_rhel
WM_BUILD_VERSION=${{ github.sha }}
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -65,6 +65,7 @@ jobs:
push: true
build-args: |
features=ee_rhel
WM_BUILD_VERSION=${{ github.sha }}
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -82,6 +83,7 @@ jobs:
push: true
build-args: |
features=ee_rhel
WM_BUILD_VERSION=${{ github.sha }}
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
+1
View File
@@ -68,6 +68,7 @@ jobs:
push: true
build-args: |
features=ce_rpi
WM_BUILD_VERSION=${{ github.sha }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ steps.meta-public.outputs.tags }}
+3
View File
@@ -93,6 +93,7 @@ jobs:
push: true
build-args: |
features=ce
WM_BUILD_VERSION=${{ github.sha }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
@@ -155,6 +156,7 @@ jobs:
push: true
build-args: |
features=ee
WM_BUILD_VERSION=${{ github.sha }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
@@ -254,6 +256,7 @@ jobs:
target: debuginfo
build-args: |
features=ee
WM_BUILD_VERSION=${{ github.sha }}
outputs: type=local,dest=./debuginfo
- name: Rename debug file with corresponding architecture
+2
View File
@@ -79,6 +79,8 @@ COPY /python-client/docs/ /frontend/static/pydocs/
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
ARG VITE_BASE_URL ""
# Must be declared for the build-arg to reach the bundle. See frontend/svelte.config.js.
ARG WM_BUILD_VERSION=""
# Read more about macro in docker/dev.nu
# -- MACRO-SPREAD-WASM-PARSER-DEV-ONLY -- #
RUN npm run build
+2
View File
@@ -41,6 +41,8 @@ COPY /python-client/docs/ /frontend/static/pydocs/
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
# Must be declared for the build-arg to reach the bundle. See frontend/svelte.config.js.
ARG WM_BUILD_VERSION=""
RUN npm run build
+2
View File
@@ -41,6 +41,8 @@ COPY /python-client/docs/ /frontend/static/pydocs/
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
# Must be declared for the build-arg to reach the bundle. See frontend/svelte.config.js.
ARG WM_BUILD_VERSION=""
RUN npm run build
+11
View File
@@ -1,6 +1,12 @@
import preprocess from 'svelte-preprocess'
import adapter from '@sveltejs/adapter-static'
import { preprocessMeltUI, sequence } from '@melt-ui/pp'
import { readFileSync } from 'fs'
import { fileURLToPath } from 'url'
const pkg = JSON.parse(
readFileSync(fileURLToPath(new URL('package.json', import.meta.url)), 'utf8')
)
/** @type {import('@sveltejs/kit').Config} */
const config = {
@@ -23,6 +29,11 @@ const config = {
assets: 'build',
fallback: '200.html'
}),
// Same for every build of one revision (SvelteKit's Date.now() default is not, so
// the per-architecture images disagreed on content-hashed asset filenames and
// mixed-arch clusters 404ed on each other's chunks), and different between
// revisions (SvelteKit only full-page reloads on a missing chunk if this changed).
version: { name: process.env.WM_BUILD_VERSION || pkg.version },
prerender: { entries: [] },
paths: {
base: process.env.VITE_BASE_URL ?? ''