Skip to content

Commit 5e02ba7

Browse files
sylvinusjbpenrath
authored andcommitted
✨(build) add cache-busting source version in build
1 parent 97010bd commit 5e02ba7

7 files changed

Lines changed: 60 additions & 3 deletions

File tree

.github/workflows/messages-ghcr.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ jobs:
100100
context: "src/frontend"
101101
target: runtime-prod
102102
arm64_reuse_amd64_build_arg: "FRONTEND_IMAGE"
103+
# Bakes the commit SHA into the build as the asset cache-buster
104+
# (vite.config.ts → __SOURCE_VERSION__); without it the build falls back to a
105+
# timestamp. The src/frontend context ships no .git, so this is the only
106+
# way the image gets the real SHA. SOURCE_VERSION matches the var name
107+
# Scalingo's buildpack sets natively, so both deploy paths agree.
108+
build_args: "SOURCE_VERSION=${{ github.sha }}"
103109

104110
docker-publish-backend:
105111
needs: docker-publish-python-uv

src/frontend/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ COPY --chown=${DOCKER_USER} . ./
4141
ARG API_ORIGIN
4242
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
4343

44+
# Commit SHA (or any per-deploy version) baked in as the asset cache-buster —
45+
# see vite.config.ts. Named SOURCE_VERSION to match the var Scalingo's buildpack
46+
# sets natively. Pass `--build-arg SOURCE_VERSION=$(git rev-parse --short HEAD)`;
47+
# if omitted the build falls back to a timestamp (still unique per deploy).
48+
ARG SOURCE_VERSION
49+
ENV SOURCE_VERSION=${SOURCE_VERSION}
50+
4451
RUN npm run build
4552

4653
# Normalize output path to /app (matches the runtime-prod layout)

src/frontend/caddy/Caddyfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@
4747
header_up X-Forwarded-For {remote_host}
4848
}
4949

50+
# --- Cache policy for the static SPA output ---
51+
# Vite fingerprints everything under /assets, so a given URL's bytes
52+
# never change: cache it forever. A new deploy emits new hashed URLs.
53+
@immutable path /assets/*
54+
header @immutable Cache-Control "public, max-age=31536000, immutable"
55+
# The SPA shell and other non-fingerprinted files (index.html, the
56+
# /locales JSON, images) must revalidate every load, or a deploy's new
57+
# asset hashes / translations only show up after a manual hard refresh.
58+
@revalidate not path /assets/*
59+
header @revalidate Cache-Control "no-cache"
60+
5061
# SPA fallback: any unmatched request returns index.html so the
5162
# client-side router can resolve the path.
5263
try_files {path} /index.html

src/frontend/public/locales/common/fr-FR.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,9 @@
332332
"Check DNS again": "Revérifier les DNS",
333333
"Check for new mail regularly": "Synchroniser ce compte régulièrement",
334334
"Checking DNS records...": "Vérification des enregistrements DNS...",
335-
"Checking every {{count}} min_one": "Synchronisation toutes les {{count}} min",
336-
"Checking every {{count}} min_many": "Synchronisation toutes les {{count}} min",
337-
"Checking every {{count}} min_other": "Synchronisation toutes les {{count}} min",
335+
"Checking every {{count}} min_one": "Synchro toutes les {{count}} min",
336+
"Checking every {{count}} min_many": "Synchro toutes les {{count}} min",
337+
"Checking every {{count}} min_other": "Synchro toutes les {{count}} min",
338338
"Choose calendar": "Choisir l'agenda",
339339
"Choose the type of integration you want to create": "Choisissez le type d'intégration que vous souhaitez créer",
340340
"Clear filters": "Retirer les filtres",

src/frontend/src/features/i18n/initI18n.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export const initI18n = (config: AppConfig) => {
4343
returnEmptyString: false,
4444
backend: {
4545
loadPath: "/locales/{{ns}}/{{lng}}.json",
46+
// Cache-bust the static locale files on every deploy: their URLs are
47+
// otherwise fixed, so a CDN/browser keeps serving a stale copy and any
48+
// newly added translation falls back to English (a half-translated UI).
49+
queryStringParams: { v: __SOURCE_VERSION__ },
4650
}
4751
})
4852
.catch((error) => {

src/frontend/src/vite-env.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ interface ImportMetaEnv {
4444
interface ImportMeta {
4545
readonly env: ImportMetaEnv;
4646
}
47+
48+
// Per-build version stamp injected by Vite (see vite.config.ts `define`). Used
49+
// to cache-bust runtime-loaded assets such as the locale JSON files.
50+
declare const __SOURCE_VERSION__: string;

src/frontend/vite.config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { execSync } from 'node:child_process';
12
import { defineConfig, type Plugin } from 'vite';
23
import react from '@vitejs/plugin-react';
34
import legacy from '@vitejs/plugin-legacy';
@@ -10,8 +11,32 @@ import pkg from './package.json';
1011
// Single source of truth for supported browsers; drives which polyfills the
1112
// legacy plugin injects (see the `legacy` plugin below).
1213
const browserslist = pkg.browserslist;
14+
// A per-build version stamp, baked in as `__SOURCE_VERSION__`. Used to cache-bust
15+
// the runtime-fetched locale JSONs (their URLs are otherwise fixed, so a CDN /
16+
// browser keeps serving a stale copy after a deploy — a half-translated UI).
17+
// Resolution order, most authoritative first:
18+
// - SOURCE_VERSION commit SHA — injected by Scalingo's buildpack natively,
19+
// and passed as a build-arg from our CI Docker build
20+
// - `git` local builds / any context that ships the .git dir
21+
// - timestamp last-resort, still unique from one deploy to the next
22+
const appVersion =
23+
process.env.SOURCE_VERSION ||
24+
(() => {
25+
try {
26+
return execSync('git rev-parse --short HEAD', {
27+
stdio: ['ignore', 'pipe', 'ignore'],
28+
})
29+
.toString()
30+
.trim();
31+
} catch {
32+
return `t${Date.now()}`;
33+
}
34+
})();
1335

1436
export default defineConfig({
37+
define: {
38+
__SOURCE_VERSION__: JSON.stringify(appVersion),
39+
},
1540
plugins: [
1641
// See ./tsr.config.json for tanstackRouter config
1742
tanstackRouter(),

0 commit comments

Comments
 (0)