All checks were successful
ITAM Code Check / build-and-config-check (push) Successful in 12s
167 lines
6.8 KiB
YAML
167 lines
6.8 KiB
YAML
name: ITAM Production Deploy
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
target_branch:
|
|
description: "Branch to deploy"
|
|
required: true
|
|
default: "main"
|
|
type: string
|
|
|
|
jobs:
|
|
deploy-production:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup SSH agent
|
|
uses: webfactory/ssh-agent@v0.9.0
|
|
with:
|
|
ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
|
|
|
|
- name: Validate required production variables
|
|
env:
|
|
PROD_HOST: ${{ vars.PROD_HOST }}
|
|
PROD_USER: ${{ vars.PROD_USER }}
|
|
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
|
|
PROD_GIT_URL: ${{ vars.PROD_GIT_URL }}
|
|
DB_HOST: ${{ vars.PROD_DB_HOST }}
|
|
DB_PORT: ${{ vars.PROD_DB_PORT }}
|
|
DB_USER: ${{ vars.PROD_DB_USER }}
|
|
DB_PASS: ${{ secrets.PROD_DB_PASS }}
|
|
DB_NAME: ${{ vars.PROD_DB_NAME }}
|
|
CLIENT_ID: ${{ vars.PROD_CLIENT_ID }}
|
|
ISSUER: ${{ vars.PROD_ISSUER }}
|
|
run: |
|
|
set -euo pipefail
|
|
check_required() {
|
|
local env_name="$1"
|
|
local source_name="$2"
|
|
if [ -z "${!env_name:-}" ]; then
|
|
echo "::error::Missing required variable or secret: ${source_name}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_required PROD_HOST PROD_HOST
|
|
check_required PROD_USER PROD_USER
|
|
check_required PROD_DEPLOY_PATH PROD_DEPLOY_PATH
|
|
check_required PROD_GIT_URL PROD_GIT_URL
|
|
check_required DB_HOST PROD_DB_HOST
|
|
check_required DB_PORT PROD_DB_PORT
|
|
check_required DB_USER PROD_DB_USER
|
|
check_required DB_PASS PROD_DB_PASS
|
|
check_required DB_NAME PROD_DB_NAME
|
|
check_required CLIENT_ID PROD_CLIENT_ID
|
|
check_required ISSUER PROD_ISSUER
|
|
|
|
- name: Create production env file
|
|
env:
|
|
DB_HOST: ${{ vars.PROD_DB_HOST }}
|
|
DB_PORT: ${{ vars.PROD_DB_PORT }}
|
|
DB_USER: ${{ vars.PROD_DB_USER }}
|
|
DB_PASS: ${{ secrets.PROD_DB_PASS }}
|
|
DB_NAME: ${{ vars.PROD_DB_NAME }}
|
|
LOG_LEVEL: ${{ vars.PROD_LOG_LEVEL }}
|
|
CLIENT_ID: ${{ vars.PROD_CLIENT_ID }}
|
|
ISSUER: ${{ vars.PROD_ISSUER }}
|
|
PROD_REDIRECT_URI: ${{ vars.PROD_REDIRECT_URI }}
|
|
PROD_JWKS_URI: ${{ vars.PROD_JWKS_URI }}
|
|
run: |
|
|
set -euo pipefail
|
|
EFFECTIVE_LOG_LEVEL="${LOG_LEVEL:-info}"
|
|
cat > .env.deploy <<EOF
|
|
DB_HOST=${DB_HOST}
|
|
DB_PORT=${DB_PORT}
|
|
DB_USER=${DB_USER}
|
|
DB_PASS=${DB_PASS}
|
|
DB_NAME=${DB_NAME}
|
|
NODE_ENV=production
|
|
PORT=3000
|
|
LOG_LEVEL=${EFFECTIVE_LOG_LEVEL}
|
|
CLIENT_ID=${CLIENT_ID}
|
|
ISSUER=${ISSUER}
|
|
PROD_REDIRECT_URI=${PROD_REDIRECT_URI:-http://172.16.10.175:9090/callback}
|
|
PROD_JWKS_URI=${PROD_JWKS_URI:-http://172.16.10.175:9090/.well-known/jwks.json}
|
|
EOF
|
|
|
|
- name: Deploy to production host
|
|
env:
|
|
PROD_HOST: ${{ vars.PROD_HOST }}
|
|
PROD_USER: ${{ vars.PROD_USER }}
|
|
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
|
|
PROD_BACKUP_ROOT: ${{ vars.PROD_BACKUP_ROOT }}
|
|
PROD_GIT_URL: ${{ vars.PROD_GIT_URL }}
|
|
DB_HOST: ${{ vars.PROD_DB_HOST }}
|
|
DB_PORT: ${{ vars.PROD_DB_PORT }}
|
|
DB_USER: ${{ vars.PROD_DB_USER }}
|
|
DB_PASS: ${{ secrets.PROD_DB_PASS }}
|
|
DB_NAME: ${{ vars.PROD_DB_NAME }}
|
|
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
|
|
run: |
|
|
set -euo pipefail
|
|
ssh-keyscan -H "${PROD_HOST}" >> ~/.ssh/known_hosts
|
|
|
|
ssh "${PROD_USER}@${PROD_HOST}" "mkdir -p '${PROD_DEPLOY_PATH}'"
|
|
|
|
ssh "${PROD_USER}@${PROD_HOST}" "if [ ! -d '${PROD_DEPLOY_PATH}/.git' ]; then git clone '${PROD_GIT_URL}' '${PROD_DEPLOY_PATH}'; else cd '${PROD_DEPLOY_PATH}' && git remote set-url origin '${PROD_GIT_URL}'; fi"
|
|
|
|
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && git checkout -- .env || true && git fetch origin '${TARGET_BRANCH}' && git checkout -B '${TARGET_BRANCH}' FETCH_HEAD && git reset --hard FETCH_HEAD"
|
|
|
|
EFFECTIVE_BACKUP_ROOT="${PROD_BACKUP_ROOT:-/home/user/dachs_backups}"
|
|
|
|
ssh "${PROD_USER}@${PROD_HOST}" "export DEPLOY_PATH='${PROD_DEPLOY_PATH}' BACKUP_ROOT='${EFFECTIVE_BACKUP_ROOT}'; sh -eu -s" <<'REMOTE_BACKUP'
|
|
case "$BACKUP_ROOT" in
|
|
"$DEPLOY_PATH"|"$DEPLOY_PATH"/*)
|
|
echo "Backup path must be outside deploy path: $BACKUP_ROOT"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [ -d "$DEPLOY_PATH/.git" ]; then
|
|
mkdir -p "$BACKUP_ROOT"
|
|
echo "Starting pre-deploy backup..."
|
|
cd "$DEPLOY_PATH"
|
|
if [ -f Makefile ] && [ -f scripts/backup.sh ] && [ -f .env ]; then
|
|
make predeploy-backup ENV_FILE=.env BACKUP_ROOT="$BACKUP_ROOT"
|
|
else
|
|
echo "Skipping pre-deploy backup because required backup files are missing in current deployment."
|
|
fi
|
|
else
|
|
echo "Skipping pre-deploy backup because no existing deployment was found."
|
|
fi
|
|
REMOTE_BACKUP
|
|
|
|
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && git clean -fd -e uploads/ -e logs/nginx/ -e mysql_data/"
|
|
|
|
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && mkdir -p uploads logs/nginx"
|
|
|
|
scp .env.deploy "${PROD_USER}@${PROD_HOST}:${PROD_DEPLOY_PATH}/.env"
|
|
|
|
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && chmod 600 .env && docker compose -f docker-compose.prod.yaml config && docker compose -f docker-compose.prod.yaml up -d --build && docker compose -f docker-compose.prod.yaml restart nginx"
|
|
|
|
- name: Post-deploy status check
|
|
env:
|
|
PROD_HOST: ${{ vars.PROD_HOST }}
|
|
PROD_USER: ${{ vars.PROD_USER }}
|
|
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
|
|
run: |
|
|
set -euo pipefail
|
|
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && docker compose -f docker-compose.prod.yaml ps"
|
|
|
|
- name: Post-deploy smoke checks
|
|
env:
|
|
PROD_HOST: ${{ vars.PROD_HOST }}
|
|
PROD_USER: ${{ vars.PROD_USER }}
|
|
PROD_DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }}
|
|
run: |
|
|
set -euo pipefail
|
|
ssh "${PROD_USER}@${PROD_HOST}" "curl -fsS http://localhost:9090/health"
|
|
ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && docker compose -f docker-compose.prod.yaml exec -T backend curl -fsS http://localhost:3000/health"
|
|
|
|
- name: Cleanup generated env file
|
|
if: ${{ always() }}
|
|
run: rm -f .env.deploy
|