1
0
forked from baron/baron-sso

fix(deploy): align staging frontend runtime with production images

This commit is contained in:
2026-06-05 09:24:44 +09:00
parent ded9dfc56b
commit 4bae1dd00d
13 changed files with 585 additions and 107 deletions

View File

@@ -18,6 +18,30 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y jq curl 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 - name: Login to Docker Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -93,6 +117,11 @@ jobs:
file: ./adminfront/Dockerfile file: ./adminfront/Dockerfile
push: true push: true
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront:${{ steps.rc_calculator.outputs.new_rc_tag }} 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 provenance: false
sbom: false sbom: false
@@ -103,6 +132,10 @@ jobs:
file: ./devfront/Dockerfile file: ./devfront/Dockerfile
push: true push: true
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront:${{ steps.rc_calculator.outputs.new_rc_tag }} 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 provenance: false
sbom: false sbom: false
@@ -113,6 +146,10 @@ jobs:
file: ./orgfront/Dockerfile file: ./orgfront/Dockerfile
push: true push: true
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront:${{ steps.rc_calculator.outputs.new_rc_tag }} 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 provenance: false
sbom: false sbom: false

View File

@@ -42,19 +42,13 @@ jobs:
sudo apt-get update -y && sudo apt-get install -y skopeo sudo apt-get update -y && sudo apt-get install -y skopeo
fi fi
# Re-tag backend image for image in backend userfront adminfront devfront orgfront; do
echo "Re-tagging backend image..." echo "Re-tagging ${image} image..."
skopeo copy --preserve-digests \ skopeo copy --preserve-digests \
--src-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" --dest-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" \ --src-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" --dest-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" \
--src-tls-verify=false --dest-tls-verify=false \ --src-tls-verify=false --dest-tls-verify=false \
"docker://${HARBOR_HOSTNAME}/baron_sso/backend:${BASE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/backend:${RE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${BASE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/${image}:${RE_TAG}"
done
# 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}"
echo "final_image_tag=${RE_TAG}" >> "$GITHUB_OUTPUT" echo "final_image_tag=${RE_TAG}" >> "$GITHUB_OUTPUT"
@@ -68,6 +62,9 @@ jobs:
IMAGE_TAG: ${{ steps.retag.outputs.final_image_tag }} IMAGE_TAG: ${{ steps.retag.outputs.final_image_tag }}
BACKEND_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend BACKEND_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend
USERFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront 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 }} DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
PROD_HOST: ${{ vars.PROD_HOST }} PROD_HOST: ${{ vars.PROD_HOST }}
PROD_USER: ${{ vars.PROD_USER }} PROD_USER: ${{ vars.PROD_USER }}
@@ -101,8 +98,12 @@ jobs:
"CLICKHOUSE_PORT_NATIVE=${{ vars.PROD_CLICKHOUSE_PORT_NATIVE }}" \ "CLICKHOUSE_PORT_NATIVE=${{ vars.PROD_CLICKHOUSE_PORT_NATIVE }}" \
"CLICKHOUSE_USER=${{ vars.PROD_CLICKHOUSE_USER }}" \ "CLICKHOUSE_USER=${{ vars.PROD_CLICKHOUSE_USER }}" \
"CLICKHOUSE_PASSWORD=${{ secrets.PROD_CLICKHOUSE_PASSWORD }}" \ "CLICKHOUSE_PASSWORD=${{ secrets.PROD_CLICKHOUSE_PASSWORD }}" \
"BACKEND_PORT=${{ vars.PROD_BACKEND_PORT }}" \ "PROD_BACKEND_PORT=${{ vars.PROD_BACKEND_PORT }}" \
"USERFRONT_PORT=${{ vars.PROD_USERFRONT_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_USER=${{ vars.PROD_DB_USER }}" \
"DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }}" \ "DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }}" \
"DB_NAME=${{ vars.PROD_DB_NAME }}" \ "DB_NAME=${{ vars.PROD_DB_NAME }}" \
@@ -117,10 +118,33 @@ jobs:
"AWS_ACCESS_KEY_ID=${{ vars.AWS_ACCESS_KEY_ID }}" \ "AWS_ACCESS_KEY_ID=${{ vars.AWS_ACCESS_KEY_ID }}" \
"AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" \ "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" \
"AWS_SES_SENDER=${{ vars.AWS_SES_SENDER }}" \ "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 }}" \ "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 > .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 # Copy compose template and .env file to the remote server
scp adminfront/seed-tenant.csv "${PROD_USER}@${PROD_HOST}:${DEPLOY_PATH}/adminfront/" 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}/" scp docker/docker-compose.template.yaml .env "${PROD_USER}@${PROD_HOST}:${DEPLOY_PATH}/"
@@ -131,6 +155,9 @@ jobs:
"export DEPLOY_PATH='${DEPLOY_PATH}'; \ "export DEPLOY_PATH='${DEPLOY_PATH}'; \
export BACKEND_IMAGE_NAME='${BACKEND_IMAGE_NAME}'; \ export BACKEND_IMAGE_NAME='${BACKEND_IMAGE_NAME}'; \
export USERFRONT_IMAGE_NAME='${USERFRONT_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 IMAGE_TAG='${IMAGE_TAG}'; \
export HARBOR_ENDPOINT='${HARBOR_ENDPOINT}'; \ export HARBOR_ENDPOINT='${HARBOR_ENDPOINT}'; \
export HARBOR_ROBOT_ACCOUNT='${HARBOR_ROBOT_ACCOUNT}'; \ export HARBOR_ROBOT_ACCOUNT='${HARBOR_ROBOT_ACCOUNT}'; \

View File

@@ -69,7 +69,7 @@ jobs:
CLICKHOUSE_PORT_NATIVE=${{ vars.CLICKHOUSE_PORT_NATIVE }} CLICKHOUSE_PORT_NATIVE=${{ vars.CLICKHOUSE_PORT_NATIVE }}
CLICKHOUSE_HOST=${{ vars.CLICKHOUSE_HOST }} CLICKHOUSE_HOST=${{ vars.CLICKHOUSE_HOST }}
CLICKHOUSE_USER=${{ vars.CLICKHOUSE_USER }} CLICKHOUSE_USER=${{ vars.CLICKHOUSE_USER }}
CLICKHOUSE_PASSWORD=${{ vars.CLICKHOUSE_PASSWORD }} CLICKHOUSE_PASSWORD=${{ secrets.CLICKHOUSE_PASSWORD }}
BACKEND_PORT=${{ vars.BACKEND_PORT }} BACKEND_PORT=${{ vars.BACKEND_PORT }}
@@ -142,9 +142,32 @@ jobs:
# OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} # OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }}
EOF 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}/docker"
ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p ${DEPLOY_PATH}/adminfront" 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이 있어야 함) # [중요] docker/ory 폴더 복사 (여기에 init-db/1-createdb.sql이 있어야 함)
scp -r docker/ory "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/docker/" scp -r docker/ory "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/docker/"
@@ -158,9 +181,10 @@ jobs:
fi fi
scp adminfront/seed-tenant.csv "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/adminfront/" 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/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.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}" \ 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 for net in baron_net public_net ory-net hydranet kratosnet; do
docker network inspect \"\$net\" >/dev/null 2>&1 || docker network create \"\$net\" docker network inspect \"\$net\" >/dev/null 2>&1 || docker network create \"\$net\"
done done
bash scripts/render_ory_config.sh; \
chmod -R 777 config/.generated/ory || true; \
envsubst < docker-compose.staging.template.yaml > docker-compose.yml; \ envsubst < docker-compose.staging.template.yaml > docker-compose.yml; \

View File

@@ -1,29 +1,40 @@
FROM node:lts FROM node:lts AS build
WORKDIR /workspace WORKDIR /workspace
# Set CI environment variable to true to avoid TTY issues with pnpm
ENV CI=true ENV CI=true
ENV ADMINFRONT_BUILD_OUT_DIR=/workspace/adminfront/dist
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10.5.2 --activate RUN corepack enable && corepack prepare pnpm@10.5.2 --activate
# Copy workspace configs and common package COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY common ./common COPY common ./common
COPY adminfront ./adminfront COPY adminfront ./adminfront
# Install dependencies for the workspace ARG VITE_ADMIN_PUBLIC_URL
RUN pnpm install --filter adminfront... --filter baron-sso... --no-frozen-lockfile --ignore-scripts 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 pnpm install --frozen-lockfile --ignore-scripts
RUN npm install -g serve
WORKDIR /workspace/adminfront 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 EXPOSE 5173
# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 CMD ["node", "./serve_frontend_prod.mjs"]
RUN chmod +x ./scripts/runtime-mode.sh
CMD ["sh", "./scripts/runtime-mode.sh"]

View File

@@ -314,48 +314,56 @@ services:
networks: [app_net] networks: [app_net]
adminfront: 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 container_name: ${COMPOSE_PROJECT_NAME}_adminfront
working_dir: /app
env_file: .env env_file: .env
environment:
- APP_ENV=${APP_ENV:-production}
- API_PROXY_TARGET=http://backend:${BACKEND_PORT}
ports: ports:
- "${ADMINFRONT_PORT}:5173" - "${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] networks: [app_net]
devfront: 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 container_name: ${COMPOSE_PROJECT_NAME}_devfront
working_dir: /app
env_file: .env env_file: .env
environment:
- APP_ENV=${APP_ENV:-production}
- API_PROXY_TARGET=http://backend:${BACKEND_PORT}
ports: ports:
- "${DEVFRONT_PORT}:5173" - "${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] networks: [app_net]
orgfront: 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 container_name: ${COMPOSE_PROJECT_NAME}_orgfront
working_dir: /app
env_file: .env env_file: .env
environment:
- APP_ENV=${APP_ENV:-production}
- API_PROXY_TARGET=http://backend:${BACKEND_PORT}
- USERFRONT_URL=${USERFRONT_URL}
ports: ports:
- "${ORGFRONT_PORT}:5175" - "${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: [app_net]
networks: networks:

View File

@@ -1,29 +1,38 @@
FROM node:lts FROM node:lts AS build
WORKDIR /workspace WORKDIR /workspace
# Set CI environment variable to true to avoid TTY issues with pnpm
ENV CI=true ENV CI=true
ENV DEVFRONT_BUILD_OUT_DIR=/workspace/devfront/dist
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10.5.2 --activate RUN corepack enable && corepack prepare pnpm@10.5.2 --activate
# Copy workspace configs and common package COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY common ./common COPY common ./common
COPY devfront ./devfront COPY devfront ./devfront
# Install dependencies for the workspace ARG VITE_DEVFRONT_PUBLIC_URL
RUN pnpm install --filter devfront... --filter baron-sso... --no-frozen-lockfile --ignore-scripts 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 pnpm install --frozen-lockfile --ignore-scripts
RUN npm install -g serve
WORKDIR /workspace/devfront 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 EXPOSE 5173
# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 CMD ["node", "./serve_frontend_prod.mjs"]
RUN chmod +x ./scripts/runtime-mode.sh
CMD ["sh", "./scripts/runtime-mode.sh"]

View File

@@ -17,15 +17,16 @@ services:
- CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron} - CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron}
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password} - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password}
- USERFRONT_URL=${USERFRONT_URL:-https://sso.hmac.kr} - USERFRONT_URL=${USERFRONT_URL:-https://sso.hmac.kr}
- BACKEND_PORT=3000
- SEED_TENANT_CSV_PATH=/app/seed-tenant.csv - SEED_TENANT_CSV_PATH=/app/seed-tenant.csv
ports: ports:
- "${BACKEND_PORT:-3010}:3010" - "${PROD_BACKEND_PORT:-3010}:3000"
volumes: volumes:
- ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro - ./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro
depends_on: depends_on:
- infra_check - infra_check
healthcheck: 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 interval: 10s
timeout: 5s timeout: 5s
retries: 3 retries: 3
@@ -37,11 +38,71 @@ services:
image: ${USERFRONT_IMAGE_NAME}:${IMAGE_TAG} image: ${USERFRONT_IMAGE_NAME}:${IMAGE_TAG}
container_name: baron_userfront container_name: baron_userfront
restart: unless-stopped restart: unless-stopped
env_file:
- .env
environment: environment:
- USERFRONT_URL=${USERFRONT_URL:-https://sso.hmac.kr} - 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: 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: depends_on:
backend: backend:
condition: service_healthy condition: service_healthy

View File

@@ -427,6 +427,11 @@ services:
build: build:
context: . context: .
dockerfile: ./adminfront/Dockerfile 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 container_name: baron_adminfront
env_file: env_file:
- .env - .env
@@ -435,11 +440,6 @@ services:
- API_PROXY_TARGET=http://baron_backend:3000 - API_PROXY_TARGET=http://baron_backend:3000
ports: ports:
- "${ADMINFRONT_PORT:-5173}:5173" - "${ADMINFRONT_PORT:-5173}:5173"
volumes:
- ./adminfront:/app
- ./common:/common
- ./locales:/locales
- /app/node_modules
networks: networks:
- baron_net - baron_net
healthcheck: healthcheck:
@@ -453,6 +453,10 @@ services:
build: build:
context: . context: .
dockerfile: ./devfront/Dockerfile 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 container_name: baron_devfront
env_file: env_file:
- .env - .env
@@ -461,11 +465,6 @@ services:
- API_PROXY_TARGET=http://baron_backend:3000 - API_PROXY_TARGET=http://baron_backend:3000
ports: ports:
- "${DEVFRONT_PORT:-5174}:5173" - "${DEVFRONT_PORT:-5174}:5173"
volumes:
- ./devfront:/app
- ./common:/common
- ./locales:/locales
- /app/node_modules
networks: networks:
- baron_net - baron_net
healthcheck: healthcheck:
@@ -479,6 +478,10 @@ services:
build: build:
context: . context: .
dockerfile: ./orgfront/Dockerfile 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 container_name: baron_orgfront
env_file: env_file:
- .env - .env
@@ -488,11 +491,6 @@ services:
- USERFRONT_URL=${USERFRONT_URL} - USERFRONT_URL=${USERFRONT_URL}
ports: ports:
- "${ORGFRONT_PORT:-5175}:5175" - "${ORGFRONT_PORT:-5175}:5175"
volumes:
- ./orgfront:/app
- ./common:/common
- ./locales:/locales
- /app/node_modules
networks: networks:
- baron_net - baron_net
healthcheck: healthcheck:

View File

@@ -1,29 +1,38 @@
FROM node:lts FROM node:lts AS build
WORKDIR /workspace WORKDIR /workspace
# Set CI environment variable to true to avoid TTY issues with pnpm
ENV CI=true ENV CI=true
ENV ORGFRONT_BUILD_OUT_DIR=/workspace/orgfront/dist
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10.5.2 --activate RUN corepack enable && corepack prepare pnpm@10.5.2 --activate
# Copy workspace configs and common package COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY common ./common COPY common ./common
COPY orgfront ./orgfront COPY orgfront ./orgfront
# Install dependencies for the workspace ARG VITE_ORGFRONT_PUBLIC_URL
RUN pnpm install --filter orgfront... --filter baron-sso... --no-frozen-lockfile --ignore-scripts 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 pnpm install --frozen-lockfile --ignore-scripts
RUN npm install -g serve
WORKDIR /workspace/orgfront 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 EXPOSE 5175
# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 CMD ["node", "./serve_frontend_prod.mjs"]
RUN chmod +x ./scripts/runtime-mode.sh
CMD ["sh", "./scripts/runtime-mode.sh"]

View File

@@ -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}`);
});

View File

@@ -64,13 +64,19 @@ for file in "$STAGING_COMPOSE" "$PULL_COMPOSE"; do
done done
assert_contains "$STAGING_COMPOSE" 'image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}' assert_contains "$STAGING_COMPOSE" 'image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG}'
assert_contains "$PULL_COMPOSE" "context: ./orgfront" assert_contains "$PULL_COMPOSE" "context: ."
assert_contains "$DEPLOY_TEMPLATE" "../../orgfront:/app" assert_contains "$PULL_COMPOSE" "dockerfile: ./orgfront/Dockerfile"
assert_contains "$DEPLOY_TEMPLATE" "./orgfront/vite.config.ts:/app/vite.config.ts:ro" assert_contains "$PULL_COMPOSE" "VITE_ORGFRONT_PUBLIC_URL: \${ORGFRONT_URL:-}"
assert_contains "$DEPLOY_TEMPLATE" "./orgfront/auth.ts:/app/src/lib/auth.ts:ro" 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" "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 "$BUILD_RC" "/baron_sso/orgfront:"
assert_contains "$CODE_CHECK" "run_orgfront_tests" assert_contains "$CODE_CHECK" "run_orgfront_tests"

View File

@@ -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"

View File

@@ -30,6 +30,9 @@ adminfront_vite="adminfront/vite.config.ts"
adminfront_runtime="adminfront/scripts/runtime-mode.sh" adminfront_runtime="adminfront/scripts/runtime-mode.sh"
devfront_runtime="devfront/scripts/runtime-mode.sh" devfront_runtime="devfront/scripts/runtime-mode.sh"
orgfront_runtime="orgfront/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 \ for file in \
"$staging_pull" \ "$staging_pull" \
@@ -42,7 +45,10 @@ for file in \
"$orgfront_vite" \ "$orgfront_vite" \
"$adminfront_runtime" \ "$adminfront_runtime" \
"$devfront_runtime" \ "$devfront_runtime" \
"$orgfront_runtime" "$orgfront_runtime" \
"$adminfront_dockerfile" \
"$devfront_dockerfile" \
"$orgfront_dockerfile"
do do
if [ ! -f "$file" ]; then if [ ! -f "$file" ]; then
echo "missing expected file: $file" >&2 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" "$app:"
assert_contains "$pull_compose" "context: ." assert_contains "$pull_compose" "context: ."
assert_contains "$pull_compose" "dockerfile: ./$app/Dockerfile" 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" "context: ./$app"
assert_not_contains "$pull_compose" "./$app:/app"
done done
assert_not_contains "$pull_compose" "/app/node_modules"
assert_contains "$pull_compose" "dockerfile: userfront/Dockerfile" assert_contains "$pull_compose" "dockerfile: userfront/Dockerfile"
assert_not_contains "$pull_compose" 'target: ${USERFRONT_BUILD_TARGET:-dev}' assert_not_contains "$pull_compose" 'target: ${USERFRONT_BUILD_TARGET:-dev}'
assert_not_contains "$pull_compose" "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" "http://127.0.0.1:5175/"
assert_contains "$pull_compose" 'APP_ENV=${APP_ENV:-stage}' 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" "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" "root /usr/share/nginx/html;"
assert_contains "$deploy_gateway" 'try_files $uri $uri/ /index.html;' assert_contains "$deploy_gateway" 'try_files $uri $uri/ /index.html;'
assert_not_contains "$deploy_gateway" "baron_userfront" 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" assert_not_contains ".gitea/workflows/build_RC.yml" "context: ./$app"
done 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-dist"
assert_contains "$adminfront_vite" "/tmp/baron-sso-adminfront-vite-cache" assert_contains "$adminfront_vite" "/tmp/baron-sso-adminfront-vite-cache"
assert_contains "adminfront/biome.json" '".vite"' assert_contains "adminfront/biome.json" '".vite"'