forked from baron/baron-sso
병합 후 flow업데이트
This commit is contained in:
241
.gitea/workflows/image_publish.yml
Normal file
241
.gitea/workflows/image_publish.yml
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
name: Publish Baron SSO Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version_prefix:
|
||||||
|
description: "stage/prod 공용 이미지 태그 prefix (예: v1.2606, 최종 태그는 v1.2606.<커밋해시4자리>)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-images:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout dev branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: dev
|
||||||
|
|
||||||
|
- name: Validate publish inputs
|
||||||
|
env:
|
||||||
|
VERSION_PREFIX: ${{ github.event.inputs.version_prefix }}
|
||||||
|
ADMINFRONT_URL: ${{ vars.ADMINFRONT_URL }}
|
||||||
|
DEVFRONT_URL: ${{ vars.DEVFRONT_URL }}
|
||||||
|
ORGFRONT_URL: ${{ vars.ORGFRONT_URL }}
|
||||||
|
VITE_OIDC_AUTHORITY: ${{ vars.VITE_OIDC_AUTHORITY }}
|
||||||
|
WORKS_DRIVE_SHARED_DRIVE_ID: ${{ vars.WORKS_DRIVE_SHARED_DRIVE_ID }}
|
||||||
|
WORKS_DRIVE_ACCESS_TOKEN_INPUT: ${{ secrets.WORKS_DRIVE_ACCESS_TOKEN }}
|
||||||
|
WORKS_DRIVE_ACCESS_TOKEN_FILE: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_FILE }}
|
||||||
|
WORKS_DRIVE_ACCESS_TOKEN_CMD: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_CMD }}
|
||||||
|
WORKS_DRIVE_OAUTH_CLIENT_ID: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_ID }}
|
||||||
|
WORKS_DRIVE_OAUTH_CLIENT_SECRET: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_SECRET }}
|
||||||
|
WORKS_DRIVE_OAUTH_REFRESH_TOKEN: ${{ secrets.WORKS_DRIVE_OAUTH_REFRESH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! printf '%s' "${VERSION_PREFIX}" | grep -Eq '^v[0-9]+\.[0-9]{4}$'; then
|
||||||
|
echo "::error::version_prefix must look like vX.YYMM (got: ${VERSION_PREFIX})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
required_values="
|
||||||
|
ADMINFRONT_URL DEVFRONT_URL ORGFRONT_URL VITE_OIDC_AUTHORITY WORKS_DRIVE_SHARED_DRIVE_ID
|
||||||
|
"
|
||||||
|
for key in ${required_values}; do
|
||||||
|
if [ -z "${!key:-}" ]; then
|
||||||
|
echo "::error::Missing required publish value: ${key}. Check Gitea repo variables/secrets."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${WORKS_DRIVE_ACCESS_TOKEN_INPUT:-}" ] \
|
||||||
|
&& [ -z "${WORKS_DRIVE_ACCESS_TOKEN_FILE:-}" ] \
|
||||||
|
&& [ -z "${WORKS_DRIVE_ACCESS_TOKEN_CMD:-}" ] \
|
||||||
|
&& [ -z "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ]; then
|
||||||
|
echo "::error::Missing WORKS Drive access auth. Provide WORKS_DRIVE_ACCESS_TOKEN, WORKS_DRIVE_ACCESS_TOKEN_FILE, WORKS_DRIVE_ACCESS_TOKEN_CMD, or WORKS_DRIVE_OAUTH_REFRESH_TOKEN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${WORKS_DRIVE_ACCESS_TOKEN_INPUT:-}" ] \
|
||||||
|
&& [ -z "${WORKS_DRIVE_ACCESS_TOKEN_FILE:-}" ] \
|
||||||
|
&& [ -z "${WORKS_DRIVE_ACCESS_TOKEN_CMD:-}" ] \
|
||||||
|
&& [ -n "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ] \
|
||||||
|
&& { [ -z "${WORKS_DRIVE_OAUTH_CLIENT_ID:-}" ] || [ -z "${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}" ]; }; then
|
||||||
|
echo "::error::WORKS_DRIVE_OAUTH_CLIENT_ID and WORKS_DRIVE_OAUTH_CLIENT_SECRET are required when WORKS_DRIVE_OAUTH_REFRESH_TOKEN is the selected auth source."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compute commit-hash image tag
|
||||||
|
id: version
|
||||||
|
env:
|
||||||
|
VERSION_PREFIX: ${{ github.event.inputs.version_prefix }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
short_sha="$(git rev-parse --short=4 HEAD)"
|
||||||
|
if ! printf '%s' "${short_sha}" | grep -Eq '^[0-9a-f]{4}$'; then
|
||||||
|
echo "::error::commit hash suffix must be 4 lowercase hexadecimal characters (got: ${short_sha})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
image_tag="${VERSION_PREFIX}.${short_sha}"
|
||||||
|
echo "image_tag=${image_tag}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "Computed shared image tag: ${image_tag}"
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build backend image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./backend
|
||||||
|
file: ./backend/Dockerfile
|
||||||
|
load: true
|
||||||
|
tags: baron_sso/backend:${{ steps.version.outputs.image_tag }}
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
|
|
||||||
|
- name: Build userfront image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./userfront/Dockerfile
|
||||||
|
target: production
|
||||||
|
load: true
|
||||||
|
tags: baron_sso/userfront:${{ steps.version.outputs.image_tag }}
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
|
|
||||||
|
- name: Build adminfront image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./adminfront/Dockerfile
|
||||||
|
target: production
|
||||||
|
load: true
|
||||||
|
tags: baron_sso/adminfront:${{ steps.version.outputs.image_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 devfront image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./devfront/Dockerfile
|
||||||
|
target: production
|
||||||
|
load: true
|
||||||
|
tags: baron_sso/devfront:${{ steps.version.outputs.image_tag }}
|
||||||
|
build-args: |
|
||||||
|
VITE_DEVFRONT_PUBLIC_URL=${{ vars.DEVFRONT_URL }}
|
||||||
|
VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }}
|
||||||
|
VITE_OIDC_CLIENT_ID=devfront
|
||||||
|
ORGFRONT_URL=${{ vars.ORGFRONT_URL }}
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
|
|
||||||
|
- name: Build orgfront image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./orgfront/Dockerfile
|
||||||
|
target: production
|
||||||
|
load: true
|
||||||
|
tags: baron_sso/orgfront:${{ steps.version.outputs.image_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: Resolve WORKS Drive access token
|
||||||
|
env:
|
||||||
|
WORKS_DRIVE_ACCESS_TOKEN_INPUT: ${{ secrets.WORKS_DRIVE_ACCESS_TOKEN }}
|
||||||
|
WORKS_DRIVE_ACCESS_TOKEN_FILE: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_FILE }}
|
||||||
|
WORKS_DRIVE_ACCESS_TOKEN_CMD: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_CMD }}
|
||||||
|
WORKS_DRIVE_OAUTH_CLIENT_ID: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_ID }}
|
||||||
|
WORKS_DRIVE_OAUTH_CLIENT_SECRET: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_SECRET }}
|
||||||
|
WORKS_DRIVE_OAUTH_REFRESH_TOKEN: ${{ secrets.WORKS_DRIVE_OAUTH_REFRESH_TOKEN }}
|
||||||
|
WORKS_ADMIN_OAUTH_TOKEN_URL: ${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
access_token=""
|
||||||
|
rotated_refresh_token_file="${RUNNER_TEMP}/works-drive-rotated-refresh-token"
|
||||||
|
|
||||||
|
if [ -n "${WORKS_DRIVE_ACCESS_TOKEN_INPUT:-}" ]; then
|
||||||
|
access_token="${WORKS_DRIVE_ACCESS_TOKEN_INPUT}"
|
||||||
|
elif [ -n "${WORKS_DRIVE_ACCESS_TOKEN_FILE:-}" ]; then
|
||||||
|
access_token="$(sed -n '1p' "${WORKS_DRIVE_ACCESS_TOKEN_FILE}")"
|
||||||
|
elif [ -n "${WORKS_DRIVE_ACCESS_TOKEN_CMD:-}" ]; then
|
||||||
|
access_token="$(sh -c "${WORKS_DRIVE_ACCESS_TOKEN_CMD}")"
|
||||||
|
else
|
||||||
|
token_url="${WORKS_ADMIN_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}"
|
||||||
|
response="$(curl -sS -w $'\n%{http_code}' -X POST \
|
||||||
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
--data-urlencode "grant_type=refresh_token" \
|
||||||
|
--data-urlencode "refresh_token=${WORKS_DRIVE_OAUTH_REFRESH_TOKEN}" \
|
||||||
|
--data-urlencode "client_id=${WORKS_DRIVE_OAUTH_CLIENT_ID}" \
|
||||||
|
--data-urlencode "client_secret=${WORKS_DRIVE_OAUTH_CLIENT_SECRET}" \
|
||||||
|
"${token_url}")"
|
||||||
|
http_status="$(tail -n 1 <<<"${response}")"
|
||||||
|
response_body="$(sed '$d' <<<"${response}")"
|
||||||
|
if [ "${http_status}" -lt 200 ] || [ "${http_status}" -ge 300 ]; then
|
||||||
|
echo "::error::WORKS Drive access token refresh failed with HTTP ${http_status}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
access_token="$(jq -er '.access_token' <<<"${response_body}")"
|
||||||
|
rotated_refresh_token="$(jq -r '.refresh_token // empty' <<<"${response_body}")"
|
||||||
|
if [ -n "${rotated_refresh_token}" ]; then
|
||||||
|
echo "::add-mask::${rotated_refresh_token}"
|
||||||
|
printf '%s\n' "${rotated_refresh_token}" >"${rotated_refresh_token_file}"
|
||||||
|
chmod 600 "${rotated_refresh_token_file}"
|
||||||
|
echo "WORKS_DRIVE_ROTATED_REFRESH_TOKEN_FILE=${rotated_refresh_token_file}" >>"${GITHUB_ENV}"
|
||||||
|
echo "::warning::WORKS returned a rotated refresh token. Persist it to the WORKS_DRIVE_OAUTH_REFRESH_TOKEN secret before old refresh tokens age out."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${access_token}" ]; then
|
||||||
|
echo "::error::WORKS Drive access token could not be resolved."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "::add-mask::${access_token}"
|
||||||
|
echo "WORKS_DRIVE_ACCESS_TOKEN=${access_token}" >>"${GITHUB_ENV}"
|
||||||
|
|
||||||
|
- name: Upload built images to WORKS Drive archive
|
||||||
|
env:
|
||||||
|
IMAGE_TAG: ${{ steps.version.outputs.image_tag }}
|
||||||
|
WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR: ${{ vars.WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR }}
|
||||||
|
WORKS_DRIVE_TARGET: sharedrive
|
||||||
|
WORKS_DRIVE_SHARED_DRIVE_ID: ${{ vars.WORKS_DRIVE_SHARED_DRIVE_ID }}
|
||||||
|
WORKS_DRIVE_PARENT_FILE_ID: ${{ vars.WORKS_DRIVE_PARENT_FILE_ID }}
|
||||||
|
WORKS_ADMIN_API_BASE_URL: ${{ vars.WORKS_ADMIN_API_BASE_URL }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:=docker-build-image}"
|
||||||
|
|
||||||
|
required_values="
|
||||||
|
IMAGE_TAG WORKS_DRIVE_SHARED_DRIVE_ID
|
||||||
|
"
|
||||||
|
for key in ${required_values}; do
|
||||||
|
if [ -z "${!key:-}" ]; then
|
||||||
|
echo "::error::Missing required WORKS image archive value: ${key}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for image in backend userfront adminfront devfront orgfront; do
|
||||||
|
image_ref="baron_sso/${image}:${IMAGE_TAG}"
|
||||||
|
DOCKER_IMAGE_REF="${image_ref}" \
|
||||||
|
WORKS_DOCKER_IMAGE_ARCHIVE_DIR="${RUNNER_TEMP}/baron-sso-docker-image-upload" \
|
||||||
|
scripts/docker-image/upload_works_drive.sh
|
||||||
|
done
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
name: Publish Baron SSO Production Images
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version_prefix:
|
|
||||||
description: "공용 저장소 이미지 태그 prefix (예: v1.2606, 최종 태그는 v1.2606.<커밋해시4자리>)"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-images:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout dev branch
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: dev
|
|
||||||
|
|
||||||
- name: Validate publish inputs
|
|
||||||
env:
|
|
||||||
VERSION_PREFIX: ${{ github.event.inputs.version_prefix }}
|
|
||||||
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
|
|
||||||
|
|
||||||
if ! printf '%s' "${VERSION_PREFIX}" | grep -Eq '^v[0-9]+\.[0-9]{4}$'; then
|
|
||||||
echo "::error::version_prefix must look like vX.YYMM (got: ${VERSION_PREFIX})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
required_values="
|
|
||||||
HARBOR_ENDPOINT HARBOR_HOSTNAME HARBOR_ROBOT_ACCOUNT HARBOR_ROBOT_KEY
|
|
||||||
ADMINFRONT_URL DEVFRONT_URL ORGFRONT_URL VITE_OIDC_AUTHORITY
|
|
||||||
"
|
|
||||||
for key in ${required_values}; do
|
|
||||||
if [ -z "${!key:-}" ]; then
|
|
||||||
echo "::error::Missing required publish value: ${key}. Check Gitea repo variables/secrets."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Compute commit-hash image tag
|
|
||||||
id: version
|
|
||||||
env:
|
|
||||||
VERSION_PREFIX: ${{ github.event.inputs.version_prefix }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
short_sha="$(git rev-parse --short=4 HEAD)"
|
|
||||||
if ! printf '%s' "${short_sha}" | grep -Eq '^[0-9a-f]{4}$'; then
|
|
||||||
echo "::error::commit hash suffix must be 4 lowercase hexadecimal characters (got: ${short_sha})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
image_tag="${VERSION_PREFIX}.${short_sha}"
|
|
||||||
echo "image_tag=${image_tag}" >> "${GITHUB_OUTPUT}"
|
|
||||||
echo "Computed production image tag: ${image_tag}"
|
|
||||||
|
|
||||||
- name: Login to shared registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.HARBOR_ENDPOINT }}
|
|
||||||
username: ${{ vars.HARBOR_ROBOT_ACCOUNT }}
|
|
||||||
password: ${{ secrets.HARBOR_ROBOT_KEY }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push backend production image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: ./backend
|
|
||||||
file: ./backend/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend:${{ steps.version.outputs.image_tag }}
|
|
||||||
provenance: false
|
|
||||||
sbom: false
|
|
||||||
|
|
||||||
- name: Build and push userfront production image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./userfront/Dockerfile
|
|
||||||
target: production
|
|
||||||
push: true
|
|
||||||
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront:${{ steps.version.outputs.image_tag }}
|
|
||||||
provenance: false
|
|
||||||
sbom: false
|
|
||||||
|
|
||||||
- name: Build and push adminfront production image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./adminfront/Dockerfile
|
|
||||||
target: production
|
|
||||||
push: true
|
|
||||||
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront:${{ steps.version.outputs.image_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 production image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./devfront/Dockerfile
|
|
||||||
target: production
|
|
||||||
push: true
|
|
||||||
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront:${{ steps.version.outputs.image_tag }}
|
|
||||||
build-args: |
|
|
||||||
VITE_DEVFRONT_PUBLIC_URL=${{ vars.DEVFRONT_URL }}
|
|
||||||
VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }}
|
|
||||||
VITE_OIDC_CLIENT_ID=devfront
|
|
||||||
ORGFRONT_URL=${{ vars.ORGFRONT_URL }}
|
|
||||||
provenance: false
|
|
||||||
sbom: false
|
|
||||||
|
|
||||||
- name: Build and push orgfront production image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./orgfront/Dockerfile
|
|
||||||
target: production
|
|
||||||
push: true
|
|
||||||
tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront:${{ steps.version.outputs.image_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: Upload pushed images to WORKS Drive archive
|
|
||||||
if: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_ARCHIVE_ENABLED == 'true' }}
|
|
||||||
env:
|
|
||||||
IMAGE_TAG: ${{ steps.version.outputs.image_tag }}
|
|
||||||
HARBOR_HOSTNAME: ${{ vars.HARBOR_HOSTNAME }}
|
|
||||||
WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR: ${{ vars.WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR }}
|
|
||||||
WORKS_DRIVE_TARGET: sharedrive
|
|
||||||
WORKS_DRIVE_SHARED_DRIVE_ID: ${{ vars.WORKS_DRIVE_SHARED_DRIVE_ID }}
|
|
||||||
WORKS_DRIVE_PARENT_FILE_ID: ${{ vars.WORKS_DRIVE_PARENT_FILE_ID }}
|
|
||||||
WORKS_DRIVE_OAUTH_CLIENT_ID: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_ID }}
|
|
||||||
WORKS_DRIVE_OAUTH_CLIENT_SECRET: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_SECRET }}
|
|
||||||
WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT }}
|
|
||||||
WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY }}
|
|
||||||
WORKS_DRIVE_OAUTH_REFRESH_TOKEN: ${{ secrets.WORKS_DRIVE_OAUTH_REFRESH_TOKEN }}
|
|
||||||
WORKS_ADMIN_API_BASE_URL: ${{ vars.WORKS_ADMIN_API_BASE_URL }}
|
|
||||||
WORKS_ADMIN_OAUTH_TOKEN_URL: ${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
: "${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:=docker-build-image}"
|
|
||||||
|
|
||||||
required_values="
|
|
||||||
IMAGE_TAG HARBOR_HOSTNAME WORKS_DRIVE_SHARED_DRIVE_ID
|
|
||||||
"
|
|
||||||
for key in ${required_values}; do
|
|
||||||
if [ -z "${!key:-}" ]; then
|
|
||||||
echo "::error::Missing required WORKS image archive value: ${key}."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for image in backend userfront adminfront devfront orgfront; do
|
|
||||||
image_ref="${HARBOR_HOSTNAME}/baron_sso/${image}:${IMAGE_TAG}"
|
|
||||||
docker pull "${image_ref}"
|
|
||||||
DOCKER_IMAGE_REF="${image_ref}" \
|
|
||||||
WORKS_DOCKER_IMAGE_ARCHIVE_DIR="${RUNNER_TEMP}/baron-sso-docker-image-upload" \
|
|
||||||
scripts/docker-image/upload_works_drive.sh
|
|
||||||
done
|
|
||||||
@@ -2,42 +2,47 @@
|
|||||||
|
|
||||||
## 목적
|
## 목적
|
||||||
|
|
||||||
WORKS Drive는 Docker Registry HTTP API v2 backend로 직접 사용하지 않는다. 대신 프로덕션 배포용 Docker 이미지를 `docker save` 결과물로 내보내고, zstd 압축 archive와 검증 파일을 WORKS Shared Drive에 보관하는 CLI 기반 보조 저장소로 사용한다.
|
WORKS Drive는 Docker Registry HTTP API v2 backend로 직접 사용하지 않는다. 대신 stage/production 공용 Docker 이미지를 `docker save` 결과물로 내보내고, zstd 압축 archive와 검증 파일을 WORKS Shared Drive에 보관하는 CLI 기반 이미지 산출물 저장소로 사용한다.
|
||||||
|
|
||||||
이 방식은 다음 상황을 목표로 한다.
|
이 방식은 다음 상황을 목표로 한다.
|
||||||
|
|
||||||
- Harbor 또는 공용 Registry 장애 시 수동 복구용 이미지 보관
|
- 공용 Registry 없이 WORKS Drive 접근 권한만으로 이미지 산출물 보관
|
||||||
- 작은 규모의 프로덕션 배포 이미지 이관
|
- 작은 규모의 stage/production 배포 이미지 이관
|
||||||
- `docker load` 기반 오프라인 배포
|
- `docker load` 기반 오프라인 배포
|
||||||
|
|
||||||
Harbor는 이 흐름의 1차 이미지 저장소다. Gitea Actions의 publish workflow가 `reg.hmac.kr/baron_sso/<service>:<image_tag>` 형태로 이미지를 push하고, staging/production deploy workflow는 같은 image tag를 Harbor에서 pull한다. WORKS Drive는 같은 이미지를 별도로 보관하는 복구용 archive이며, staging/prod가 평상시에 직접 pull하는 대상이 아니다.
|
Gitea Actions의 shared image publish workflow는 `baron_sso/<service>:<image_tag>` 형태의 로컬 이미지를 빌드한 뒤 WORKS Drive archive로 업로드한다. Harbor registry login/push/pull은 이 publish 흐름의 필수 조건이 아니다. staging/production은 같은 image tag 계약을 공유하며, WORKS Drive archive를 검증한 뒤 `docker load`로 배포 대상 호스트에 적재하는 흐름으로 확장한다.
|
||||||
|
|
||||||
## 현재 Gitea Actions 설정 상태
|
## 현재 Gitea Actions 설정 상태
|
||||||
|
|
||||||
2026-06-19 기준 repo Actions 설정에서 Harbor 변수/시크릿은 등록되어 있다.
|
2026-06-19 기준 Docker image archive 업로드 단계는 `.gitea/workflows/image_publish.yml`의 `Upload built images to WORKS Drive archive` step에서 실행된다. 이 workflow는 stage/production 공용 산출물을 만들며 `dev` branch의 commit hash 4자리로 immutable tag를 계산한다.
|
||||||
|
|
||||||
- `HARBOR_ENDPOINT=https://reg.hmac.kr`
|
업로드를 실행하려면 최소한 다음 값을 등록해야 한다.
|
||||||
- `HARBOR_HOSTNAME=reg.hmac.kr`
|
|
||||||
- `HARBOR_ROBOT_ACCOUNT=robot$namecard_sso`
|
|
||||||
- secret `HARBOR_ROBOT_KEY`
|
|
||||||
|
|
||||||
Docker image archive 업로드 단계는 `.gitea/workflows/production_image_publish.yml`의 `Upload pushed images to WORKS Drive archive` step에서 실행된다. 단, 이 step은 다음 조건을 만족할 때만 실행된다.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
if: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_ARCHIVE_ENABLED == 'true' }}
|
|
||||||
```
|
|
||||||
|
|
||||||
현재 repo Actions 설정에는 Docker image archive용 WORKS Drive 변수/시크릿이 등록되어 있지 않다. 업로드를 켜려면 최소한 다음 값을 등록해야 한다.
|
|
||||||
|
|
||||||
- variable `WORKS_DRIVE_DOCKER_IMAGE_ARCHIVE_ENABLED=true`
|
|
||||||
- variable `WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR=docker-build-image`
|
- variable `WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR=docker-build-image`
|
||||||
- variable `WORKS_DRIVE_SHARED_DRIVE_ID`
|
- variable `WORKS_DRIVE_SHARED_DRIVE_ID`
|
||||||
- 선택 variable `WORKS_DRIVE_PARENT_FILE_ID`
|
- 선택 variable `WORKS_DRIVE_PARENT_FILE_ID`
|
||||||
- secret `WORKS_DRIVE_OAUTH_CLIENT_ID`
|
- secret `WORKS_DRIVE_ACCESS_TOKEN`, 또는 variable `WORKS_DRIVE_ACCESS_TOKEN_FILE`, 또는 variable `WORKS_DRIVE_ACCESS_TOKEN_CMD`, 또는 refresh-token 방식의 secret `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`
|
||||||
- secret `WORKS_DRIVE_OAUTH_CLIENT_SECRET`
|
- refresh-token 방식을 쓸 경우 secret `WORKS_DRIVE_OAUTH_CLIENT_ID`, secret `WORKS_DRIVE_OAUTH_CLIENT_SECRET`
|
||||||
- secret `WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT`
|
|
||||||
- secret `WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY`
|
서비스 계정 JWT 방식은 upload script의 fallback으로 남아 있지만 shared image publish workflow의 기본 필수 인증값은 아니다.
|
||||||
- refresh token 방식을 쓸 경우 secret `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`
|
|
||||||
|
## WORKS Drive 토큰 운영
|
||||||
|
|
||||||
|
WORKS OAuth의 Access Token은 Developer Console 설정에 따라 1시간 또는 24시간 동안 유효하고, Refresh Token은 90일 동안 유효하다. 따라서 Gitea secret에 `WORKS_DRIVE_ACCESS_TOKEN`만 고정해 두는 방식은 publish workflow가 장시간 중단된 뒤 재실행될 때 실패할 수 있다.
|
||||||
|
|
||||||
|
`image_publish.yml`은 업로드 직전에 `Resolve WORKS Drive access token` step을 실행한다.
|
||||||
|
|
||||||
|
- `WORKS_DRIVE_ACCESS_TOKEN`이 있으면 이를 마스킹한 뒤 해당 workflow run 안에서만 사용한다.
|
||||||
|
- `WORKS_DRIVE_ACCESS_TOKEN_FILE` 또는 `WORKS_DRIVE_ACCESS_TOKEN_CMD`가 있으면 그 결과를 같은 방식으로 사용한다.
|
||||||
|
- 위 값이 없고 `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`이 있으면 `grant_type=refresh_token`으로 새 Access Token을 발급하고, 이후 다섯 개 이미지 업로드는 모두 이 Access Token을 공유한다.
|
||||||
|
|
||||||
|
Refresh Token Rotation이 켜져 있으면 WORKS가 refresh 응답에 새 Refresh Token을 포함할 수 있다. workflow는 이 값을 로그에 노출하지 않도록 마스킹하고 `${RUNNER_TEMP}/works-drive-rotated-refresh-token`에 `0600` 권한으로 캡처한다. 다만 Gitea repository secret을 자동 갱신하려면 별도의 secret 쓰기 권한이 있는 Gitea token과 secret update 절차가 필요하므로, 기본 publish workflow는 repository secret을 직접 변경하지 않는다.
|
||||||
|
|
||||||
|
운영 권장값은 다음 중 하나다.
|
||||||
|
|
||||||
|
- Refresh Token Rotation을 끄고 `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`으로 매 run마다 Access Token만 자동 발급한다.
|
||||||
|
- Rotation을 켠 경우 publish run에서 rotated refresh token 경고가 나오면 `WORKS_DRIVE_OAUTH_REFRESH_TOKEN` secret을 수동 갱신한다.
|
||||||
|
- secret 자동 갱신이 필요하면 Gitea secret write 전용 token을 별도 설계로 추가한다.
|
||||||
|
|
||||||
## 저장 구조
|
## 저장 구조
|
||||||
|
|
||||||
@@ -207,13 +212,13 @@ scripts/docker-image/verify_archive.sh \
|
|||||||
|
|
||||||
## Staging/Production 계약
|
## Staging/Production 계약
|
||||||
|
|
||||||
Action에서 `dev` 브랜치를 checkout한 뒤 한 번만 이미지를 빌드하고 immutable `image_tag`를 계산한다. staging과 production은 같은 image_tag를 입력받아 같은 registry image를 pull한다.
|
Action에서 `dev` 브랜치를 checkout한 뒤 한 번만 이미지를 빌드하고 immutable `image_tag`를 계산한다. staging과 production은 같은 image_tag를 입력받아 같은 image archive를 사용한다.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
dev branch -> publish image tag vX.YYMM.<commit4> -> staging deploy -> production deploy
|
dev branch -> publish image tag vX.YYMM.<commit4> -> staging deploy -> production deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
WORKS Drive archive도 Action에서 push된 이미지를 다시 pull한 뒤 `docker save`로 만든다. 따라서 WORKS archive, staging, production은 모두 같은 registry image tag를 기준으로 한다.
|
WORKS Drive archive는 Action에서 로컬로 빌드된 이미지를 `docker save`로 내보내 생성한다. 따라서 WORKS archive, staging, production은 모두 같은 immutable image tag를 기준으로 한다.
|
||||||
|
|
||||||
## 제한
|
## 제한
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
publish_workflow="$repo_root/.gitea/workflows/production_image_publish.yml"
|
publish_workflow="$repo_root/.gitea/workflows/image_publish.yml"
|
||||||
|
legacy_publish_workflow="$repo_root/.gitea/workflows/production_image_publish.yml"
|
||||||
staging_deploy_workflow="$repo_root/.gitea/workflows/staging_image_deploy.yml"
|
staging_deploy_workflow="$repo_root/.gitea/workflows/staging_image_deploy.yml"
|
||||||
deploy_workflow="$repo_root/.gitea/workflows/production_image_deploy.yml"
|
deploy_workflow="$repo_root/.gitea/workflows/production_image_deploy.yml"
|
||||||
image_compose="$repo_root/deploy/templates/docker-compose.images.yaml"
|
image_compose="$repo_root/deploy/templates/docker-compose.images.yaml"
|
||||||
@@ -14,15 +15,16 @@ fail() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
[[ -f "$publish_workflow" ]] || fail "production image publish workflow must exist."
|
[[ -f "$publish_workflow" ]] || fail "shared image publish workflow must exist."
|
||||||
|
[[ ! -f "$legacy_publish_workflow" ]] || fail "production-only image publish workflow must be renamed to image_publish.yml."
|
||||||
[[ -f "$staging_deploy_workflow" ]] || fail "staging image deploy workflow must exist."
|
[[ -f "$staging_deploy_workflow" ]] || fail "staging image deploy workflow must exist."
|
||||||
[[ -f "$deploy_workflow" ]] || fail "production image deploy workflow must exist."
|
[[ -f "$deploy_workflow" ]] || fail "production image deploy workflow must exist."
|
||||||
[[ -f "$image_compose" ]] || fail "image-based production compose template must exist."
|
[[ -f "$image_compose" ]] || fail "image-based production compose template must exist."
|
||||||
[[ -f "$bundle_script" ]] || fail "shared image deployment bundle script must exist."
|
[[ -f "$bundle_script" ]] || fail "shared image deployment bundle script must exist."
|
||||||
[[ -f "$remote_deploy_script" ]] || fail "shared image remote deploy script must exist."
|
[[ -f "$remote_deploy_script" ]] || fail "shared image remote deploy script must exist."
|
||||||
|
|
||||||
grep -Fq "name: Publish Baron SSO Production Images" "$publish_workflow" \
|
grep -Fq "name: Publish Baron SSO Images" "$publish_workflow" \
|
||||||
|| fail "publish workflow must have the expected name."
|
|| fail "publish workflow must have the shared stage/production name."
|
||||||
grep -Fq "workflow_dispatch:" "$publish_workflow" \
|
grep -Fq "workflow_dispatch:" "$publish_workflow" \
|
||||||
|| fail "publish workflow must be manually dispatchable."
|
|| fail "publish workflow must be manually dispatchable."
|
||||||
if grep -Fq "source_ref:" "$publish_workflow"; then
|
if grep -Fq "source_ref:" "$publish_workflow"; then
|
||||||
@@ -37,21 +39,40 @@ grep -Fq 'git rev-parse --short=4 HEAD' "$publish_workflow" \
|
|||||||
grep -Fq 'image_tag="${VERSION_PREFIX}.${short_sha}"' "$publish_workflow" \
|
grep -Fq 'image_tag="${VERSION_PREFIX}.${short_sha}"' "$publish_workflow" \
|
||||||
|| fail "publish workflow must append the 4-character commit hash as the last version segment."
|
|| fail "publish workflow must append the 4-character commit hash as the last version segment."
|
||||||
grep -Fq "steps.version.outputs.image_tag" "$publish_workflow" \
|
grep -Fq "steps.version.outputs.image_tag" "$publish_workflow" \
|
||||||
|| fail "publish workflow must use the computed image tag for pushed images."
|
|| fail "publish workflow must use the computed image tag for built image archives."
|
||||||
grep -Fq "Upload pushed images to WORKS Drive archive" "$publish_workflow" \
|
grep -Fq "Upload built images to WORKS Drive archive" "$publish_workflow" \
|
||||||
|| fail "publish workflow must archive the exact pushed images to WORKS Drive."
|
|| fail "publish workflow must archive locally built images to WORKS Drive."
|
||||||
grep -Fq "docker pull" "$publish_workflow" \
|
|
||||||
|| fail "publish workflow must pull the pushed image before WORKS archive upload."
|
|
||||||
grep -Fq "scripts/docker-image/upload_works_drive.sh" "$publish_workflow" \
|
grep -Fq "scripts/docker-image/upload_works_drive.sh" "$publish_workflow" \
|
||||||
|| fail "publish workflow must use the shared WORKS Drive image archive script."
|
|| fail "publish workflow must use the shared WORKS Drive image archive script."
|
||||||
grep -Fq "docker/login-action@v3" "$publish_workflow" \
|
|
||||||
|| fail "publish workflow must login to the shared registry."
|
|
||||||
grep -Fq "docker/build-push-action@v5" "$publish_workflow" \
|
grep -Fq "docker/build-push-action@v5" "$publish_workflow" \
|
||||||
|| fail "publish workflow must build and push images."
|
|| fail "publish workflow must build images."
|
||||||
|
grep -Fq "load: true" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must load built images into the local Docker daemon for WORKS archive upload."
|
||||||
for image in backend userfront adminfront devfront orgfront; do
|
for image in backend userfront adminfront devfront orgfront; do
|
||||||
grep -Fq "/baron_sso/${image}:" "$publish_workflow" \
|
grep -Fq "baron_sso/${image}:" "$publish_workflow" \
|
||||||
|| fail "publish workflow must push ${image} image."
|
|| fail "publish workflow must build ${image} image."
|
||||||
done
|
done
|
||||||
|
grep -Fq "WORKS_DRIVE_ACCESS_TOKEN_INPUT: \${{ secrets.WORKS_DRIVE_ACCESS_TOKEN }}" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must support direct WORKS Drive access token auth."
|
||||||
|
grep -Fq "WORKS_DRIVE_OAUTH_REFRESH_TOKEN: \${{ secrets.WORKS_DRIVE_OAUTH_REFRESH_TOKEN }}" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must support WORKS Drive refresh-token auth."
|
||||||
|
grep -Fq "WORKS_DRIVE_PARENT_FILE_ID" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must target a WORKS Drive folder."
|
||||||
|
grep -Fq "Resolve WORKS Drive access token" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must resolve a short-lived WORKS Drive access token before uploads."
|
||||||
|
grep -Fq "WORKS_DRIVE_ACCESS_TOKEN_INPUT" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must avoid passing the long-lived access-token secret name into upload steps directly."
|
||||||
|
grep -Fq "grant_type=refresh_token" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must support issuing an access token from WORKS_DRIVE_OAUTH_REFRESH_TOKEN."
|
||||||
|
grep -Fq "WORKS_DRIVE_ACCESS_TOKEN=" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must export the resolved access token through GITHUB_ENV."
|
||||||
|
grep -Fq "::add-mask::" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must mask resolved WORKS tokens in logs."
|
||||||
|
grep -Fq "rotated_refresh_token_file" "$publish_workflow" \
|
||||||
|
|| fail "publish workflow must capture rotated refresh tokens when WORKS returns one."
|
||||||
|
if grep -Eiq 'harbor|docker/login-action|push:[[:space:]]*true|docker pull|docker push|HARBOR_' "$publish_workflow"; then
|
||||||
|
fail "shared image publish workflow must not depend on Harbor registry login/push/pull."
|
||||||
|
fi
|
||||||
|
|
||||||
grep -Fq "name: Deploy Baron SSO Staging Images" "$staging_deploy_workflow" \
|
grep -Fq "name: Deploy Baron SSO Staging Images" "$staging_deploy_workflow" \
|
||||||
|| fail "staging deploy workflow must have the expected name."
|
|| fail "staging deploy workflow must have the expected name."
|
||||||
|
|||||||
Reference in New Issue
Block a user