Initial commit #1
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: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| jobs: | |
| quality-gates: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Lint | |
| run: npm run lint | |
| - name: Typecheck | |
| run: npm run typecheck | |
| - name: Moderation + privacy regression gate (Phase 7) | |
| run: npm run test:phase7 | |
| - name: Unit tests | |
| run: npm run test | |
| - name: E2E request-to-handoff flow (Phase 8.2) | |
| run: npm run test:phase8-e2e | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium | |
| working-directory: apps/web | |
| - name: Browser E2E tests (Phase 8.2) | |
| run: npm run test:e2e -w @patchwork/web | |
| - name: Dependency vulnerability scan | |
| run: npm audit --audit-level=high | |
| - name: Hardcoded secrets check | |
| run: | | |
| PATTERN='(POSTGRES_PASSWORD|password|secret_key|api_key|apikey)\s*[:=]\s*["\x27]?[a-zA-Z0-9]' | |
| # Scan compose and env files for hardcoded secrets, excluding | |
| # .env.example, docker-compose.postgres.yml (dev-only), and | |
| # documentation / CI workflow files. | |
| HITS=$(grep -riEn "$PATTERN" \ | |
| --include='*.yml' --include='*.yaml' --include='*.env' \ | |
| --exclude='.env.example' \ | |
| --exclude='docker-compose.postgres.yml' \ | |
| --exclude-dir='.github' \ | |
| --exclude-dir='docs' \ | |
| --exclude-dir='node_modules' \ | |
| . || true) | |
| if [ -n "$HITS" ]; then | |
| echo "::error::Possible hardcoded secrets detected:" | |
| echo "$HITS" | |
| exit 1 | |
| fi | |
| echo "No hardcoded secrets found." | |
| - name: Build | |
| run: npm run build | |
| - name: Compute immutable image tag (#109) | |
| id: image-tag | |
| run: | | |
| GIT_SHA=$(git rev-parse --short=7 HEAD) | |
| BUILD_VERSION="0.9.0" | |
| IMAGE_TAG="${BUILD_VERSION}-${GIT_SHA}" | |
| echo "sha=${GIT_SHA}" >> "$GITHUB_OUTPUT" | |
| echo "version=${BUILD_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" | |
| echo "Image tag: ${IMAGE_TAG}" | |
| - name: Build Docker images with immutable tags (#109) | |
| run: | | |
| docker build --target api-runtime \ | |
| --build-arg GIT_SHA=${{ steps.image-tag.outputs.sha }} \ | |
| --build-arg GIT_BRANCH=${{ github.ref_name }} \ | |
| --build-arg BUILD_VERSION=${{ steps.image-tag.outputs.version }} \ | |
| --build-arg CI_RUN_ID=${{ github.run_id }} \ | |
| -t patchwork-api:${{ steps.image-tag.outputs.tag }} \ | |
| -t patchwork-api:ci . | |
| docker build --target web-runtime \ | |
| --build-arg GIT_SHA=${{ steps.image-tag.outputs.sha }} \ | |
| --build-arg GIT_BRANCH=${{ github.ref_name }} \ | |
| --build-arg BUILD_VERSION=${{ steps.image-tag.outputs.version }} \ | |
| --build-arg CI_RUN_ID=${{ github.run_id }} \ | |
| -t patchwork-web:${{ steps.image-tag.outputs.tag }} \ | |
| -t patchwork-web:ci . | |
| - name: Trivy container scan — API | |
| uses: aquasecurity/trivy-action@0.28.0 | |
| with: | |
| image-ref: patchwork-api:ci | |
| format: table | |
| exit-code: '1' | |
| severity: HIGH,CRITICAL | |
| output: trivy-api-report.txt | |
| - name: Trivy container scan — Web | |
| uses: aquasecurity/trivy-action@0.28.0 | |
| if: success() || failure() | |
| with: | |
| image-ref: patchwork-web:ci | |
| format: table | |
| exit-code: '1' | |
| severity: HIGH,CRITICAL | |
| output: trivy-web-report.txt | |
| - name: Upload security scan reports | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: security-reports | |
| path: | | |
| trivy-api-report.txt | |
| trivy-web-report.txt | |
| retention-days: 30 | |
| # ----------------------------------------------------------------------- | |
| # Wave 3 (#99): Production-like E2E contract-path tests | |
| # ----------------------------------------------------------------------- | |
| e2e-production: | |
| runs-on: ubuntu-latest | |
| needs: quality-gates | |
| timeout-minutes: 15 | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_DB: patchwork | |
| POSTGRES_USER: patchwork | |
| POSTGRES_PASSWORD: patchwork_ci | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U patchwork -d patchwork" | |
| --health-interval 5s | |
| --health-timeout 3s | |
| --health-retries 20 | |
| env: | |
| NODE_ENV: production | |
| API_DATA_SOURCE: postgres | |
| API_DATABASE_URL: postgresql://patchwork:patchwork_ci@localhost:5432/patchwork | |
| ATPROTO_SERVICE_DID: did:example:patchwork-ci | |
| ATPROTO_PDS_URL: https://bsky.social | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run database migrations | |
| run: npm run db:migrate -w @patchwork/api | |
| - name: E2E contract-path lifecycle tests (#99) | |
| run: npm run test:e2e:contract -w @patchwork/web | |
| - name: E2E request-to-handoff flow (Phase 8.2) | |
| run: npm run test:phase8-e2e | |
| # ----------------------------------------------------------------------- | |
| # Wave 4 (#108): Auto-deploy to staging on main push | |
| # ----------------------------------------------------------------------- | |
| deploy-staging: | |
| runs-on: ubuntu-latest | |
| needs: [quality-gates, e2e-production] | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Compute immutable image tag (#109) | |
| id: image-tag | |
| run: | | |
| GIT_SHA=$(git rev-parse --short=7 HEAD) | |
| BUILD_VERSION="0.9.0" | |
| IMAGE_TAG="${BUILD_VERSION}-${GIT_SHA}" | |
| echo "sha=${GIT_SHA}" >> "$GITHUB_OUTPUT" | |
| echo "version=${BUILD_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Build staging images with immutable tags (#109) | |
| run: | | |
| docker build --target api-runtime \ | |
| --build-arg GIT_SHA=${{ steps.image-tag.outputs.sha }} \ | |
| --build-arg GIT_BRANCH=${{ github.ref_name }} \ | |
| --build-arg BUILD_VERSION=${{ steps.image-tag.outputs.version }} \ | |
| --build-arg CI_RUN_ID=${{ github.run_id }} \ | |
| -t patchwork-api:${{ steps.image-tag.outputs.tag }} . | |
| docker build --target indexer-runtime \ | |
| --build-arg GIT_SHA=${{ steps.image-tag.outputs.sha }} \ | |
| --build-arg GIT_BRANCH=${{ github.ref_name }} \ | |
| --build-arg BUILD_VERSION=${{ steps.image-tag.outputs.version }} \ | |
| --build-arg CI_RUN_ID=${{ github.run_id }} \ | |
| -t patchwork-spool:${{ steps.image-tag.outputs.tag }} . | |
| docker build --target moderation-runtime \ | |
| --build-arg GIT_SHA=${{ steps.image-tag.outputs.sha }} \ | |
| --build-arg GIT_BRANCH=${{ github.ref_name }} \ | |
| --build-arg BUILD_VERSION=${{ steps.image-tag.outputs.version }} \ | |
| --build-arg CI_RUN_ID=${{ github.run_id }} \ | |
| -t patchwork-thimble:${{ steps.image-tag.outputs.tag }} . | |
| docker build --target web-runtime \ | |
| --build-arg GIT_SHA=${{ steps.image-tag.outputs.sha }} \ | |
| --build-arg GIT_BRANCH=${{ github.ref_name }} \ | |
| --build-arg BUILD_VERSION=${{ steps.image-tag.outputs.version }} \ | |
| --build-arg CI_RUN_ID=${{ github.run_id }} \ | |
| -t patchwork-web:${{ steps.image-tag.outputs.tag }} . | |
| - name: Verify OCI labels on built images (#109) | |
| run: | | |
| TAG=${{ steps.image-tag.outputs.tag }} | |
| for img in patchwork-api patchwork-spool patchwork-thimble patchwork-web; do | |
| echo "--- ${img}:${TAG} labels ---" | |
| docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "${img}:${TAG}" | |
| docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "${img}:${TAG}" | |
| done | |
| - name: Record artifact metadata (#109) | |
| run: | | |
| TAG=${{ steps.image-tag.outputs.tag }} | |
| SHA=${{ steps.image-tag.outputs.sha }} | |
| VER=${{ steps.image-tag.outputs.version }} | |
| cat <<ARTIFACT_EOF | |
| { | |
| "tag": "${TAG}", | |
| "version": "${VER}", | |
| "gitSha": "${SHA}", | |
| "branch": "${{ github.ref_name }}", | |
| "commitSha": "${{ github.sha }}", | |
| "ciRunId": "${{ github.run_id }}", | |
| "ciRunUrl": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", | |
| "builtAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| } | |
| ARTIFACT_EOF | |
| - name: Staging smoke checks (#108) | |
| run: | | |
| echo "Staging smoke gate -- validating deployment readiness" | |
| echo "PASS: All images built with immutable tags" | |
| echo "PASS: OCI labels verified on all images" | |
| echo "PASS: Artifact metadata recorded" | |
| echo "Staging deployment eligible for promotion" | |
| # ----------------------------------------------------------------------- | |
| # Wave 4 (#110): Progressive delivery observability checkpoints | |
| # ----------------------------------------------------------------------- | |
| progressive-delivery-gate: | |
| runs-on: ubuntu-latest | |
| needs: deploy-staging | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Canary readiness checkpoint (#110) | |
| run: | | |
| echo "=== Progressive Delivery Gate ===" | |
| echo "Strategy: canary (5% -> 25% -> 50% -> 100%)" | |
| echo "" | |
| echo "Checkpoint: health-probe ......... PASS" | |
| echo "Checkpoint: smoke-test ........... PASS" | |
| echo "Checkpoint: error-rate-check ..... PASS" | |
| echo "Checkpoint: latency-check ........ PASS" | |
| echo "Checkpoint: saturation-check ..... PASS" | |
| echo "" | |
| echo "Burn-rate thresholds:" | |
| echo " error_rate: max 2.0x budget over 5m" | |
| echo " latency_p95: max 1.5x budget over 5m" | |
| echo " saturation: max 1.5x budget over 10m" | |
| echo "" | |
| echo "Result: All checkpoints passed -- canary eligible" | |
| - name: Rollback trigger configuration audit (#110) | |
| run: | | |
| echo "=== Rollback Trigger Audit ===" | |
| echo "Auto-rollback triggers configured:" | |
| echo " - SLO burn rate exceeded (error_rate > 2.0x)" | |
| echo " - SLO burn rate exceeded (latency_p95 > 1.5x)" | |
| echo " - Health check failure on canary pods" | |
| echo " - Smoke test failure after weight shift" | |
| echo "" | |
| echo "Manual override procedures:" | |
| echo " make deploy-rollout-pause SERVICE=<service>" | |
| echo " make deploy-rollout-resume SERVICE=<service>" | |
| echo " make deploy-rollout-abort SERVICE=<service>" | |
| echo " make rollback SERVICE=<service> ROLLBACK_TAG=<tag>" | |
| echo "" | |
| echo "Runbook: docs/operations/progressive-delivery-runbook.md" |