diff --git a/.gitea/workflows/build_RC.yml b/.gitea/workflows/build_RC.yml index 3db84ccd..2704fa24 100644 --- a/.gitea/workflows/build_RC.yml +++ b/.gitea/workflows/build_RC.yml @@ -18,6 +18,30 @@ jobs: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y jq curl + - name: Validate RC build configuration + env: + HARBOR_ENDPOINT: ${{ vars.HARBOR_ENDPOINT }} + HARBOR_HOSTNAME: ${{ vars.HARBOR_HOSTNAME }} + HARBOR_ROBOT_ACCOUNT: ${{ vars.HARBOR_ROBOT_ACCOUNT }} + HARBOR_ROBOT_KEY: ${{ secrets.HARBOR_ROBOT_KEY }} + ADMINFRONT_URL: ${{ vars.ADMINFRONT_URL }} + DEVFRONT_URL: ${{ vars.DEVFRONT_URL }} + ORGFRONT_URL: ${{ vars.ORGFRONT_URL }} + VITE_OIDC_AUTHORITY: ${{ vars.VITE_OIDC_AUTHORITY }} + run: | + set -euo pipefail + + required_action_env=" + HARBOR_ENDPOINT HARBOR_HOSTNAME HARBOR_ROBOT_ACCOUNT HARBOR_ROBOT_KEY + ADMINFRONT_URL DEVFRONT_URL ORGFRONT_URL VITE_OIDC_AUTHORITY + " + for key in ${required_action_env}; do + if [ -z "${!key:-}" ]; then + echo "::error::Missing required RC build value: ${key}. Check Gitea repo variables/secrets." + exit 1 + fi + done + - name: Login to Docker Registry uses: docker/login-action@v3 with: @@ -93,6 +117,11 @@ jobs: file: ./adminfront/Dockerfile push: true tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront:${{ steps.rc_calculator.outputs.new_rc_tag }} + build-args: | + VITE_ADMIN_PUBLIC_URL=${{ vars.ADMINFRONT_URL }} + VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }} + VITE_OIDC_CLIENT_ID=adminfront + ORGFRONT_URL=${{ vars.ORGFRONT_URL }} provenance: false sbom: false @@ -103,6 +132,10 @@ jobs: file: ./devfront/Dockerfile push: true tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront:${{ steps.rc_calculator.outputs.new_rc_tag }} + build-args: | + VITE_DEVFRONT_PUBLIC_URL=${{ vars.DEVFRONT_URL }} + VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }} + VITE_OIDC_CLIENT_ID=devfront provenance: false sbom: false @@ -113,6 +146,10 @@ jobs: file: ./orgfront/Dockerfile push: true tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront:${{ steps.rc_calculator.outputs.new_rc_tag }} + build-args: | + VITE_ORGFRONT_PUBLIC_URL=${{ vars.ORGFRONT_URL }} + VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }} + VITE_OIDC_CLIENT_ID=orgfront provenance: false sbom: false diff --git a/.gitea/workflows/production_release.yml b/.gitea/workflows/production_release.yml index 71c7fdb8..9c51ad89 100644 --- a/.gitea/workflows/production_release.yml +++ b/.gitea/workflows/production_release.yml @@ -42,19 +42,13 @@ jobs: sudo apt-get update -y && sudo apt-get install -y skopeo fi - # Re-tag backend image - echo "Re-tagging backend image..." - skopeo copy --preserve-digests \ - --src-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" --dest-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" \ - --src-tls-verify=false --dest-tls-verify=false \ - "docker://${HARBOR_HOSTNAME}/baron_sso/backend:${BASE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/backend:${RE_TAG}" - - # Re-tag userfront image - echo "Re-tagging userfront image..." - skopeo copy --preserve-digests \ - --src-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" --dest-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" \ - --src-tls-verify=false --dest-tls-verify=false \ - "docker://${HARBOR_HOSTNAME}/baron_sso/userfront:${BASE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/userfront:${RE_TAG}" + for image in backend userfront adminfront devfront orgfront; do + echo "Re-tagging ${image} image..." + skopeo copy --preserve-digests \ + --src-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" --dest-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" \ + --src-tls-verify=false --dest-tls-verify=false \ + "docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${BASE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${RE_TAG}" + done echo "final_image_tag=${RE_TAG}" >> "$GITHUB_OUTPUT" @@ -68,6 +62,9 @@ jobs: IMAGE_TAG: ${{ steps.retag.outputs.final_image_tag }} BACKEND_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend USERFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront + ADMINFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront + DEVFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront + ORGFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }} PROD_HOST: ${{ vars.PROD_HOST }} PROD_USER: ${{ vars.PROD_USER }} @@ -101,8 +98,12 @@ jobs: "CLICKHOUSE_PORT_NATIVE=${{ vars.PROD_CLICKHOUSE_PORT_NATIVE }}" \ "CLICKHOUSE_USER=${{ vars.PROD_CLICKHOUSE_USER }}" \ "CLICKHOUSE_PASSWORD=${{ secrets.PROD_CLICKHOUSE_PASSWORD }}" \ - "BACKEND_PORT=${{ vars.PROD_BACKEND_PORT }}" \ - "USERFRONT_PORT=${{ vars.PROD_USERFRONT_PORT }}" \ + "PROD_BACKEND_PORT=${{ vars.PROD_BACKEND_PORT }}" \ + "BACKEND_PORT=3000" \ + "USERFRONT_PORT=${{ vars.PROD_FRONTEND_PORT }}" \ + "ADMINFRONT_PORT=${{ vars.ADMINFRONT_PORT }}" \ + "DEVFRONT_PORT=${{ vars.DEVFRONT_PORT }}" \ + "ORGFRONT_PORT=${{ vars.ORGFRONT_PORT }}" \ "DB_USER=${{ vars.PROD_DB_USER }}" \ "DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }}" \ "DB_NAME=${{ vars.PROD_DB_NAME }}" \ @@ -117,10 +118,33 @@ jobs: "AWS_ACCESS_KEY_ID=${{ vars.AWS_ACCESS_KEY_ID }}" \ "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" \ "AWS_SES_SENDER=${{ vars.AWS_SES_SENDER }}" \ - "USERFRONT_URL=${{ vars.PROD_USERFRONT_URL }}" \ + "USERFRONT_URL=${{ vars.PROD_FRONTEND_URL }}" \ + "ADMINFRONT_URL=${{ vars.ADMINFRONT_URL }}" \ + "DEVFRONT_URL=${{ vars.DEVFRONT_URL }}" \ + "ORGFRONT_URL=${{ vars.ORGFRONT_URL }}" \ "BACKEND_URL=${{ vars.PROD_BACKEND_URL }}" \ + "VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }}" \ + "ADMINFRONT_CALLBACK_URLS=${{ vars.ADMINFRONT_CALLBACK_URLS }}" \ + "DEVFRONT_CALLBACK_URLS=${{ vars.DEVFRONT_CALLBACK_URLS }}" \ + "ORGFRONT_CALLBACK_URLS=${{ vars.ORGFRONT_CALLBACK_URLS }}" \ > .env + required_dotenv_keys=" + APP_ENV TZ DB_PORT CLICKHOUSE_PORT_HTTP CLICKHOUSE_PORT_NATIVE CLICKHOUSE_USER CLICKHOUSE_PASSWORD + PROD_BACKEND_PORT BACKEND_PORT USERFRONT_PORT ADMINFRONT_PORT DEVFRONT_PORT ORGFRONT_PORT + DB_USER DB_PASSWORD DB_NAME COOKIE_SECRET JWT_SECRET REDIS_ADDR + NAVER_CLOUD_ACCESS_KEY NAVER_CLOUD_SECRET_KEY NAVER_CLOUD_SERVICE_ID NAVER_SENDER_PHONE_NUMBER + AWS_REGION AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SES_SENDER + USERFRONT_URL ADMINFRONT_URL DEVFRONT_URL ORGFRONT_URL BACKEND_URL VITE_OIDC_AUTHORITY + ADMINFRONT_CALLBACK_URLS DEVFRONT_CALLBACK_URLS ORGFRONT_CALLBACK_URLS + " + for key in ${required_dotenv_keys}; do + if ! grep -Eq "^${key}=.+" .env; then + echo "::error::Missing required production .env value: ${key}. Check Gitea repo variables/secrets." + exit 1 + fi + done + # Copy compose template and .env file to the remote server scp adminfront/seed-tenant.csv "${PROD_USER}@${PROD_HOST}:${DEPLOY_PATH}/adminfront/" scp docker/docker-compose.template.yaml .env "${PROD_USER}@${PROD_HOST}:${DEPLOY_PATH}/" @@ -131,6 +155,9 @@ jobs: "export DEPLOY_PATH='${DEPLOY_PATH}'; \ export BACKEND_IMAGE_NAME='${BACKEND_IMAGE_NAME}'; \ export USERFRONT_IMAGE_NAME='${USERFRONT_IMAGE_NAME}'; \ + export ADMINFRONT_IMAGE_NAME='${ADMINFRONT_IMAGE_NAME}'; \ + export DEVFRONT_IMAGE_NAME='${DEVFRONT_IMAGE_NAME}'; \ + export ORGFRONT_IMAGE_NAME='${ORGFRONT_IMAGE_NAME}'; \ export IMAGE_TAG='${IMAGE_TAG}'; \ export HARBOR_ENDPOINT='${HARBOR_ENDPOINT}'; \ export HARBOR_ROBOT_ACCOUNT='${HARBOR_ROBOT_ACCOUNT}'; \ diff --git a/.gitea/workflows/staging_release.yml b/.gitea/workflows/staging_release.yml index 8edfa7a3..fa1c9eba 100644 --- a/.gitea/workflows/staging_release.yml +++ b/.gitea/workflows/staging_release.yml @@ -69,7 +69,7 @@ jobs: CLICKHOUSE_PORT_NATIVE=${{ vars.CLICKHOUSE_PORT_NATIVE }} CLICKHOUSE_HOST=${{ vars.CLICKHOUSE_HOST }} CLICKHOUSE_USER=${{ vars.CLICKHOUSE_USER }} - CLICKHOUSE_PASSWORD=${{ vars.CLICKHOUSE_PASSWORD }} + CLICKHOUSE_PASSWORD=${{ secrets.CLICKHOUSE_PASSWORD }} BACKEND_PORT=${{ vars.BACKEND_PORT }} @@ -142,9 +142,32 @@ jobs: # OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} EOF + required_dotenv_keys=" + APP_ENV BACKEND_LOG_LEVEL CLIENT_LOG_DEBUG WORKS_ADMIN_API_BASE_URL WORKS_ADMIN_OAUTH_TOKEN_URL TZ IDP_PROVIDER + DB_PORT CLICKHOUSE_PORT_HTTP CLICKHOUSE_PORT_NATIVE CLICKHOUSE_HOST CLICKHOUSE_USER CLICKHOUSE_PASSWORD + BACKEND_PORT ADMINFRONT_PORT DEVFRONT_PORT ORGFRONT_PORT USERFRONT_PORT OATHKEEPER_API_URL + DB_USER DB_PASSWORD DB_NAME COOKIE_SECRET JWT_SECRET REDIS_ADDR CORS_ALLOWED_ORIGINS PROFILE_CACHE_TTL + NAVER_CLOUD_ACCESS_KEY NAVER_CLOUD_SECRET_KEY NAVER_CLOUD_SERVICE_ID NAVER_SENDER_PHONE_NUMBER + AWS_REGION AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SES_SENDER ADMIN_EMAIL ADMIN_PASSWORD + USERFRONT_URL ORGFRONT_URL BACKEND_PUBLIC_URL BACKEND_URL OATHKEEPER_PUBLIC_URL + ORY_POSTGRES_TAG ORY_POSTGRES_USER ORY_POSTGRES_PASSWORD ORY_POSTGRES_DB KRATOS_DB HYDRA_DB KETO_DB + KRATOS_VERSION KRATOS_UI_NODE_VERSION HYDRA_VERSION KETO_VERSION ORY_SDK_URL KRATOS_PUBLIC_URL + KRATOS_ADMIN_URL KRATOS_BROWSER_URL KRATOS_UI_URL HYDRA_ADMIN_URL HYDRA_PUBLIC_URL JWKS_URL + OATHKEEPER_VERSION OATHKEEPER_UID OATHKEEPER_GID OATHKEEPER_HEALTH_URL OATHKEEPER_HEALTH_INTERVAL_SECONDS + OATHKEEPER_HEALTH_TIMEOUT_SECONDS OATHKEEPER_HEALTH_ENABLED CSRF_COOKIE_NAME CSRF_COOKIE_SECRET + VITE_OIDC_AUTHORITY ADMINFRONT_CALLBACK_URLS DEVFRONT_CALLBACK_URLS ORGFRONT_CALLBACK_URLS + " + for key in ${required_dotenv_keys}; do + if ! grep -Eq "^${key}=.+" .env; then + echo "::error::Missing required staging .env value: ${key}. Check Gitea repo variables/secrets." + exit 1 + fi + done + # 파일 복사 ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p ${DEPLOY_PATH}/docker" ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p ${DEPLOY_PATH}/adminfront" + ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p ${DEPLOY_PATH}/scripts" # [중요] docker/ory 폴더 복사 (여기에 init-db/1-createdb.sql이 있어야 함) scp -r docker/ory "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/docker/" @@ -158,9 +181,10 @@ jobs: fi scp adminfront/seed-tenant.csv "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/adminfront/" + scp scripts/render_ory_config.sh "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/scripts/" scp docker/docker-compose.staging.template.yaml .env "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/" scp docker/compose.infra.yaml "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/compose.infra.yml" - scp docker/compose.ory.yaml "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/compose.ory.yml" + scp compose.ory.yaml "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/compose.ory.yml" # 배포 실행 echo "${HARBOR_ROBOT_KEY}" | ssh "${STAGE_USER}@${STAGE_HOST}" \ @@ -181,6 +205,9 @@ jobs: for net in baron_net public_net ory-net hydranet kratosnet; do docker network inspect \"\$net\" >/dev/null 2>&1 || docker network create \"\$net\" done + + bash scripts/render_ory_config.sh; \ + chmod -R 777 config/.generated/ory || true; \ envsubst < docker-compose.staging.template.yaml > docker-compose.yml; \ diff --git a/adminfront/Dockerfile b/adminfront/Dockerfile index 876dbc57..305e3026 100644 --- a/adminfront/Dockerfile +++ b/adminfront/Dockerfile @@ -1,29 +1,40 @@ -FROM node:lts +FROM node:lts AS build WORKDIR /workspace -# Set CI environment variable to true to avoid TTY issues with pnpm ENV CI=true +ENV ADMINFRONT_BUILD_OUT_DIR=/workspace/adminfront/dist -# Install pnpm RUN corepack enable && corepack prepare pnpm@10.5.2 --activate -# Copy workspace configs and common package -COPY pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY common ./common COPY adminfront ./adminfront -# Install dependencies for the workspace -RUN pnpm install --filter adminfront... --filter baron-sso... --no-frozen-lockfile --ignore-scripts +ARG VITE_ADMIN_PUBLIC_URL +ARG VITE_OIDC_AUTHORITY +ARG VITE_OIDC_CLIENT_ID +ARG ORGFRONT_URL +ENV VITE_ADMIN_PUBLIC_URL=$VITE_ADMIN_PUBLIC_URL +ENV VITE_OIDC_AUTHORITY=$VITE_OIDC_AUTHORITY +ENV VITE_OIDC_CLIENT_ID=$VITE_OIDC_CLIENT_ID +ENV ORGFRONT_URL=$ORGFRONT_URL -# 프로덕션 서빙을 위한 serve 패키지 글로벌 설치 -RUN npm install -g serve +RUN pnpm install --frozen-lockfile --ignore-scripts WORKDIR /workspace/adminfront +RUN npm run build + +FROM node:24-alpine AS production + +WORKDIR /app +ENV NODE_ENV=production +ENV FRONTEND_DIST_DIR=/app/dist +ENV PORT=5173 + +COPY scripts/serve_frontend_prod.mjs ./serve_frontend_prod.mjs +COPY --from=build /workspace/adminfront/dist ./dist -# Vite 기본 포트 EXPOSE 5173 -# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 -RUN chmod +x ./scripts/runtime-mode.sh -CMD ["sh", "./scripts/runtime-mode.sh"] +CMD ["node", "./serve_frontend_prod.mjs"] diff --git a/deploy/templates/docker-compose.yaml b/deploy/templates/docker-compose.yaml index d8bc843a..392f8095 100644 --- a/deploy/templates/docker-compose.yaml +++ b/deploy/templates/docker-compose.yaml @@ -314,48 +314,56 @@ services: networks: [app_net] adminfront: - image: node:20-alpine + build: + context: ../.. + dockerfile: ./adminfront/Dockerfile + args: + VITE_ADMIN_PUBLIC_URL: ${ADMINFRONT_URL} + VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY} + VITE_OIDC_CLIENT_ID: adminfront + ORGFRONT_URL: ${ORGFRONT_URL} container_name: ${COMPOSE_PROJECT_NAME}_adminfront - working_dir: /app env_file: .env + environment: + - APP_ENV=${APP_ENV:-production} + - API_PROXY_TARGET=http://backend:${BACKEND_PORT} ports: - "${ADMINFRONT_PORT}:5173" - volumes: - - ../../common:/common - - ../../adminfront:/app - - ./adminfront/vite.config.ts:/app/vite.config.ts:ro - - ./adminfront/auth.ts:/app/src/lib/auth.ts:ro - command: sh ./scripts/runtime-mode.sh networks: [app_net] devfront: - image: node:20-alpine + build: + context: ../.. + dockerfile: ./devfront/Dockerfile + args: + VITE_DEVFRONT_PUBLIC_URL: ${DEVFRONT_URL} + VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY} + VITE_OIDC_CLIENT_ID: devfront container_name: ${COMPOSE_PROJECT_NAME}_devfront - working_dir: /app env_file: .env + environment: + - APP_ENV=${APP_ENV:-production} + - API_PROXY_TARGET=http://backend:${BACKEND_PORT} ports: - "${DEVFRONT_PORT}:5173" - volumes: - - ../../common:/common - - ../../devfront:/app - - ./devfront/vite.config.ts:/app/vite.config.ts:ro - - ./devfront/auth.ts:/app/src/lib/auth.ts:ro - command: sh ./scripts/runtime-mode.sh networks: [app_net] orgfront: - image: node:20-alpine + build: + context: ../.. + dockerfile: ./orgfront/Dockerfile + args: + VITE_ORGFRONT_PUBLIC_URL: ${ORGFRONT_URL} + VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY} + VITE_OIDC_CLIENT_ID: orgfront container_name: ${COMPOSE_PROJECT_NAME}_orgfront - working_dir: /app env_file: .env + environment: + - APP_ENV=${APP_ENV:-production} + - API_PROXY_TARGET=http://backend:${BACKEND_PORT} + - USERFRONT_URL=${USERFRONT_URL} ports: - "${ORGFRONT_PORT}:5175" - volumes: - - ../../common:/common - - ../../orgfront:/app - - ./orgfront/vite.config.ts:/app/vite.config.ts:ro - - ./orgfront/auth.ts:/app/src/lib/auth.ts:ro - command: sh ./scripts/runtime-mode.sh networks: [app_net] networks: diff --git a/devfront/Dockerfile b/devfront/Dockerfile index e9d8d763..32d7cdef 100644 --- a/devfront/Dockerfile +++ b/devfront/Dockerfile @@ -1,29 +1,38 @@ -FROM node:lts +FROM node:lts AS build WORKDIR /workspace -# Set CI environment variable to true to avoid TTY issues with pnpm ENV CI=true +ENV DEVFRONT_BUILD_OUT_DIR=/workspace/devfront/dist -# Install pnpm RUN corepack enable && corepack prepare pnpm@10.5.2 --activate -# Copy workspace configs and common package -COPY pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY common ./common COPY devfront ./devfront -# Install dependencies for the workspace -RUN pnpm install --filter devfront... --filter baron-sso... --no-frozen-lockfile --ignore-scripts +ARG VITE_DEVFRONT_PUBLIC_URL +ARG VITE_OIDC_AUTHORITY +ARG VITE_OIDC_CLIENT_ID +ENV VITE_DEVFRONT_PUBLIC_URL=$VITE_DEVFRONT_PUBLIC_URL +ENV VITE_OIDC_AUTHORITY=$VITE_OIDC_AUTHORITY +ENV VITE_OIDC_CLIENT_ID=$VITE_OIDC_CLIENT_ID -# 프로덕션 서빙을 위한 serve 패키지 글로벌 설치 -RUN npm install -g serve +RUN pnpm install --frozen-lockfile --ignore-scripts WORKDIR /workspace/devfront +RUN npm run build + +FROM node:24-alpine AS production + +WORKDIR /app +ENV NODE_ENV=production +ENV FRONTEND_DIST_DIR=/app/dist +ENV PORT=5173 + +COPY scripts/serve_frontend_prod.mjs ./serve_frontend_prod.mjs +COPY --from=build /workspace/devfront/dist ./dist -# Vite 기본 포트 EXPOSE 5173 -# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 -RUN chmod +x ./scripts/runtime-mode.sh -CMD ["sh", "./scripts/runtime-mode.sh"] +CMD ["node", "./serve_frontend_prod.mjs"] diff --git a/docker/docker-compose.template.yaml b/docker/docker-compose.template.yaml index d1250fd2..c9826ca9 100644 --- a/docker/docker-compose.template.yaml +++ b/docker/docker-compose.template.yaml @@ -17,15 +17,16 @@ services: - CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron} - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password} - USERFRONT_URL=${USERFRONT_URL:-https://sso.hmac.kr} + - BACKEND_PORT=3000 - SEED_TENANT_CSV_PATH=/app/seed-tenant.csv ports: - - "${BACKEND_PORT:-3010}:3010" + - "${PROD_BACKEND_PORT:-3010}:3000" volumes: - ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro depends_on: - infra_check healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3010/health"] + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"] interval: 10s timeout: 5s retries: 3 @@ -37,11 +38,71 @@ services: image: ${USERFRONT_IMAGE_NAME}:${IMAGE_TAG} container_name: baron_userfront restart: unless-stopped + env_file: + - .env environment: - USERFRONT_URL=${USERFRONT_URL:-https://sso.hmac.kr} - - BACKEND_URL=${USERFRONT_URL:-https://sso.hmac.kr} + - BACKEND_URL=${BACKEND_URL:-https://sso.hmac.kr} ports: - - "${USERFRONT_PORT:-80}:80" + - "${USERFRONT_PORT:-80}:5000" + depends_on: + backend: + condition: service_healthy + networks: + - baron_net + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5000/"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + adminfront: + image: ${ADMINFRONT_IMAGE_NAME}:${IMAGE_TAG} + container_name: baron_adminfront + restart: unless-stopped + env_file: + - .env + environment: + - APP_ENV=production + - API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000} + ports: + - "${ADMINFRONT_PORT:-5173}:5173" + depends_on: + backend: + condition: service_healthy + networks: + - baron_net + + devfront: + image: ${DEVFRONT_IMAGE_NAME}:${IMAGE_TAG} + container_name: baron_devfront + restart: unless-stopped + env_file: + - .env + environment: + - APP_ENV=production + - API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000} + ports: + - "${DEVFRONT_PORT:-5174}:5173" + depends_on: + backend: + condition: service_healthy + networks: + - baron_net + + orgfront: + image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG} + container_name: baron_orgfront + restart: unless-stopped + env_file: + - .env + environment: + - APP_ENV=production + - API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000} + - USERFRONT_URL=${USERFRONT_URL} + ports: + - "${ORGFRONT_PORT:-5175}:5175" depends_on: backend: condition: service_healthy diff --git a/docker/staging_pull_compose.template.yaml b/docker/staging_pull_compose.template.yaml index 5e7d405a..e804b112 100644 --- a/docker/staging_pull_compose.template.yaml +++ b/docker/staging_pull_compose.template.yaml @@ -427,6 +427,11 @@ services: build: context: . dockerfile: ./adminfront/Dockerfile + args: + VITE_ADMIN_PUBLIC_URL: ${ADMINFRONT_URL:-} + VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY:-} + VITE_OIDC_CLIENT_ID: adminfront + ORGFRONT_URL: ${ORGFRONT_URL:-} container_name: baron_adminfront env_file: - .env @@ -435,11 +440,6 @@ services: - API_PROXY_TARGET=http://baron_backend:3000 ports: - "${ADMINFRONT_PORT:-5173}:5173" - volumes: - - ./adminfront:/app - - ./common:/common - - ./locales:/locales - - /app/node_modules networks: - baron_net healthcheck: @@ -453,6 +453,10 @@ services: build: context: . dockerfile: ./devfront/Dockerfile + args: + VITE_DEVFRONT_PUBLIC_URL: ${DEVFRONT_URL:-} + VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY:-} + VITE_OIDC_CLIENT_ID: devfront container_name: baron_devfront env_file: - .env @@ -461,11 +465,6 @@ services: - API_PROXY_TARGET=http://baron_backend:3000 ports: - "${DEVFRONT_PORT:-5174}:5173" - volumes: - - ./devfront:/app - - ./common:/common - - ./locales:/locales - - /app/node_modules networks: - baron_net healthcheck: @@ -479,6 +478,10 @@ services: build: context: . dockerfile: ./orgfront/Dockerfile + args: + VITE_ORGFRONT_PUBLIC_URL: ${ORGFRONT_URL:-} + VITE_OIDC_AUTHORITY: ${VITE_OIDC_AUTHORITY:-} + VITE_OIDC_CLIENT_ID: orgfront container_name: baron_orgfront env_file: - .env @@ -488,11 +491,6 @@ services: - USERFRONT_URL=${USERFRONT_URL} ports: - "${ORGFRONT_PORT:-5175}:5175" - volumes: - - ./orgfront:/app - - ./common:/common - - ./locales:/locales - - /app/node_modules networks: - baron_net healthcheck: diff --git a/orgfront/Dockerfile b/orgfront/Dockerfile index c5f6d107..706bada0 100644 --- a/orgfront/Dockerfile +++ b/orgfront/Dockerfile @@ -1,29 +1,38 @@ -FROM node:lts +FROM node:lts AS build WORKDIR /workspace -# Set CI environment variable to true to avoid TTY issues with pnpm ENV CI=true +ENV ORGFRONT_BUILD_OUT_DIR=/workspace/orgfront/dist -# Install pnpm RUN corepack enable && corepack prepare pnpm@10.5.2 --activate -# Copy workspace configs and common package -COPY pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY common ./common COPY orgfront ./orgfront -# Install dependencies for the workspace -RUN pnpm install --filter orgfront... --filter baron-sso... --no-frozen-lockfile --ignore-scripts +ARG VITE_ORGFRONT_PUBLIC_URL +ARG VITE_OIDC_AUTHORITY +ARG VITE_OIDC_CLIENT_ID +ENV VITE_ORGFRONT_PUBLIC_URL=$VITE_ORGFRONT_PUBLIC_URL +ENV VITE_OIDC_AUTHORITY=$VITE_OIDC_AUTHORITY +ENV VITE_OIDC_CLIENT_ID=$VITE_OIDC_CLIENT_ID -# 프로덕션 서빙을 위한 serve 패키지 글로벌 설치 -RUN npm install -g serve +RUN pnpm install --frozen-lockfile --ignore-scripts WORKDIR /workspace/orgfront +RUN npm run build + +FROM node:24-alpine AS production + +WORKDIR /app +ENV NODE_ENV=production +ENV FRONTEND_DIST_DIR=/app/dist +ENV PORT=5175 + +COPY scripts/serve_frontend_prod.mjs ./serve_frontend_prod.mjs +COPY --from=build /workspace/orgfront/dist ./dist -# Vite 기본 포트 EXPOSE 5175 -# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 -RUN chmod +x ./scripts/runtime-mode.sh -CMD ["sh", "./scripts/runtime-mode.sh"] +CMD ["node", "./serve_frontend_prod.mjs"] diff --git a/scripts/serve_frontend_prod.mjs b/scripts/serve_frontend_prod.mjs new file mode 100644 index 00000000..72eb8828 --- /dev/null +++ b/scripts/serve_frontend_prod.mjs @@ -0,0 +1,155 @@ +import { readFile, stat } from "node:fs/promises"; +import { createServer } from "node:http"; +import { extname, join, normalize, resolve } from "node:path"; + +const distDir = resolve(process.env.FRONTEND_DIST_DIR ?? "/app/dist"); +const host = process.env.HOST ?? "0.0.0.0"; +const port = Number(process.env.PORT ?? 5173); +const backendTarget = new URL( + process.env.API_PROXY_TARGET || "http://localhost:3000", +); + +const contentTypes = { + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml", + ".txt": "text/plain; charset=utf-8", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; + +function getContentType(filePath) { + return ( + contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream" + ); +} + +function sendJson(res, statusCode, body) { + res.writeHead(statusCode, { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + }); + res.end(JSON.stringify(body)); +} + +function toSafePath(pathname) { + const decoded = decodeURIComponent(pathname); + const relative = decoded.replace(/^\/+/, ""); + const safe = normalize(relative).replace(/^(\.\.(?:[\\/]|$))+/, ""); + return join(distDir, safe); +} + +async function tryReadFile(filePath) { + try { + return await readFile(filePath); + } catch { + return null; + } +} + +async function proxyToBackend(req, res, pathname, search) { + const target = new URL(pathname + search, backendTarget); + const headers = new Headers(); + + for (const [key, value] of Object.entries(req.headers)) { + if (!value) continue; + if (key === "host" || key === "content-length" || key === "connection") { + continue; + } + headers.set(key, Array.isArray(value) ? value.join(", ") : value); + } + + const hasBody = !["GET", "HEAD"].includes(req.method ?? "GET"); + const response = await fetch(target, { + method: req.method, + headers, + body: hasBody ? req : undefined, + duplex: hasBody ? "half" : undefined, + }); + + const responseHeaders = new Headers(response.headers); + responseHeaders.delete("content-length"); + responseHeaders.delete("transfer-encoding"); + responseHeaders.delete("connection"); + + res.writeHead(response.status, Object.fromEntries(responseHeaders.entries())); + + if (req.method === "HEAD") { + res.end(); + return; + } + + const arrayBuffer = await response.arrayBuffer(); + res.end(Buffer.from(arrayBuffer)); +} + +async function serveStatic(req, res, pathname) { + const indexPath = join(distDir, "index.html"); + const filePath = toSafePath(pathname); + + let resolvedPath = filePath; + try { + const fileStat = await stat(resolvedPath); + if (fileStat.isDirectory()) { + resolvedPath = join(resolvedPath, "index.html"); + } + } catch { + resolvedPath = indexPath; + } + + let body = await tryReadFile(resolvedPath); + if (!body) { + body = await tryReadFile(indexPath); + resolvedPath = indexPath; + } + + if (!body) { + sendJson(res, 500, { error: "dist_not_found" }); + return; + } + + res.writeHead(200, { + "Content-Type": getContentType(resolvedPath), + "Cache-Control": resolvedPath.endsWith("index.html") + ? "no-cache" + : "public, max-age=31536000, immutable", + }); + + if (req.method === "HEAD") { + res.end(); + return; + } + + res.end(body); +} + +createServer(async (req, res) => { + try { + const url = new URL( + req.url ?? "/", + `http://${req.headers.host ?? "localhost"}`, + ); + const { pathname, search } = url; + + if (pathname === "/api" || pathname.startsWith("/api/")) { + await proxyToBackend(req, res, pathname, search); + return; + } + + await serveStatic(req, res, pathname === "/" ? "/index.html" : pathname); + } catch (error) { + sendJson(res, 500, { + error: "internal_server_error", + message: error instanceof Error ? error.message : String(error), + }); + } +}).listen(port, host, () => { + console.log(`Frontend server listening on http://${host}:${port}`); +}); diff --git a/test/orgfront_integration_policy_test.sh b/test/orgfront_integration_policy_test.sh index 31595e90..9f168406 100644 --- a/test/orgfront_integration_policy_test.sh +++ b/test/orgfront_integration_policy_test.sh @@ -64,13 +64,19 @@ for file in "$STAGING_COMPOSE" "$PULL_COMPOSE"; do done assert_contains "$STAGING_COMPOSE" 'image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}' -assert_contains "$PULL_COMPOSE" "context: ./orgfront" -assert_contains "$DEPLOY_TEMPLATE" "../../orgfront:/app" -assert_contains "$DEPLOY_TEMPLATE" "./orgfront/vite.config.ts:/app/vite.config.ts:ro" -assert_contains "$DEPLOY_TEMPLATE" "./orgfront/auth.ts:/app/src/lib/auth.ts:ro" +assert_contains "$PULL_COMPOSE" "context: ." +assert_contains "$PULL_COMPOSE" "dockerfile: ./orgfront/Dockerfile" +assert_contains "$PULL_COMPOSE" "VITE_ORGFRONT_PUBLIC_URL: \${ORGFRONT_URL:-}" +assert_not_contains "$PULL_COMPOSE" "./orgfront:/app" +assert_contains "$DEPLOY_TEMPLATE" "dockerfile: ./orgfront/Dockerfile" +assert_contains "$DEPLOY_TEMPLATE" "VITE_ORGFRONT_PUBLIC_URL: \${ORGFRONT_URL}" +assert_not_contains "$DEPLOY_TEMPLATE" "../../orgfront:/app" +assert_not_contains "$DEPLOY_TEMPLATE" "./orgfront/vite.config.ts:/app/vite.config.ts:ro" +assert_not_contains "$DEPLOY_TEMPLATE" "./orgfront/auth.ts:/app/src/lib/auth.ts:ro" assert_contains "$BUILD_RC" "Build and push orgfront RC image" -assert_contains "$BUILD_RC" "context: ./orgfront" +assert_contains "$BUILD_RC" "context: ." +assert_contains "$BUILD_RC" "file: ./orgfront/Dockerfile" assert_contains "$BUILD_RC" "/baron_sso/orgfront:" assert_contains "$CODE_CHECK" "run_orgfront_tests" diff --git a/test/production_image_release_policy_test.sh b/test/production_image_release_policy_test.sh new file mode 100644 index 00000000..f52a5022 --- /dev/null +++ b/test/production_image_release_policy_test.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +assert_contains() { + local file="$1" + local pattern="$2" + if ! grep -Fq -- "$pattern" "$file"; then + echo "ERROR: missing pattern in $file: $pattern" >&2 + exit 1 + fi +} + +assert_not_contains() { + local file="$1" + local pattern="$2" + if grep -Fq -- "$pattern" "$file"; then + echo "ERROR: forbidden pattern remains in $file: $pattern" >&2 + exit 1 + fi +} + +build_rc="$ROOT_DIR/.gitea/workflows/build_RC.yml" +staging_release="$ROOT_DIR/.gitea/workflows/staging_release.yml" +production_release="$ROOT_DIR/.gitea/workflows/production_release.yml" +production_compose="$ROOT_DIR/docker/docker-compose.template.yaml" + +for file in "$build_rc" "$staging_release" "$production_release" "$production_compose"; do + if [[ ! -f "$file" ]]; then + echo "ERROR: expected file not found: $file" >&2 + exit 1 + fi +done + +for app in adminfront devfront orgfront; do + assert_contains "$build_rc" "Build and push $app RC image" + assert_contains "$build_rc" "file: ./$app/Dockerfile" + assert_contains "$build_rc" "build-args: |" + assert_contains "$build_rc" "VITE_OIDC_AUTHORITY=\${{ vars.VITE_OIDC_AUTHORITY }}" +done +assert_contains "$build_rc" "Validate RC build configuration" +assert_contains "$build_rc" "Missing required RC build value" +assert_contains "$build_rc" "Check Gitea repo variables/secrets" +assert_contains "$build_rc" "VITE_ADMIN_PUBLIC_URL=\${{ vars.ADMINFRONT_URL }}" +assert_contains "$build_rc" "VITE_DEVFRONT_PUBLIC_URL=\${{ vars.DEVFRONT_URL }}" +assert_contains "$build_rc" "VITE_ORGFRONT_PUBLIC_URL=\${{ vars.ORGFRONT_URL }}" +assert_contains "$build_rc" "ORGFRONT_URL=\${{ vars.ORGFRONT_URL }}" + +assert_contains "$staging_release" "CLICKHOUSE_PASSWORD=\${{ secrets.CLICKHOUSE_PASSWORD }}" +assert_not_contains "$staging_release" "CLICKHOUSE_PASSWORD=\${{ vars.CLICKHOUSE_PASSWORD }}" +assert_contains "$staging_release" "PROFILE_CACHE_TTL=\${{ vars.PROFILE_CACHE_TTL }}" +assert_contains "$staging_release" "KRATOS_UI_NODE_VERSION=\${{ vars.KRATOS_UI_NODE_VERSION }}" +assert_contains "$staging_release" "Missing required staging .env value" +assert_contains "$staging_release" "Check Gitea repo variables/secrets" +assert_contains "$staging_release" "scp scripts/render_ory_config.sh" +assert_contains "$staging_release" "scp compose.ory.yaml" +assert_not_contains "$staging_release" "scp docker/compose.ory.yaml" +assert_contains "$staging_release" "bash scripts/render_ory_config.sh" +assert_contains "$staging_release" "chmod -R 777 config/.generated/ory" + +assert_contains "$production_release" "for image in backend userfront adminfront devfront orgfront; do" +assert_contains "$production_release" 'docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${BASE_TAG}' +assert_contains "$production_release" 'docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${RE_TAG}' +assert_contains "$production_release" "ADMINFRONT_IMAGE_NAME: \${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront" +assert_contains "$production_release" "DEVFRONT_IMAGE_NAME: \${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront" +assert_contains "$production_release" "ORGFRONT_IMAGE_NAME: \${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront" +assert_contains "$production_release" "USERFRONT_URL=\${{ vars.PROD_FRONTEND_URL }}" +assert_contains "$production_release" "BACKEND_URL=\${{ vars.PROD_BACKEND_URL }}" +assert_contains "$production_release" "USERFRONT_PORT=\${{ vars.PROD_FRONTEND_PORT }}" +assert_contains "$production_release" "PROD_BACKEND_PORT=\${{ vars.PROD_BACKEND_PORT }}" +assert_contains "$production_release" "BACKEND_PORT=3000" +assert_contains "$production_release" "ADMINFRONT_URL=\${{ vars.ADMINFRONT_URL }}" +assert_contains "$production_release" "DEVFRONT_URL=\${{ vars.DEVFRONT_URL }}" +assert_contains "$production_release" "ORGFRONT_URL=\${{ vars.ORGFRONT_URL }}" +assert_contains "$production_release" "VITE_OIDC_AUTHORITY=\${{ vars.VITE_OIDC_AUTHORITY }}" +assert_contains "$production_release" "ADMINFRONT_CALLBACK_URLS=\${{ vars.ADMINFRONT_CALLBACK_URLS }}" +assert_contains "$production_release" "DEVFRONT_CALLBACK_URLS=\${{ vars.DEVFRONT_CALLBACK_URLS }}" +assert_contains "$production_release" "ORGFRONT_CALLBACK_URLS=\${{ vars.ORGFRONT_CALLBACK_URLS }}" +assert_contains "$production_release" "ADMINFRONT_PORT=\${{ vars.ADMINFRONT_PORT }}" +assert_contains "$production_release" "DEVFRONT_PORT=\${{ vars.DEVFRONT_PORT }}" +assert_contains "$production_release" "ORGFRONT_PORT=\${{ vars.ORGFRONT_PORT }}" +assert_contains "$production_release" "export ADMINFRONT_IMAGE_NAME='\${ADMINFRONT_IMAGE_NAME}'" +assert_contains "$production_release" "export DEVFRONT_IMAGE_NAME='\${DEVFRONT_IMAGE_NAME}'" +assert_contains "$production_release" "export ORGFRONT_IMAGE_NAME='\${ORGFRONT_IMAGE_NAME}'" +assert_contains "$production_release" "Missing required production .env value" +assert_not_contains "$production_release" "PROD_USERFRONT_URL" +assert_not_contains "$production_release" "PROD_USERFRONT_PORT" + +for app in adminfront devfront orgfront; do + assert_contains "$production_compose" "$app:" +done +assert_contains "$production_compose" 'image: ${ADMINFRONT_IMAGE_NAME}:${IMAGE_TAG}' +assert_contains "$production_compose" 'image: ${DEVFRONT_IMAGE_NAME}:${IMAGE_TAG}' +assert_contains "$production_compose" 'image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}' +assert_contains "$production_compose" 'API_PROXY_TARGET=http://baron_backend:${BACKEND_PORT:-3000}' +assert_contains "$production_compose" '${PROD_BACKEND_PORT:-3010}:3000' +assert_contains "$production_compose" '${USERFRONT_PORT:-80}:5000' +assert_contains "$production_compose" 'BACKEND_PORT=3000' +assert_contains "$production_compose" 'http://127.0.0.1:3000/health' + +echo "production image release policy checks passed" diff --git a/test/staging_frontend_deploy_policy_test.sh b/test/staging_frontend_deploy_policy_test.sh index 95900b2b..3a11c6ca 100644 --- a/test/staging_frontend_deploy_policy_test.sh +++ b/test/staging_frontend_deploy_policy_test.sh @@ -30,6 +30,9 @@ adminfront_vite="adminfront/vite.config.ts" adminfront_runtime="adminfront/scripts/runtime-mode.sh" devfront_runtime="devfront/scripts/runtime-mode.sh" orgfront_runtime="orgfront/scripts/runtime-mode.sh" +adminfront_dockerfile="adminfront/Dockerfile" +devfront_dockerfile="devfront/Dockerfile" +orgfront_dockerfile="orgfront/Dockerfile" for file in \ "$staging_pull" \ @@ -42,7 +45,10 @@ for file in \ "$orgfront_vite" \ "$adminfront_runtime" \ "$devfront_runtime" \ - "$orgfront_runtime" + "$orgfront_runtime" \ + "$adminfront_dockerfile" \ + "$devfront_dockerfile" \ + "$orgfront_dockerfile" do if [ ! -f "$file" ]; then echo "missing expected file: $file" >&2 @@ -72,8 +78,11 @@ for app in adminfront devfront orgfront; do assert_contains "$pull_compose" "$app:" assert_contains "$pull_compose" "context: ." assert_contains "$pull_compose" "dockerfile: ./$app/Dockerfile" + assert_contains "$pull_compose" "VITE_OIDC_AUTHORITY: \${VITE_OIDC_AUTHORITY:-}" assert_not_contains "$pull_compose" "context: ./$app" + assert_not_contains "$pull_compose" "./$app:/app" done +assert_not_contains "$pull_compose" "/app/node_modules" assert_contains "$pull_compose" "dockerfile: userfront/Dockerfile" assert_not_contains "$pull_compose" 'target: ${USERFRONT_BUILD_TARGET:-dev}' assert_not_contains "$pull_compose" "target: dev" @@ -82,8 +91,12 @@ assert_contains "$pull_compose" "http://127.0.0.1:5173/" assert_contains "$pull_compose" "http://127.0.0.1:5175/" assert_contains "$pull_compose" 'APP_ENV=${APP_ENV:-stage}' -assert_contains "$deploy_compose" "sh ./scripts/runtime-mode.sh" +assert_contains "$deploy_compose" "dockerfile: ./adminfront/Dockerfile" +assert_contains "$deploy_compose" "dockerfile: ./devfront/Dockerfile" +assert_contains "$deploy_compose" "dockerfile: ./orgfront/Dockerfile" +assert_not_contains "$deploy_compose" "sh ./scripts/runtime-mode.sh" assert_not_contains "$deploy_compose" "command: npm run dev" +assert_not_contains "$deploy_compose" "image: node:20-alpine" assert_contains "$deploy_gateway" "root /usr/share/nginx/html;" assert_contains "$deploy_gateway" 'try_files $uri $uri/ /index.html;' assert_not_contains "$deploy_gateway" "baron_userfront" @@ -96,6 +109,21 @@ for app in adminfront devfront orgfront; do assert_not_contains ".gitea/workflows/build_RC.yml" "context: ./$app" done +for app in adminfront devfront orgfront; do + dockerfile="$app/Dockerfile" + assert_contains "$dockerfile" "COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./" + assert_contains "$dockerfile" "RUN pnpm install --frozen-lockfile --ignore-scripts" + assert_contains "$dockerfile" "FROM node:24-alpine AS production" + assert_contains "$dockerfile" "COPY scripts/serve_frontend_prod.mjs ./serve_frontend_prod.mjs" + assert_contains "$dockerfile" "RUN npm run build" + assert_contains "$dockerfile" 'CMD ["node", "./serve_frontend_prod.mjs"]' + assert_not_contains "$dockerfile" "cd common && pnpm install" + assert_not_contains "$dockerfile" "npm install -g serve" + assert_not_contains "$dockerfile" "runtime-mode.sh" +done +assert_contains "scripts/serve_frontend_prod.mjs" "pathname === \"/api\" || pathname.startsWith(\"/api/\")" +assert_contains "scripts/serve_frontend_prod.mjs" "API_PROXY_TARGET" + assert_contains "$adminfront_vite" "/tmp/baron-sso-adminfront-dist" assert_contains "$adminfront_vite" "/tmp/baron-sso-adminfront-vite-cache" assert_contains "adminfront/biome.json" '".vite"'