name: ITAM Production Deploy on: workflow_dispatch: inputs: target_branch: description: "Branch to deploy" required: true default: "Dockerizing" 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 }} run: | set -euo pipefail required_keys="PROD_HOST PROD_USER PROD_DEPLOY_PATH PROD_GIT_URL DB_HOST DB_PORT DB_USER DB_PASS DB_NAME" for key in ${required_keys}; do if [ -z "${!key:-}" ]; then echo "::error::Missing required variable or secret: ${key}" exit 1 fi done - 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 }} run: | set -euo pipefail EFFECTIVE_LOG_LEVEL="${LOG_LEVEL:-info}" cat > .env.deploy <> ~/.ssh/known_hosts ssh "${PROD_USER}@${PROD_HOST}" "mkdir -p '${PROD_DEPLOY_PATH}'" EFFECTIVE_BACKUP_ROOT="${PROD_BACKUP_ROOT:-${PROD_DEPLOY_PATH%/}_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" cd "$DEPLOY_PATH" if [ -f Makefile ] && [ -f scripts/backup.sh ]; then make predeploy-backup BACKUP_ROOT="$BACKUP_ROOT" else echo "Skipping pre-deploy backup because current deployed revision does not contain Makefile backup tooling." fi else echo "Skipping pre-deploy backup because no existing deployment was found." fi REMOTE_BACKUP 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 fetch origin '${TARGET_BRANCH}' && git checkout -B '${TARGET_BRANCH}' FETCH_HEAD && git reset --hard FETCH_HEAD && git clean -fd" 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" - 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/health" ssh "${PROD_USER}@${PROD_HOST}" "curl -fsS http://localhost/ > /dev/null" ssh "${PROD_USER}@${PROD_HOST}" "cd '${PROD_DEPLOY_PATH}' && docker compose -f docker-compose.prod.yaml exec -T backend curl -fsS http://localhost:3000/ready" - name: Cleanup generated env file if: ${{ always() }} run: rm -f .env.deploy