merge: auth password rotation and QR code UI #2
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: [develop, staging, main, master] | |
| jobs: | |
| k8s-lint: | |
| name: Kubernetes Manifest Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install kustomize | |
| run: | | |
| curl -sL "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash | |
| sudo mv kustomize /usr/local/bin/ | |
| - name: Render dev overlay | |
| id: render-dev | |
| run: kustomize build k8s/overlays/dev > /tmp/dev-manifests.yaml | |
| - name: Render prod overlay | |
| id: render-prod | |
| run: kustomize build k8s/overlays/prod > /tmp/prod-manifests.yaml | |
| - name: Reject placeholder secret values | |
| run: | | |
| for f in /tmp/dev-manifests.yaml /tmp/prod-manifests.yaml; do | |
| if grep -qE 'change-me|USER:PASS|placeholder' "$f"; then | |
| echo "ERROR: $f contains placeholder secret values" | |
| grep -nE 'change-me|USER:PASS|placeholder' "$f" | |
| exit 1 | |
| fi | |
| done | |
| echo "No placeholder secrets found." | |
| - name: Require TLS on all Ingress resources | |
| run: | | |
| python3 - <<'EOF' | |
| import sys, yaml | |
| failed = False | |
| for path in ["/tmp/dev-manifests.yaml", "/tmp/prod-manifests.yaml"]: | |
| with open(path) as f: | |
| docs = list(yaml.safe_load_all(f)) | |
| for doc in docs: | |
| if doc and doc.get("kind") == "Ingress": | |
| name = doc.get("metadata", {}).get("name", "<unknown>") | |
| tls = doc.get("spec", {}).get("tls") | |
| if not tls: | |
| print(f"ERROR: Ingress '{name}' in {path} has no TLS block") | |
| failed = True | |
| if failed: | |
| sys.exit(1) | |
| print("All Ingress resources have TLS configured.") | |
| EOF | |
| - name: Validate cert-manager issuer names | |
| run: | | |
| # Check that prod overlay does not reference staging issuer | |
| if grep -r "letsencrypt-staging" k8s/overlays/prod/; then | |
| echo "ERROR: Production overlay references staging Let's Encrypt issuer" | |
| exit 1 | |
| fi | |
| echo "✓ Production overlay uses correct cert-manager issuer" | |
| - name: Reject dev namespace references in non-dev overlays | |
| run: | | |
| if grep -n "socialflow-dev" /tmp/prod-manifests.yaml; then | |
| echo "ERROR: prod overlay's rendered manifests reference the socialflow-dev namespace" | |
| exit 1 | |
| fi | |
| echo "✓ No non-dev overlay references the socialflow-dev namespace" | |
| security: | |
| name: Security Vulnerability Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: "20" | |
| - name: Audit root workspace dependencies | |
| run: npm audit --audit-level=high | |
| - name: Audit backend dependencies | |
| working-directory: backend | |
| run: npm audit --audit-level=high | |
| - name: Detect Snyk token availability | |
| id: snyk-token | |
| run: echo "available=${{ secrets.SNYK_TOKEN != '' }}" >> "$GITHUB_OUTPUT" | |
| - name: Run Snyk scan (frontend) | |
| if: steps.snyk-token.outputs.available == 'true' | |
| uses: snyk/actions/node@master | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| with: | |
| args: --severity-threshold=high --file=package-lock.json | |
| - name: Run Snyk scan (backend) | |
| if: steps.snyk-token.outputs.available == 'true' | |
| uses: snyk/actions/node@master | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| with: | |
| args: --severity-threshold=high --file=backend/package-lock.json | |
| - name: Snyk scan fallback notice | |
| if: steps.snyk-token.outputs.available != 'true' | |
| run: | | |
| echo "SNYK_TOKEN is not available in this workflow context." | |
| echo "Dependency audit checks still ran; Snyk steps were skipped." | |
| coverage: | |
| name: Test Coverage | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: package-lock.json | |
| - name: Cache backend dependencies | |
| uses: actions/cache@v6 | |
| with: | |
| path: backend/node_modules | |
| key: ${{ runner.os }}-backend-${{ hashFiles('backend/package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-backend- | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Validate Jest config and project discovery | |
| working-directory: backend | |
| run: | | |
| node -e "require('./jest.config.js')" && echo "jest.config.js: parse OK" | |
| npx jest --showConfig 2>/dev/null | node -e " | |
| const chunks = []; process.stdin.on('data', c => chunks.push(c)); | |
| process.stdin.on('end', () => { | |
| const cfg = JSON.parse(chunks.join('')); | |
| const projects = cfg.configs.map(c => c.displayName?.name ?? c.displayName); | |
| const expected = ['unit', 'e2e', 'db', 'mocks']; | |
| const missing = expected.filter(p => !projects.includes(p)); | |
| if (missing.length) { console.error('Missing projects:', missing); process.exit(1); } | |
| console.log('Projects discovered:', projects.join(', ')); | |
| }); | |
| " | |
| - name: Generate Prisma client | |
| working-directory: backend | |
| run: npx prisma generate | |
| - name: Run tests with coverage | |
| working-directory: backend | |
| run: npm run test:ci | |
| env: | |
| NODE_ENV: test | |
| JWT_SECRET: ci-test-secret-32-characters-long | |
| JWT_REFRESH_SECRET: ci-refresh-secret-32-characters!! | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| TWITTER_API_KEY: test-key | |
| TWITTER_API_SECRET: test-secret | |
| - name: Upload coverage report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: backend/coverage/ | |
| retention-days: 30 | |
| - name: Post coverage summary | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { loadSummary, formatComment, THRESHOLDS } = await import('${{ github.workspace }}/.github/scripts/post-coverage-comment.mjs'); | |
| const summary = loadSummary('backend/coverage/coverage-summary.json'); | |
| if (!summary) return; | |
| if (context.eventName !== 'pull_request') return; | |
| const body = formatComment(summary, THRESHOLDS); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| - name: Enforce coverage thresholds | |
| run: node .github/scripts/post-coverage-comment.mjs backend/coverage/coverage-summary.json |