Skip to content

Merge pull request #30 from TXT0Law/law_unstable #101

Merge pull request #30 from TXT0Law/law_unstable

Merge pull request #30 from TXT0Law/law_unstable #101

Workflow file for this run

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."