diff --git a/.gitignore b/.gitignore index c49ef32f..b2e0bbc2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ obj/ .env .env.* !deploy/.env.example +# #458 — VITE_APP_VERSION only, a public string baked into the client bundle +# regardless of whether it's committed; release-please bumps it in lockstep +# with version.txt (see release-please-config.json's extra-files). +!web/.env.production appsettings.Development.local.json # OS/editor noise diff --git a/docs/decisions/351-releases.md b/docs/decisions/351-releases.md index eb641f12..02fd2edc 100644 --- a/docs/decisions/351-releases.md +++ b/docs/decisions/351-releases.md @@ -362,6 +362,15 @@ Two stages, deliberately separate: **CI publishes, the release PR versions.** `.release-please-manifest.json`, and `version.txt`. **Never hand-edit the manifest or `version.txt`**; release-please owns them and a manual edit desynchronises the version it believes from the tags that exist. +- **`extra-files` (#458) extends that ownership to `web/.env.production`'s + `VITE_APP_VERSION`** — release-please's `generic` updater bumps it in the same + release PR as `version.txt`/the manifest, anchored by an `x-release-please-version` + marker comment on the line above (not a same-line trailing comment — dotenv-style + parsers don't strip inline `#` comments the way YAML/generic key:value formats do, + so a trailing marker would have become part of the value). Vite loads + `.env.production` automatically for a production build; no Dockerfile/CI plumbing + needed beyond the file itself. Same "never hand-edit" rule applies to the value — + only the marker's presence and the file's existence are this repo's to maintain. - **The release PR is opened with a GitHub App token, not `GITHUB_TOKEN`** — and both reasons are load-bearing, so do not "simplify" it back. 1. `GITHUB_TOKEN` **cannot open a pull request at all** unless the repo-wide diff --git a/release-please-config.json b/release-please-config.json index f4dde593..5c127128 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,6 +11,12 @@ "bump-patch-for-minor-pre-major": true, "changelog-path": "CHANGELOG.md", "include-component-in-tag": false, + "extra-files": [ + { + "type": "generic", + "path": "web/.env.production" + } + ], "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug fixes" }, diff --git a/web/.env.production b/web/.env.production new file mode 100644 index 00000000..16acae7f --- /dev/null +++ b/web/.env.production @@ -0,0 +1,2 @@ +# x-release-please-version +VITE_APP_VERSION=0.0.3 diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index e6e5b148..ec480a23 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -170,6 +170,10 @@ export const en = { skipToContent: "Skip to main content", primaryNavAriaLabel: "Primary", signOut: "Sign out", + // {{version}} is a bare semver string (e.g. "0.0.2"), never itself + // translated — only the surrounding "v" template varies by locale in + // principle, though every catalog currently agrees on it (#458). + versionLabel: "v{{version}}", farmLoadFailedNeverLoaded: "Could not load this farm's settings, so dates follow this device rather than the farm.", farmLoadFailedStale: diff --git a/web/src/i18n/es.ts b/web/src/i18n/es.ts index 8b03b2cd..d96749ed 100644 --- a/web/src/i18n/es.ts +++ b/web/src/i18n/es.ts @@ -138,6 +138,7 @@ export const es = { skipToContent: "Saltar al contenido principal", primaryNavAriaLabel: "Principal", signOut: "Cerrar sesión", + versionLabel: "v{{version}}", farmLoadFailedNeverLoaded: "No se pudo cargar la configuración de esta granja, así que las " + "fechas siguen a este dispositivo en lugar de a la granja.", diff --git a/web/src/i18n/tl.ts b/web/src/i18n/tl.ts index 18cd702b..39eaefd4 100644 --- a/web/src/i18n/tl.ts +++ b/web/src/i18n/tl.ts @@ -148,6 +148,7 @@ export const tl = { skipToContent: "Lumaktaw papunta sa pangunahing content", primaryNavAriaLabel: "Pangunahin", signOut: "Mag-sign out", + versionLabel: "v{{version}}", farmLoadFailedNeverLoaded: "Hindi na-load ang mga setting ng bukid na ito, kaya susundin ng mga " + "petsa ang device na ito sa halip na ang bukid.", diff --git a/web/src/routes/AppLayout.test.tsx b/web/src/routes/AppLayout.test.tsx index 4f38758f..bdd703a8 100644 --- a/web/src/routes/AppLayout.test.tsx +++ b/web/src/routes/AppLayout.test.tsx @@ -103,6 +103,21 @@ describe("AppLayout sidebar", () => { expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); + it("shows no version line when VITE_APP_VERSION is unset (#458 — dev/test builds)", () => { + // import.meta.env.VITE_APP_VERSION is read once at module scope + // (AppLayout.tsx), matching errorReport.ts's own "absent in dev builds" + // contract — this test environment never sets it, so this proves the + // component renders nothing rather than a literal "vundefined". + renderWithProviders(, { token: { sub: "u1", role: "Admin" } }); + // Matches either a real version ("v0.0.2") or i18next's literal rendering + // of a missing interpolation (a bare "v", the {{version}} slot rendering + // empty) — both must be absent. Measured directly: i18next does not + // render "vundefined" for a missing var, it renders an empty + // interpolation, so a regex assuming the JS-string-concat shape would + // have silently passed against the exact bug this guards. + expect(screen.queryByText(/^v(\d.*)?$/)).not.toBeInTheDocument(); + }); + it("toggles light ↔ night, flipping the control and the root data-theme", () => { renderWithProviders(, { token: { sub: "u1", role: "Admin" } }); // jsdom has no matchMedia → initial theme resolves to light, so the control diff --git a/web/src/routes/AppLayout.tsx b/web/src/routes/AppLayout.tsx index ad7b13cb..06219747 100644 --- a/web/src/routes/AppLayout.tsx +++ b/web/src/routes/AppLayout.tsx @@ -12,6 +12,10 @@ import { navGroups, tabEntries } from "./nav"; const ICON = 17; +// #458 — same env var, same "absent in dev" contract errorReport.ts already +// relies on for crash reports; read once at module scope rather than per render. +const APP_VERSION = import.meta.env.VITE_APP_VERSION as string | undefined; + // Authenticated shell (#52 redesign): an aubergine sidebar — the brand's // navigation spine — with the 15+ destinations grouped by job, each with a // lucide glyph. Role-tiered (#103): links and whole groups hide per role; the @@ -73,6 +77,10 @@ export function AppLayout() { + {/* #458 — set at build time (VITE_APP_VERSION, release-please-owned + via web/.env.production); absent in dev builds, so this line + simply doesn't render rather than showing "vundefined". */} + {APP_VERSION &&

{t("versionLabel", { version: APP_VERSION })}

} diff --git a/web/src/styles.css b/web/src/styles.css index 69dce497..39db25d0 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1223,6 +1223,14 @@ input.cell { text-decoration: none; } +.sidebar-version { + margin: 0.35rem 0 0.6rem; + padding: 0 0.25rem; + font-size: 0.75rem; + color: var(--on-brand-mute); + opacity: 0.7; +} + /* -------------------------------------------- Bottom nav (mobile) */ /* The tab bar and its More sheet own navigation below 900px; the sidebar owns