Skip to content

deploy-branch

deploy-branch #36

Workflow file for this run

name: deploy-branch
on:
workflow_run:
workflows:
- build-images
types:
- completed
branches:
- deploy
workflow_dispatch:
inputs:
image_tag:
description: "Image tag to deploy (e.g. a specific commit sha or deploy-latest)"
required: true
default: deploy-latest
type: string
env:
REGISTRY: ghcr.io
BOT_IMAGE_NAME: ${{ github.repository_owner }}/evorsio-bot-service
USER_IMAGE_NAME: ${{ github.repository_owner }}/evorsio-user-service
concurrency:
group: deploy-branch-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }}
- name: Prepare deployment files
run: |
OUTPUT_DIR="Evorsio.AppHost/aspire-output"
if [ "${{ github.event_name }}" = "workflow_run" ]; then
IMAGE_TAG="${{ github.event.workflow_run.head_sha }}"
else
IMAGE_TAG="${{ inputs.image_tag }}"
fi
if [ -z "$IMAGE_TAG" ]; then
echo "::error::Unable to resolve IMAGE_TAG"
exit 1
fi
if [ ! -f "$OUTPUT_DIR/docker-compose.yaml" ]; then
echo "::error::Evorsio.AppHost/aspire-output/docker-compose.yaml not found. Please generate and commit aspire-output from local before deploy."
exit 1
fi
if [ ! -f "$OUTPUT_DIR/docker-compose.override.yaml" ]; then
echo "::error::Evorsio.AppHost/aspire-output/docker-compose.override.yaml not found."
exit 1
fi
if [ ! -f "$OUTPUT_DIR/.env" ]; then
echo "::error::Evorsio.AppHost/aspire-output/.env not found. Please generate and commit aspire-output from local before deploy."
exit 1
fi
if [ ! -f "$OUTPUT_DIR/components/statestore.yaml" ]; then
echo "::error::Evorsio.AppHost/aspire-output/components/statestore.yaml not found."
exit 1
fi
if [ ! -f "$OUTPUT_DIR/components/pubsub.yaml" ]; then
echo "::error::Evorsio.AppHost/aspire-output/components/pubsub.yaml not found."
exit 1
fi
mkdir -p "$OUTPUT_DIR/Nginx" "$OUTPUT_DIR/DatabaseInit"
cp "Evorsio.AppHost/Realms/realm-export.json" "$OUTPUT_DIR/realm-export.json"
cp -r "Evorsio.AppHost/Nginx/." "$OUTPUT_DIR/Nginx/"
cp -r "Evorsio.AppHost/DatabaseInit/." "$OUTPUT_DIR/DatabaseInit/"
cat > "$OUTPUT_DIR/.env" <<EOF
EVORSIO_DEFAULT_USER_PASSWORD=${{ secrets.EVORSIO_DEFAULT_USER_PASSWORD }}
ASPIRE_DASHBOARD_PUBLIC_URL=${{ vars.ASPIRE_DASHBOARD_PUBLIC_URL }}
ASPIRE_DASHBOARD_CLIENT_SECRET=${{ secrets.ASPIRE_DASHBOARD_CLIENT_SECRET}}
BLOG_PUBLIC_URL=${{ vars.BLOG_PUBLIC_URL }}
BOT_SERVICE_PORT=${{ vars.BOT_SERVICE_PORT }}
BOT_SERVICE_SECRET=${{ secrets.BOT_SERVICE_SECRET }}
KEYCLOAK_ADMIN_PASSWORD=${{ secrets.KEYCLOAK_ADMIN_PASSWORD }}
KEYCLOAK_ADMIN_USERNAME=${{ vars.KEYCLOAK_ADMIN_USERNAME }}
KEYCLOAK_BINDMOUNT_0=./Realms
KEYCLOAK_HOSTNAME=${{ vars.KEYCLOAK_HOSTNAME }}
KEYCLOAK_REALM=${{ vars.KEYCLOAK_REALM }}
POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_USERNAME=${{ vars.POSTGRES_USERNAME }}
PUBLIC_BASE_URL=${{ vars.PUBLIC_BASE_URL }}
REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
TELEGRAM_BOT_TOKEN=${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_WEBHOOK_SECRET=${{ secrets.TELEGRAM_WEBHOOK_SECRET }}
USER_SERVICE_PORT=${{ vars.USER_SERVICE_PORT }}
BOT_SERVICE_IMAGE=${{ env.REGISTRY }}/${{ env.BOT_IMAGE_NAME }}:$IMAGE_TAG
USER_SERVICE_IMAGE=${{ env.REGISTRY }}/${{ env.USER_IMAGE_NAME }}:$IMAGE_TAG
POSTGRES_BINDMOUNT_0=./DatabaseInit
NGINX_BINDMOUNT_0=./Nginx/nginx.conf
NGINX_BINDMOUNT_1=./Nginx/certs
NGINX_BINDMOUNT_2=./Nginx/acme-challenge
EOF
if ! grep -qE '^BOT_SERVICE_IMAGE=.+$' "$OUTPUT_DIR/.env"; then
echo "::error::BOT_SERVICE_IMAGE is missing in generated .env"
exit 1
fi
if ! grep -qE '^USER_SERVICE_IMAGE=.+$' "$OUTPUT_DIR/.env"; then
echo "::error::USER_SERVICE_IMAGE is missing in generated .env"
exit 1
fi
if ! grep -qE '^BLOG_PUBLIC_URL=.+$' "$OUTPUT_DIR/.env"; then
echo "::error::BLOG_PUBLIC_URL is missing in generated .env"
exit 1
fi
if ! grep -qE '^POSTGRES_PASSWORD=.+$' "$OUTPUT_DIR/.env"; then
echo "::error::POSTGRES_PASSWORD is missing in generated .env"
exit 1
fi
if ! grep -qE '^REDIS_PASSWORD=.+$' "$OUTPUT_DIR/.env"; then
echo "::error::REDIS_PASSWORD is missing in generated .env"
exit 1
fi
- name: Upload compose bundle
uses: appleboy/scp-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: ${{ secrets.SERVER_PORT }}
source: "Evorsio.AppHost/aspire-output/docker-compose.yaml,Evorsio.AppHost/aspire-output/docker-compose.override.yaml,Evorsio.AppHost/aspire-output/.env,Evorsio.AppHost/aspire-output/realm-export.json,Evorsio.AppHost/aspire-output/Nginx,Evorsio.AppHost/aspire-output/DatabaseInit,Evorsio.AppHost/aspire-output/components"
target: "~/incoming"
overwrite: true
- name: Log in to GHCR for validation
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_PAT }}
- name: Validate images exist in GHCR
run: |
set -e
source "Evorsio.AppHost/aspire-output/.env"
check_image() {
IMAGE="$1"
echo "Validating manifest: $IMAGE"
if ! docker manifest inspect "$IMAGE" >/dev/null 2>&1; then
echo "::error::Image manifest not found in GHCR: $IMAGE"
exit 1
fi
}
check_image "$BOT_SERVICE_IMAGE"
check_image "$USER_SERVICE_IMAGE"
- name: Run deployment on server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: ${{ secrets.SERVER_PORT }}
script: |
set -e
APP_SUBDIR="${{ secrets.SERVER_APP_DIR }}"
if [ -z "$APP_SUBDIR" ]; then
echo "SERVER_APP_DIR is required"
exit 1
fi
APP_DIR="$HOME/$APP_SUBDIR"
mkdir -p "$APP_DIR/Realms" "$APP_DIR/Nginx" "$APP_DIR/DatabaseInit" "$APP_DIR/components"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/docker-compose.yaml" "$APP_DIR/docker-compose.yaml"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/docker-compose.override.yaml" "$APP_DIR/docker-compose.override.yaml"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/.env" "$APP_DIR/.env"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/realm-export.json" "$APP_DIR/Realms/realm-export.json"
rm -rf "$APP_DIR/DatabaseInit"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/DatabaseInit" "$APP_DIR/DatabaseInit"
rm -rf "$APP_DIR/components"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/components" "$APP_DIR/components"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/Nginx/nginx.conf" "$APP_DIR/Nginx/nginx.conf"
rm -rf "$APP_DIR/Nginx/acme-challenge"
mv "$HOME/incoming/Evorsio.AppHost/aspire-output/Nginx/acme-challenge" "$APP_DIR/Nginx/acme-challenge"
mkdir -p "$APP_DIR/Nginx/certs"
rm -rf "$HOME/incoming"
cd "$APP_DIR"
set -a
. ./.env
set +a
export KEYCLOAK_BINDMOUNT_0="$APP_DIR/Realms"
export POSTGRES_BINDMOUNT_0="$APP_DIR/DatabaseInit"
export NGINX_BINDMOUNT_0="$APP_DIR/Nginx/nginx.conf"
export NGINX_BINDMOUNT_1="$APP_DIR/Nginx/certs"
export NGINX_BINDMOUNT_2="$APP_DIR/Nginx/acme-challenge"
ESCAPED_REDIS_PASSWORD=$(printf '%s' "$REDIS_PASSWORD" | sed -e 's/[\\/&]/\\\\&/g')
sed "s/__REDIS_PASSWORD__/${ESCAPED_REDIS_PASSWORD}/g" "$APP_DIR/components/statestore.yaml" > "$APP_DIR/components/statestore.rendered.yaml"
sed "s/__REDIS_PASSWORD__/${ESCAPED_REDIS_PASSWORD}/g" "$APP_DIR/components/pubsub.yaml" > "$APP_DIR/components/pubsub.rendered.yaml"
mv "$APP_DIR/components/statestore.rendered.yaml" "$APP_DIR/components/statestore.yaml"
mv "$APP_DIR/components/pubsub.rendered.yaml" "$APP_DIR/components/pubsub.yaml"
cleanup_on_error() {
echo "Deployment failed, removing current release images..."
if [ -n "${BOT_SERVICE_IMAGE:-}" ]; then
if docker image inspect "$BOT_SERVICE_IMAGE" >/dev/null 2>&1; then
docker image rm -f "$BOT_SERVICE_IMAGE" || true
else
echo "BOT_SERVICE_IMAGE not found locally, skip remove"
fi
fi
if [ -n "${USER_SERVICE_IMAGE:-}" ]; then
if docker image inspect "$USER_SERVICE_IMAGE" >/dev/null 2>&1; then
docker image rm -f "$USER_SERVICE_IMAGE" || true
else
echo "USER_SERVICE_IMAGE not found locally, skip remove"
fi
fi
}
trap cleanup_on_error ERR
echo "${{ secrets.GHCR_PAT }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
pull_with_retry() {
IMAGE="$1"
MAX_RETRIES=5
RETRY=1
while [ "$RETRY" -le "$MAX_RETRIES" ]; do
echo "Pulling $IMAGE (attempt $RETRY/$MAX_RETRIES)..."
if docker pull "$IMAGE"; then
echo "Pulled $IMAGE successfully"
return 0
fi
if [ "$RETRY" -lt "$MAX_RETRIES" ]; then
SLEEP_SECONDS=$((RETRY * 5))
echo "Pull failed, retry in ${SLEEP_SECONDS}s..."
sleep "$SLEEP_SECONDS"
fi
RETRY=$((RETRY + 1))
done
echo "::error::Failed to pull image after retries: $IMAGE"
return 1
}
pull_with_retry "$BOT_SERVICE_IMAGE"
pull_with_retry "$USER_SERVICE_IMAGE"
docker compose --env-file .env -f docker-compose.yaml -f docker-compose.override.yaml up -d --remove-orphans
docker compose --env-file .env -f docker-compose.yaml -f docker-compose.override.yaml up -d --force-recreate nginx
docker compose --env-file .env -f docker-compose.yaml -f docker-compose.override.yaml ps