diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 92382b05fb..d0409dadb5 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -210,6 +210,10 @@ jobs: platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + GIT_SHA=${{ steps.vars.outputs.commit_sha }} + secrets: | + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} # Multi-source cache strategy: # - Read from: main cache (stable baseline) + branch cache (incremental) # - Write to: branch-specific cache (or main if on main branch) @@ -220,6 +224,8 @@ jobs: - name: Build and push Heroku targets if: inputs.heroku_app != '' + env: + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} run: | set -e IMAGE_NAME="${{ steps.vars.outputs.image_name }}" @@ -232,6 +238,8 @@ jobs: --platform linux/amd64 \ --target "$target" \ --tag "registry.heroku.com/${{ inputs.heroku_app }}/$target" \ + --build-arg GIT_SHA=${{ steps.vars.outputs.commit_sha }} \ + --secret id=NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,env=NEXT_SERVER_ACTIONS_ENCRYPTION_KEY \ --push \ --cache-from "type=registry,ref=${IMAGE_NAME}:buildcache-main" \ --cache-from "type=registry,ref=${IMAGE_NAME}:buildcache-${CACHE_SCOPE}" \ diff --git a/.github/workflows/pr_preview.yml b/.github/workflows/pr_preview.yml index 304515fda8..8dacfbd099 100644 --- a/.github/workflows/pr_preview.yml +++ b/.github/workflows/pr_preview.yml @@ -330,6 +330,7 @@ jobs: PUBLIC_FRONTEND_SENTRY_DSN="${{ vars.PUBLIC_FRONTEND_SENTRY_DSN }}" \ SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" \ PUBLIC_POSTHOG_KEY="${{ vars.PUBLIC_POSTHOG_KEY }}" \ + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}" \ --app "${APP_NAME}" \ --stage diff --git a/Dockerfile b/Dockerfile index 87a39abfa3..97dc915264 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,6 +63,9 @@ RUN . venv/bin/activate && ./manage.py collectstatic --noinput FROM base AS frontend_build WORKDIR /app +ARG GIT_SHA +ENV GIT_SHA=$GIT_SHA + # Copy only frontend source files COPY front_end/ /app/front_end/ @@ -71,8 +74,10 @@ COPY --from=frontend_deps /app/front_end/node_modules /app/front_end/node_module # Build frontend ENV NODE_ENV=production -RUN cd front_end \ - && NODE_OPTIONS=--max-old-space-size=8192 npm run build \ +RUN --mount=type=secret,id=NEXT_SERVER_ACTIONS_ENCRYPTION_KEY \ + cd front_end \ + && NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=$(cat /run/secrets/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY) \ + NODE_OPTIONS=--max-old-space-size=8192 npm run build \ && rm -rf .next/cache # Inject Sentry sourcemaps @@ -82,6 +87,8 @@ RUN cd front_end && npx sentry-cli sourcemaps inject .next # FINAL ENVIRONMENT # ============================================================ FROM base AS final_env +ARG GIT_SHA +ENV GIT_SHA=$GIT_SHA RUN mkdir -p /app && chown 1001:0 /app WORKDIR /app diff --git a/front_end/next.config.mjs b/front_end/next.config.mjs index 3fbfe17b9a..b758328126 100644 --- a/front_end/next.config.mjs +++ b/front_end/next.config.mjs @@ -6,8 +6,13 @@ const withNextIntl = createNextIntlPlugin(); const AWS_STORAGE_BUCKET_NAME = process.env.AWS_STORAGE_BUCKET_NAME; const AWS_S3_CUSTOM_DOMAIN = process.env.AWS_S3_CUSTOM_DOMAIN; +// This enables automatic cache busting and automatic hard reloads when the build id changes. +const BUILD_ID = process.env.GIT_SHA || "development"; + /** @type {import("next").NextConfig} */ const nextConfig = { + generateBuildId: () => BUILD_ID, + deploymentId: BUILD_ID, trailingSlash: true, productionBrowserSourceMaps: true, env: { @@ -33,21 +38,21 @@ const nextConfig = { }, ...(AWS_STORAGE_BUCKET_NAME ? [ - { - protocol: "https", - hostname: `${AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com`, - pathname: "/**", - }, - ] + { + protocol: "https", + hostname: `${AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com`, + pathname: "/**", + }, + ] : []), ...(AWS_S3_CUSTOM_DOMAIN ? [ - { - protocol: "https", - hostname: AWS_S3_CUSTOM_DOMAIN, - pathname: "/**", - }, - ] + { + protocol: "https", + hostname: AWS_S3_CUSTOM_DOMAIN, + pathname: "/**", + }, + ] : []), ], }, @@ -83,44 +88,20 @@ const nextConfig = { }, ]; }, - webpack: (config, { buildId, webpack }) => { - // propagate buildId to environment so we could trigger prompt message on outdated version - config.plugins.push( - new webpack.DefinePlugin({ - "process.env.BUILD_ID": JSON.stringify(buildId), - }) - ); - - config.output.filename = config.output.filename.replace( - "[chunkhash]", - buildId - ); - - // Grab the existing rule that handles SVG imports - const fileLoaderRule = config.module.rules.find((rule) => - rule.test?.test?.(".svg") - ); - - config.module.rules.push( - // Reapply the existing rule, but only for svg imports ending in ?url - { - ...fileLoaderRule, - test: /\.svg$/i, - resourceQuery: /url/, // *.svg?url - }, - // Convert all other *.svg imports to React components - { - test: /\.svg$/i, - issuer: fileLoaderRule.issuer, - resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url - use: ["@svgr/webpack"], - } - ); - - // Modify the file loader rule to ignore *.svg, since we have it handled now. - fileLoaderRule.exclude = /\.svg$/i; - - return config; + turbopack: { + rules: { + "*.svg": [ + { + condition: { query: /\burl\b/ }, + type: "asset", + }, + { + condition: { not: { query: /\burl\b/ } }, + loaders: ["@svgr/webpack"], + as: "*.js", + }, + ], + }, }, }; diff --git a/front_end/package.json b/front_end/package.json index 8834a42121..bfd5d231cf 100644 --- a/front_end/package.json +++ b/front_end/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build", "start": "next start", "lint": "run-p lint:*", "lint:js": "eslint .", diff --git a/front_end/src/app/(api)/app-version/route.ts b/front_end/src/app/(api)/app-version/route.ts deleted file mode 100644 index 1aff956fd6..0000000000 --- a/front_end/src/app/(api)/app-version/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Used for accessing the app version on server. - */ -export async function GET() { - return Response.json({ - buildId: process.env.BUILD_ID, - }); -} diff --git a/front_end/src/app/(futureeval)/futureeval/layout.tsx b/front_end/src/app/(futureeval)/futureeval/layout.tsx index 26db6bd5e8..1b9c84bd22 100644 --- a/front_end/src/app/(futureeval)/futureeval/layout.tsx +++ b/front_end/src/app/(futureeval)/futureeval/layout.tsx @@ -3,7 +3,6 @@ import "@fortawesome/fontawesome-svg-core/styles.css"; import type { Metadata } from "next"; import CookiesBanner from "@/app/(main)/components/cookies_banner"; -import VersionChecker from "@/app/(main)/components/version_checker"; import { defaultDescription } from "@/constants/metadata"; import FutureEvalFooter from "./components/futureeval-footer"; @@ -31,7 +30,6 @@ export default function FutureEvalLayout({