v3.3.2 #1383
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: Docker | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*.*.*'] | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: docker-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| GHCR_REGISTRY: ghcr.io | |
| GHCR_IMAGE_NAME: planningsup | |
| PUBLISH_WHITELIST: | | |
| main | |
| IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} | |
| jobs: | |
| # ============================================================================ | |
| # Stage 1: Detect changes and prepare build context | |
| # ============================================================================ | |
| prepare: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| web_ui_changed: ${{ steps.changes.outputs.web_ui }} | |
| should_publish: ${{ steps.publish_check.outputs.should_publish }} | |
| is_whitelisted: ${{ steps.publish_check.outputs.is_whitelisted }} | |
| is_valid_tag: ${{ steps.publish_check.outputs.is_valid_tag }} | |
| bun_version: ${{ steps.bun_version.outputs.version }} | |
| image_tags: ${{ steps.meta.outputs.tags }} | |
| image_labels: ${{ steps.meta.outputs.labels }} | |
| steps: | |
| - name: Set environment variables | |
| run: echo "BUN_INSTALL=${{ runner.temp }}/bun/install" >> $GITHUB_ENV | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version-file: '.bun-version' | |
| - name: Restore Bun cache | |
| id: bun-cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| ${{ runner.temp }}/bun/install/cache | |
| node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Save Bun cache | |
| if: steps.bun-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: | | |
| ${{ runner.temp }}/bun/install/cache | |
| node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} | |
| - name: Detect UI-related changes | |
| id: changes | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| web_ui: | |
| - 'apps/web/**' | |
| - 'apps/web/package.json' | |
| - 'playwright.config.ts' | |
| - 'playwright.dev.config.ts' | |
| - 'test/e2e/**' | |
| - name: Check branch whitelist and tag validation | |
| id: publish_check | |
| run: | | |
| echo "is_whitelisted=false" >> "$GITHUB_OUTPUT" | |
| echo "is_valid_tag=false" >> "$GITHUB_OUTPUT" | |
| echo "should_publish=false" >> "$GITHUB_OUTPUT" | |
| if [[ "${{ github.event_name }}" == "push" ]]; then | |
| if [[ "${{ github.ref }}" == refs/heads/* ]]; then | |
| BRANCH_NAME="${{ github.ref_name }}" | |
| WHITELIST="${{ env.PUBLISH_WHITELIST }}" | |
| if echo "$WHITELIST" | grep -Fxq "$BRANCH_NAME"; then | |
| echo "is_whitelisted=true" >> "$GITHUB_OUTPUT" | |
| echo "should_publish=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ Branch '$BRANCH_NAME' is whitelisted" | |
| fi | |
| elif [[ "${{ github.ref }}" == refs/tags/v*.*.* ]]; then | |
| if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "is_valid_tag=true" >> "$GITHUB_OUTPUT" | |
| echo "should_publish=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ Valid semver tag: ${{ github.ref_name }}" | |
| fi | |
| fi | |
| elif [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| SOURCE_BRANCH="${{ github.head_ref }}" | |
| WHITELIST="${{ env.PUBLISH_WHITELIST }}" | |
| if echo "$WHITELIST" | grep -Fxq "$SOURCE_BRANCH"; then | |
| echo "is_whitelisted=true" >> "$GITHUB_OUTPUT" | |
| echo "should_publish=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| if [[ "${{ env.IS_FORK_PR }}" == "true" ]]; then | |
| echo "should_publish=false" >> "$GITHUB_OUTPUT" | |
| echo "🔒 Fork PR - publishing disabled" | |
| fi | |
| - name: Read Bun version | |
| id: bun_version | |
| run: | | |
| echo "version=$(cat .bun-version)" >> $GITHUB_OUTPUT | |
| - name: Extract Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: | | |
| ${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ env.GHCR_IMAGE_NAME }} | |
| tags: | | |
| type=ref,event=branch,enable=${{ steps.publish_check.outputs.is_whitelisted == 'true' }} | |
| type=semver,pattern={{version}},enable=${{ steps.publish_check.outputs.is_valid_tag == 'true' }} | |
| type=raw,value=${{ github.head_ref }},enable=${{ github.event_name == 'pull_request' && steps.publish_check.outputs.is_whitelisted == 'true' }} | |
| # ============================================================================ | |
| # Stage 2: Build Docker image once and share via artifact | |
| # ============================================================================ | |
| build: | |
| runs-on: ubuntu-latest | |
| needs: [prepare] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build and export Docker image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| tags: planningsup-test:latest | |
| build-args: | | |
| BUN_VERSION=${{ needs.prepare.outputs.bun_version }} | |
| push: false | |
| outputs: type=docker,dest=/tmp/planningsup-image.tar | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Upload Docker image artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: docker-image | |
| path: /tmp/planningsup-image.tar | |
| retention-days: 1 | |
| compression-level: 1 | |
| # ============================================================================ | |
| # Stage 4: Integration tests (auth disabled) | |
| # ============================================================================ | |
| integration-tests: | |
| runs-on: ubuntu-latest | |
| needs: [prepare, build] | |
| services: | |
| postgres: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_PASSWORD: testpass | |
| POSTGRES_USER: testuser | |
| POSTGRES_DB: planningsup_test | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| steps: | |
| - name: Set environment variables | |
| run: echo "BUN_INSTALL=${{ runner.temp }}/bun/install" >> $GITHUB_ENV | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version-file: '.bun-version' | |
| - name: Restore Bun cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| ${{ runner.temp }}/bun/install/cache | |
| node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Download Docker image | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: docker-image | |
| path: /tmp | |
| - name: Load Docker image | |
| run: docker load --input /tmp/planningsup-image.tar | |
| - name: Start application container | |
| run: | | |
| docker run -d \ | |
| --name planningsup-app-test \ | |
| --network host \ | |
| -e NODE_ENV=production \ | |
| -e PORT=20000 \ | |
| -e PUBLIC_ORIGIN="http://localhost:20000" \ | |
| -e DATABASE_URL="postgresql://testuser:testpass@localhost:5432/planningsup_test" \ | |
| -e RUN_JOBS=false \ | |
| -e AUTH_ENABLED=false \ | |
| planningsup-test:latest | |
| echo "Waiting for application to start..." | |
| timeout 60 bash -c 'until curl -f http://localhost:20000/api/ping 2>/dev/null; do sleep 2; done' || { | |
| echo "❌ Application failed to start" | |
| docker logs planningsup-app-test | |
| exit 1 | |
| } | |
| echo "✅ Application ready" | |
| - name: Run integration tests | |
| run: BASE_URL=http://localhost:20000 bun test test/integration/api.test.ts test/integration/auth.routes.test.ts | |
| - name: Run API smoke tests | |
| run: | | |
| response=$(curl -s http://localhost:20000/api/ping) | |
| [[ "$response" == "pong" ]] || { echo "❌ Ping failed"; exit 1; } | |
| plannings=$(curl -s http://localhost:20000/api/plannings) | |
| echo "$plannings" | grep -q '^\[' || { echo "❌ Plannings failed"; exit 1; } | |
| echo "✅ Smoke tests passed" | |
| - name: Show logs on failure | |
| if: failure() | |
| run: docker logs planningsup-app-test || true | |
| - name: Cleanup | |
| if: always() | |
| run: docker rm -f planningsup-app-test || true | |
| # ============================================================================ | |
| # Stage 4: Integration tests (auth enabled) - runs in parallel | |
| # ============================================================================ | |
| auth-integration-tests: | |
| runs-on: ubuntu-latest | |
| needs: [prepare, build] | |
| services: | |
| postgres-auth: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_PASSWORD: testpass | |
| POSTGRES_USER: testuser | |
| POSTGRES_DB: planningsup_test_auth | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5434:5432 | |
| steps: | |
| - name: Set environment variables | |
| run: echo "BUN_INSTALL=${{ runner.temp }}/bun/install" >> $GITHUB_ENV | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version-file: '.bun-version' | |
| - name: Restore Bun cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| ${{ runner.temp }}/bun/install/cache | |
| node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Download Docker image | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: docker-image | |
| path: /tmp | |
| - name: Load Docker image | |
| run: docker load --input /tmp/planningsup-image.tar | |
| - name: Start auth-enabled application container | |
| run: | | |
| docker run -d \ | |
| --name planningsup-app-auth-test \ | |
| --network host \ | |
| -e NODE_ENV=production \ | |
| -e PORT=20001 \ | |
| -e PUBLIC_ORIGIN="http://localhost:20001" \ | |
| -e DATABASE_URL="postgresql://testuser:testpass@localhost:5434/planningsup_test_auth" \ | |
| -e RUN_JOBS=false \ | |
| -e AUTH_ENABLED=true \ | |
| -e BETTER_AUTH_SECRET="test-secret-for-ci-only-do-not-use-in-production" \ | |
| -e DISCORD_CLIENT_ID="test-discord-client-id" \ | |
| -e DISCORD_CLIENT_SECRET="test-discord-client-secret" \ | |
| -e GITHUB_CLIENT_ID="test-github-client-id" \ | |
| -e GITHUB_CLIENT_SECRET="test-github-client-secret" \ | |
| -e TRUSTED_ORIGINS="http://localhost:20001,http://localhost:3000" \ | |
| planningsup-test:latest | |
| echo "Waiting for auth-enabled application to start..." | |
| timeout 60 bash -c 'until curl -f http://localhost:20001/api/ping 2>/dev/null; do sleep 2; done' || { | |
| echo "❌ Auth-enabled application failed to start" | |
| docker logs planningsup-app-auth-test | |
| exit 1 | |
| } | |
| echo "✅ Auth-enabled application ready" | |
| - name: Verify AUTH_ENABLED=true | |
| run: | | |
| CONFIG=$(curl -s http://localhost:20001/config.js) | |
| if ! echo "$CONFIG" | grep -q '"authEnabled":true'; then | |
| echo "❌ AUTH_ENABLED is not true!" | |
| echo "$CONFIG" | |
| exit 1 | |
| fi | |
| echo "✅ AUTH_ENABLED=true confirmed" | |
| - name: Run auth-enabled integration tests | |
| run: BASE_URL=http://localhost:20001 bun test test/integration/auth.routes.enabled.test.ts --timeout 30000 | |
| - name: Show logs on failure | |
| if: failure() | |
| run: docker logs planningsup-app-auth-test || true | |
| - name: Cleanup | |
| if: always() | |
| run: docker rm -f planningsup-app-auth-test || true | |
| # ============================================================================ | |
| # Stage 4: E2E tests (only if web UI changed) - runs in parallel | |
| # ============================================================================ | |
| e2e-tests: | |
| runs-on: ubuntu-latest | |
| needs: [prepare, build] | |
| if: needs.prepare.outputs.web_ui_changed == 'true' | |
| services: | |
| postgres: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_PASSWORD: testpass | |
| POSTGRES_USER: testuser | |
| POSTGRES_DB: planningsup_test_e2e | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5433:5432 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Set environment variables | |
| run: | | |
| echo "BUN_INSTALL=${{ runner.temp }}/bun/install" >> $GITHUB_ENV | |
| echo "PLAYWRIGHT_BROWSERS_PATH=${{ runner.temp }}/ms-playwright" >> $GITHUB_ENV | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version-file: '.bun-version' | |
| - name: Restore Bun cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: | | |
| ${{ runner.temp }}/bun/install/cache | |
| node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Determine Playwright version | |
| id: pw | |
| run: | | |
| echo "version=$(bunx -y playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" | |
| - name: Restore Playwright browsers cache | |
| id: playwright-cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: ${{ runner.os }}-playwright-${{ steps.pw.outputs.version }} | |
| restore-keys: | | |
| ${{ runner.os }}-playwright- | |
| - name: Install Playwright browsers | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: bunx playwright install chromium | |
| - name: Save Playwright browsers cache | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: ${{ runner.os }}-playwright-${{ steps.pw.outputs.version }} | |
| - name: Download Docker image | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: docker-image | |
| path: /tmp | |
| - name: Load Docker image | |
| run: docker load --input /tmp/planningsup-image.tar | |
| - name: Start application container | |
| run: | | |
| docker run -d \ | |
| --name planningsup-app-e2e \ | |
| --network host \ | |
| -e NODE_ENV=production \ | |
| -e PORT=20000 \ | |
| -e PUBLIC_ORIGIN="http://localhost:20000" \ | |
| -e DATABASE_URL="postgresql://testuser:testpass@localhost:5433/planningsup_test_e2e" \ | |
| -e RUN_JOBS=false \ | |
| -e AUTH_ENABLED=false \ | |
| planningsup-test:latest | |
| echo "Waiting for application to start..." | |
| timeout 60 bash -c 'until curl -f http://localhost:20000/api/ping 2>/dev/null; do sleep 2; done' || { | |
| echo "❌ Application failed to start" | |
| docker logs planningsup-app-e2e | |
| exit 1 | |
| } | |
| echo "✅ Application ready" | |
| - name: Run E2E tests | |
| run: BASE_URL=http://localhost:20000 bunx playwright test --config playwright.config.ts --project=desktop --project=mobile | |
| - name: Show logs on failure | |
| if: failure() | |
| run: docker logs planningsup-app-e2e || true | |
| - name: Upload Playwright report | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: playwright-report-${{ github.run_number }} | |
| path: playwright-report/ | |
| retention-days: 7 | |
| - name: Cleanup | |
| if: always() | |
| run: docker rm -f planningsup-app-e2e || true | |
| # ============================================================================ | |
| # Stage 5: Publish (only after all tests pass) | |
| # ============================================================================ | |
| publish: | |
| runs-on: ubuntu-latest | |
| needs: [prepare, build, integration-tests, auth-integration-tests, e2e-tests] | |
| if: | | |
| always() && | |
| needs.prepare.outputs.should_publish == 'true' && | |
| needs.integration-tests.result == 'success' && | |
| needs.auth-integration-tests.result == 'success' && | |
| (needs.e2e-tests.result == 'success' || needs.e2e-tests.result == 'skipped') | |
| permissions: | |
| contents: read | |
| packages: write | |
| attestations: write | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log into GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Push Docker image | |
| id: push_image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| labels: ${{ needs.prepare.outputs.image_labels }} | |
| tags: ${{ needs.prepare.outputs.image_tags }} | |
| build-args: | | |
| BUN_VERSION=${{ needs.prepare.outputs.bun_version }} | |
| push: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Attest build provenance | |
| if: steps.push_image.outputs.digest != '' | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-name: ${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ env.GHCR_IMAGE_NAME }} | |
| subject-digest: ${{ steps.push_image.outputs.digest }} | |
| push-to-registry: true | |
| - name: Deploy to Dokploy | |
| if: steps.push_image.outcome == 'success' && needs.prepare.outputs.is_valid_tag == 'true' | |
| env: | |
| DOKPLOY_DOMAIN: ${{ secrets.DOKPLOY_DOMAIN }} | |
| DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }} | |
| DOKPLOY_APPLICATION_ID: ${{ secrets.DOKPLOY_APPLICATION_ID }} | |
| run: | | |
| curl -X 'POST' \ | |
| "https://${DOKPLOY_DOMAIN}/api/application.deploy" \ | |
| -H 'accept: application/json' \ | |
| -H 'Content-Type: application/json' \ | |
| -H "x-api-key: ${DOKPLOY_API_KEY}" \ | |
| -d "{\"applicationId\": \"${DOKPLOY_APPLICATION_ID}\"}" | |
| # ============================================================================ | |
| # Summary job (always runs) | |
| # ============================================================================ | |
| summary: | |
| runs-on: ubuntu-latest | |
| needs: [prepare, build, integration-tests, auth-integration-tests, e2e-tests] | |
| if: always() | |
| steps: | |
| - name: Build and test summary | |
| run: | | |
| echo "## 🐳 Docker Build and Test Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Configuration" >> $GITHUB_STEP_SUMMARY | |
| echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Event Type | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Ref | \`${{ github.ref }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Bun Version | \`${{ needs.prepare.outputs.bun_version }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Should Publish | ${{ needs.prepare.outputs.should_publish == 'true' && '✅ Yes' || '❌ No' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Job Results" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🏗️ Build | ${{ needs.build.result == 'success' && '✅ Passed' || needs.build.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🔗 Integration Tests | ${{ needs.integration-tests.result == 'success' && '✅ Passed' || needs.integration-tests.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🔐 Auth Integration Tests | ${{ needs.auth-integration-tests.result == 'success' && '✅ Passed' || needs.auth-integration-tests.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| 🎭 E2E Tests | ${{ needs.e2e-tests.result == 'success' && '✅ Passed' || needs.e2e-tests.result == 'skipped' && '⏭️ Skipped' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY |