From 21b014937191f3a14b0b1b178c87a7e006f566de Mon Sep 17 00:00:00 2001 From: Fabio1988 Date: Tue, 3 Nov 2026 23:05:55 +0100 Subject: [PATCH 1/3] chore(docker): slim image and drop runtime git - Multi-stage Dockerfile (build -> prune -> runtime) with a BuildKit yarn cache mount, so the 2.2 GB yarn cache never lands in a layer - Add .dockerignore (keeps .git, node_modules and local configs/secrets out of the build context; configs are mounted as documented) - Pass GIT_SHA/GIT_REF as build args instead of .gitsha/.gitref files, and remove the dead placeholder files - checkForUpdates: read sha/ref from env, fetch the remote sha via the GitHub API (no git binary needed), skip non-branch refs (PR images) - Move vite + build plugins to dependencies since `yarn start` builds the client on boot; lazy-load vite-plugin-checker (dev only) - Name the Sentry release from SENTRY_RELEASE/GIT_SHA (the plugin used `git rev-parse HEAD`, which only worked because the image had .git) - Move openai/gpt-tokenizer to @rm/locales devDependencies and drop the unused `generate` re-export; keep loading .env in @rm/locales - Remove unused monaco-editor devDependency (the playground loads Monaco from jsDelivr via @monaco-editor/loader) - docker.yml: buildx, current action versions, GHA layer cache, provenance disabled to keep publishing a single manifest Image: 3 GB -> 608 MB uncompressed (~1.4 GB -> ~207 MB compressed) Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 32 +++++++++++++++++ .github/workflows/docker.yml | 25 ++++++++----- .gitignore | 2 -- .gitref | 0 .gitsha | 0 Dockerfile | 50 ++++++++++++++++++++------ package.json | 11 +++--- packages/locales/lib/generate.js | 1 + packages/locales/lib/index.js | 5 +-- packages/locales/package.json | 8 ++--- server/src/services/checkForUpdates.js | 50 +++++++++++++++++--------- vite.config.js | 11 ++++-- yarn.lock | 5 --- 13 files changed, 144 insertions(+), 56 deletions(-) create mode 100644 .dockerignore delete mode 100644 .gitref delete mode 100644 .gitsha diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..91a3a07bc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# VCS / CI +.git +.github +.husky + +# Dependencies & build output (rebuilt inside the image) +node_modules +**/node_modules +dist +dist-* + +# Local runtime state & secrets (mount these instead) +.env +config/local.json +config/areas.json +server/src/configs/** +!server/src/configs/.gitkeep +server/.cache/** +!server/.cache/.gitkeep +logs/** +!logs/.gitkeep +.eslintcache + +# Docs & editor files +docs +*.md +.idea +.vscode +.DS_Store +docker-compose*.yml +Dockerfile +.dockerignore diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e4eef64a0..8a775bd21 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,15 +8,17 @@ env: jobs: release: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - - uses: actions/checkout@v4.1.1 - - name: Set .gitsha - run: 'echo ${{ github.sha }} > .gitsha' - - name: Set .gitref - run: 'echo ${{ github.ref }} > .gitref' + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Log in to the Container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -24,14 +26,21 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + GIT_SHA=${{ github.sha }} + GIT_REF=${{ github.ref }} + # keep publishing a single image manifest like before (no attestation index) + provenance: false + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index ce91cb8c7..accb7b059 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ packages/masterfile/lib/data/masterfile.json # Docker file /docker-compose.yml -.gitref -.gitsha # Favicon public/favicon/* diff --git a/.gitref b/.gitref deleted file mode 100644 index e69de29bb..000000000 diff --git a/.gitsha b/.gitsha deleted file mode 100644 index e69de29bb..000000000 diff --git a/Dockerfile b/Dockerfile index 150089eac..16939bb9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,48 @@ -## Simple Dockerfile to build ReactMap (main branch) -# - Inside the container, the content of this git repo lives in /home/node/ +# syntax=docker/dockerfile:1 +## ReactMap Docker image +# - Inside the container, the app lives in /home/node/ +# - `yarn start` builds the client with your config, then starts the server. ## You have to mount your configs into the container: # - mount local.json to /home/node/server/src/configs/local.json # - mount areas.json to /home/node/server/src/configs/areas.json # - Also mount every other configuration file necessary into the according directory. -FROM node:22.23.4-alpine - -ENV NPM_CONFIG_PREFIX=/home/node/.npm-global -ENV PATH=$PATH:/home/node/.npm-global/bin +ARG NODE_IMAGE=node:22.23.4-alpine +# ---- build: full install + production build (verifies the bundle compiles) ---- +FROM ${NODE_IMAGE} AS build WORKDIR /home/node -COPY package.json . -COPY yarn.lock . -RUN apk add git -RUN npm install -g yarn +# git is only needed to fetch git-URL dependencies during install +RUN apk add --no-cache git +ENV HUSKY=0 + +# Workspace manifests, packages and default config are needed by `postinstall` +COPY package.json yarn.lock ./ +COPY packages ./packages +COPY config ./config +RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \ + yarn install --frozen-lockfile --network-timeout 600000 + COPY . . -RUN yarn install RUN yarn build + +# ---- prune: drop devDependencies in place (native builds + masterfile are kept) ---- +FROM build AS prune +RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \ + yarn install --production --frozen-lockfile --ignore-scripts --network-timeout 600000 + +# ---- runtime ---- +FROM ${NODE_IMAGE} +# Kept for parity with previous images (global npm installs in derived images) +ENV NPM_CONFIG_PREFIX=/home/node/.npm-global +ENV PATH=$PATH:/home/node/.npm-global/bin +WORKDIR /home/node +COPY --from=prune /home/node /home/node + +ARG GIT_SHA="" +ARG GIT_REF="" +ENV GIT_SHA=${GIT_SHA} \ + GIT_REF=${GIT_REF} + +EXPOSE 8080 +CMD ["yarn", "start"] diff --git a/package.json b/package.json index 58581544b..d956e5d1c 100644 --- a/package.json +++ b/package.json @@ -115,8 +115,10 @@ "@rm/locales": "*", "@rm/logger": "*", "@rm/masterfile": "*", + "@rm/vite-plugins": "*", "@sentry/node": "^7.65.0", "@sentry/react": "^8.55.2", + "@sentry/vite-plugin": "4.6.2", "@turf/boolean-contains": "7.3.5", "@turf/boolean-overlap": "7.3.5", "@turf/boolean-point-in-polygon": "7.3.5", @@ -124,6 +126,7 @@ "@turf/destination": "7.3.5", "@turf/distance": "7.3.5", "@turf/helpers": "7.3.5", + "@vitejs/plugin-react-swc": "^3.8.1", "bcrypt": "^5.0.1", "body-parser": "2.4.0", "bytes": "3.1.2", @@ -175,28 +178,27 @@ "react-leaflet": "5.0.0", "react-router": "^8.3.0", "react-virtuoso": "4.18.7", + "rollup-plugin-delete": "^2.0.0", "rtree": "^1.4.2", "source-map": "^0.7.4", "suncalc": "^1.9.0", "supercluster": "^8.0.1", "uicons.js": "2.3.0", + "vite": "^6.4.3", "zustand": "4.5.7" }, "devDependencies": { "@commitlint/cli": "^19.4.0", "@commitlint/config-conventional": "^19.2.2", "@rm/types": "*", - "@rm/vite-plugins": "*", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", - "@sentry/vite-plugin": "4.6.2", "@types/dlv": "^1.1.2", "@types/leaflet": "^1.9.21", "@types/node": "^22.20.1", "@types/node-fetch": "2.6.13", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react-swc": "^3.8.1", "commitizen": "^4.3.0", "cz-conventional-commit": "^1.0.6", "eslint": "^8.44.0", @@ -209,13 +211,10 @@ "eslint-plugin-react-hooks": "^4.6.0", "husky": "^8.0.1", "lint-staged": "^15.2.9", - "monaco-editor": "^0.41.0", "nodemon": "3.1.14", "prettier": "3.3.3", - "rollup-plugin-delete": "^2.0.0", "semantic-release": "^22", "typescript": "5.5.4", - "vite": "^6.4.3", "vite-plugin-checker": "0.7.2" }, "resolutions": { diff --git a/packages/locales/lib/generate.js b/packages/locales/lib/generate.js index 406d2f762..ecc5ecdde 100644 --- a/packages/locales/lib/generate.js +++ b/packages/locales/lib/generate.js @@ -2,6 +2,7 @@ // @ts-check /* eslint-disable no-restricted-syntax */ /* eslint-disable guard-for-in */ +/* eslint-disable import/no-extraneous-dependencies */ require('dotenv').config() const { OpenAI } = require('openai') diff --git a/packages/locales/lib/index.js b/packages/locales/lib/index.js index 32cb828e0..6f28a53ca 100644 --- a/packages/locales/lib/index.js +++ b/packages/locales/lib/index.js @@ -1,8 +1,10 @@ // @ts-check +// Consumers such as vite.config.js rely on `.env` being loaded here +// (e.g. NODE_CONFIG_ENV selects the dist-* output dir), so keep this +require('dotenv').config() const { create } = require('./create') const { missing } = require('./missing') -const { generate } = require('./generate') const { readLocaleDirectory, writeAll, @@ -17,6 +19,5 @@ module.exports.locales = locales module.exports.status = status module.exports.create = create module.exports.missing = missing -module.exports.generate = generate module.exports.writeAll = writeAll module.exports.readAndParseJson = readAndParseJson diff --git a/packages/locales/package.json b/packages/locales/package.json index 4f47b8e05..cfcd20fd2 100644 --- a/packages/locales/package.json +++ b/packages/locales/package.json @@ -18,11 +18,11 @@ "@rm/config": "*", "@rm/logger": "*", "dotenv": "^16.3.1", - "gpt-tokenizer": "2.2.1", - "node-fetch": "2.7.0", - "openai": "6.9.1" + "node-fetch": "2.7.0" }, "devDependencies": { - "@types/node-fetch": "2.6.13" + "@types/node-fetch": "2.6.13", + "gpt-tokenizer": "2.2.1", + "openai": "6.9.1" } } diff --git a/server/src/services/checkForUpdates.js b/server/src/services/checkForUpdates.js index c3d2b52ba..5dacb79cb 100644 --- a/server/src/services/checkForUpdates.js +++ b/server/src/services/checkForUpdates.js @@ -1,20 +1,30 @@ // @ts-check const { exec } = require('child_process') const { promisify } = require('util') -const path = require('path') -const fs = require('fs') const { log, TAGS } = require('@rm/logger') const execPromise = promisify(exec) +const REPO_API = 'https://api.github.com/repos/WatWowMap/ReactMap' + +/** + * Docker images receive `GIT_REF` / `GIT_SHA` at build time, + * bare-metal installs fall back to the local git checkout. + * + * @returns {Promise<{ branch: string, isDocker: boolean }>} + */ async function getCurrentBranch() { + const gitRef = process.env.GIT_REF?.trim() + if (gitRef) { + return { + branch: gitRef.startsWith('refs/heads/') + ? gitRef.slice('refs/heads/'.length) + : '', + isDocker: true, + } + } try { - const gitRef = fs - .readFileSync(path.resolve(`${__dirname}/../../../.gitref`), 'utf8') - .trim() - if (gitRef) return { branch: gitRef.split('/')[2].trim(), isDocker: true } - const { stdout } = await execPromise('git branch --show-current') return { branch: stdout.trim(), isDocker: false } } catch (e) { @@ -29,12 +39,9 @@ async function getCurrentBranch() { } async function getCurrentSha() { + const gitSha = process.env.GIT_SHA?.trim() + if (gitSha) return gitSha try { - const gitSha = fs - .readFileSync(path.resolve(`${__dirname}/../../../.gitsha`), 'utf8') - .trim() - if (gitSha) return gitSha - const { stdout } = await execPromise('git rev-parse HEAD') return stdout.trim() } catch (e) { @@ -51,10 +58,17 @@ async function getCurrentSha() { /** @param {string} branch */ async function getRemoteSha(branch) { try { - const { stdout } = await execPromise( - `git ls-remote https://github.com/WatWowMap/ReactMap/ refs/heads/${branch}`, + const response = await fetch( + `${REPO_API}/commits/${encodeURIComponent(branch)}`, + { + headers: { Accept: 'application/vnd.github.sha' }, + signal: AbortSignal.timeout(10_000), + }, ) - return stdout.split('\t')[0] + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText}`) + } + return (await response.text()).trim() } catch (e) { log.info( TAGS.update, @@ -71,9 +85,13 @@ async function getRemoteSha(branch) { async function checkForUpdates() { try { const { branch, isDocker } = await getCurrentBranch() + if (!branch) { + log.info(TAGS.update, 'Not running from a branch, skipping update check') + return + } const sha = await getCurrentSha() const remoteSha = await getRemoteSha(branch) - if (remoteSha !== sha) { + if (sha && remoteSha && remoteSha !== sha) { log.info( TAGS.update, 'There is a new version available: ', diff --git a/vite.config.js b/vite.config.js index f630f4815..d7961884a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,7 +4,6 @@ const { defineConfig, loadEnv, createLogger } = require('vite') const { default: react } = require('@vitejs/plugin-react-swc') -const { default: checker } = require('vite-plugin-checker') const removeFiles = require('rollup-plugin-delete') const { resolve } = require('path') const fs = require('fs') @@ -74,7 +73,7 @@ const viteConfig = defineConfig(({ mode }) => { react(), ...(isDevelopment ? [ - checker({ + require('vite-plugin-checker').default({ overlay: { initialIsOpen: false, }, @@ -94,6 +93,14 @@ const viteConfig = defineConfig(({ mode }) => { org: sentry.org, project: sentry.project, authToken: sentry.authToken, + // Docker images have no git, so fall back to the baked GIT_SHA + // (the plugin would otherwise run `git rev-parse HEAD`) + release: { + name: + process.env.SENTRY_RELEASE || + process.env.GIT_SHA || + undefined, + }, }), ] : []), diff --git a/yarn.lock b/yarn.lock index 98e3ee3ca..08b96e093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7171,11 +7171,6 @@ moment@^2.29.4: resolved "https://registry.yarnpkg.com/moment/-/moment-2.31.0.tgz#6e19184e8005a9dfc61c9351263d4d6ea2ace7aa" integrity sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw== -monaco-editor@^0.41.0: - version "0.41.0" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.41.0.tgz#2ba31e5af7e3ae93ac5d7467ec2772ef9b3d967f" - integrity sha512-1o4olnZJsiLmv5pwLEAmzHTE/5geLKQ07BrGxlF4Ri/AXAc2yyDGZwHjiTqD8D/ROKUZmwMA28A+yEowLNOEcA== - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" From 6db824217fbb3d7808aab0fa2dc50b9e1de30f62 Mon Sep 17 00:00:00 2001 From: Fabio1988 Date: Tue, 3 Nov 2026 23:36:28 +0100 Subject: [PATCH 2/3] fix(update): skip the check for images built without git metadata The Dockerfile always defines GIT_REF/GIT_SHA (empty without --build-arg), so treat their presence as Docker and never fall back to git, which the image doesn't ship. Also skip the remote lookup whenever the local sha is unknown instead of awaiting a fetch whose result can't be compared. Co-Authored-By: Claude Opus 5.5 --- server/src/services/checkForUpdates.js | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/server/src/services/checkForUpdates.js b/server/src/services/checkForUpdates.js index 5dacb79cb..a112ce41d 100644 --- a/server/src/services/checkForUpdates.js +++ b/server/src/services/checkForUpdates.js @@ -9,14 +9,15 @@ const execPromise = promisify(exec) const REPO_API = 'https://api.github.com/repos/WatWowMap/ReactMap' /** - * Docker images receive `GIT_REF` / `GIT_SHA` at build time, - * bare-metal installs fall back to the local git checkout. - * - * @returns {Promise<{ branch: string, isDocker: boolean }>} + * Docker images always define `GIT_REF` / `GIT_SHA` (empty when built without + * `--build-arg`) and ship no git, bare-metal installs use the local checkout. */ +const isDocker = 'GIT_REF' in process.env + +/** @returns {Promise<{ branch: string, isDocker: boolean }>} */ async function getCurrentBranch() { - const gitRef = process.env.GIT_REF?.trim() - if (gitRef) { + if (isDocker) { + const gitRef = (process.env.GIT_REF || '').trim() return { branch: gitRef.startsWith('refs/heads/') ? gitRef.slice('refs/heads/'.length) @@ -39,8 +40,7 @@ async function getCurrentBranch() { } async function getCurrentSha() { - const gitSha = process.env.GIT_SHA?.trim() - if (gitSha) return gitSha + if (isDocker) return (process.env.GIT_SHA || '').trim() try { const { stdout } = await execPromise('git rev-parse HEAD') return stdout.trim() @@ -84,12 +84,15 @@ async function getRemoteSha(branch) { async function checkForUpdates() { try { - const { branch, isDocker } = await getCurrentBranch() - if (!branch) { - log.info(TAGS.update, 'Not running from a branch, skipping update check') + const { branch } = await getCurrentBranch() + const sha = branch ? await getCurrentSha() : '' + if (!branch || !sha) { + log.info( + TAGS.update, + 'No branch or commit information available, skipping update check', + ) return } - const sha = await getCurrentSha() const remoteSha = await getRemoteSha(branch) if (sha && remoteSha && remoteSha !== sha) { log.info( From 0047ed6fcb00c9f64c7d3d488b0a6dabcf5c9fd2 Mon Sep 17 00:00:00 2001 From: Fabio1988 Date: Tue, 3 Nov 2026 23:45:48 +0100 Subject: [PATCH 3/3] ci(docker): bump actions to Node 24 majors and trim log noise - checkout v7, setup-buildx v4, login v4, metadata v6, build-push v7 (none of their breaking changes affect this workflow) - BUILDX_METADATA_PROVENANCE=disabled drops the ~600 line provenance trace from the printed build metadata - DOCKER_BUILD_RECORD_UPLOAD=false stops uploading a .dockerbuild artifact on every run; the job summary is kept Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docker.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8a775bd21..48e0d27dc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,13 +12,13 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -26,12 +26,17 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 + env: + # keep the log readable: no provenance trace in the printed metadata, + # and no .dockerbuild record artifact on every run (summary stays) + BUILDX_METADATA_PROVENANCE: disabled + DOCKER_BUILD_RECORD_UPLOAD: false with: context: . push: true