Deploy #162
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 | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| extension: | |
| description: 'Deploy with extension' | |
| required: false | |
| type: choice | |
| options: | |
| - '' | |
| - gala | |
| - rally | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| env: | |
| PRODUCTION: true | |
| # Secrets (genuine credentials) | |
| POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} | |
| MONGO_PASSWORD: ${{ secrets.MONGO_PASSWORD }} | |
| EMAIL_SMTP_PASSWORD: ${{ secrets.EMAIL_SMTP_PASSWORD }} | |
| IDP_SECRET_KEY: ${{ secrets.IDP_SECRET_KEY }} | |
| RECAPTCHA_SECRET_KEY: ${{ secrets.RECAPTCHA_SECRET_KEY }} | |
| R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }} | |
| AUTHENTIK_TOKEN: ${{ secrets.AUTHENTIK_TOKEN }} | |
| # Variables (public / non-sensitive config) | |
| EMAIL_SENDER_ADDRESS: ${{ vars.EMAIL_SENDER_ADDRESS }} | |
| EMAIL_SMTP_HOST: ${{ vars.EMAIL_SMTP_HOST }} | |
| EMAIL_SMTP_USER: ${{ vars.EMAIL_SMTP_USER }} | |
| R2_ENDPOINT_URL: ${{ vars.R2_ENDPOINT_URL }} | |
| R2_BUCKET: ${{ vars.R2_BUCKET }} | |
| R2_PUBLIC_BASE_URL: ${{ vars.R2_PUBLIC_BASE_URL }} | |
| OIDC_ENABLED: ${{ vars.OIDC_ENABLED || 'true' }} | |
| OIDC_CLIENT_ID: ${{ vars.OIDC_CLIENT_ID }} | |
| ENABLED_EXTENSIONS: ${{ github.event.inputs.extension }} | |
| steps: | |
| - name: Fix permissions before checkout | |
| run: | | |
| set -euo pipefail | |
| echo "Fixing permissions on root-owned files..." | |
| # Only run find commands if the current directory exists and has content | |
| if [ -d . ] && [ -n "$(ls -A . 2>/dev/null)" ]; then | |
| # Remove __pycache__ directories (they'll be regenerated) | |
| # Use -depth to process directories bottom-up to avoid errors with subdirectories | |
| find . -depth -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true | |
| # Fix ownership of any remaining root-owned files (files and directories in single pass) | |
| # Use numeric IDs to ensure correct group assignment | |
| find . -user root \( -type f -o -type d \) -exec chown $(id -u):$(id -g) {} + 2>/dev/null || true | |
| else | |
| echo "Workspace is empty, skipping permission fixes (will be handled after checkout)" | |
| fi | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| submodules-recursive: false | |
| fetch-depth: 0 | |
| token: ${{ secrets.GH_PAT }} | |
| # Disable workspace cleaning to preserve permission fixes from previous runs | |
| clean: false | |
| - name: Update extension submodules to tracked branches | |
| run: | | |
| git submodule sync || true | |
| # Initialize Rally submodule (required) | |
| git submodule update --init extensions/rally || true | |
| git -C extensions/rally fetch origin main --prune || true | |
| git -C extensions/rally checkout main || true | |
| git -C extensions/rally reset --hard origin/main || true | |
| # Initialize Gala submodule | |
| git submodule update --init extensions/gala | |
| git -C extensions/gala fetch origin main --prune | |
| git -C extensions/gala checkout main | |
| git -C extensions/gala reset --hard origin/main | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v2 | |
| - name: Login to Github Packages | |
| uses: docker/login-action@v2 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Get compose files | |
| id: compose-files | |
| run: | | |
| if [ "${{ github.event.inputs.extension }}" = "rally" ]; then | |
| if [ ! -f "extensions/rally/compose.override.prod.yml" ]; then | |
| echo "Error: rally extension selected but extensions/rally/compose.override.prod.yml not found (submodule not initialized?)" >&2 | |
| exit 1 | |
| fi | |
| echo "COMPOSE_FILE=compose.prod.yml,extensions/rally/compose.override.prod.yml" >> "$GITHUB_OUTPUT" | |
| elif [ "${{ github.event.inputs.extension }}" = "gala" ]; then | |
| if [ ! -f "extensions/gala/compose.override.prod.yml" ]; then | |
| echo "Error: gala extension selected but extensions/gala/compose.override.prod.yml not found (submodule not initialized?)" >&2 | |
| exit 1 | |
| fi | |
| echo "COMPOSE_FILE=compose.prod.yml,extensions/gala/compose.override.prod.yml" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "COMPOSE_FILE=compose.prod.yml" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Clear stale Docker build cache | |
| run: | | |
| echo "Clearing Docker build cache to ensure fresh builds..." | |
| docker builder prune -af --filter "until=1h" || true | |
| echo "Cache cleared" | |
| - name: Build and push | |
| uses: docker/bake-action@v5 | |
| with: | |
| push: true | |
| files: ${{ steps.compose-files.outputs.COMPOSE_FILE }} | |
| set: | | |
| *.cache-to=type=gha,scope=cached-stage,mode=max | |
| *.cache-from=type=gha,scope=cached-stage | |
| deploy: | |
| needs: build | |
| runs-on: self-hosted | |
| env: | |
| PRODUCTION: true | |
| # Secrets (genuine credentials) | |
| POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} | |
| MONGO_PASSWORD: ${{ secrets.MONGO_PASSWORD }} | |
| EMAIL_SMTP_PASSWORD: ${{ secrets.EMAIL_SMTP_PASSWORD }} | |
| IDP_SECRET_KEY: ${{ secrets.IDP_SECRET_KEY }} | |
| RECAPTCHA_SECRET_KEY: ${{ secrets.RECAPTCHA_SECRET_KEY }} | |
| R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| TEAM_JWT_SECRET_KEY: ${{ secrets.TEAM_JWT_SECRET_KEY }} | |
| OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }} | |
| AUTHENTIK_TOKEN: ${{ secrets.AUTHENTIK_TOKEN }} | |
| # Variables (public / non-sensitive config) | |
| EMAIL_SENDER_ADDRESS: ${{ vars.EMAIL_SENDER_ADDRESS }} | |
| EMAIL_SMTP_HOST: ${{ vars.EMAIL_SMTP_HOST }} | |
| EMAIL_SMTP_USER: ${{ vars.EMAIL_SMTP_USER }} | |
| R2_ENDPOINT_URL: ${{ vars.R2_ENDPOINT_URL }} | |
| R2_BUCKET: ${{ vars.R2_BUCKET }} | |
| R2_PUBLIC_BASE_URL: ${{ vars.R2_PUBLIC_BASE_URL }} | |
| OIDC_ENABLED: ${{ vars.OIDC_ENABLED || 'true' }} | |
| OIDC_CLIENT_ID: ${{ vars.OIDC_CLIENT_ID }} | |
| ENABLED_EXTENSIONS: ${{ github.event.inputs.extension }} | |
| steps: | |
| - name: Fix permissions before checkout | |
| run: | | |
| set -euo pipefail | |
| echo "Fixing permissions on root-owned files..." | |
| # Only run find commands if the current directory exists and has content | |
| if [ -d . ] && [ -n "$(ls -A . 2>/dev/null)" ]; then | |
| # Remove __pycache__ directories (they'll be regenerated) | |
| # Use -depth to process directories bottom-up to avoid errors with subdirectories | |
| find . -depth -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true | |
| # Fix ownership of any remaining root-owned files (files and directories in single pass) | |
| # Use numeric IDs to ensure correct group assignment | |
| find . -user root \( -type f -o -type d \) -exec chown $(id -u):$(id -g) {} + 2>/dev/null || true | |
| else | |
| echo "Workspace is empty, skipping permission fixes (will be handled after checkout)" | |
| fi | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| fetch-depth: 0 | |
| token: ${{ secrets.GH_PAT }} | |
| # Disable workspace cleaning to preserve .env files and permission fixes from previous runs | |
| clean: false | |
| - name: Update extension submodules to tracked branches | |
| run: | | |
| git submodule sync || true | |
| # Initialize Rally submodule (required) | |
| git submodule update --init extensions/rally || true | |
| git -C extensions/rally fetch origin main --prune || true | |
| git -C extensions/rally checkout main || true | |
| git -C extensions/rally reset --hard origin/main || true | |
| # Initialize Gala submodule | |
| git submodule update --init extensions/gala | |
| git -C extensions/gala fetch origin main --prune | |
| git -C extensions/gala checkout main | |
| git -C extensions/gala reset --hard origin/main | |
| - name: Login to DockerHub | |
| uses: docker/login-action@v2 | |
| with: | |
| username: neiaauav | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Login to Github Packages | |
| uses: docker/login-action@v2 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Deploy with docker-compose | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ github.event.inputs.extension }}" = "rally" ]; then | |
| COMPOSE_ARGS="-f compose.prod.yml -f extensions/rally/compose.override.prod.yml" | |
| elif [ "${{ github.event.inputs.extension }}" = "gala" ]; then | |
| COMPOSE_ARGS="-f compose.prod.yml -f extensions/gala/compose.override.prod.yml" | |
| else | |
| COMPOSE_ARGS="-f compose.prod.yml" | |
| fi | |
| # Pull images sequentially to avoid OOM on low-memory systems | |
| for service in $(docker compose $COMPOSE_ARGS config --services); do | |
| echo "Pulling $service..." | |
| docker compose $COMPOSE_ARGS pull "$service" || echo "Warning: Failed to pull $service, continuing..." | |
| done | |
| docker compose $COMPOSE_ARGS up -d --remove-orphans | |
| - name: Manage extensions and sync nginx | |
| run: | | |
| # Extension management script handles: | |
| # - Starting/stopping extension containers | |
| # - Database schema management | |
| # - Dev proxy config generation | |
| # - External nginx sync (Infrastructure/nginx) | |
| ENABLED_EXTENSIONS="${{ github.event.inputs.extension }}" ./scripts/manage-extensions.sh | |
| - name: Cleanup Docker resources | |
| run: | | |
| docker container prune -f | |
| docker image prune -af | |
| docker builder prune -af | |