Skip to content

feat(auth): per-user credential scoping for hosted mode (#790) #1917

feat(auth): per-user credential scoping for hosted mode (#790)

feat(auth): per-user credential scoping for hosted mode (#790) #1917

Workflow file for this run

name: Test Suite (Unit + E2E)
on:
push:
branches: [main, develop, '0*']
pull_request:
branches: [main, develop]
workflow_call: # Allow this workflow to be called by other workflows
# Nightly browser-E2E schedule: runs the full Playwright suite (all browsers,
# all specs) against the current Phase-3+ UI. Rewritten in #684.
schedule:
- cron: '0 2 * * *'
env:
PYTHON_VERSION: '3.11'
# ESLint 10's deps require Node ^20.19 || ^22.13 || >=24; pin the minimum
# explicitly so a floating '20' tag can't resolve to an older 20.x patch.
NODE_VERSION: '20.19'
jobs:
# ============================================
# Code Quality Checks (Run First - Fast Fail)
# ============================================
code-quality:
name: Code Quality (Lint + Type Check)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- name: Create virtual environment
run: uv venv
- name: Install dependencies
run: uv sync --extra dev
- name: Run ruff (linting)
run: uv run ruff check .
- name: Run mypy (type checking)
run: uv run mypy codeframe/
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: 'web-ui/package-lock.json'
- name: Install frontend dependencies
working-directory: web-ui
run: npm ci
- name: Run frontend lint (eslint)
working-directory: web-ui
run: npm run lint
# ============================================
# Static Analysis - Check for Hardcoded URLs
# ============================================
check-hardcoded-urls:
name: Check for Hardcoded URLs
runs-on: ubuntu-latest
needs: code-quality
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Check for hardcoded localhost URLs in frontend
run: |
echo "🔍 Checking for hardcoded localhost URLs..."
# Skip if web-ui/src doesn't exist (v2 CLI-first mode)
if [ ! -d "web-ui/src" ]; then
echo "⏭️ Skipping: web-ui/src not found (v2 CLI-first mode)"
exit 0
fi
# Check for hardcoded localhost URLs with common ports (3000-9999)
# We ONLY allow: process.env.NEXT_PUBLIC_* || 'http://localhost:...' (or ws://, wss://, https://)
# We reject: 'http://localhost:...' (without env var)
# We reject: 'http://localhost:...' || process.env.NEXT_PUBLIC_* (reversed)
HARDCODED=$(grep -rn -E "localhost:[0-9]{2,5}" web-ui/src \
--include="*.ts" \
--include="*.tsx" \
--include="*.js" \
--include="*.jsx" \
| grep -v "\.test\." \
| grep -v "\.spec\." \
| grep -v "// " \
| grep -v "/\*" \
| grep -vE "process\.env\.NEXT_PUBLIC_[A-Z0-9_]+\s*\|\|\s*['\"](https?|wss?)://" \
|| true)
if [ -n "$HARDCODED" ]; then
echo "❌ Found hardcoded localhost URLs:"
echo "$HARDCODED"
echo ""
echo "💡 Fix: Use process.env.NEXT_PUBLIC_API_URL instead"
echo " Example: const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';"
echo " Note: The env var MUST come BEFORE the fallback (not reversed)"
exit 1
fi
echo "✅ No hardcoded localhost URLs found"
- name: Check for Vite-style env vars in Next.js code
run: |
echo "🔍 Checking for Vite-style environment variables..."
# Skip if web-ui/src doesn't exist (v2 CLI-first mode)
if [ ! -d "web-ui/src" ]; then
echo "⏭️ Skipping: web-ui/src not found (v2 CLI-first mode)"
exit 0
fi
# Check for window.VITE_ or import.meta.env patterns
VITE_VARS=$(grep -rn "window\.VITE_\|import\.meta\.env" web-ui/src \
--include="*.ts" \
--include="*.tsx" \
--include="*.js" \
--include="*.jsx" \
| grep -v "// " \
| grep -v "/\*" \
|| true)
if [ -n "$VITE_VARS" ]; then
echo "❌ Found Vite-style environment variables in Next.js code:"
echo "$VITE_VARS"
echo ""
echo "💡 Fix: Use process.env.NEXT_PUBLIC_* for Next.js"
echo " Next.js uses NEXT_PUBLIC_ prefix for client-side env vars"
exit 1
fi
echo "✅ No Vite-style environment variables found"
- name: Verify API files use consistent env vars
run: |
echo "🔍 Verifying API files use process.env.NEXT_PUBLIC_API_URL..."
# Skip if web-ui/src doesn't exist (v2 CLI-first mode)
if [ ! -d "web-ui/src" ]; then
echo "⏭️ Skipping: web-ui/src not found (v2 CLI-first mode)"
exit 0
fi
# This check is a heuristic to catch files making API calls to localhost
# without using environment variables. The primary hardcoded URL check above
# is more precise; this is a secondary sanity check.
#
# Note: Files that have BOTH a fetch/axios localhost call AND a process.env
# reference somewhere in the file are considered OK. This may miss edge cases
# where the env var is for something unrelated to the localhost URL.
ISSUES=""
# Check common API-related files for hardcoded URLs
for file in $(find web-ui/src -type f \( -name "*.ts" -o -name "*.tsx" \) \
-not -path "*/.next/*" \
-not -path "*/node_modules/*"); do
# Skip test files
if [[ $file == *".test."* ]] || [[ $file == *".spec."* ]]; then
continue
fi
# Check if file has fetch or axios calls with hardcoded localhost
# AND does not have any NEXT_PUBLIC env var reference
if grep -qE "fetch\(.*localhost|axios.*localhost" "$file" 2>/dev/null; then
if ! grep -q "process.env.NEXT_PUBLIC" "$file" 2>/dev/null; then
ISSUES="${ISSUES}\n - ${file}: Contains localhost URL without env var reference"
fi
fi
done
if [ -n "$ISSUES" ]; then
echo "⚠️ API files with potential issues (heuristic check):"
echo -e "$ISSUES"
echo ""
echo "💡 These files appear to use localhost URLs without environment variable configuration"
echo " This is a heuristic check - verify manually if these are intentional."
# Changed from exit 1 to warning - the primary check above is more reliable
# exit 1
else
echo "✅ All API files properly configured"
fi
# ============================================
# Backend Unit Tests (After Code Quality)
# ============================================
backend-tests:
name: Backend Unit Tests
runs-on: ubuntu-latest
needs: code-quality
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- name: Create virtual environment
run: uv venv
- name: Install dependencies
run: uv sync --extra dev
- name: Configure git for tests
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Install codeframe package
run: |
uv pip install -e .
echo "✅ Package installed in editable mode"
- name: Run pytest (full non-e2e suite) with coverage
timeout-minutes: 20
run: |
uv run pytest tests/ \
--ignore=tests/e2e \
-m "not lifecycle" \
-q \
--tb=short \
--cov=codeframe \
--cov-report=term \
--cov-report=xml \
--cov-report=html
echo "Coverage measured for reporting purposes."
echo "Note: gate runs every non-e2e test except 'lifecycle' (real-LLM,"
echo " run locally via scripts/lifecycle before opening a PR)."
- name: Upload coverage reports
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage.xml
flags: backend
name: backend-coverage
# ============================================
# Frontend Unit Tests (After Code Quality)
# ============================================
frontend-tests:
name: Frontend Unit Tests
runs-on: ubuntu-latest
needs: code-quality
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: 'web-ui/package-lock.json'
- name: Install dependencies
working-directory: web-ui
run: npm ci
- name: Build (fail PR on TypeScript/build break)
working-directory: web-ui
env:
NEXT_TELEMETRY_DISABLED: 1
run: npm run build
- name: Run Jest tests with coverage
working-directory: web-ui
run: npm run test:coverage
- name: Check coverage threshold (65%)
working-directory: web-ui
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.statements.pct')
echo "Coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 65" | bc -l) )); then
echo "❌ Coverage ${COVERAGE}% is below 65% threshold"
exit 1
else
echo "✅ Coverage ${COVERAGE}% meets 65% threshold"
fi
- name: Upload coverage reports
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
directory: web-ui/coverage
flags: frontend
name: frontend-coverage
# ============================================
# E2E Backend Tests (Pytest)
# ============================================
e2e-backend-tests:
name: E2E Backend Tests
runs-on: ubuntu-latest
# Only run on main branch or scheduled runs (not every PR)
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- name: Create virtual environment
run: uv venv
- name: Install dependencies
run: uv sync --extra dev
- name: Initialize git for E2E tests
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Install codeframe package
run: |
uv pip install -e .
echo "✅ Package installed in editable mode"
- name: Initialize database
run: |
mkdir -p .codeframe
source .venv/bin/activate
python -c "from codeframe.platform_store.database import Database; db = Database('.codeframe/state.db'); db.initialize(); db.close()"
echo "✅ Database initialized"
- name: Start FastAPI server in background
env:
DATABASE_PATH: ${{ github.workspace }}/.codeframe/state.db
WORKSPACE_ROOT: ${{ github.workspace }}
CODEFRAME_DEPLOYMENT_MODE: self_hosted
# Auth is ON by default and the server refuses to start on the default
# JWT secret (issue #643); supply a throwaway test secret so startup
# succeeds. Not a real credential — CI/E2E only.
AUTH_SECRET: ci-e2e-test-secret-not-a-real-credential
run: |
source .venv/bin/activate
python -m uvicorn codeframe.ui.server:app --port 8080 > /tmp/server.log 2>&1 &
echo "BACKEND_PID=$!" >> $GITHUB_ENV
echo "Server started with PID: $!"
- name: Verify server startup
run: |
sleep 5
if ! ps -p $BACKEND_PID > /dev/null; then
echo "❌ Server process died immediately"
cat /tmp/server.log
exit 1
fi
echo "✅ Server process is running (PID: $BACKEND_PID)"
- name: Wait for backend to be ready
run: |
echo "Waiting for server to start..."
for i in {1..120}; do
if curl -s http://localhost:8080/health > /dev/null; then
echo "✅ Server is ready!"
curl -s http://localhost:8080/health | jq .
exit 0
fi
echo "Attempt $i/120: Server not ready yet..."
sleep 1
done
echo "❌ Server failed to start within 120 seconds"
echo "=== Server Logs ==="
cat /tmp/server.log
exit 1
- name: Run E2E backend tests
run: |
uv run pytest tests/e2e/ \
-v \
--tb=short \
-m "e2e"
- name: Stop FastAPI server
if: always()
run: |
if [ -n "$BACKEND_PID" ]; then
kill $BACKEND_PID || true
fi
- name: Upload E2E test reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-backend-reports
path: |
tests/e2e/fixtures/
.pytest_cache/
- name: Upload server logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-backend-server-logs
path: /tmp/server.log
retention-days: 7
# ============================================
# E2E Browser Tests (Playwright) — rewritten for the Phase-3+ UI (#684)
# ============================================
# `playwright.config.ts` (tests/e2e) starts the backend (uv uvicorn) and the
# frontend (next build + start) itself via its `webServer` block, and
# `global-setup.ts` seeds a workspace + login user. So these jobs only install
# deps + browsers and run Playwright.
#
# - smoke: chromium, @smoke only, on every PR/push (gates merges via summary)
# - full: all browsers, all specs, nightly schedule
e2e-browser-smoke:
name: E2E Browser Smoke (Chromium)
runs-on: ubuntu-latest
needs: code-quality
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- name: Install Python deps
run: |
uv venv
uv sync --extra dev
uv pip install -e .
- name: Configure git (review diff needs a repo)
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: 'web-ui/package-lock.json'
- name: Install frontend deps
working-directory: web-ui
run: npm ci
- name: Install E2E deps
working-directory: tests/e2e
run: npm ci
- name: Install Playwright browser (chromium)
working-directory: tests/e2e
run: npx playwright install --with-deps chromium
- name: Run Playwright smoke suite
working-directory: tests/e2e
run: npx playwright test --project=chromium --grep @smoke
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-browser-smoke-report
path: tests/e2e/playwright-report/
retention-days: 7
e2e-browser-full:
name: E2E Browser Full (All Browsers)
runs-on: ubuntu-latest
# Nightly only — the full cross-browser sweep is too heavy for every PR.
if: github.event_name == 'schedule'
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- name: Install Python deps
run: |
uv venv
uv sync --extra dev
uv pip install -e .
- name: Configure git (review diff needs a repo)
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: 'web-ui/package-lock.json'
- name: Install frontend deps
working-directory: web-ui
run: npm ci
- name: Install E2E deps
working-directory: tests/e2e
run: npm ci
- name: Install Playwright browsers (all)
working-directory: tests/e2e
run: npx playwright install --with-deps
- name: Run full Playwright suite
working-directory: tests/e2e
run: npx playwright test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-browser-full-report
path: tests/e2e/playwright-report/
retention-days: 7
# ============================================
# Test Summary
# ============================================
test-summary:
name: Test Summary
runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests, code-quality, check-hardcoded-urls, e2e-browser-smoke]
if: always()
steps:
- name: Report test results
run: |
echo "## Test Suite Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Code Quality | ${{ needs.code-quality.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Hardcoded URLs | ${{ needs.check-hardcoded-urls.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Backend Tests | ${{ needs.backend-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Frontend Tests | ${{ needs.frontend-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| E2E Browser Smoke | ${{ needs.e2e-browser-smoke.result }} |" >> $GITHUB_STEP_SUMMARY
# The smoke job is a merge gate: treat any non-success terminal state
# (failure / cancelled / timed_out) as a gate failure, not just
# "failure". Other jobs keep the file's existing "failure"-only check.
if [ "${{ needs.code-quality.result }}" == "failure" ] || \
[ "${{ needs.check-hardcoded-urls.result }}" == "failure" ] || \
[ "${{ needs.backend-tests.result }}" == "failure" ] || \
[ "${{ needs.e2e-browser-smoke.result }}" == "failure" ] || \
[ "${{ needs.e2e-browser-smoke.result }}" == "cancelled" ] || \
[ "${{ needs.e2e-browser-smoke.result }}" == "timed_out" ] || \
[ "${{ needs.frontend-tests.result }}" == "failure" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "❌ Some checks failed. Please review the logs above." >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ All checks passed!" >> $GITHUB_STEP_SUMMARY
fi