Skip to content

v3.3.2

v3.3.2 #1383

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