Skip to content

Commit f5e2734

Browse files
timwessmanclaudeterovirtanen
authored
feat: migrate kukkuu admin yarn to pnpm
* chore: migrate from yarn to pnpm KK-1539 - Pin pnpm@11.5.0 via packageManager field + engines.pnpm - Convert yarn.lock to pnpm-lock.yaml via pnpm import - Translate yarn resolutions to pnpm overrides in pnpm-workspace.yaml - Add supply-chain guardrail: minimumReleaseAge 10080 - Allow native build scripts: esbuild, full-icu, @ffmpeg-installer - Declare formerly-phantom deps that yarn's flat node_modules hoisted (lodash, react-query, react-hook-form, react-router, react-router-dom, ra-core, ra-i18n-polyglot, @mui/styled-engine), deduped to the versions react-admin already resolves - Drop stale v5 @types/react-router* (code uses v6, ships own types) - Remove CRA-leftover src/react-app-env.d.ts (imported react-scripts) - Update Dockerfile to install pnpm via corepack (ubi9/Node 20) - CI to City-of-Helsinki/.github ci-pnpm-node.yml reusable workflow - Replace yarn with pnpm in husky hooks, lint-staged, README, scripts - Add yarn.lock, .yarn/, .yarnrc.yml to .gitignore Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: runtime env config, Node 24 and Helsinki base images KK-1539 Phase 2 of the pnpm migration: adopt the runtime environment configuration approach from kukkuu-ui (PR #757) and move onto the shared Helsinki base images on Node 24. - Add envUtils getEnvValue(key): window._env_ -> process.env -> import.meta.env fallback (src/common/utils/envUtils.ts) - Generate public/env-config.js via scripts/update-runtime-env.ts, load it in index.html; add src/types/env.d.ts and tsx devDep - Prefix start/build scripts with update-runtime-env - Migrate all import.meta.env.VITE_* reads to getEnvValue - Bump to Node 24 (.nvmrc, engines.node, @types/node, CI) - Dockerfile: ubi9/nodejs-24-pnpm-builder-base builder + ubi10/nginx-126-spa-standard production with runtime env.sh injection; COPY .env.example as .env; drop build-time VITE_ args (kept VITE_CSP_REPORT_URI for the index.html CSP) - Remove the custom .prod/nginx.conf.template (base image provides nginx and the /readiness endpoint) - Ignore generated env-config.js in eslint/prettier Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: restrict runtime env-config.js to VITE_ keys (no secret leakage) KK-1539 env-config.js is served publicly, so anything in window._env_ is world-readable. Harden the runtime env injection so it cannot expose secrets, restoring the secure-by-default boundary that Vite's envPrefix gave the old import.meta.env model. - update-runtime-env.ts: emit only VITE_-prefixed keys (plus NODE_ENV) into env-config.js, mirroring Vite's envPrefix. Secrets such as BROWSER_TESTS_JWT_SIGN_SECRET can no longer leak into the browser bundle even if present in the build/runtime environment. - .env.example: drop BROWSER_TESTS_* keys. This file is also copied into the production image as the env-config.js key template; browser-test settings (incl. secrets) belong in .env.test.local (see .env.test.local.example). Recommended hardening for other City-of-Helsinki SPAs using the same runtime env-config.js pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: temporaly ref to review pipeline * chore: sync pnpm lockfile with main's vite 8 / vitest 4 bumps * fix: parse boolean env vars correctly, fix OIDC scope error label Address bot review findings on PR #394: - Add getEnvBoolean() helper. getEnvValue() returns env values as strings, so Boolean(value) treated "false"/"0" as true. This broke IS_TEST_ENVIRONMENT (".env.example"/".env.test" set it to 0, so it was always true) and made VITE_OIDC_AUTOMATIC_SILENT_RENEW_ENABLED impossible to disable. The helper recognizes true/1 and false/0 (case-insensitive). - Fix trailing-comma typo in the VITE_OIDC_SCOPE getEnvOrError label. - Add regression tests for IS_TEST_ENVIRONMENT parsing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: trim Docker build context via .dockerignore Address review feedback (mikkojamG) on the broad `COPY . ./` in the build stage. The final production image is already lean (only /build is copied into the nginx stage), but the shared appbase/staticbuilder layers and the build context pulled in everything. Exclude paths outside the production build graph (tsconfig "include" is ["src"]): tests, browser-tests, docs, pipelines, .github, and the codegen/lint/release tooling and quality reports. Also: - exclude *.local env files so local secrets (e.g. the browser-test JWT secret in .env.test.local) are never baked into an image layer. - fix a stale entry: LICENSE -> LICENSE.md (old pattern never matched). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: copy only build-needed files in Dockerfile Replace the broad `COPY . ./` in the build stage with explicit copies, matching the pattern used in other City-of-Helsinki UI repos (e.g. kerrokantasi-ui). Per stage: - copy scripts/, public/ and .env* before running update-runtime-env; - copy the build configs (Vite, .babelrc for @vitejs/plugin-react, and the eslint/prettier configs run by @nabla/vite-plugin-eslint) plus src/ for the `tsc && vite build`. Editing src/ no longer invalidates the install/update-runtime-env layers, and only the production build graph enters the image. Verified locally: staticbuilder, production and development targets all build successfully (tsc + vite build pass, assets emitted). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: sanitize env before copying to container * fix: set dev OIDC config for review env browser tests The browser-test harness mints its login-bypass session for the dev OIDC client (kukkuu-admin-ui-dev) and uses the dev API, but the deployed review app read its client id from env-config.js, which fell back to the test default (kukkuu-admin-ui-test) from .env.example. oidc-client-ts keys the stored session as oidc.user:{authority}:{client_id}, so the mismatch meant the app never found the injected session and every test was stuck on the login page. Override the dev VITE_OIDC_* / VITE_API_URI values in the review pipeline's configMap so the runtime env-config.js matches what the harness expects. The kukkuu-pipelines template ref (pnpm_KK-1539) currently omits these; this override can be dropped once that ref is reverted to default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: removed temporal ref --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Tero Virtanen <tero.tapio.virtanen@gmail.com>
1 parent 94a1eb7 commit f5e2734

29 files changed

Lines changed: 13684 additions & 10515 deletions

.dockerignore

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,55 @@
1+
# Keep the build context small and the appbase/staticbuilder layers cache-stable:
2+
# only src/, index.html, public/, scripts/ and the build configs are needed for
3+
# `pnpm build` (tsconfig "include" is just ["src"]). Everything excluded here is
4+
# outside the production build graph.
5+
6+
# Dependencies & build output (reinstalled/regenerated in the image)
17
node_modules
8+
build
29
npm-debug.log
10+
.eslintcache
11+
12+
# Docker / compose (not needed inside the image)
313
Dockerfile*
414
docker-compose*
515
compose.*
616
.dockerignore
17+
18+
# VCS & editor
719
.git
820
.gitignore
9-
README.md
10-
LICENSE
1121
.vscode
12-
build
22+
23+
# Local env files that may contain secrets. The image gets its config at runtime
24+
# (env.sh) and its key template from .env.example — never bake local secrets into
25+
# an image layer.
26+
*.local
27+
28+
# Docs, CI/CD and repo meta — not part of the production build
29+
README.md
30+
LICENSE.md
31+
CHANGELOG.md
32+
CODEOWNERS
33+
docs
34+
.github
35+
pipelines
36+
37+
# Tests & test tooling (the build only compiles src/)
38+
tests
39+
browser-tests
40+
.testcaferc.json
41+
42+
# GraphQL codegen (run separately, not during the production build)
43+
codegen.ts
44+
apollo.config.js
45+
46+
# Lint / commit / release tooling (git-hook & release-time only)
47+
.husky
48+
lint-staged.config.js
49+
commitlint.config.cjs
50+
release-please-config.json
51+
.release-please-manifest.json
52+
53+
# Quality reports
54+
sonar-project.properties
55+
sonar-report.json

.env.example

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ VITE_API_URI=https://kukkuu.api.test.hel.ninja/graphql
1212
VITE_IS_TEST_ENVIRONMENT=0
1313
VITE_IDLE_TIMEOUT_IN_MS=3600000
1414

15-
BROWSER_TESTS_UID=
16-
BROWSER_TESTS_PWD=
17-
BROWSER_TESTS_ENV_URL=http://localhost:3001
15+
# NOTE: Only client-safe VITE_* variables (and PORT) belong here. This file is
16+
# the source of truth for the app's runtime config and is also copied into the
17+
# production image as the env-config.js key template, which is served publicly.
18+
# Browser-test settings (incl. secrets) live in .env.test.local — see
19+
# .env.test.local.example.

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
src/domain/api/generatedTypes/graphql.tsx
22
build/*
3+
public/env-config.js
4+
public/test-env-config.js

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ on:
99

1010
jobs:
1111
common:
12-
uses: City-of-Helsinki/.github/.github/workflows/ci-node.yml@main
12+
uses: City-of-Helsinki/.github/.github/workflows/ci-pnpm-node.yml@main
1313
secrets: inherit
1414
with:
15-
node-version: 20
15+
node-version: 24

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@
1111
# production
1212
/build
1313

14+
# runtime env config generated by scripts/update-runtime-env.ts
15+
public/env-config.js
16+
public/test-env-config.js
17+
1418
# misc
1519
.DS_Store
20+
.eslintcache
1621
.env.local
1722
.env.development.local
1823
.env.test.local
@@ -22,6 +27,9 @@
2227
npm-debug.log*
2328
yarn-debug.log*
2429
yarn-error.log*
30+
yarn.lock
31+
.yarn/
32+
.yarnrc.yml
2533

2634
# browser test screenshots
2735
screenshots

.husky/pre-commit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
yarn doctoc .
2-
yarn lint-staged --relative
1+
pnpm doctoc .
2+
pnpm lint-staged --relative

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v20.19.5
1+
v24.15.0

.prettierignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.yarn
22
graphql.ts
3-
graphql.tsx
3+
graphql.tsx
4+
public/env-config.js
5+
public/test-env-config.js

.prod/nginx.conf.template

Lines changed: 0 additions & 52 deletions
This file was deleted.

Dockerfile

Lines changed: 86 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,54 @@
1-
# ===============================================
2-
FROM registry.access.redhat.com/ubi9/nodejs-20 AS appbase
3-
# ===============================================
4-
5-
# install yarn
6-
USER root
7-
RUN curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo
8-
RUN yum -y install yarn
9-
1+
# ============================================================
2+
# STAGE 1: Build base (install dependencies)
3+
# ============================================================
4+
FROM helsinki.azurecr.io/ubi9/nodejs-24-pnpm-builder-base AS appbase
105
WORKDIR /app
116

12-
# Offical image has npm log verbosity as info. More info - https://github.com/nodejs/docker-node#verbosity
13-
ENV NPM_CONFIG_LOGLEVEL warn
14-
15-
# set our node environment, either development or production
16-
# defaults to production, compose overrides this to development on build and run
17-
ARG NODE_ENV=${NODE_ENV:-"production"}
18-
ENV NODE_ENV $NODE_ENV
19-
20-
# Global npm deps in a non-root user directory
21-
ENV NPM_CONFIG_PREFIX=/app/.npm-global
22-
ENV PATH=$PATH:/app/.npm-global/bin
23-
24-
# Yarn
25-
ENV YARN_VERSION 1.22.22
26-
RUN yarn policies set-version ${YARN_VERSION}
27-
28-
# Copy package.json and package-lock.json/yarn.lock files
29-
COPY --chown=default:root package*.json *yarn* ./
30-
31-
# Install npm dependencies
32-
ENV PATH /app/node_modules/.bin:$PATH
33-
34-
RUN yarn install --frozen-lockfile --ignore-scripts && yarn cache clean --force
35-
36-
# =============================
7+
# Defaults to production; compose overrides this to development on build and run.
8+
ARG NODE_ENV=production
9+
ENV NODE_ENV=$NODE_ENV
10+
11+
# 1. Install dependencies (cached unless the manifests change).
12+
# corepack in the base image uses the pnpm version from package.json's
13+
# "packageManager" field automatically.
14+
COPY --chown=default:root package.json pnpm-lock.yaml pnpm-workspace.yaml ./
15+
RUN pnpm install --frozen-lockfile --ignore-scripts && pnpm store prune
16+
17+
# 2. Generate a build-time public/env-config.js. Its *contents* are overwritten
18+
# at container start by the base image's env.sh with the real runtime values.
19+
# update-runtime-env only needs the script, the public/ output dir and the .env
20+
# key templates, so copy just those — editing src/ won't invalidate this layer.
21+
COPY --chown=default:root scripts ./scripts
22+
COPY --chown=default:root public ./public
23+
COPY --chown=default:root .env* ./
24+
RUN pnpm update-runtime-env
25+
26+
# 3. Copy only the sources the production build (`tsc && vite build`) consumes.
27+
# tsconfig "include" is ["src"]; the build also reads these root configs — Vite
28+
# itself, the babel presets used by @vitejs/plugin-react, and the eslint/prettier
29+
# configs run by @nabla/vite-plugin-eslint. Tests, docs, codegen and other
30+
# tooling are intentionally left out (and also pruned via .dockerignore).
31+
COPY --chown=default:root \
32+
index.html \
33+
vite.config.ts \
34+
tsconfig.json \
35+
vite-env.d.ts \
36+
.babelrc \
37+
.eslintrc.cjs \
38+
.eslintignore \
39+
.prettierrc.json \
40+
.prettierignore \
41+
./
42+
COPY --chown=default:root src ./src
43+
44+
# ============================================================
45+
# STAGE 2: Development
46+
# ============================================================
3747
FROM appbase AS development
38-
# =============================
48+
WORKDIR /app
3949

40-
# Set NODE_ENV to development in the development container
4150
ARG NODE_ENV=development
42-
ENV NODE_ENV $NODE_ENV
51+
ENV NODE_ENV=$NODE_ENV
4352

4453
# Enable hot reload by default by polling for file changes.
4554
#
@@ -48,77 +57,51 @@ ENV NODE_ENV $NODE_ENV
4857
ARG CHOKIDAR_USEPOLLING=true
4958
ENV CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING}
5059

51-
# copy in our source code last, as it changes the most
52-
COPY --chown=default:root . .
60+
# `pnpm start` runs update-runtime-env then vite (port from PORT env, default 3001).
61+
CMD ["pnpm", "start", "--no-open", "--host"]
5362

54-
# Bake package.json start command into the image
55-
CMD ["yarn", "start", "--no-open", "--host"]
56-
57-
# ===================================
63+
# ============================================================
64+
# STAGE 3: Static builder for production
65+
# ============================================================
5866
FROM appbase AS staticbuilder
59-
# ===================================
60-
61-
ARG VITE_API_URI
62-
ARG VITE_OIDC_SERVER_TYPE
63-
ARG VITE_OIDC_RETURN_TYPE
64-
ARG VITE_OIDC_AUTHORITY
65-
ARG VITE_OIDC_CLIENT_ID
66-
ARG VITE_OIDC_KUKKUU_API_CLIENT_ID
67-
ARG VITE_OIDC_SCOPE
68-
ARG VITE_OIDC_AUDIENCES
69-
ARG VITE_OIDC_AUTOMATIC_SILENT_RENEW_ENABLED
70-
ARG VITE_OIDC_SESSION_POLLING_INTERVAL_MS
71-
ARG VITE_KUKKUU_API_OIDC_SCOPE
72-
ARG VITE_SENTRY_ENVIRONMENT
73-
ARG VITE_SENTRY_DSN
74-
ARG VITE_SENTRY_TRACES_SAMPLE_RATE
75-
ARG VITE_SENTRY_TRACE_PROPAGATION_TARGETS
76-
ARG VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
77-
ARG VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
67+
68+
# VITE_CSP_REPORT_URI is substituted into index.html's CSP at *build* time by
69+
# Vite (it is not a runtime value). Default to empty so the placeholder is not
70+
# left unreplaced in the output.
7871
ARG VITE_CSP_REPORT_URI
79-
ARG VITE_IS_TEST_ENVIRONMENT
80-
ARG VITE_BUILDTIME
81-
ARG VITE_SENTRY_RELEASE
82-
ARG VITE_COMMITHASH
83-
ARG VITE_IDLE_TIMEOUT_IN_MS
84-
ARG NODE_OPTIONS
85-
86-
# Fix FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed -
87-
# JavaScript heap out of memory: https://github.com/vitejs/vite/issues/2433.
88-
ENV NODE_OPTIONS=${NODE_OPTIONS}
8972
ENV VITE_CSP_REPORT_URI=${VITE_CSP_REPORT_URI:-""}
9073

91-
# Use template and inject the environment variables into .prod/nginx.conf
92-
ENV VITE_BUILDTIME=${VITE_BUILDTIME:-""}
93-
ENV VITE_RELEASE=${VITE_SENTRY_RELEASE:-""}
94-
ENV VITE_COMMITHASH=${VITE_COMMITHASH:-""}
95-
COPY .prod/nginx.conf.template /tmp/.prod/nginx.conf.template
96-
RUN export APP_VERSION=$(yarn --silent app:version) && \
97-
envsubst '${APP_VERSION},${VITE_BUILDTIME},${VITE_RELEASE},${VITE_COMMITHASH}' < \
98-
"/tmp/.prod/nginx.conf.template" > \
99-
"/tmp/.prod/nginx.conf"
100-
101-
COPY . /app
102-
RUN yarn build
103-
104-
# =============================
105-
FROM registry.access.redhat.com/ubi9/nginx-122 AS production
106-
# =============================
107-
# Add application sources to a directory that the assemble script expects them
108-
# and set permissions so that the container runs without root access
109-
USER root
110-
111-
RUN chgrp -R 0 /usr/share/nginx/html && \
112-
chmod -R g=u /usr/share/nginx/html
113-
114-
# Copy static build
115-
COPY --from=staticbuilder /app/build /usr/share/nginx/html
116-
# Copy nginx config
117-
COPY --from=staticbuilder /tmp/.prod/nginx.conf /etc/nginx/nginx.conf
74+
RUN pnpm build
75+
76+
# Produce a sanitized runtime env template for the production image. The base
77+
# image's env.sh is a naive KEY=VALUE parser: it does NOT skip comments or blank
78+
# lines, so any "# comment"/blank line in .env makes it emit an empty-key entry
79+
# (`: ""`) or duplicate the previous key — corrupting env-config.js into invalid
80+
# JavaScript (SyntaxError at load → window._env_ undefined → app can't boot).
81+
# Keep only `KEY=VALUE` lines so env.sh always generates valid JS.
82+
RUN grep -E '^[A-Za-z_][A-Za-z0-9_]*=' .env.example > .env.runtime
11883

119-
USER 1001
84+
# ============================================================
85+
# STAGE 4: Production runtime
86+
# ============================================================
87+
FROM helsinki.azurecr.io/ubi10/nginx-126-spa-standard AS production
12088

121-
# Run script uses standard ways to run the application
122-
CMD ["/bin/bash", "-c", "nginx -g \"daemon off;\""]
89+
# 1. Copy the compiled assets.
90+
COPY --from=staticbuilder /app/build /usr/share/nginx/html
12391

124-
EXPOSE 8080
92+
# 2. Runtime env injection inputs (env.sh from the base image reads these).
93+
# .env lists which keys to expose in env-config.js; the values are overwritten
94+
# at container start with the real runtime environment. Use the sanitized,
95+
# comment-free template (see staticbuilder) — the base image's env.sh cannot
96+
# parse comments/blank lines and would otherwise emit invalid JavaScript.
97+
WORKDIR /usr/share/nginx/html
98+
COPY --from=staticbuilder /app/.env.runtime ./.env
99+
100+
# 3. package.json powers the base image's /readiness version endpoint.
101+
COPY package.json .
102+
103+
# Inherited from the base image:
104+
# - env.sh at /usr/share/nginx/html/env.sh
105+
# - USER 1001
106+
# - EXPOSE 8080
107+
# - ENTRYPOINT/CMD

0 commit comments

Comments
 (0)