Files
egbim_qa_platform/.gitea/workflows/deploy-staging.yml
T
root 33453ecc55
Deploy staging / deploy (push) Failing after 6s
Initial deployment setup
2026-08-31 16:45:24 +09:00

292 lines
15 KiB
YAML

name: Deploy staging
run-name: Deploy staging from main
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate deployment settings
env:
STAGING_HOST: ${{ vars.STAGING_HOST }}
STAGING_PORT: ${{ vars.STAGING_PORT }}
STAGING_APP_DIR: ${{ vars.STAGING_APP_DIR }}
STAGING_COMPOSE_FILE: ${{ vars.STAGING_COMPOSE_FILE }}
STAGING_USER: ${{ secrets.STAGING_USER }}
STAGING_SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
STAGING_SSH_KNOWN_HOSTS: ${{ secrets.STAGING_SSH_KNOWN_HOSTS }}
NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL }}
ADMIN_CANDIDATE_TENANT_ID: ${{ vars.ADMIN_CANDIDATE_TENANT_ID }}
INITIAL_SUPER_ADMIN_PHONE_NUMBER: ${{ vars.INITIAL_SUPER_ADMIN_PHONE_NUMBER }}
ADMIN_WEB_URL: ${{ vars.ADMIN_WEB_URL }}
BASE_URL: ${{ vars.BASE_URL }}
SMTP_ENABLED: ${{ vars.SMTP_ENABLED }}
SMTP_HOST: ${{ vars.SMTP_HOST }}
SMTP_SENDER: ${{ vars.SMTP_SENDER }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
MASTER_API_KEY: ${{ secrets.MASTER_API_KEY }}
SSO_ISSUER: ${{ vars.SSO_ISSUER }}
SSO_CLIENT_ID: ${{ vars.SSO_CLIENT_ID }}
SSO_CLIENT_SECRET: ${{ secrets.SSO_CLIENT_SECRET }}
SECRETARY_ABC_API_KEY: ${{ secrets.SECRETARY_ABC_API_KEY }}
GITEA_API_URL: ${{ vars.GITEA_API_URL }}
GITEA_API_TOKEN: ${{ secrets.EXTERNAL_ISSUE_API_TOKEN }}
GITHUB_API_TOKEN: ${{ secrets.EXTERNAL_GITHUB_API_TOKEN }}
JIRA_API_EMAIL: ${{ vars.JIRA_API_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_WEBHOOK_SECRET: ${{ secrets.JIRA_WEBHOOK_SECRET }}
JIRA_ISSUE_TYPE: ${{ vars.JIRA_ISSUE_TYPE }}
NAVER_WORKS_ENABLED: ${{ vars.NAVER_WORKS_ENABLED }}
NAVER_WORKS_API_BASE_URL: ${{ vars.NAVER_WORKS_API_BASE_URL }}
NAVER_WORKS_AUTH_URL: ${{ vars.NAVER_WORKS_AUTH_URL }}
NAVER_WORKS_BOT_ID: ${{ vars.NAVER_WORKS_BOT_ID }}
NAVER_WORKS_DEFAULT_ROOM_ID: ${{ vars.NAVER_WORKS_DEFAULT_ROOM_ID }}
NAVER_WORKS_SCOPE: ${{ vars.NAVER_WORKS_SCOPE }}
NAVER_WORKS_CLIENT_ID: ${{ secrets.NAVER_WORKS_CLIENT_ID }}
NAVER_WORKS_CLIENT_SECRET: ${{ secrets.NAVER_WORKS_CLIENT_SECRET }}
NAVER_WORKS_SERVICE_ACCOUNT: ${{ secrets.NAVER_WORKS_SERVICE_ACCOUNT }}
NAVER_WORKS_PRIVATE_KEY: ${{ secrets.NAVER_WORKS_PRIVATE_KEY }}
run: |
set -eu
STAGING_HOST="${STAGING_HOST:-172.16.10.175}"
STAGING_PORT="${STAGING_PORT:-22}"
STAGING_APP_DIR="${STAGING_APP_DIR:-/home/user/baron_qa}"
STAGING_COMPOSE_FILE="${STAGING_COMPOSE_FILE:-docker/docker-compose.prod.yml}"
export STAGING_HOST STAGING_PORT STAGING_APP_DIR STAGING_COMPOSE_FILE
required_vars="NEXT_PUBLIC_API_BASE_URL ADMIN_WEB_URL BASE_URL SSO_ISSUER SSO_CLIENT_ID ADMIN_CANDIDATE_TENANT_ID INITIAL_SUPER_ADMIN_PHONE_NUMBER NAVER_WORKS_ENABLED NAVER_WORKS_BOT_ID NAVER_WORKS_DEFAULT_ROOM_ID"
required_secrets="STAGING_USER STAGING_SSH_PRIVATE_KEY STAGING_SSH_KNOWN_HOSTS JWT_SECRET MASTER_API_KEY SSO_CLIENT_SECRET SECRETARY_ABC_API_KEY GITEA_API_TOKEN NAVER_WORKS_CLIENT_ID NAVER_WORKS_CLIENT_SECRET NAVER_WORKS_SERVICE_ACCOUNT NAVER_WORKS_PRIVATE_KEY"
for name in $required_vars $required_secrets; do
if [ -z "${!name:-}" ]; then
echo "Missing Gitea variable or secret: $name" >&2
exit 1
fi
done
test "$STAGING_COMPOSE_FILE" = "docker/docker-compose.prod.yml" || {
echo "STAGING_COMPOSE_FILE must point to docker/docker-compose.prod.yml" >&2
exit 1
}
echo "Deployment settings are present."
- name: Deploy to staging over SSH
env:
STAGING_HOST: ${{ vars.STAGING_HOST }}
STAGING_PORT: ${{ vars.STAGING_PORT }}
STAGING_APP_DIR: ${{ vars.STAGING_APP_DIR }}
STAGING_COMPOSE_FILE: ${{ vars.STAGING_COMPOSE_FILE }}
STAGING_USER: ${{ secrets.STAGING_USER }}
STAGING_SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
STAGING_SSH_KNOWN_HOSTS: ${{ secrets.STAGING_SSH_KNOWN_HOSTS }}
APP_ENV: ${{ vars.APP_ENV }}
WEB_PORT: ${{ vars.WEB_PORT }}
API_PORT: ${{ vars.API_PORT }}
SECRETARY_API_PORT: ${{ vars.SECRETARY_API_PORT }}
MYSQL_PORT: ${{ vars.MYSQL_PORT }}
MYSQL_SECRETARY_PORT: ${{ vars.MYSQL_SECRETARY_PORT }}
NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL }}
ADMIN_CANDIDATE_TENANT_ID: ${{ vars.ADMIN_CANDIDATE_TENANT_ID }}
ADMIN_WEB_URL: ${{ vars.ADMIN_WEB_URL }}
BASE_URL: ${{ vars.BASE_URL }}
SMTP_ENABLED: ${{ vars.SMTP_ENABLED }}
SMTP_HOST: ${{ vars.SMTP_HOST }}
SMTP_PORT: ${{ vars.SMTP_PORT }}
SMTP_SENDER: ${{ vars.SMTP_SENDER }}
SMTP_TLS: ${{ vars.SMTP_TLS }}
SMTP_CIPHER_SPEC: ${{ vars.SMTP_CIPHER_SPEC }}
SMTP_OPPORTUNISTIC_TLS: ${{ vars.SMTP_OPPORTUNISTIC_TLS }}
ACCESS_TOKEN_EXPIRED_TIME: ${{ vars.ACCESS_TOKEN_EXPIRED_TIME }}
REFRESH_TOKEN_EXPIRED_TIME: ${{ vars.REFRESH_TOKEN_EXPIRED_TIME }}
AUTO_MIGRATION: ${{ vars.AUTO_MIGRATION }}
OPENSEARCH_USE: ${{ vars.OPENSEARCH_USE }}
OPENSEARCH_NODE: ${{ vars.OPENSEARCH_NODE }}
OPENSEARCH_USERNAME: ${{ vars.OPENSEARCH_USERNAME }}
OPENSEARCH_PASSWORD: ${{ vars.OPENSEARCH_PASSWORD }}
SSO_ISSUER: ${{ vars.SSO_ISSUER }}
SSO_CLIENT_ID: ${{ vars.SSO_CLIENT_ID }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
MASTER_API_KEY: ${{ secrets.MASTER_API_KEY }}
INITIAL_SUPER_ADMIN_PHONE_NUMBER: ${{ vars.INITIAL_SUPER_ADMIN_PHONE_NUMBER }}
ADMIN_CANDIDATE_EMAILS: ${{ secrets.ADMIN_CANDIDATE_EMAILS }}
SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
SSO_CLIENT_SECRET: ${{ secrets.SSO_CLIENT_SECRET }}
SECRETARY_ABC_API_KEY: ${{ secrets.SECRETARY_ABC_API_KEY }}
GITEA_API_URL: ${{ vars.GITEA_API_URL }}
GITEA_API_TOKEN: ${{ secrets.EXTERNAL_ISSUE_API_TOKEN }}
GITHUB_API_TOKEN: ${{ secrets.EXTERNAL_GITHUB_API_TOKEN }}
JIRA_API_EMAIL: ${{ vars.JIRA_API_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_WEBHOOK_SECRET: ${{ secrets.JIRA_WEBHOOK_SECRET }}
JIRA_ISSUE_TYPE: ${{ vars.JIRA_ISSUE_TYPE }}
NAVER_WORKS_ENABLED: ${{ vars.NAVER_WORKS_ENABLED }}
NAVER_WORKS_API_BASE_URL: ${{ vars.NAVER_WORKS_API_BASE_URL }}
NAVER_WORKS_AUTH_URL: ${{ vars.NAVER_WORKS_AUTH_URL }}
NAVER_WORKS_BOT_ID: ${{ vars.NAVER_WORKS_BOT_ID }}
NAVER_WORKS_DEFAULT_ROOM_ID: ${{ vars.NAVER_WORKS_DEFAULT_ROOM_ID }}
NAVER_WORKS_SCOPE: ${{ vars.NAVER_WORKS_SCOPE }}
NAVER_WORKS_MAX_RETRIES: ${{ vars.NAVER_WORKS_MAX_RETRIES }}
NAVER_WORKS_MAX_MESSAGE_LENGTH: ${{ vars.NAVER_WORKS_MAX_MESSAGE_LENGTH }}
NAVER_WORKS_ACCESS_TOKEN: ${{ secrets.NAVER_WORKS_ACCESS_TOKEN }}
NAVER_WORKS_CLIENT_ID: ${{ secrets.NAVER_WORKS_CLIENT_ID }}
NAVER_WORKS_CLIENT_SECRET: ${{ secrets.NAVER_WORKS_CLIENT_SECRET }}
NAVER_WORKS_SERVICE_ACCOUNT: ${{ secrets.NAVER_WORKS_SERVICE_ACCOUNT }}
NAVER_WORKS_PRIVATE_KEY: ${{ secrets.NAVER_WORKS_PRIVATE_KEY }}
run: |
set -eu
set -o pipefail
umask 077
STAGING_HOST="${STAGING_HOST:-172.16.10.175}"
STAGING_PORT="${STAGING_PORT:-22}"
STAGING_APP_DIR="${STAGING_APP_DIR:-/home/user/baron_qa}"
STAGING_COMPOSE_FILE="${STAGING_COMPOSE_FILE:-docker/docker-compose.prod.yml}"
export STAGING_HOST STAGING_PORT STAGING_APP_DIR STAGING_COMPOSE_FILE
ssh_dir="$RUNNER_TEMP/staging-ssh"
mkdir -p "$ssh_dir"
chmod 700 "$ssh_dir"
key_file="$ssh_dir/id_ed25519"
known_hosts_file="$ssh_dir/known_hosts"
printf '%s\n' "$STAGING_SSH_PRIVATE_KEY" > "$key_file"
printf '%s\n' "$STAGING_SSH_KNOWN_HOSTS" > "$known_hosts_file"
chmod 600 "$key_file" "$known_hosts_file"
trap 'rm -rf "$ssh_dir"' EXIT
derived_public_key="$ssh_dir/id_ed25519.pub"
if ! ssh-keygen -y -f "$key_file" > "$derived_public_key" 2>/dev/null; then
echo "STAGING_SSH_PRIVATE_KEY is invalid or passphrase-protected." >&2
exit 1
fi
key_fingerprint="$(ssh-keygen -lf "$derived_public_key" | awk '{print $2}')"
echo "SSH target: $STAGING_USER@$STAGING_HOST:$STAGING_PORT"
echo "SSH key fingerprint: $key_fingerprint"
shell_quote() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\"'\\\"'/g")"
}
remote="$STAGING_USER@$STAGING_HOST"
remote_app_dir="$(shell_quote "$STAGING_APP_DIR")"
ssh_args=(
-i "$key_file"
-p "$STAGING_PORT"
-o BatchMode=yes
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
-o UserKnownHostsFile="$known_hosts_file"
)
ssh "${ssh_args[@]}" "$remote" "mkdir -p $remote_app_dir"
ssh "${ssh_args[@]}" "$remote" "rm -f $remote_app_dir/apps/secretary-api/alembic/versions/0002_role_access_workspaces.py $remote_app_dir/apps/secretary-api/alembic/versions/0009_preassigned_admin_candidates.py"
tar \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.next' \
--exclude='dist' \
--exclude='uploads' \
--exclude='.venv' \
--exclude='.venv-*' \
--exclude='.cache' \
--exclude='.turbo' \
--exclude='apps/docs/static' \
-czf - . | ssh "${ssh_args[@]}" "$remote" "tar -xzf - -C $remote_app_dir"
payload_file="$RUNNER_TEMP/staging-env.json"
python3 - <<'PY' > "$payload_file"
import base64
import json
import os
keys = """
APP_ENV WEB_PORT API_PORT SECRETARY_API_PORT MYSQL_PORT MYSQL_SECRETARY_PORT
NEXT_PUBLIC_API_BASE_URL ADMIN_WEB_URL BASE_URL SMTP_ENABLED SMTP_HOST SMTP_PORT SMTP_SENDER
SMTP_TLS SMTP_CIPHER_SPEC SMTP_OPPORTUNISTIC_TLS ACCESS_TOKEN_EXPIRED_TIME
REFRESH_TOKEN_EXPIRED_TIME AUTO_MIGRATION OPENSEARCH_USE OPENSEARCH_NODE
OPENSEARCH_USERNAME OPENSEARCH_PASSWORD SSO_ISSUER SSO_CLIENT_ID JWT_SECRET
MASTER_API_KEY INITIAL_SUPER_ADMIN_PHONE_NUMBER ADMIN_CANDIDATE_EMAILS ADMIN_CANDIDATE_TENANT_ID
SMTP_USERNAME SMTP_PASSWORD SSO_CLIENT_SECRET SECRETARY_ABC_API_KEY GITEA_API_URL GITEA_API_TOKEN GITHUB_API_TOKEN
JIRA_API_EMAIL JIRA_API_TOKEN JIRA_WEBHOOK_SECRET JIRA_ISSUE_TYPE
NAVER_WORKS_ENABLED NAVER_WORKS_API_BASE_URL NAVER_WORKS_AUTH_URL NAVER_WORKS_ACCESS_TOKEN
NAVER_WORKS_BOT_ID NAVER_WORKS_DEFAULT_ROOM_ID NAVER_WORKS_CLIENT_ID NAVER_WORKS_CLIENT_SECRET
NAVER_WORKS_SERVICE_ACCOUNT NAVER_WORKS_PRIVATE_KEY NAVER_WORKS_SCOPE NAVER_WORKS_MAX_RETRIES
NAVER_WORKS_MAX_MESSAGE_LENGTH
""".split()
values = {key: os.environ.get(key, "") for key in keys}
encoded = base64.b64encode(
json.dumps(values, ensure_ascii=False, separators=(",", ":")).encode()
).decode()
print(encoded)
PY
remote_command="set -eu
APP_DIR=$(shell_quote "$STAGING_APP_DIR")
COMPOSE_FILE=$(shell_quote "$STAGING_COMPOSE_FILE")
test -d \"\$APP_DIR\"
cd \"\$APP_DIR\"
python3 -c 'import base64,json,os,subprocess,sys,time,urllib.request
compose=sys.argv[1]
payload=sys.stdin.buffer.read().strip()
if not payload:
raise SystemExit(\"Deployment payload is empty.\")
values=json.loads(base64.b64decode(payload))
os.environ.update(values)
subprocess.run([\"docker\",\"compose\",\"-f\",compose,\"config\",\"--quiet\"],check=True)
subprocess.run([\"docker\",\"compose\",\"-f\",compose,\"up\",\"-d\",\"--build\"],check=True)
checks=[\"http://127.0.0.1:\"+os.environ.get(\"WEB_PORT\",\"3030\")+\"/api/health\"]
for url in checks:
last_error=\"no response\"
for _ in range(60):
try:
with urllib.request.urlopen(url,timeout=3) as response:
if response.status < 500:
break
last_error=\"HTTP \"+str(response.status)
except Exception as error:
last_error=repr(error)
time.sleep(2)
else:
subprocess.run([\"docker\",\"compose\",\"-f\",compose,\"ps\"],check=False)
for service in (\"api\",\"secretary-api\",\"web\",\"mysql\",\"mysql-secretary\"):
subprocess.run([\"docker\",\"compose\",\"-f\",compose,\"logs\",\"--tail=100\",service],check=False)
raise SystemExit(\"Health check failed: \"+url+\" (\"+last_error+\")\")
secretary_check=[\"docker\",\"compose\",\"-f\",compose,\"exec\",\"-T\",\"secretary-api\",\"python3\",\"-c\",\"import urllib.request; urllib.request.urlopen(\\\"http://127.0.0.1:8010/api/health\\\", timeout=5)\"]
secretary_error=\"no response\"
for _ in range(60):
try:
subprocess.run(secretary_check,check=True)
break
except subprocess.CalledProcessError as error:
secretary_error=repr(error)
time.sleep(2)
else:
subprocess.run([\"docker\",\"compose\",\"-f\",compose,\"ps\"],check=False)
subprocess.run([\"docker\",\"compose\",\"-f\",compose,\"logs\",\"--tail=200\",\"secretary-api\"],check=False)
raise SystemExit(\"Secretary API health check failed: \"+secretary_error)
subprocess.run([\"docker\",\"compose\",\"-f\",compose,\"ps\"],check=True)
print(\"Staging deployment and health checks passed.\")' \"\$COMPOSE_FILE\""
cat "$payload_file" | ssh \
-i "$key_file" \
-p "$STAGING_PORT" \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$known_hosts_file" \
"$remote" "$remote_command"
rm -f "$payload_file"