Merge pull request #30 from TXT0Law/law_unstable #101
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 Pipeline | |
| on: | |
| push: | |
| branches: ["**"] | |
| pull_request: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test-required: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Self-test require-tests gate | |
| run: bash scripts/ci/test-require-tests.sh | |
| - name: Enforce test file requirement for feature changes | |
| env: | |
| BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} | |
| HEAD_SHA: ${{ github.sha }} | |
| run: | | |
| bash scripts/ci/require-tests.sh "${BASE_SHA}" "${HEAD_SHA}" | |
| lint: | |
| needs: test-required | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: pnpm | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install frontend dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Install backend dependencies | |
| run: | | |
| cd backend | |
| UV_LINK_MODE=copy uv sync --extra dev | |
| - name: Install osint-engine dependencies | |
| run: | | |
| cd backend/scan | |
| npm ci | |
| - name: Lint frontend | |
| run: pnpm lint | |
| - name: Lint backend | |
| run: | | |
| cd backend | |
| UV_LINK_MODE=copy uv run ruff check . | |
| - name: Lint osint-engine | |
| run: | | |
| cd backend/scan | |
| npm run lint | |
| harness: | |
| needs: test-required | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install backend dependencies | |
| run: | | |
| cd backend | |
| UV_LINK_MODE=copy uv sync --extra dev | |
| - name: Run harness checks | |
| env: | |
| BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} | |
| HEAD_SHA: ${{ github.sha }} | |
| run: bash scripts/ci/check-harness.sh "${BASE_SHA}" "${HEAD_SHA}" | |
| frontend: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build frontend and enforce bundle budgets | |
| run: | | |
| pnpm build | |
| pnpm check:bundle | |
| - name: Run frontend tests with coverage | |
| run: bash scripts/ci/test.sh frontend | |
| - name: Build frontend coverage summary | |
| if: always() | |
| run: | | |
| mkdir -p coverage-summary | |
| if [ -f coverage/coverage-summary.json ]; then | |
| node -e "const fs=require('fs'); const s=JSON.parse(fs.readFileSync('coverage/coverage-summary.json','utf8')); const pct=s?.total?.lines?.pct; fs.writeFileSync('coverage-summary/frontend.txt', Number.isFinite(pct)?String(pct):'unavailable');" | |
| else | |
| echo unavailable > coverage-summary/frontend.txt | |
| fi | |
| - name: Upload frontend coverage artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-frontend | |
| if-no-files-found: ignore | |
| path: | | |
| coverage | |
| coverage-summary/frontend.txt | |
| - name: Upload frontend artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: frontend-test-artifacts | |
| if-no-files-found: ignore | |
| path: | | |
| coverage | |
| test-results | |
| playwright-report | |
| backend: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: backend | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install backend dependencies | |
| run: UV_LINK_MODE=copy uv sync --extra dev | |
| - name: Run backend tests with coverage | |
| run: bash ../scripts/ci/test.sh backend | |
| - name: Build backend coverage summary | |
| if: always() | |
| run: | | |
| mkdir -p coverage-summary | |
| python - <<'PY' | |
| import xml.etree.ElementTree as ET | |
| from pathlib import Path | |
| output = Path("coverage-summary/backend.txt") | |
| coverage_xml = Path("coverage.xml") | |
| if not coverage_xml.exists(): | |
| output.write_text("unavailable", encoding="utf-8") | |
| else: | |
| root = ET.parse(coverage_xml).getroot() | |
| line_rate = root.attrib.get("line-rate") | |
| if line_rate is None: | |
| output.write_text("unavailable", encoding="utf-8") | |
| else: | |
| pct = round(float(line_rate) * 100, 2) | |
| output.write_text(str(pct), encoding="utf-8") | |
| PY | |
| - name: Upload backend coverage artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-backend | |
| if-no-files-found: ignore | |
| path: | | |
| backend/coverage.xml | |
| backend/coverage-summary/backend.txt | |
| - name: Upload backend artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: backend-test-artifacts | |
| if-no-files-found: ignore | |
| path: | | |
| backend/.pytest_cache | |
| backend/.coverage | |
| backend/coverage.xml | |
| migrations: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: orbicheck_migrations | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres -d orbicheck_migrations" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 12 | |
| env: | |
| DATABASE_URL: postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/orbicheck_migrations | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install backend dependencies | |
| run: | | |
| cd backend | |
| UV_LINK_MODE=copy uv sync --extra dev | |
| - name: Verify single migration head and upgrade empty database | |
| run: | | |
| cd backend | |
| test "$(UV_LINK_MODE=copy uv run alembic heads | wc -l | tr -d ' ')" = "1" | |
| UV_LINK_MODE=copy uv run alembic upgrade head | |
| osint-engine: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: backend/scan | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: backend/scan/package-lock.json | |
| - name: Install scan service dependencies | |
| run: npm ci | |
| - name: Run osint-engine tests with coverage | |
| run: bash ../../scripts/ci/test.sh osint | |
| - name: Build osint-engine coverage summary | |
| if: always() | |
| run: | | |
| mkdir -p coverage-summary | |
| if [ -f coverage/coverage-summary.json ]; then | |
| node -e "const fs=require('fs'); const s=JSON.parse(fs.readFileSync('coverage/coverage-summary.json','utf8')); const pct=s?.total?.lines?.pct; fs.writeFileSync('coverage-summary/osint.txt', Number.isFinite(pct)?String(pct):'unavailable');" | |
| else | |
| echo unavailable > coverage-summary/osint.txt | |
| fi | |
| - name: Upload osint-engine coverage artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-osint | |
| if-no-files-found: ignore | |
| path: | | |
| backend/scan/coverage | |
| backend/scan/coverage-summary/osint.txt | |
| - name: Upload osint-engine artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: osint-engine-test-artifacts | |
| if-no-files-found: ignore | |
| path: | | |
| backend/scan/coverage | |
| scanner: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install backend dependencies | |
| run: | | |
| cd backend | |
| UV_LINK_MODE=copy uv sync --extra dev | |
| - name: Run scanner security and readiness tests | |
| run: | | |
| cd backend | |
| UV_LINK_MODE=copy uv run python -m unittest discover \ | |
| -s ../docker/scanner -p 'test_*.py' | |
| connected-e2e: | |
| needs: lint | |
| runs-on: ubuntu-latest | |
| env: | |
| CI: "true" | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| redis: | |
| image: redis:7 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: pnpm | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install frontend dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Install backend dependencies | |
| run: | | |
| cd backend | |
| UV_LINK_MODE=copy uv sync --extra dev | |
| - name: Install scan service dependencies | |
| run: | | |
| cd backend/scan | |
| npm ci | |
| - name: Install Playwright Chromium | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Document connected service ports | |
| run: | | |
| echo "scan-service: http://127.0.0.1:4000" | |
| echo "linked backend: http://127.0.0.1:8010" | |
| echo "Next.js: http://127.0.0.1:3101" | |
| - name: Run connected Playwright tests | |
| run: pnpm exec playwright test --config=playwright.config.ts | |
| - name: Upload connected Playwright artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: connected-e2e-artifacts | |
| if-no-files-found: ignore | |
| path: | | |
| playwright-report | |
| test-results | |
| coverage-summary: | |
| if: ${{ github.event_name == 'pull_request' }} | |
| needs: [frontend, backend, osint-engine] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - name: Download coverage artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: coverage-* | |
| merge-multiple: true | |
| path: coverage-input | |
| - name: Upsert sticky PR coverage comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- orbicheck-coverage-summary -->'; | |
| const readValue = (file) => { | |
| const path = `coverage-input/${file}`; | |
| if (!fs.existsSync(path)) { | |
| return { label: 'unavailable', numeric: null }; | |
| } | |
| const raw = fs.readFileSync(path, 'utf8').trim(); | |
| const numeric = Number(raw); | |
| if (Number.isFinite(numeric)) { | |
| return { label: `${numeric.toFixed(2)}%`, numeric }; | |
| } | |
| return { label: 'unavailable', numeric: null }; | |
| }; | |
| const frontend = readValue('frontend.txt'); | |
| const backend = readValue('backend.txt'); | |
| const osint = readValue('osint.txt'); | |
| const values = [frontend.numeric, backend.numeric, osint.numeric]; | |
| let overall = 'improved'; | |
| if (values.some((value) => value === null)) { | |
| overall = 'unavailable'; | |
| } else if (values.some((value) => value < 70)) { | |
| overall = 'regressed'; | |
| } | |
| const body = `${marker} | |
| ## Coverage Summary | |
| | Service | Coverage | | |
| | --- | --- | | |
| | Frontend coverage | ${frontend.label} | | |
| | Backend coverage | ${backend.label} | | |
| | OSINT coverage | ${osint.label} | | |
| **Overall status:** ${overall} | |
| `; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find((comment) => | |
| comment.user.type === 'Bot' && comment.body.includes(marker) | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number, | |
| body, | |
| }); | |
| } | |
| quality-gate: | |
| if: ${{ always() }} | |
| needs: [test-required, lint, harness, frontend, backend, migrations, osint-engine, scanner, connected-e2e] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Enforce lint and test gate | |
| run: | | |
| if [ "${{ needs.test-required.result }}" != "success" ] || \ | |
| [ "${{ needs.lint.result }}" != "success" ] || \ | |
| [ "${{ needs.harness.result }}" != "success" ] || \ | |
| [ "${{ needs.frontend.result }}" != "success" ] || \ | |
| [ "${{ needs.backend.result }}" != "success" ] || \ | |
| [ "${{ needs.migrations.result }}" != "success" ] || \ | |
| [ "${{ needs.osint-engine.result }}" != "success" ] || \ | |
| [ "${{ needs.scanner.result }}" != "success" ] || \ | |
| [ "${{ needs.connected-e2e.result }}" != "success" ]; then | |
| echo "Quality gate failed. lint, Harness, coverage-gated tests, migrations, scanner, and connected E2E must be green." | |
| exit 1 | |
| fi | |
| echo "Quality gate passed." |