diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 464c282..81ab5b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -360,3 +360,98 @@ jobs: name: playwright-report path: apps/web/playwright-report/ retention-days: 14 + + # ────────────────────────────────────────────── + # Fresh Docker Compose deploy test — validates + # that the prod compose bundle comes up clean + # from scratch and the smoke tests pass. + # ────────────────────────────────────────────── + docker-deploy-test: + name: Fresh-Linux Docker Deploy + needs: [build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create minimal .env + run: | + cat <<'EOF' > .env + NEXTAUTH_URL=http://localhost:3100 + NEXTAUTH_SECRET=ci-test-secret-minimum-32-chars-xx + PGADMIN_PASSWORD=ci-pgadmin + EOF + + - name: Start infrastructure (postgres + redis) + run: docker compose up -d postgres redis + + - name: Wait for postgres + run: | + for i in $(seq 1 20); do + docker compose exec -T postgres pg_isready -U capakraken -d capakraken && break + sleep 3 + done + + - name: Build and start app (full profile) + run: docker compose --profile full up -d --build app + + - name: Wait for /api/health (up to 3 minutes) + run: | + for i in $(seq 1 36); do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3100/api/health || echo "000") + echo "Attempt $i: HTTP $STATUS" + if [ "$STATUS" = "200" ]; then exit 0; fi + sleep 5 + done + echo "Health check timed out" + docker compose logs app --tail=50 + exit 1 + + - name: Verify health response contains status ok + run: | + BODY=$(curl -sf http://localhost:3100/api/health) + echo "$BODY" + echo "$BODY" | grep '"status":"ok"' + + - name: Seed admin user + run: | + docker compose exec -T app node /app/scripts/setup-admin.mjs \ + --email admin@capakraken.dev \ + --name "Admin" \ + --password admin123 + + - name: Set up Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Playwright and Chromium + run: | + npm install -g @playwright/test@1.49 + playwright install chromium --with-deps + + - name: Run smoke tests + run: npx playwright test --config apps/web/playwright.ci.config.ts + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-smoke-report + path: apps/web/playwright-report/ + retention-days: 7 + + - name: Show logs on failure + if: failure() + run: docker compose logs --tail=100 + + # ────────────────────────────────────────────── + # Release images — only on push to main, after + # every check has passed. Calls the reusable + # release-image.yml workflow. + # ────────────────────────────────────────────── + release-images: + name: Release Images + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: [lint, test, e2e, assistant-split, docker-deploy-test] + uses: ./.github/workflows/release-image.yml + secrets: inherit diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml deleted file mode 100644 index ad29102..0000000 --- a/.github/workflows/deploy-test.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Docker Deploy Test - -on: - push: - branches: [main] - paths-ignore: - - "docs/**" - - ".gitea/**" - - "**/*.md" - - "LICENSE" - pull_request: - branches: [main] - paths-ignore: - - "docs/**" - - ".gitea/**" - - "**/*.md" - - "LICENSE" - -concurrency: - group: deploy-test-${{ github.ref }} - cancel-in-progress: true - -jobs: - docker-deploy-test: - name: Fresh-Linux Docker Deploy - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Create minimal .env - run: | - cat <<'EOF' > .env - NEXTAUTH_URL=http://localhost:3100 - NEXTAUTH_SECRET=ci-test-secret-minimum-32-chars-xx - PGADMIN_PASSWORD=ci-pgadmin - EOF - - - name: Start infrastructure (postgres + redis) - run: docker compose up -d postgres redis - - - name: Wait for postgres - run: | - for i in $(seq 1 20); do - docker compose exec -T postgres pg_isready -U capakraken -d capakraken && break - sleep 3 - done - - - name: Build and start app (full profile) - run: docker compose --profile full up -d --build app - - - name: Wait for /api/health (up to 3 minutes) - run: | - for i in $(seq 1 36); do - STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3100/api/health || echo "000") - echo "Attempt $i: HTTP $STATUS" - if [ "$STATUS" = "200" ]; then exit 0; fi - sleep 5 - done - echo "Health check timed out" - docker compose logs app --tail=50 - exit 1 - - - name: Verify health response contains status ok - run: | - BODY=$(curl -sf http://localhost:3100/api/health) - echo "$BODY" - echo "$BODY" | grep '"status":"ok"' - - - name: Seed admin user - run: | - docker compose exec -T app node /app/scripts/setup-admin.mjs \ - --email admin@capakraken.dev \ - --name "Admin" \ - --password admin123 - - - name: Set up Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Install Playwright and Chromium - run: | - npm install -g @playwright/test@1.49 - playwright install chromium --with-deps - - - name: Run smoke tests - run: npx playwright test --config apps/web/playwright.ci.config.ts - - - name: Upload Playwright report - if: failure() - uses: actions/upload-artifact@v4 - with: - name: playwright-smoke-report - path: apps/web/playwright-report/ - retention-days: 7 - - - name: Show logs on failure - if: failure() - run: docker compose logs --tail=100 diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index 29e9a8d..c851f40 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -1,13 +1,19 @@ name: Release Image +# Reusable workflow: called from ci.yml after all checks pass. +# Can also be dispatched manually for rebuilds or tag overrides. on: - push: - branches: [main] - paths-ignore: - - "docs/**" - - ".gitea/**" - - "**/*.md" - - "LICENSE" + workflow_call: + inputs: + image_tag: + description: Optional tag override, defaults to sha- + required: false + type: string + secrets: + GHCR_USERNAME: + required: true + GHCR_TOKEN: + required: true workflow_dispatch: inputs: image_tag: diff --git a/scripts/check-architecture-guardrails.mjs b/scripts/check-architecture-guardrails.mjs index 228e8aa..b6e6a33 100644 --- a/scripts/check-architecture-guardrails.mjs +++ b/scripts/check-architecture-guardrails.mjs @@ -663,8 +663,8 @@ export const rules = [ file: ".github/workflows/release-image.yml", required: [ { - pattern: /push:\s*\n\s*branches:\s*\[main\]/, - message: "image releases must build automatically on pushes to main", + pattern: /workflow_call:/, + message: "release workflow must remain callable as a reusable workflow from ci.yml", }, { pattern: /workflow_dispatch:/, @@ -708,6 +708,14 @@ export const rules = [ pattern: /run:\s+pnpm db:generate/, message: "CI must route Prisma client generation through the workspace env/schema wrapper", }, + { + pattern: /uses:\s+\.\/\.github\/workflows\/release-image\.yml/, + message: "ci.yml must chain release-image.yml so image builds run after checks pass", + }, + { + pattern: /github\.event_name == 'push' && github\.ref == 'refs\/heads\/main'/, + message: "release-images job must be gated to main-branch pushes to avoid PR image pushes", + }, ], forbidden: [ {