첫 커밋: 로컬 프로젝트 업로드

This commit is contained in:
2026-06-10 15:51:34 +09:00
commit 6a8dbeb2e9
1211 changed files with 312864 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
name: Backend Test Coverage Check
on:
push:
branches:
- dev
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
cache-dependency-path: backend/go.sum
- name: Run tests with coverage
working-directory: ./backend
run: |
go test -v -coverprofile=coverage.out -covermode=atomic ./internal/handler/... ./internal/service/...
- name: Check coverage
uses: vladopajic/go-test-coverage@v2
with:
config: ./.gitea/coverage.json

View File

@@ -0,0 +1,165 @@
name: Build Baron SSO RC
on:
workflow_dispatch:
inputs:
version_tag:
description: "The version tag to release to staging (e.g., v1.2601.1)"
required: true
type: string
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- 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:
registry: ${{ vars.HARBOR_ENDPOINT }}
username: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
password: ${{ secrets.HARBOR_ROBOT_KEY }}
- name: Calculate next RC tag
id: rc_calculator
env:
INPUT_TAG: ${{ github.event.inputs.version_tag }}
REGISTRY_URL: ${{ vars.HARBOR_ENDPOINT }}
HARBOR_USER: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_KEY }}
run: |
# Generate YYMM dynamically for the new tag
CURRENT_YYMM=$(date +'%y%m')
# Reconstruct the base tag with the current YYMM
MAJOR_VERSION=$(echo "${INPUT_TAG}" | cut -d'.' -f1)
MINOR_VERSION=$(echo "${INPUT_TAG}" | cut -d'.' -f3)
BASE_TAG="${MAJOR_VERSION}.${CURRENT_YYMM}.${MINOR_VERSION}"
echo "Input tag: ${INPUT_TAG}"
echo "Generated dynamic base tag: ${BASE_TAG}"
# Using the backend repository as the source for RC version calculation
API_URL="${REGISTRY_URL}/api/v2.0/projects/baron_sso/repositories/backend/artifacts?sort=-creation_time&page_size=100"
AUTH_HEADER=$(echo -n "${HARBOR_USER}:${HARBOR_PASSWORD}" | base64)
API_RESPONSE=$(curl -s -k -H "Authorization: Basic ${AUTH_HEADER}" "${API_URL}")
# Define a search pattern to find RCs across different months for the same major/minor version
# e.g., matches v1.2508.1-RC, v1.2509.1-RC, etc.
SEARCH_PATTERN="^${MAJOR_VERSION}\.[0-9]{4}\.${MINOR_VERSION}-RC"
echo "Using search pattern: ${SEARCH_PATTERN}"
# Disable pipefail for grep, as it will exit with 1 if no match is found
set +o pipefail
# Find the highest RC number regardless of the YYMM part
LATEST_RC_NUM=$(echo "${API_RESPONSE}" | jq -r '.[] | .tags[]? | .name' | grep -E "${SEARCH_PATTERN}" | sed 's/.*-RC//' | sort -rn | head -n 1)
set -o pipefail
if [ -z "$LATEST_RC_NUM" ]; then
NEXT_RC_NUM=1
else
NEXT_RC_NUM=$((LATEST_RC_NUM + 1))
fi
# Create the new tag using the dynamically generated BASE_TAG and the incremented RC number
NEW_RC_TAG="${BASE_TAG}-RC${NEXT_RC_NUM}"
echo "new_rc_tag=$NEW_RC_TAG" >> $GITHUB_OUTPUT
echo "Found latest RC number: ${LATEST_RC_NUM:-0}"
echo "Calculated new RC tag: $NEW_RC_TAG"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push backend RC image
uses: docker/build-push-action@v5
with:
context: ./backend
file: ./backend/Dockerfile
push: true
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend:${{ steps.rc_calculator.outputs.new_rc_tag }}
provenance: false
sbom: false
- name: Build and push adminfront RC image
uses: docker/build-push-action@v5
with:
context: .
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
- name: Build and push devfront RC image
uses: docker/build-push-action@v5
with:
context: .
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
- name: Build and push orgfront RC image
uses: docker/build-push-action@v5
with:
context: .
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
- name: Build and push userfront RC image
uses: docker/build-push-action@v5
with:
context: .
file: ./userfront/Dockerfile
target: production
push: true
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront:${{ steps.rc_calculator.outputs.new_rc_tag }}
provenance: false
sbom: false

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
name: Release Baron SSO to Production
on:
workflow_dispatch:
inputs:
rc_version_tag:
description: "The version tag to release to production (e.g., v1.2601.1-RC1)"
required: true
type: string
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Harbor Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.HARBOR_ENDPOINT }}
username: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
password: ${{ secrets.HARBOR_ROBOT_KEY }}
- name: Parse RC and re-tag to final
id: retag
env:
HARBOR_HOSTNAME: ${{ vars.HARBOR_HOSTNAME }}
HARBOR_USER: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_KEY }}
run: |
BASE_TAG=$(echo "${{ github.event.inputs.rc_version_tag }}" | xargs)
if [[ ! "$BASE_TAG" =~ ^v[0-9]+\.[0-9]{4}\.[0-9]+-RC[0-9]+$ ]]; then
echo "::error::rc_version_tag must look like vX.YYMM.Z-RC## (got: $BASE_TAG)"
exit 1
fi
RE_TAG="${BASE_TAG%-RC*}"
echo "Final tag will be: ${RE_TAG}"
if ! command -v skopeo >/dev/null 2>&1; then
sudo apt-get update -y && sudo apt-get install -y skopeo
fi
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"
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
- name: Deploy to Production
env:
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 }}
HARBOR_ENDPOINT: ${{ vars.HARBOR_ENDPOINT }}
HARBOR_ROBOT_ACCOUNT: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
HARBOR_ROBOT_KEY: ${{ secrets.HARBOR_ROBOT_KEY }}
run: |
set -euo pipefail
echo "DEBUG: PROD_USER='${PROD_USER}'"
echo "DEBUG: PROD_HOST='${PROD_HOST}'"
echo "DEBUG: DEPLOY_PATH='${DEPLOY_PATH}'"
# Sanity check (fail fast with a clear message)
if [ -z "${PROD_USER}" ] || [ -z "${PROD_HOST}" ] || [ -z "${DEPLOY_PATH}" ]; then
echo "::error::Missing required vars (PROD_USER/PROD_HOST/DEPLOY_PATH). Check Gitea repo variables."
exit 1
fi
ssh-keyscan -H "${PROD_HOST}" >> ~/.ssh/known_hosts
ssh "${PROD_USER}@${PROD_HOST}" "mkdir -p '${DEPLOY_PATH}/adminfront'"
# Create the main .env file for Baron SSO on the remote server
# Note: All values are pulled from Gitea secrets and variables
printf '%s\n' \
"APP_ENV=production" \
"TZ=Asia/Seoul" \
"DB_PORT=${{ vars.PROD_DB_PORT }}" \
"CLICKHOUSE_PORT_HTTP=${{ vars.PROD_CLICKHOUSE_PORT_HTTP }}" \
"CLICKHOUSE_PORT_NATIVE=${{ vars.PROD_CLICKHOUSE_PORT_NATIVE }}" \
"CLICKHOUSE_USER=${{ vars.PROD_CLICKHOUSE_USER }}" \
"CLICKHOUSE_PASSWORD=${{ secrets.PROD_CLICKHOUSE_PASSWORD }}" \
"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 }}" \
"COOKIE_SECRET=${{ secrets.PROD_COOKIE_SECRET }}" \
"JWT_SECRET=${{ secrets.PROD_JWT_SECRET }}" \
"REDIS_ADDR=${{ vars.PROD_REDIS_ADDR }}" \
"NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }}" \
"NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }}" \
"NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }}" \
"NAVER_SENDER_PHONE_NUMBER=${{ vars.NAVER_SENDER_PHONE_NUMBER }}" \
"AWS_REGION=${{ vars.AWS_REGION }}" \
"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_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}/"
scp docker/compose.infra.prd.yaml "${PROD_USER}@${PROD_HOST}:${DEPLOY_PATH}/compose.infra.yml"
# Deploy Baron SSO
echo "${HARBOR_ROBOT_KEY}" | ssh "${PROD_USER}@${PROD_HOST}" \
"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}'; \
set -e; \
cd \"\${DEPLOY_PATH}\"; \
docker login \"\${HARBOR_ENDPOINT}\" -u \"\${HARBOR_ROBOT_ACCOUNT}\" --password-stdin; \
set -a; \
. ./.env; \
set +a; \
envsubst < docker-compose.template.yaml > docker-compose.yml; \
docker compose -f compose.infra.yml -f docker-compose.yml pull; \
docker compose -f compose.infra.yml -f docker-compose.yml up -d --remove-orphans"

View File

@@ -0,0 +1,83 @@
name: Staging Build Check
on:
pull_request:
paths:
- ".gitea/workflows/staging_build_check.yml"
- "docker/staging_pull_compose.template.yaml"
- "adminfront/**"
- "devfront/**"
- "userfront/**"
- "backend/**"
- "common/**"
- "scripts/**"
- "locales/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
workflow_dispatch:
jobs:
build-check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- service: adminfront
- service: devfront
- service: userfront
- service: backend
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Prepare staging build inputs
run: |
set -euo pipefail
cat <<'EOF' > .env
APP_ENV=stage
TZ=Asia/Seoul
IDP_PROVIDER=ory
ADMINFRONT_URL=https://adminfront.staging.example.com
DEVFRONT_URL=https://devfront.staging.example.com
USERFRONT_URL=https://userfront.staging.example.com
ORGFRONT_URL=https://orgfront.staging.example.com
BACKEND_URL=https://backend.staging.example.com
BACKEND_PUBLIC_URL=https://backend.staging.example.com
VITE_OIDC_AUTHORITY=https://sso.staging.example.com/oidc
WORKS_ADMIN_API_BASE_URL=https://works-admin.staging.example.com/api
WORKS_ADMIN_OAUTH_TOKEN_URL=https://works-admin.staging.example.com/oauth/token
ORY_POSTGRES_USER=ory
ORY_POSTGRES_PASSWORD=ory-password
COOKIE_SECRET=staging-build-cookie-secret
JWT_SECRET=staging-build-jwt-secret
NAVER_CLOUD_ACCESS_KEY=dummy
NAVER_CLOUD_SECRET_KEY=dummy
NAVER_CLOUD_SERVICE_ID=dummy
NAVER_SENDER_PHONE_NUMBER=00000000000
AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY_ID=dummy
AWS_SECRET_ACCESS_KEY=dummy
AWS_SES_SENDER=dummy@example.com
REDIS_ADDR=redis:6389
CLICKHOUSE_PORT_NATIVE=9000
CLICKHOUSE_USER=baron
CLICKHOUSE_PASSWORD=password
HYDRA_PUBLIC_URL=https://hydra.staging.example.com
KRATOS_BROWSER_URL=https://sso.staging.example.com
KRATOS_ADMIN_URL=http://kratos:4434
KRATOS_UI_URL=https://sso.staging.example.com
EOF
cp docker/staging_pull_compose.template.yaml staging_pull_compose.yaml
- name: Build ${{ matrix.service }} with staging compose
env:
DOCKER_BUILDKIT: "1"
COMPOSE_DOCKER_CLI_BUILD: "1"
run: |
set -euo pipefail
docker compose -f staging_pull_compose.yaml build --pull --progress=plain "${{ matrix.service }}"

View File

@@ -0,0 +1,256 @@
name: Release Baron SSO to Staging
on:
workflow_dispatch:
inputs:
target_branch:
description: "Branch to deploy"
required: true
default: "dev"
jobs:
deploy-staging:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.STAGE_SSH_PRIVATE_KEY }}
- name: Deploy to Staging by git pull
env:
DEPLOY_PATH: ${{ vars.STAGE_DEPLOY_PATH }}
STAGE_HOST: ${{ vars.STAGE_HOST }}
STAGE_USER: ${{ vars.STAGE_USER }}
TARGET_BRANCH: ${{ inputs.target_branch }}
run: |
set -euo pipefail
echo "DEBUG: STAGE_USER='${STAGE_USER}'"
echo "DEBUG: STAGE_HOST='${STAGE_HOST}'"
echo "DEBUG: DEPLOY_PATH='${DEPLOY_PATH}'"
echo "DEBUG: TARGET_BRANCH='${TARGET_BRANCH}'"
# Sanity check
if [ -z "${STAGE_USER}" ] || [ -z "${STAGE_HOST}" ] || [ -z "${DEPLOY_PATH}" ] || [ -z "${TARGET_BRANCH}" ]; then
echo "::error::Missing required vars (STAGE_USER/STAGE_HOST/DEPLOY_PATH/TARGET_BRANCH)."
exit 1
fi
ssh-keyscan -H "${STAGE_HOST}" >> ~/.ssh/known_hosts
ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p '${DEPLOY_PATH}'"
# .env 파일 생성
cat <<'EOF' > .env
APP_ENV=stage
BACKEND_LOG_LEVEL=debug
CLIENT_LOG_DEBUG=true
WORKS_ADMIN_API_BASE_URL=${{ vars.WORKS_ADMIN_API_BASE_URL }}
WORKS_ADMIN_OAUTH_TOKEN_URL=${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}
TZ=Asia/Seoul
IDP_PROVIDER=ory
# DB & Clickhouse
DB_PORT=${{ vars.DB_PORT }}
CLICKHOUSE_PORT_HTTP=${{ vars.CLICKHOUSE_PORT_HTTP }}
CLICKHOUSE_PORT_NATIVE=${{ vars.CLICKHOUSE_PORT_NATIVE }}
CLICKHOUSE_HOST=${{ vars.CLICKHOUSE_HOST }}
CLICKHOUSE_USER=${{ vars.CLICKHOUSE_USER }}
CLICKHOUSE_PASSWORD=${{ secrets.CLICKHOUSE_PASSWORD }}
BACKEND_PORT=${{ vars.BACKEND_PORT }}
ADMINFRONT_PORT=${{ vars.ADMINFRONT_PORT }}
DEVFRONT_PORT=${{ vars.DEVFRONT_PORT }}
ORGFRONT_PORT=${{ vars.ORGFRONT_PORT }}
USERFRONT_PORT=${{ vars.USERFRONT_PORT }}
OATHKEEPER_API_URL=${{ vars.OATHKEEPER_API_URL }}
DB_USER=${{ vars.DB_USER }}
DB_PASSWORD=${{ secrets.STG_DB_PASSWORD }}
DB_NAME=${{ vars.DB_NAME }}
COOKIE_SECRET=${{ secrets.STG_COOKIE_SECRET }}
JWT_SECRET=${{ secrets.STG_JWT_SECRET }}
REDIS_ADDR=${{ vars.REDIS_ADDR }}
CORS_ALLOWED_ORIGINS=${{ vars.CORS_ALLOWED_ORIGINS }}
AUDIT_WORKER_COUNT=5
AUDIT_QUEUE_SIZE=2000
PROFILE_CACHE_TTL=${{ vars.PROFILE_CACHE_TTL }}
ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=${{ vars.ORGFRONT_ORGCHART_CACHE_TTL_SECONDS }}
NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }}
NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }}
NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }}
NAVER_SENDER_PHONE_NUMBER=${{ vars.NAVER_SENDER_PHONE_NUMBER }}
AWS_REGION=${{ vars.AWS_REGION }}
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 }}
ADMIN_EMAIL=${{ vars.ADMIN_EMAIL }}
ADMIN_PASSWORD=${{ secrets.STG_ADMIN_PASSWORD }}
USERFRONT_URL=${{ vars.USERFRONT_URL }}
ADMINFRONT_URL=${{ vars.ADMINFRONT_URL }}
DEVFRONT_URL=${{ vars.DEVFRONT_URL }}
ORGFRONT_URL=${{ vars.ORGFRONT_URL }}
BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }}
BACKEND_URL=${{ vars.BACKEND_URL }}
OATHKEEPER_PUBLIC_URL=${{ vars.OATHKEEPER_PUBLIC_URL }}
ORY_POSTGRES_TAG=${{ vars.ORY_POSTGRES_TAG }}
ORY_POSTGRES_USER=${{ vars.ORY_POSTGRES_USER }}
ORY_POSTGRES_PASSWORD=${{ secrets.STG_ORY_POSTGRES_PASSWORD }}
ORY_POSTGRES_DB=${{ vars.ORY_POSTGRES_DB }}
KRATOS_DB=${{ vars.KRATOS_DB }}
HYDRA_DB=${{ vars.HYDRA_DB }}
KETO_DB=${{ vars.KETO_DB }}
KRATOS_VERSION=${{ vars.KRATOS_VERSION }}
KRATOS_UI_NODE_VERSION=${{ vars.KRATOS_UI_NODE_VERSION }}
HYDRA_VERSION=${{ vars.HYDRA_VERSION }}
KETO_VERSION=${{ vars.KETO_VERSION }}
ORY_SDK_URL=${{ vars.ORY_SDK_URL }}
KRATOS_PUBLIC_URL=${{ vars.KRATOS_PUBLIC_URL }}
KRATOS_ADMIN_URL=${{ vars.KRATOS_ADMIN_URL }}
KRATOS_BROWSER_URL=${{ vars.KRATOS_BROWSER_URL }}
KRATOS_UI_URL=${{ vars.KRATOS_UI_URL }}
HYDRA_ADMIN_URL=${{ vars.HYDRA_ADMIN_URL }}
HYDRA_PUBLIC_URL=${{ vars.HYDRA_PUBLIC_URL }}
JWKS_URL=${{ vars.JWKS_URL }}
OATHKEEPER_VERSION=${{ vars.OATHKEEPER_VERSION }}
OATHKEEPER_UID=${{ vars.OATHKEEPER_UID }}
OATHKEEPER_GID=${{ vars.OATHKEEPER_GID }}
OATHKEEPER_HEALTH_URL=${{ vars.OATHKEEPER_HEALTH_URL }}
OATHKEEPER_HEALTH_INTERVAL_SECONDS=${{ vars.OATHKEEPER_HEALTH_INTERVAL_SECONDS }}
OATHKEEPER_HEALTH_TIMEOUT_SECONDS=${{ vars.OATHKEEPER_HEALTH_TIMEOUT_SECONDS }}
OATHKEEPER_HEALTH_ENABLED=${{ vars.OATHKEEPER_HEALTH_ENABLED }}
CSRF_COOKIE_NAME=${{ vars.CSRF_COOKIE_NAME }}
CSRF_COOKIE_SECRET=${{ secrets.STG_CSRF_COOKIE_SECRET }}
# Frontend/Ory URL configs for Staging
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 }}
KRATOS_ALLOWED_RETURN_URLS_JSON=${{ vars.KRATOS_ALLOWED_RETURN_URLS_JSON }}
KRATOS_ALLOWED_RETURN_URLS_EXTRA=${{ vars.KRATOS_ALLOWED_RETURN_URLS_EXTRA }}
# OATHKEEPER_INTROSPECT_CLIENT_ID=${{ vars.OATHKEEPER_INTROSPECT_CLIENT_ID }}
# OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }}
# Monitoring & Alerts
SMS_WEBHOOK_PORT=${{ vars.SMS_WEBHOOK_PORT || '8080' }}
MONITOR_RECIPIENT_PHONES=${{ vars.MONITOR_RECIPIENT_PHONES || '01012345678,01098765432' }}
LOKI_URL=${{ vars.LOKI_URL || 'http://loki:3100/loki/api/v1/push' }}
EOF
if ! grep -Eq "^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.+" .env; then
sed -i "s/^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.*/ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=3600/" .env
fi
# 코드 업데이트 (Git)
ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p '${DEPLOY_PATH}' && cd '${DEPLOY_PATH}' && \
if [ ! -d .git ]; then
git init
git remote add origin ssh://git@172.16.10.175:222/baron/baron-sso.git
else
git remote set-url origin ssh://git@172.16.10.175:222/baron/baron-sso.git
fi
git fetch --depth 1 origin '${TARGET_BRANCH}' && \
git reset --hard FETCH_HEAD && \
git clean -fd && \
git checkout -B '${TARGET_BRANCH}' FETCH_HEAD"
# .env 파일 복사
scp .env "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/"
# 배포 실행
ssh "${STAGE_USER}@${STAGE_HOST}" "DEPLOY_PATH='${DEPLOY_PATH}' bash -s" <<'EOSSH'
set -euo pipefail
cd "${DEPLOY_PATH}"
set -a; . ./.env; set +a;
# 네트워크 생성
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
# Ory 컨테이너가 직접 읽는 설정은 env 기반으로 완성한 뒤 mount합니다.
bash scripts/render_ory_config.sh
chmod -R 777 config/.generated/ory || true
cp docker/staging_pull_compose.template.yaml staging_pull_compose.yaml
docker compose -f staging_pull_compose.yaml pull
# 코드 변경 반영을 위해 build 수행 (userfront nginx.conf 등)
docker compose -f staging_pull_compose.yaml build --pull
docker compose -f staging_pull_compose.yaml up -d --remove-orphans --renew-anon-volumes
docker compose -f staging_pull_compose.yaml up -d --force-recreate kratos hydra keto oathkeeper
docker compose -f staging_pull_compose.yaml up -d --force-recreate ory_stack_check
docker compose -f staging_pull_compose.yaml up -d init-rp
# 배포 후 상태 확인 (실패 시 로그 출력을 위함)
sleep 10
check_container_http() {
name="$1"
port="$2"
max="${FRONTEND_HEALTH_MAX_ATTEMPTS:-60}"
i=1
while [ "${i}" -le "${max}" ]; do
if docker exec "${name}" sh -c "if command -v wget >/dev/null 2>&1; then wget -qO- 'http://127.0.0.1:${port}/' >/dev/null; elif command -v node >/dev/null 2>&1; then node -e \"fetch('http://127.0.0.1:${port}/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"; else exit 127; fi" >/dev/null 2>&1; then
echo "Frontend ready: ${name}:${port}"
return 0
fi
echo "Waiting for frontend: ${name}:${port} (${i}/${max})"
i=$((i + 1))
sleep 2
done
echo "ERROR: frontend not ready: ${name}:${port}" >&2
docker logs "${name}" --tail 200 >&2 || true
return 1
}
check_container_url() {
name="$1"
url="$2"
max="${FRONTEND_HEALTH_MAX_ATTEMPTS:-60}"
i=1
while [ "${i}" -le "${max}" ]; do
if docker exec "${name}" sh -c "if command -v wget >/dev/null 2>&1; then wget -qO- '${url}' >/dev/null; elif command -v node >/dev/null 2>&1; then node -e \"fetch('${url}').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\"; else exit 127; fi" >/dev/null 2>&1; then
echo "Container URL ready: ${name} ${url}"
return 0
fi
echo "Waiting for container URL: ${name} ${url} (${i}/${max})"
i=$((i + 1))
sleep 2
done
echo "ERROR: container URL not ready: ${name} ${url}" >&2
docker logs "${name}" --tail 200 >&2 || true
return 1
}
check_container_url baron_backend http://127.0.0.1:3000/health
check_container_http baron_userfront 5000
check_container_http baron_gateway 5000
check_container_http baron_adminfront 5173
check_container_http baron_devfront 5173
check_container_http baron_orgfront 5175
echo "===== INIT-RP LOGS ====="
docker compose -f staging_pull_compose.yaml logs init-rp || true
echo "========================"
kratos_migrate_cid="$(docker compose -f staging_pull_compose.yaml ps -q kratos-migrate || true)"
if [ -n "${kratos_migrate_cid}" ]; then
if [ "$(docker inspect -f '{{.State.ExitCode}}' "${kratos_migrate_cid}")" -ne 0 ]; then
echo 'Kratos Migrate Failed. Logs:'
docker logs "${kratos_migrate_cid}"
exit 1
fi
else
echo "WARN: kratos-migrate container not found; skipping exit-code check."
fi
EOSSH

View File

@@ -0,0 +1,238 @@
name: Release Baron SSO to Staging
on:
workflow_dispatch:
inputs:
rc_version_tag:
description: "The version tag to deploy to staging (e.g., v1.2601.1-RC1)"
required: true
type: string
jobs:
deploy-staging:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.STAGE_SSH_PRIVATE_KEY }}
- name: Deploy to Staging
env:
IMAGE_TAG: ${{ github.event.inputs.rc_version_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
# Staging-specific variables
DEPLOY_PATH: ${{ vars.STAGE_DEPLOY_PATH }}
STAGE_HOST: ${{ vars.STAGE_HOST }}
STAGE_USER: ${{ vars.STAGE_USER }}
HARBOR_ENDPOINT: ${{ vars.HARBOR_ENDPOINT }}
HARBOR_ROBOT_ACCOUNT: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
HARBOR_ROBOT_KEY: ${{ secrets.HARBOR_ROBOT_KEY }}
run: |
set -euo pipefail
echo "DEBUG: STAGE_USER='${STAGE_USER}'"
echo "DEBUG: STAGE_HOST='${STAGE_HOST}'"
echo "DEBUG: DEPLOY_PATH='${DEPLOY_PATH}'"
# Sanity check
if [ -z "${STAGE_USER}" ] || [ -z "${STAGE_HOST}" ] || [ -z "${DEPLOY_PATH}" ]; then
echo "::error::Missing required vars (STAGE_USER/STAGE_HOST/DEPLOY_PATH)."
exit 1
fi
ssh-keyscan -H "${STAGE_HOST}" >> ~/.ssh/known_hosts
ssh "${STAGE_USER}@${STAGE_HOST}" "mkdir -p '${DEPLOY_PATH}'"
# .env 파일 생성
cat <<'EOF' > .env
APP_ENV=stage
BACKEND_LOG_LEVEL=debug
CLIENT_LOG_DEBUG=true
WORKS_ADMIN_API_BASE_URL=${{ vars.WORKS_ADMIN_API_BASE_URL }}
WORKS_ADMIN_OAUTH_TOKEN_URL=${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}
TZ=Asia/Seoul
IDP_PROVIDER=ory
# DB & Clickhouse
DB_PORT=${{ vars.DB_PORT }}
CLICKHOUSE_PORT_HTTP=${{ vars.CLICKHOUSE_PORT_HTTP }}
CLICKHOUSE_PORT_NATIVE=${{ vars.CLICKHOUSE_PORT_NATIVE }}
CLICKHOUSE_HOST=${{ vars.CLICKHOUSE_HOST }}
CLICKHOUSE_USER=${{ vars.CLICKHOUSE_USER }}
CLICKHOUSE_PASSWORD=${{ secrets.CLICKHOUSE_PASSWORD }}
BACKEND_PORT=${{ vars.BACKEND_PORT }}
ADMINFRONT_PORT=${{ vars.ADMINFRONT_PORT }}
DEVFRONT_PORT=${{ vars.DEVFRONT_PORT }}
ORGFRONT_PORT=${{ vars.ORGFRONT_PORT }}
USERFRONT_PORT=${{ vars.USERFRONT_PORT }}
OATHKEEPER_API_URL=${{ vars.OATHKEEPER_API_URL }}
DB_USER=${{ vars.DB_USER }}
DB_PASSWORD=${{ secrets.STG_DB_PASSWORD }}
DB_NAME=${{ vars.DB_NAME }}
COOKIE_SECRET=${{ secrets.STG_COOKIE_SECRET }}
JWT_SECRET=${{ secrets.STG_JWT_SECRET }}
REDIS_ADDR=${{ vars.REDIS_ADDR }}
CORS_ALLOWED_ORIGINS=${{ vars.CORS_ALLOWED_ORIGINS }}
AUDIT_WORKER_COUNT=5
AUDIT_QUEUE_SIZE=2000
PROFILE_CACHE_TTL=${{ vars.PROFILE_CACHE_TTL }}
ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=${{ vars.ORGFRONT_ORGCHART_CACHE_TTL_SECONDS }}
NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }}
NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }}
NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }}
NAVER_SENDER_PHONE_NUMBER=${{ vars.NAVER_SENDER_PHONE_NUMBER }}
AWS_REGION=${{ vars.AWS_REGION }}
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 }}
ADMIN_EMAIL=${{ vars.ADMIN_EMAIL }}
ADMIN_PASSWORD=${{ secrets.STG_ADMIN_PASSWORD }}
USERFRONT_URL=${{ vars.USERFRONT_URL }}
ORGFRONT_URL=${{ vars.ORGFRONT_URL }}
BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }}
BACKEND_URL=${{ vars.BACKEND_URL }}
OATHKEEPER_PUBLIC_URL=${{ vars.OATHKEEPER_PUBLIC_URL }}
ORY_POSTGRES_TAG=${{ vars.ORY_POSTGRES_TAG }}
ORY_POSTGRES_USER=${{ vars.ORY_POSTGRES_USER }}
ORY_POSTGRES_PASSWORD=${{ secrets.STG_ORY_POSTGRES_PASSWORD }}
ORY_POSTGRES_DB=${{ vars.ORY_POSTGRES_DB }}
KRATOS_DB=${{ vars.KRATOS_DB }}
HYDRA_DB=${{ vars.HYDRA_DB }}
KETO_DB=${{ vars.KETO_DB }}
KRATOS_VERSION=${{ vars.KRATOS_VERSION }}
KRATOS_UI_NODE_VERSION=${{ vars.KRATOS_UI_NODE_VERSION }}
HYDRA_VERSION=${{ vars.HYDRA_VERSION }}
KETO_VERSION=${{ vars.KETO_VERSION }}
ORY_SDK_URL=${{ vars.ORY_SDK_URL }}
KRATOS_PUBLIC_URL=${{ vars.KRATOS_PUBLIC_URL }}
KRATOS_ADMIN_URL=${{ vars.KRATOS_ADMIN_URL }}
KRATOS_BROWSER_URL=${{ vars.KRATOS_BROWSER_URL }}
KRATOS_UI_URL=${{ vars.KRATOS_UI_URL }}
HYDRA_ADMIN_URL=${{ vars.HYDRA_ADMIN_URL }}
HYDRA_PUBLIC_URL=${{ vars.HYDRA_PUBLIC_URL }}
JWKS_URL=${{ vars.JWKS_URL }}
OATHKEEPER_VERSION=${{ vars.OATHKEEPER_VERSION }}
OATHKEEPER_UID=${{ vars.OATHKEEPER_UID }}
OATHKEEPER_GID=${{ vars.OATHKEEPER_GID }}
OATHKEEPER_HEALTH_URL=${{ vars.OATHKEEPER_HEALTH_URL }}
OATHKEEPER_HEALTH_INTERVAL_SECONDS=${{ vars.OATHKEEPER_HEALTH_INTERVAL_SECONDS }}
OATHKEEPER_HEALTH_TIMEOUT_SECONDS=${{ vars.OATHKEEPER_HEALTH_TIMEOUT_SECONDS }}
OATHKEEPER_HEALTH_ENABLED=${{ vars.OATHKEEPER_HEALTH_ENABLED }}
CSRF_COOKIE_NAME=${{ vars.CSRF_COOKIE_NAME }}
CSRF_COOKIE_SECRET=${{ secrets.STG_CSRF_COOKIE_SECRET }}
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 }}
# OATHKEEPER_INTROSPECT_CLIENT_ID=${{ vars.OATHKEEPER_INTROSPECT_CLIENT_ID }}
# OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }}
EOF
if ! grep -Eq "^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.+" .env; then
sed -i "s/^ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=.*/ORGFRONT_ORGCHART_CACHE_TTL_SECONDS=3600/" .env
fi
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
ORGFRONT_ORGCHART_CACHE_TTL_SECONDS
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/"
if [ -d "docker/init-metadata" ]; then
scp -r docker/init-metadata "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/docker/"
fi
if [ -d "gateway" ]; then
scp -r gateway "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/"
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 compose.ory.yaml "${STAGE_USER}@${STAGE_HOST}:${DEPLOY_PATH}/compose.ory.yml"
# 배포 실행
echo "${HARBOR_ROBOT_KEY}" | ssh "${STAGE_USER}@${STAGE_HOST}" \
"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}'; \
cd \"\${DEPLOY_PATH}\"; \
docker login \"\${HARBOR_ENDPOINT}\" -u \"\${HARBOR_ROBOT_ACCOUNT}\" --password-stdin; \
set -a; . ./.env; set +a; \
# 네트워크 생성
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; \
# [중요] 설정 파일 권한 문제 해결 (Ory 이미지는 root가 아닌 사용자로 실행됨)
chmod -R 777 docker/ory
docker compose -f compose.infra.yml -f compose.ory.yml -f docker-compose.yml pull; \
# [주의] DB 초기화 스크립트는 '새로운 볼륨'에서만 실행됨.
# DB 초기화 문제를 확실히 해결하기 위해 기존 볼륨을 날리고 다시 띄움 (데이터 삭제됨 주의)
# 스테이징이므로 초기화 진행. 데이터 보존이 필요하면 이 줄 제거하고 수동으로 DB 만들어야 함.
docker compose -f compose.infra.yml -f compose.ory.yml -f docker-compose.yml down -v || true
docker compose -f compose.infra.yml -f compose.ory.yml -f docker-compose.yml up -d --remove-orphans; \
# 배포 후 상태 확인 (실패 시 로그 출력을 위함)
sleep 10; \
if [ \$(docker inspect -f '{{.State.ExitCode}}' baron-sso-staging-kratos-migrate-1) -ne 0 ]; then \
echo 'Kratos Migrate Failed. Logs:'; \
docker logs baron-sso-staging-kratos-migrate-1; \
exit 1; \
fi"

View File

@@ -0,0 +1,273 @@
name: Userfront E2E Full Nightly
on:
schedule:
- cron: "0 18 * * *"
workflow_dispatch:
permissions:
contents: write
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
cache-dependency-path: backend/go.sum
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Run common lint checks
run: |
make code-check-lint
full-test-policy:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.policy.outputs.should_run }}
reason: ${{ steps.policy.outputs.reason }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Decide whether full E2E is needed
id: policy
run: |
set -euo pipefail
target_sha="${GITHUB_SHA}"
should_run="true"
reason="manual-dispatch"
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
reason="missing-full-result"
git fetch origin "+refs/heads/badges:refs/remotes/origin/badges" || true
if git show-ref --verify --quiet refs/remotes/origin/badges && \
git cat-file -e "refs/remotes/origin/badges:dev/${target_sha}/badges.json" 2>/dev/null; then
full_message="$(
git show "refs/remotes/origin/badges:dev/${target_sha}/badges.json" |
node -e "let input=''; process.stdin.on('data', c => input += c); process.stdin.on('end', () => { const data = JSON.parse(input); const keys = ['userfront-chrome', 'userfront-firefox', 'userfront-safari']; const messages = keys.map((key) => data.badges?.[key]?.message || 'unknown'); process.stdout.write(messages.join(',')); });"
)"
if [ -n "${full_message}" ] && ! printf '%s' "${full_message}" | grep -q "unknown"; then
should_run="false"
reason="full-result-exists:${full_message}"
fi
fi
fi
echo "should_run=${should_run}" >> "$GITHUB_OUTPUT"
echo "reason=${reason}" >> "$GITHUB_OUTPUT"
echo "target_sha=${target_sha}"
echo "should_run=${should_run}"
echo "reason=${reason}"
userfront-e2e-full:
needs:
- lint
- full-test-policy
if: ${{ needs.lint.result == 'success' && needs.full-test-policy.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 80
outputs:
chromium_desktop: ${{ steps.full-results.outputs.chromium_desktop }}
chromium_mobile: ${{ steps.full-results.outputs.chromium_mobile }}
firefox_desktop: ${{ steps.full-results.outputs.firefox_desktop }}
firefox_mobile: ${{ steps.full-results.outputs.firefox_mobile }}
webkit_desktop: ${{ steps.full-results.outputs.webkit_desktop }}
webkit_mobile: ${{ steps.full-results.outputs.webkit_mobile }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "npm"
cache-dependency-path: userfront-e2e/package-lock.json
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Sync userfront locales
run: |
/bin/sh ./scripts/sync_userfront_locales.sh
- name: Install userfront-e2e dependencies
run: |
cd userfront-e2e
npm ci
- name: Build userfront WASM
run: |
cd userfront
rm -rf build/web
flutter build web --wasm --release
cd ..
node userfront/scripts/optimize-web-build.mjs userfront/build/web
- name: Provision full browser matrix
run: |
cd userfront-e2e
npx playwright install --with-deps
- name: Run full userfront-e2e tests
id: full-results
run: |
mkdir -p reports
cd userfront-e2e
workers="${PLAYWRIGHT_WORKERS:-4}"
case "$workers" in
''|*[!0-9]*|0) workers=4 ;;
esac
any_failure=0
run_project() {
output_name="$1"
project_name="$2"
log_path="../reports/userfront-e2e-full-${project_name}.log"
set +e
echo "[userfront-e2e-full] PLAYWRIGHT_WORKERS=${workers} npx playwright test --project=${project_name}" | tee "$log_path"
PLAYWRIGHT_WORKERS="$workers" npx playwright test --project="$project_name" --reporter=list 2>&1 | tee -a "$log_path"
exit_code=${PIPESTATUS[0]}
set -e
if [ "$exit_code" -eq 0 ]; then
result="success"
else
result="failure"
any_failure=1
fi
echo "${output_name}=${result}" >> "$GITHUB_OUTPUT"
}
run_project chromium_desktop chromium-desktop
run_project chromium_mobile chromium-mobile-webapp
run_project firefox_desktop firefox-desktop
echo "firefox_mobile=skipped" >> "$GITHUB_OUTPUT"
run_project webkit_desktop webkit-desktop
run_project webkit_mobile webkit-mobile-webapp
exit "$any_failure"
- name: Upload userfront-e2e full artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v3
continue-on-error: true
with:
name: userfront-e2e-full-report
path: |
reports/userfront-e2e-full-*.log
userfront-e2e/playwright-report
userfront-e2e/test-results
if-no-files-found: ignore
badge-updater:
needs:
- lint
- full-test-policy
- userfront-e2e-full
if: ${{ always() && needs.lint.result == 'success' && needs.full-test-policy.outputs.should_run == 'true' && github.ref == 'refs/heads/dev' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Restore published badge state
run: |
git fetch origin "+refs/heads/badges:refs/remotes/origin/badges" || true
if git show-ref --verify --quiet refs/remotes/origin/badges && \
git cat-file -e refs/remotes/origin/badges:latest/badges.json 2>/dev/null; then
mkdir -p docs/badges
git archive --format=tar refs/remotes/origin/badges latest | tar -x
cp latest/* docs/badges/
rm -rf latest
else
echo "No published badge state found."
fi
- name: Update full E2E badge files
env:
USERFRONT_E2E_RESULT: ${{ needs.userfront-e2e-full.result }}
USERFRONT_E2E_FULL: "true"
USERFRONT_E2E_CHROMIUM_DESKTOP_RESULT: ${{ needs.userfront-e2e-full.outputs.chromium_desktop }}
USERFRONT_E2E_CHROMIUM_MOBILE_RESULT: ${{ needs.userfront-e2e-full.outputs.chromium_mobile }}
USERFRONT_E2E_FIREFOX_DESKTOP_RESULT: ${{ needs.userfront-e2e-full.outputs.firefox_desktop }}
USERFRONT_E2E_FIREFOX_MOBILE_RESULT: ${{ needs.userfront-e2e-full.outputs.firefox_mobile }}
USERFRONT_E2E_WEBKIT_DESKTOP_RESULT: ${{ needs.userfront-e2e-full.outputs.webkit_desktop }}
USERFRONT_E2E_WEBKIT_MOBILE_RESULT: ${{ needs.userfront-e2e-full.outputs.webkit_mobile }}
BADGE_UPDATE_CODE_CHECK: "false"
BADGE_SOURCE_BRANCH: dev
BADGE_SOURCE_SHA: ${{ github.sha }}
run: |
node scripts/update_code_check_badges.mjs
cat docs/badges/badges.json
- name: Publish full E2E badge assets
run: |
if [ -z "$(git status --porcelain docs/badges)" ]; then
echo "No badge changes."
exit 0
fi
BADGE_BRANCH=badges
BADGE_WORKTREE="$(mktemp -d)"
BADGE_LATEST_DIR="${BADGE_WORKTREE}/latest"
BADGE_SHA_DIR="${BADGE_WORKTREE}/dev/${GITHUB_SHA}"
trap 'rm -rf "${BADGE_WORKTREE}"' EXIT
git config user.name "gitea-actions"
git config user.email "gitea-actions@hmac.kr"
git fetch origin "+refs/heads/${BADGE_BRANCH}:refs/remotes/origin/${BADGE_BRANCH}" || true
if git show-ref --verify --quiet "refs/remotes/origin/${BADGE_BRANCH}"; then
git worktree add --detach "${BADGE_WORKTREE}" "origin/${BADGE_BRANCH}"
else
git worktree add --detach "${BADGE_WORKTREE}"
git -C "${BADGE_WORKTREE}" checkout --orphan "${BADGE_BRANCH}"
git -C "${BADGE_WORKTREE}" rm -rf . || true
fi
find "${BADGE_WORKTREE}" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
mkdir -p "${BADGE_LATEST_DIR}" "${BADGE_SHA_DIR}"
cp docs/badges/*.svg "${BADGE_LATEST_DIR}/"
cp docs/badges/badges.json "${BADGE_LATEST_DIR}/badges.json"
cp docs/badges/*.svg "${BADGE_SHA_DIR}/"
cp docs/badges/badges.json "${BADGE_SHA_DIR}/badges.json"
git -C "${BADGE_WORKTREE}" add .
if [ -z "$(git -C "${BADGE_WORKTREE}" status --porcelain)" ]; then
echo "No published badge changes."
exit 0
fi
git -C "${BADGE_WORKTREE}" commit -m "chore: publish userfront e2e full badge [skip ci]"
git -C "${BADGE_WORKTREE}" push origin HEAD:${BADGE_BRANCH}