diff --git a/Dockerfile.react b/Dockerfile.react index b197dd49..f160165f 100644 --- a/Dockerfile.react +++ b/Dockerfile.react @@ -20,6 +20,13 @@ COPY src/ src/ # broken backgrounds, missing tile icons). COPY config/ config/ +# Runtime-tunable env vars. modern.config.ts reads these when Modern.js +# boots the SSR server at container start, so values set via k8s env +# propagate without a rebuild. The ARG line lets CI also bake a value +# into the image if the cluster can't set env at runtime. +ARG MAP_TILE_URL +ENV MAP_TILE_URL=${MAP_TILE_URL} + RUN pnpm build:standalone FROM node:24-slim AS runtime diff --git a/deployment/build-docker.sh b/deployment/build-docker.sh new file mode 100644 index 00000000..59d877df --- /dev/null +++ b/deployment/build-docker.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +echo -e "\E[1;34mCleanup docker build cache to reduce disk usage\E[1;0m" +docker image prune -f +docker container prune -f + +function build { + K8NAMESPACE="flights-ui" + echo -e "\E[1;34mBuilding $2 ($1)\E[1;0m" + export CopyRetryCount=100 + docker rmi -f $(docker images aeroflot.$K8NAMESPACE/$1 -a -q|uniq) >/dev/null 2>/dev/null + ls -l "$2/Dockerfile" + # MAP_TILE_URL must be a build-arg (not just a runtime env) because + # Modern.js bakes html.tags into the HTML template at `pnpm build:standalone` + # time. Setting it only on the pod won't change the served HTML. + # Defaults here keep the prod-cluster behavior (same-origin) intact; + # devwebzavod builds override with the flights.test.aeroflot.ru URL. + : "${MAP_TILE_URL:=/map/api/tile/{z}/{x}/{y}.jpeg}" + export MAP_TILE_URL + docker build -t aeroflot.$K8NAMESPACE/$1:v$VERSION.$BUILD_NUMBER \ + --build-arg ENVIRONMENT=${ENVIRONMENT} \ + --build-arg=NPM_PROXY \ + --build-arg MAP_TILE_URL=${MAP_TILE_URL} \ + -f "$2/Dockerfile" $2 +} + +build flights-ui "../Aeroflot.Flights.Front" + +exit 0 \ No newline at end of file diff --git a/deployment/deploy.sh b/deployment/deploy.sh new file mode 100644 index 00000000..5a1deade --- /dev/null +++ b/deployment/deploy.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +export K8NAMESPACE="flights-ui" + +echo "Create namespace [$K8NAMESPACE] if doesn't exist" +kubectl --kubeconfig $KUBECONFIG get namespace | grep -q "^$K8NAMESPACE " || kubectl --kubeconfig $KUBECONFIG create namespace $K8NAMESPACE + +function deployImage { + export AFL_PRJ_NAME=$1 + echo -e "\E[1;34mDeploy $AFL_PRJ_NAME\E[1;0m" + + if [ "$IMAGE_PULL_SECRET" != "" ] + then + export IMAGE_PULL_SECRETS=$' imagePullSecrets:\n - name: '$IMAGE_PULL_SECRET$'\n' + else + export IMAGE_PULL_SECRETS="" + fi + + echo "Using k8s yml file as following:" + envsubst < ./k8s/$AFL_PRJ_NAME.yaml + + echo "Start kubectl apply..." + envsubst < ./k8s/$AFL_PRJ_NAME.yaml | kubectl --kubeconfig $KUBECONFIG apply -n $K8NAMESPACE -f - + + echo -e "\E[1;34m ...done\E[1;0m" +} + +function serve { + NAME=$1 + PORT=$2 + echo -e "\E[1;34mService $1 at port $2\E[1;0m" + + cat < = {}; +for (const k of PUBLIC_ENV_KEYS) { + const v = process.env[k]; + if (typeof v === "string" && v.length > 0) PUBLIC_ENV[k] = v; +} +const PUBLIC_ENV_SCRIPT = + `window.__ENV__ = Object.assign(window.__ENV__ || {}, ${JSON.stringify(PUBLIC_ENV)});`; + export default defineConfig({ plugins: [appTools({ bundler: "rspack" }), moduleFederationPlugin()], source: { @@ -12,6 +27,14 @@ export default defineConfig({ html: { favicon: "./config/public/favicon.ico", tags: [ + // Inline script runs before the main bundle so client-side getEnv() + // sees the pod's env (read when the SSR server starts). + { + tag: "script", + head: true, + append: false, + children: PUBLIC_ENV_SCRIPT, + }, { tag: "link", attrs: { diff --git a/src/env/index.ts b/src/env/index.ts index 07e3d20e..dd22e12a 100644 --- a/src/env/index.ts +++ b/src/env/index.ts @@ -59,15 +59,23 @@ let cached: Env | undefined; export function getEnv(): Env { if (cached) return cached; - // In the browser, process.env is not available (Rspack doesn't replace - // bracket-notation access). Fall back to an SSR-injected window.__ENV__ - // or sensible development defaults. - const envSource = - typeof process !== "undefined" && process.env - ? process.env - : typeof window !== "undefined" && (window as unknown as Record)["__ENV__"] - ? (window as unknown as Record)["__ENV__"] as Record - : {}; + // In Node (SSR build) process.env is authoritative. In the browser, + // Rspack injects a stub `process.env` that only carries NODE_ENV, so we + // can't rely on it for per-deployment values — prefer `window.__ENV__` + // (populated at SSR server start by modern.config.ts) and fall through + // to the stub only for NODE_ENV / defaults. + const isBrowser = typeof window !== "undefined"; + const windowEnv = isBrowser + ? ((window as unknown as Record)["__ENV__"] as + | Record + | undefined) + : undefined; + const processEnv = + typeof process !== "undefined" && process.env ? process.env : undefined; + const envSource: Record = { + ...(processEnv ?? {}), + ...(windowEnv ?? {}), + }; const parsed = EnvSchema.safeParse(envSource); if (!parsed.success) {