Skip to content

merge: auth password rotation and QR code UI #2

merge: auth password rotation and QR code UI

merge: auth password rotation and QR code UI #2

Workflow file for this run

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