deploy-branch #36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |