Skip to content

Commit 2f15820

Browse files
committed
feat: add runtime env injection and wire app to use it
- add runtime env generator script and shell wrappers for local/test and container startup - generate public env config files and load env-config.js before app scripts - add getEnvValue helper with runtime window._env_ fallback to process.env - migrate Matomo, Sentry, feature flags, registration utils, and runtime config reads to getEnvValue - update Dockerfile to include scripts, run env generation on startup, and use pnpm install flow - run runtime env generation in dev/start/test scripts and document the workflow - update ignore and coverage settings for generated/runtime scripts Refs: LINK-2574
1 parent f3ff676 commit 2f15820

25 files changed

Lines changed: 397 additions & 105 deletions

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
!.sentryclirc
55
!public
66
!src
7+
!scripts
8+
!scripts/**
79
!next-i18next.config.js
810
!next.config.js
911
!package.json

.env

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@ NEXT_ENV=development
44
NEXTAUTH_SECRET=secret
55
NEXTAUTH_URL=http://localhost:3001
66

7-
7+
# OIDC configuration
88
OIDC_ISSUER=https://tunnistus.hel.fi/auth/realms/helsinki-tunnistus
99
OIDC_API_TOKENS_URL=https://tunnistus.hel.fi/auth/realms/helsinki-tunnistus/protocol/openid-connect/token/
1010
OIDC_CLIENT_ID=linkedregistrations-ui
1111
OIDC_CLIENT_SECRET=
1212
OIDC_LINKED_EVENTS_API_SCOPE=linkedevents-api
1313

14-
SENTRY_IGNORE_API_RESOLUTION_ERROR=true
14+
# Sentry configuration
1515
SENTRY_PROJECT=linkedregistrations-ui
1616

17+
NEXT_PUBLIC_SENTRY_ENVIRONMENT=local
18+
NEXT_PUBLIC_SENTRY_DSN=
19+
NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=0
20+
NEXT_PUBLIC_SENTRY_TRACE_PROPAGATION_TARGETS=
21+
NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0
22+
NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=0
23+
NEXT_PUBLIC_SENTRY_RELEASE=
24+
25+
# Matomo configuration
1726
NEXT_PUBLIC_MATOMO_URL=//matomo.hel.fi/
1827
NEXT_PUBLIC_MATOMO_SITE_ID=70
1928
NEXT_PUBLIC_MATOMO_JS_TRACKER_FILE=matomo.js
@@ -23,6 +32,7 @@ NEXT_PUBLIC_MATOMO_ENABLED=false
2332
NEXT_PUBLIC_LINKED_EVENTS_URL=https://linkedevents-api.dev.hel.ninja/linkedevents-dev/v1
2433
NEXT_PUBLIC_WEB_STORE_INTEGRATION_ENABLED=false
2534
NEXT_PUBLIC_WEB_STORE_API_BASE_URL=https://checkout-test-api.test.hel.ninja/v1
35+
NEXT_PUBLIC_USE_IMAGE_PROXY=false
2636

2737
NEXT_PUBLIC_ATTENDANCE_LIST_LOGIN_METHODS=suomi_fi
2838
NEXT_PUBLIC_SIGNUPS_LOGIN_METHODS=helsinki_tunnus,helsinkiad

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ yarn-error.log*
3232
.env.test.local
3333
.env.production.local
3434

35+
# generated runtime env files
36+
public/env-config.js
37+
public/test-env-config.js
38+
3539
# vercel
3640
.vercel
3741

Dockerfile

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ USER default
1717
RUN pnpm install --frozen-lockfile --ignore-scripts
1818

1919
COPY .env* next-i18next.config.js next.config.js sentry.edge.config.ts sentry.properties sentry.server.config.ts tsconfig.json /app/
20+
COPY /scripts/ /app/scripts
2021
COPY /public/ /app/public
2122
COPY /src/ /app/src
2223

@@ -40,44 +41,15 @@ USER default:root
4041
# Next.js collects completely anonymous telemetry data about general usage.
4142
# Learn more here: https://nextjs.org/telemetry
4243
# Uncomment the following line in case you want to disable telemetry during the build.
43-
ENV NEXT_TELEMETRY_DISABLED 1
44-
45-
# Set environmental variables (when building image on Azure)
46-
# specified in pipelines/library files
47-
ARG NEXT_PUBLIC_LINKED_EVENTS_URL
48-
ARG NEXT_PUBLIC_SENTRY_DSN
49-
ARG NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE
50-
ARG NEXT_PUBLIC_SENTRY_TRACE_PROPAGATION_TARGETS
51-
ARG NEXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
52-
ARG NEXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
53-
ARG NEXT_PUBLIC_SENTRY_RELEASE
54-
ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT
55-
ARG NEXT_PUBLIC_ATTENDANCE_LIST_LOGIN_METHODS
56-
ARG NEXT_PUBLIC_SIGNUPS_LOGIN_METHODS
57-
58-
ARG OIDC_ISSUER
59-
ARG OIDC_API_TOKENS_URL
60-
ARG OIDC_CLIENT_ID
61-
ARG OIDC_CLIENT_SECRET
62-
ARG OIDC_LINKED_EVENTS_API_SCOPE
44+
ENV NEXT_TELEMETRY_DISABLED=1
6345

46+
# Build-time values injected by Azure pipelines (via --build-arg).
47+
ARG PORT
6448
ARG SENTRY_PROJECT
65-
66-
ARG NEXT_PUBLIC_MATOMO_URL
67-
ARG NEXT_PUBLIC_MATOMO_SITE_ID
68-
ARG NEXT_PUBLIC_MATOMO_JS_TRACKER_FILE
69-
ARG NEXT_PUBLIC_MATOMO_PHP_TRACKER_FILE
70-
ARG NEXT_PUBLIC_MATOMO_ENABLED
71-
72-
ARG NEXTAUTH_SECRET
73-
ARG NEXTAUTH_URL
74-
49+
ARG SENTRY_ORG
50+
ARG SENTRY_URL
7551
ARG NEXT_ENV
76-
77-
ARG NEXT_PUBLIC_WEB_STORE_INTEGRATION_ENABLED
78-
ARG NEXT_PUBLIC_WEB_STORE_API_BASE_URL
79-
80-
ARG NEXT_PUBLIC_USE_IMAGE_PROXY
52+
ARG NEXT_PUBLIC_SENTRY_RELEASE
8153

8254
# When building locally with Docker Compose, the auth token can be provided using SENTRY_AUTH_TOKEN environment variable.
8355
# Our AzDO pipeline uses /secrets/SENTRY_AUTH_TOKEN to pass the auth token so this works there too.
@@ -96,10 +68,16 @@ RUN microdnf install -y shadow-utils && microdnf clean all && \
9668

9769
USER default
9870

99-
ENV NODE_ENV production
100-
ENV NEXT_TELEMETRY_DISABLED 1
71+
ENV NODE_ENV=production
72+
ENV NEXT_TELEMETRY_DISABLED=1
73+
74+
COPY --from=builder --chown=default:root /app/public ./public
75+
COPY --from=builder --chown=default:root /app/scripts ./scripts
10176

102-
COPY --from=builder /app/public ./public
77+
# Allow OpenShift/Azure-style random non-root users (gid 0) to write runtime env file.
78+
RUN chmod -R g=u /app/public /app/scripts && \
79+
rm -f /app/public/env-config.js && \
80+
ln -s /tmp/env-config.js /app/public/env-config.js
10381

10482
# Automatically leverage output traces to reduce image size
10583
# https://nextjs.org/docs/advanced-features/output-file-tracing
@@ -109,4 +87,5 @@ COPY --from=builder --chown=default:nodejs /app/.next/static ./.next/static
10987
# Expose port
11088
EXPOSE $PORT
11189

112-
CMD node server.js -p ${PORT}
90+
ENTRYPOINT ["sh", "/app/scripts/docker-entrypoint.sh"]
91+
CMD ["sh", "-c", "node server.js -p ${PORT}"]

README.md

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Run `pnpm i && pnpm dev`
6464

6565
## Running development environment locally without docker
6666

67-
Run `pnpm i && pnpm start`
67+
Run `pnpm i && pnpm dev`
6868

6969
## Configurable environment variables
7070

@@ -134,7 +134,7 @@ In the project directory, you can run:
134134
### `pnpm dev`
135135

136136
Runs the app in the development mode.<br />
137-
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
137+
Open [http://localhost:3001](http://localhost:3001) to view it in the browser.
138138

139139
The page will reload if you make edits.<br />
140140
You will also see any lint errors in the console.
@@ -146,7 +146,7 @@ Builds the app for production to the `build` folder.
146146
### `pnpm start`
147147

148148
Runs the built app in the production mode.<br />
149-
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
149+
Open [http://localhost:3001](http://localhost:3001) to view it in the browser.
150150

151151
### `pnpm test`
152152

@@ -156,33 +156,25 @@ Launches the test runner in the interactive watch mode.
156156

157157
Run tests and generate coverage report
158158

159+
### `pnpm update-runtime-env`
160+
161+
Generates `public/env-config.js` (and in test mode `public/test-env-config.js`) from runtime environment variables.
162+
This is run automatically by `pnpm dev`, `pnpm start`, `pnpm test`, and `pnpm test:coverage`, but can also be run manually when needed.
163+
159164
## Debugging
160165

161166
### Debugging project in VS Code
162167

163168
To debug in VS Code:
164169

165-
1. Install the "Debugger for Chrome" extension to VS Code
166-
2. Run `pnpm dev`
170+
1. Run `pnpm dev`
171+
2. Use one of the provided Next.js debug configurations in VS Code
167172
3. Set a breakpoint
168-
4. Run "Chrome" debug configuration in VS Code
169-
5. Reload the project in your browser
173+
4. Reload the project in your browser
170174

171175
### Debugging Tests in VS Code
172176

173177
No plugin is needed.
174178

175179
1. Set a breakpoint
176180
2. Run the "Run tests" debugger configuration
177-
178-
### Debugging Tests in Chrome
179-
180-
We recommend using VS Code's debugger.
181-
182-
1. Place a `debugger;` statement in any test
183-
2. Run pnpm `test:debug`
184-
3. Open `about:inspect` in Chrome
185-
4. Select `inspect` on you process, press Play and you're good to go.
186-
187-
See more detailed instructions here:
188-
https://create-react-app.dev/docs/debugging-tests#debugging-tests-in-chrome

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
"pnpm": ">=11.1.3"
99
},
1010
"scripts": {
11-
"dev": "next dev -p 3001",
11+
"dev": "pnpm update-runtime-env && next dev -p 3001",
1212
"build": "next build",
13-
"start": "next start -p 3001",
13+
"start": "pnpm update-runtime-env && next start -p 3001",
1414
"lint": "eslint .",
1515
"prepare": "husky",
16-
"test": "TZ=UTC vitest --watch",
17-
"test:coverage": "TZ=UTC vitest run --coverage",
16+
"test": "NODE_ENV=test pnpm update-runtime-env && TZ=UTC vitest --watch",
17+
"test:coverage": "NODE_ENV=test pnpm update-runtime-env && TZ=UTC vitest run --coverage",
1818
"test:e2e:install": "playwright install",
1919
"test:e2e:ci": "playwright test",
2020
"test:e2e:start": "playwright test",
21-
"test:e2e:record": "playwright codegen https://linkedregistrations.dev.hel.ninja/"
21+
"test:e2e:record": "playwright codegen https://linkedregistrations.dev.hel.ninja/",
22+
"update-runtime-env": "node scripts/update-runtime-env.js . ./public"
2223
},
2324
"dependencies": {
2425
"@sentry/nextjs": "^10.47.0",

scripts/docker-entrypoint.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
sh ./scripts/env.sh /app /app/public
6+
7+
exec "$@"

scripts/env.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
ENVDIR=${1:-.}
6+
TARGETDIR=${2:-public}
7+
8+
node "$ENVDIR/scripts/update-runtime-env.js" "$ENVDIR" "$TARGETDIR"

scripts/update-runtime-env.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
const USE_TEST_ENV = process.env.NODE_ENV === 'test';
7+
const PUBLIC_PREFIX = 'NEXT_PUBLIC_';
8+
9+
const envDir = process.argv[2] || process.cwd();
10+
const targetDir = process.argv[3] || path.resolve(process.cwd(), 'public');
11+
const configFile = USE_TEST_ENV ? 'test-env-config.js' : 'env-config.js';
12+
const targetFile = path.resolve(targetDir, configFile);
13+
14+
const envFiles = USE_TEST_ENV ? ['.env', '.env.test'] : ['.env', '.env.local'];
15+
16+
const parseEnvContent = (content) => {
17+
const parsed = {};
18+
19+
content.split(/\r?\n/).forEach((line) => {
20+
const trimmed = line.trim();
21+
22+
if (!trimmed || trimmed.startsWith('#')) {
23+
return;
24+
}
25+
26+
const separatorIndex = trimmed.indexOf('=');
27+
28+
if (separatorIndex < 0) {
29+
return;
30+
}
31+
32+
const key = trimmed.slice(0, separatorIndex).trim();
33+
let value = trimmed.slice(separatorIndex + 1).trim();
34+
35+
if (
36+
(value.startsWith('"') && value.endsWith('"')) ||
37+
(value.startsWith("'") && value.endsWith("'"))
38+
) {
39+
value = value.slice(1, -1);
40+
}
41+
42+
parsed[key] = value;
43+
});
44+
45+
return parsed;
46+
};
47+
48+
const loadEnvFromFiles = () => {
49+
const env = {};
50+
51+
envFiles.forEach((fileName) => {
52+
const filePath = path.resolve(envDir, fileName);
53+
if (!fs.existsSync(filePath)) {
54+
return;
55+
}
56+
57+
const parsed = parseEnvContent(fs.readFileSync(filePath, 'utf8'));
58+
Object.assign(env, parsed);
59+
});
60+
61+
return env;
62+
};
63+
64+
const pickPublicEnv = (source) => {
65+
return Object.fromEntries(
66+
Object.entries(source).filter(([key, value]) => {
67+
return key.startsWith(PUBLIC_PREFIX) && value !== undefined;
68+
})
69+
);
70+
};
71+
72+
const start = async () => {
73+
try {
74+
const fileEnv = loadEnvFromFiles();
75+
const processPublicEnv = pickPublicEnv(process.env);
76+
const defaultPublicEnv = pickPublicEnv(fileEnv);
77+
78+
const envVariables = {
79+
...defaultPublicEnv,
80+
...processPublicEnv,
81+
NODE_ENV: process.env.NODE_ENV || (USE_TEST_ENV ? 'test' : 'development'),
82+
};
83+
84+
fs.mkdirSync(targetDir, { recursive: true });
85+
fs.writeFileSync(
86+
targetFile,
87+
`window._env_ = ${JSON.stringify(envVariables, null, 2)};\n`,
88+
'utf8'
89+
);
90+
91+
console.log(`Runtime env file created: ${targetFile}`);
92+
} catch (error) {
93+
console.error(error && error.message ? error.message : error);
94+
process.exit(1);
95+
}
96+
};
97+
98+
start();

sonar-project.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ sonar.test.inclusions= \
2020
**/*.test.tsx
2121

2222
sonar.coverage.exclusions= \
23+
scripts/**/*, \
2324
src/tests/**/*, \
2425
src/utils/mockDataUtils.ts

0 commit comments

Comments
 (0)