(build) add cache-busting source version in build

This commit is contained in:
Sylvain Zimmer
2026-07-22 16:11:19 +02:00
committed by jbpenrath
parent 31144b1a88
commit 7fdca12426
8 changed files with 89 additions and 24 deletions
+6
View File
@@ -100,6 +100,12 @@ jobs:
context: "src/frontend"
target: runtime-prod
arm64_reuse_amd64_build_arg: "FRONTEND_IMAGE"
# Bakes the commit SHA into the build as the asset cache-buster
# (vite.config.ts → __SOURCE_VERSION__); without it the build falls back to a
# timestamp. The src/frontend context ships no .git, so this is the only
# way the image gets the real SHA. SOURCE_VERSION matches the var name
# Scalingo's buildpack sets natively, so both deploy paths agree.
build_args: "SOURCE_VERSION=${{ github.sha }}"
docker-publish-backend:
needs: docker-publish-python-uv
+7
View File
@@ -41,6 +41,13 @@ COPY --chown=${DOCKER_USER} . ./
ARG API_ORIGIN
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
# Commit SHA (or any per-deploy version) baked in as the asset cache-buster —
# see vite.config.ts. Named SOURCE_VERSION to match the var Scalingo's buildpack
# sets natively. Pass `--build-arg SOURCE_VERSION=$(git rev-parse --short HEAD)`;
# if omitted the build falls back to a timestamp (still unique per deploy).
ARG SOURCE_VERSION
ENV SOURCE_VERSION=${SOURCE_VERSION}
RUN npm run build
# Normalize output path to /app (matches the runtime-prod layout)
+11
View File
@@ -47,6 +47,17 @@
header_up X-Forwarded-For {remote_host}
}
# --- Cache policy for the static SPA output ---
# Vite fingerprints everything under /assets, so a given URL's bytes
# never change: cache it forever. A new deploy emits new hashed URLs.
@immutable path /assets/*
header @immutable Cache-Control "public, max-age=31536000, immutable"
# The SPA shell and other non-fingerprinted files (index.html, the
# /locales JSON, images) must revalidate every load, or a deploy's new
# asset hashes / translations only show up after a manual hard refresh.
@revalidate not path /assets/*
header @revalidate Cache-Control "no-cache"
# SPA fallback: any unmatched request returns index.html so the
# client-side router can resolve the path.
try_files {path} /index.html
@@ -332,9 +332,9 @@
"Check DNS again": "Revérifier les DNS",
"Check for new mail regularly": "Synchroniser ce compte régulièrement",
"Checking DNS records...": "Vérification des enregistrements DNS...",
"Checking every {{count}} min_one": "Synchronisation toutes les {{count}} min",
"Checking every {{count}} min_many": "Synchronisation toutes les {{count}} min",
"Checking every {{count}} min_other": "Synchronisation toutes les {{count}} min",
"Checking every {{count}} min_one": "Synchro toutes les {{count}} min",
"Checking every {{count}} min_many": "Synchro toutes les {{count}} min",
"Checking every {{count}} min_other": "Synchro toutes les {{count}} min",
"Choose calendar": "Choisir l'agenda",
"Choose the type of integration you want to create": "Choisissez le type d'intégration que vous souhaitez créer",
"Clear filters": "Retirer les filtres",
@@ -43,6 +43,10 @@ export const initI18n = (config: AppConfig) => {
returnEmptyString: false,
backend: {
loadPath: "/locales/{{ns}}/{{lng}}.json",
// Cache-bust the static locale files on every deploy: their URLs are
// otherwise fixed, so a CDN/browser keeps serving a stale copy and any
// newly added translation falls back to English (a half-translated UI).
queryStringParams: { v: __SOURCE_VERSION__ },
}
})
.catch((error) => {
+4
View File
@@ -44,3 +44,7 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// Per-build version stamp injected by Vite (see vite.config.ts `define`). Used
// to cache-bust runtime-loaded assets such as the locale JSON files.
declare const __SOURCE_VERSION__: string;
+25
View File
@@ -1,3 +1,4 @@
import { execSync } from 'node:child_process';
import { defineConfig, type Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
@@ -10,8 +11,32 @@ import pkg from './package.json';
// Single source of truth for supported browsers; drives which polyfills the
// legacy plugin injects (see the `legacy` plugin below).
const browserslist = pkg.browserslist;
// A per-build version stamp, baked in as `__SOURCE_VERSION__`. Used to cache-bust
// the runtime-fetched locale JSONs (their URLs are otherwise fixed, so a CDN /
// browser keeps serving a stale copy after a deploy — a half-translated UI).
// Resolution order, most authoritative first:
// - SOURCE_VERSION commit SHA — injected by Scalingo's buildpack natively,
// and passed as a build-arg from our CI Docker build
// - `git` local builds / any context that ships the .git dir
// - timestamp last-resort, still unique from one deploy to the next
const appVersion =
process.env.SOURCE_VERSION ||
(() => {
try {
return execSync('git rev-parse --short HEAD', {
stdio: ['ignore', 'pipe', 'ignore'],
})
.toString()
.trim();
} catch {
return `t${Date.now()}`;
}
})();
export default defineConfig({
define: {
__SOURCE_VERSION__: JSON.stringify(appVersion),
},
plugins: [
// See ./tsr.config.json for tanstackRouter config
tanstackRouter(),
+20 -12
View File
@@ -1,7 +1,22 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
import { defineConfig, mergeConfig } from 'vitest/config';
export default defineConfig({
import viteConfig from './vite.config';
export default mergeConfig(
// Inherit the app's build config — `define`, `resolve.alias`, `envPrefix` —
// so tests compile modules exactly like the app does and can't drift from it.
// Plugins are dropped rather than merged: the router codegen, the legacy
// polyfill pass and the bundle analyzer are build-time concerns that only
// slow the runner down. `mergeConfig` concatenates arrays, so the reset has
// to happen before the merge, not in the override below.
{ ...viteConfig, plugins: [] },
defineConfig({
// Pin the build stamp: vite.config.ts derives it from the git HEAD (or a
// timestamp when .git is absent, as in the test container), which would
// make every run compile to different output for no benefit.
define: {
__SOURCE_VERSION__: JSON.stringify('test'),
},
test: {
globals: true,
environment: 'jsdom',
@@ -13,12 +28,5 @@ export default defineConfig({
reportsDirectory: '.coverage',
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
// Keep parity with vite.config.ts so tests can stub the NEXT_PUBLIC_*
// deprecated fallbacks via import.meta.env.
envPrefix: 'NEXT_PUBLIC_',
});
}),
);