ci: add release-gate pipeline, fix CI duplication, add OSS playbook docs
- Add release-gate.yml: 11-job pipeline (lint, tests, Docker build/smoke, Trivy scan, dep audit, audit checks, e2e API+UI, PR summary comment) - Fix CI double-trigger on release branches (push + PR) - Add CODE_OF_CONDUCT.md, SECURITY.md, PR template - Add docs: git-workflow, architecture (ADR), release, operations, community - Add profiles/fastapi-vue for automated audit checks - Add docs/PROCESSES.md: index of 19 available processes
This commit is contained in:
parent
6d3453dc7f
commit
941028e705
22 changed files with 2450 additions and 1 deletions
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
32
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
## Type
|
||||
|
||||
- [ ] Feature (`feature/*`)
|
||||
- [ ] Bug fix (`fix/*`)
|
||||
- [ ] Hotfix (`hotfix/*`)
|
||||
- [ ] Documentation
|
||||
- [ ] Refactoring
|
||||
- [ ] CI/CD
|
||||
- [ ] Other: ___
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- 1-3 sentences: what changed and why -->
|
||||
|
||||
## Related issues
|
||||
|
||||
<!-- Closes #123, Fixes #456 -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Branch follows naming convention (`feature/`, `fix/`, `hotfix/`)
|
||||
- [ ] Commits follow [Conventional Commits](docs/git-workflow/commit-conventions.md)
|
||||
- [ ] Tests added/updated for the change
|
||||
- [ ] All tests pass (`pytest tests/ -v` + `npm run test:run`)
|
||||
- [ ] Linting passes (`ruff check .` + `npx eslint src/`)
|
||||
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
|
||||
- [ ] Documentation updated if behavior changed
|
||||
- [ ] No secrets or credentials committed
|
||||
|
||||
## Screenshots / Evidence
|
||||
|
||||
<!-- If UI change: before/after screenshots. If API change: curl example or test output. -->
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -2,7 +2,7 @@ name: CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 'release/**']
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main, 'release/**']
|
||||
|
||||
|
|
|
|||
776
.github/workflows/release-gate.yml
vendored
Normal file
776
.github/workflows/release-gate.yml
vendored
Normal file
|
|
@ -0,0 +1,776 @@
|
|||
name: Release Gate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['release/**']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
# Only run on PRs from release branches
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: release-gate-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Phase 1 — Parallel validation (no Docker needed)
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
lint-typecheck:
|
||||
name: Lint & type-check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: document-parser/requirements.txt
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install backend deps
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r document-parser/requirements.txt
|
||||
pip install ruff
|
||||
|
||||
- name: Install frontend deps
|
||||
run: cd frontend && npm ci
|
||||
|
||||
- name: Backend lint
|
||||
run: cd document-parser && ruff check .
|
||||
|
||||
- name: Frontend lint
|
||||
run: cd frontend && npx eslint src/
|
||||
|
||||
- name: Frontend type-check
|
||||
run: cd frontend && npm run type-check
|
||||
|
||||
unit-tests:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: document-parser/requirements.txt
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install system deps
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends poppler-utils
|
||||
|
||||
- name: Backend tests
|
||||
run: |
|
||||
cd document-parser
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-asyncio httpx ruff
|
||||
pytest tests/ -v --tb=short 2>&1 | tee /tmp/backend-tests.txt
|
||||
|
||||
- name: Frontend tests
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
npm run test:run 2>&1 | tee /tmp/frontend-tests.txt
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-test-results
|
||||
path: /tmp/*-tests.txt
|
||||
|
||||
dep-audit:
|
||||
name: Dependency audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: document-parser/requirements.txt
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
cd document-parser && pip install --upgrade pip && pip install -r requirements.txt && pip install pip-audit
|
||||
cd ../frontend && npm ci
|
||||
|
||||
- name: Python audit
|
||||
id: pip_audit
|
||||
continue-on-error: true
|
||||
run: |
|
||||
cd document-parser
|
||||
pip-audit --format=json --output=/tmp/pip-audit.json 2>&1 || true
|
||||
pip-audit 2>&1 | tee /tmp/pip-audit.txt
|
||||
# Check for critical vulnerabilities
|
||||
if pip-audit --format=json 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
vulns = data.get('dependencies', [])
|
||||
crits = [v for dep in vulns for v in dep.get('vulns', []) if 'CRITICAL' in v.get('fix_versions', [''])]
|
||||
sys.exit(1 if crits else 0)
|
||||
" 2>/dev/null; then
|
||||
echo "critical=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "critical=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Node audit
|
||||
id: npm_audit
|
||||
continue-on-error: true
|
||||
run: |
|
||||
cd frontend
|
||||
npm audit --json > /tmp/npm-audit.json 2>&1 || true
|
||||
npm audit 2>&1 | tee /tmp/npm-audit.txt
|
||||
# Check for critical vulnerabilities
|
||||
CRITICAL_COUNT=$(npm audit --json 2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
print(data.get('metadata', {}).get('vulnerabilities', {}).get('critical', 0))
|
||||
except: print(0)
|
||||
" 2>/dev/null || echo "0")
|
||||
if [ "$CRITICAL_COUNT" -gt 0 ]; then
|
||||
echo "critical=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "critical=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Upload audit results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dep-audit-results
|
||||
path: /tmp/*-audit.*
|
||||
|
||||
- name: Fail on critical vulnerabilities
|
||||
if: steps.pip_audit.outputs.critical == 'true' || steps.npm_audit.outputs.critical == 'true'
|
||||
run: |
|
||||
echo "::error::Critical vulnerabilities found in dependencies"
|
||||
exit 1
|
||||
|
||||
audit-checks:
|
||||
name: Audit checks (commands.sh)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run automated audit checks
|
||||
id: audit
|
||||
run: |
|
||||
bash profiles/fastapi-vue/commands.sh 2>&1 | tee /tmp/audit-checks.txt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Parse audit results
|
||||
id: results
|
||||
if: always()
|
||||
run: |
|
||||
PASS=$(grep -c "PASS" /tmp/audit-checks.txt || echo "0")
|
||||
WARN=$(grep -c "WARN" /tmp/audit-checks.txt || echo "0")
|
||||
FAIL=$(grep -c "FAIL" /tmp/audit-checks.txt || echo "0")
|
||||
echo "pass=$PASS" >> "$GITHUB_OUTPUT"
|
||||
echo "warn=$WARN" >> "$GITHUB_OUTPUT"
|
||||
echo "fail=$FAIL" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload audit results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: audit-checks-results
|
||||
path: /tmp/audit-checks.txt
|
||||
|
||||
- name: Fail on audit failures
|
||||
if: steps.audit.outcome == 'failure'
|
||||
run: |
|
||||
echo "::error::Audit checks failed — review profiles/fastapi-vue/commands.sh output"
|
||||
exit 1
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Phase 2 — Docker build & validation
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
docker-build:
|
||||
name: Docker build — ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [remote, local]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract version from branch
|
||||
id: version
|
||||
run: |
|
||||
# release/1.2.3 → 1.2.3
|
||||
BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
|
||||
VERSION=$(echo "$BRANCH" | sed 's|release/||')
|
||||
echo "value=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: ${{ matrix.target }}
|
||||
load: true
|
||||
tags: docling-studio:${{ matrix.target }}
|
||||
build-args: APP_VERSION=${{ steps.version.outputs.value }}
|
||||
cache-from: type=gha,scope=${{ matrix.target }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.target }}
|
||||
|
||||
- name: Save image
|
||||
run: |
|
||||
docker save docling-studio:${{ matrix.target }} | gzip > /tmp/docling-studio-${{ matrix.target }}.tar.gz
|
||||
|
||||
- name: Upload image artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-image-${{ matrix.target }}
|
||||
path: /tmp/docling-studio-${{ matrix.target }}.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
- name: Record image size
|
||||
run: |
|
||||
SIZE_BYTES=$(docker image inspect docling-studio:${{ matrix.target }} --format='{{.Size}}')
|
||||
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
|
||||
echo "$SIZE_MB" > /tmp/image-size-${{ matrix.target }}.txt
|
||||
echo "Image size (${{ matrix.target }}): ${SIZE_MB} MB"
|
||||
|
||||
- name: Upload size artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: image-size-${{ matrix.target }}
|
||||
path: /tmp/image-size-${{ matrix.target }}.txt
|
||||
|
||||
docker-smoke:
|
||||
name: Docker smoke test
|
||||
needs: [docker-build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download remote image
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-image-remote
|
||||
path: /tmp
|
||||
|
||||
- name: Load remote image
|
||||
run: docker load < /tmp/docling-studio-remote.tar.gz
|
||||
|
||||
- name: Smoke test — remote
|
||||
run: |
|
||||
docker run -d --name smoke-remote -p 3000:3000 \
|
||||
-e RATE_LIMIT_RPM=0 \
|
||||
docling-studio:remote
|
||||
|
||||
echo "Waiting for container to start..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:3000/api/health > /dev/null 2>&1; then
|
||||
echo "Remote image healthy!"
|
||||
curl -s http://localhost:3000/api/health | python3 -m json.tool
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "::error::Remote image failed health check"
|
||||
docker logs smoke-remote
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Verify response fields
|
||||
HEALTH=$(curl -s http://localhost:3000/api/health)
|
||||
ENGINE=$(echo "$HEALTH" | python3 -c "import sys,json; print(json.load(sys.stdin)['engine'])")
|
||||
STATUS=$(echo "$HEALTH" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
|
||||
|
||||
if [ "$STATUS" != "ok" ]; then
|
||||
echo "::error::Health status is '$STATUS', expected 'ok'"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ENGINE" != "remote" ]; then
|
||||
echo "::error::Engine is '$ENGINE', expected 'remote'"
|
||||
exit 1
|
||||
fi
|
||||
echo "Remote smoke test passed"
|
||||
|
||||
docker stop smoke-remote && docker rm smoke-remote
|
||||
|
||||
image-scan:
|
||||
name: Security scan — ${{ matrix.target }}
|
||||
needs: [docker-build]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [remote, local]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download image
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-image-${{ matrix.target }}
|
||||
path: /tmp
|
||||
|
||||
- name: Load image
|
||||
run: docker load < /tmp/docling-studio-${{ matrix.target }}.tar.gz
|
||||
|
||||
- name: Run Trivy — CRITICAL (blocking)
|
||||
uses: aquasecurity/trivy-action@0.28.0
|
||||
with:
|
||||
image-ref: docling-studio:${{ matrix.target }}
|
||||
format: table
|
||||
exit-code: 1
|
||||
severity: CRITICAL
|
||||
output: /tmp/trivy-critical-${{ matrix.target }}.txt
|
||||
|
||||
- name: Run Trivy — HIGH (informational)
|
||||
if: always()
|
||||
uses: aquasecurity/trivy-action@0.28.0
|
||||
with:
|
||||
image-ref: docling-studio:${{ matrix.target }}
|
||||
format: table
|
||||
exit-code: 0
|
||||
severity: HIGH
|
||||
output: /tmp/trivy-high-${{ matrix.target }}.txt
|
||||
|
||||
- name: Annotate HIGH vulnerabilities
|
||||
if: always()
|
||||
run: |
|
||||
HIGH_COUNT=$(grep -c "HIGH" /tmp/trivy-high-${{ matrix.target }}.txt 2>/dev/null || echo "0")
|
||||
if [ "$HIGH_COUNT" -gt 0 ]; then
|
||||
echo "::warning::${{ matrix.target }} image has $HIGH_COUNT HIGH severity vulnerabilities — review Trivy report"
|
||||
else
|
||||
echo "No HIGH vulnerabilities found in ${{ matrix.target }} image"
|
||||
fi
|
||||
|
||||
- name: Upload Trivy reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: trivy-${{ matrix.target }}
|
||||
path: /tmp/trivy-*-${{ matrix.target }}.txt
|
||||
|
||||
image-size:
|
||||
name: Image size check
|
||||
needs: [docker-build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download size artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: image-size-*
|
||||
merge-multiple: true
|
||||
path: /tmp/sizes
|
||||
|
||||
- name: Get previous release sizes
|
||||
id: prev
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Find the latest release tag
|
||||
PREV_TAG=$(gh release list --repo "${{ github.repository }}" --limit 1 --json tagName -q '.[0].tagName' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "No previous release found — skipping delta check"
|
||||
echo "remote=0" >> "$GITHUB_OUTPUT"
|
||||
echo "local=0" >> "$GITHUB_OUTPUT"
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Previous release: $PREV_TAG"
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Pull previous images and get sizes
|
||||
for target in remote local; do
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${PREV_TAG#v}-${target}"
|
||||
if docker pull "$IMAGE" 2>/dev/null; then
|
||||
PREV_SIZE=$(docker image inspect "$IMAGE" --format='{{.Size}}')
|
||||
PREV_MB=$((PREV_SIZE / 1024 / 1024))
|
||||
echo "${target}=${PREV_MB}" >> "$GITHUB_OUTPUT"
|
||||
echo "Previous $target size: ${PREV_MB} MB"
|
||||
else
|
||||
echo "Could not pull $IMAGE — skipping $target delta"
|
||||
echo "${target}=0" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Compare sizes
|
||||
id: delta
|
||||
run: |
|
||||
for target in remote local; do
|
||||
CURRENT=$(cat /tmp/sizes/image-size-${target}.txt 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$target" = "remote" ]; then
|
||||
PREV="${{ steps.prev.outputs.remote }}"
|
||||
else
|
||||
PREV="${{ steps.prev.outputs.local }}"
|
||||
fi
|
||||
|
||||
if [ "$PREV" -gt 0 ] 2>/dev/null; then
|
||||
DELTA=$((CURRENT - PREV))
|
||||
DELTA_PCT=$((DELTA * 100 / PREV))
|
||||
echo "${target}_current=${CURRENT}" >> "$GITHUB_OUTPUT"
|
||||
echo "${target}_prev=${PREV}" >> "$GITHUB_OUTPUT"
|
||||
echo "${target}_delta=${DELTA}" >> "$GITHUB_OUTPUT"
|
||||
echo "${target}_delta_pct=${DELTA_PCT}" >> "$GITHUB_OUTPUT"
|
||||
echo "$target: ${CURRENT}MB (was ${PREV}MB, delta: ${DELTA_PCT}%)"
|
||||
|
||||
if [ "$DELTA_PCT" -gt 10 ]; then
|
||||
echo "::warning::$target image grew by ${DELTA_PCT}% (${PREV}MB → ${CURRENT}MB)"
|
||||
elif [ "$DELTA_PCT" -lt -10 ]; then
|
||||
echo "::notice::$target image shrank by ${DELTA_PCT#-}% (${PREV}MB → ${CURRENT}MB)"
|
||||
fi
|
||||
else
|
||||
echo "${target}_current=${CURRENT}" >> "$GITHUB_OUTPUT"
|
||||
echo "${target}_prev=n/a" >> "$GITHUB_OUTPUT"
|
||||
echo "${target}_delta=n/a" >> "$GITHUB_OUTPUT"
|
||||
echo "${target}_delta_pct=n/a" >> "$GITHUB_OUTPUT"
|
||||
echo "$target: ${CURRENT}MB (no previous baseline)"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Save size report
|
||||
run: |
|
||||
cat > /tmp/image-size-report.txt <<EOF
|
||||
Image Size Report
|
||||
=================
|
||||
remote: ${{ steps.delta.outputs.remote_current }}MB (prev: ${{ steps.delta.outputs.remote_prev }}MB, delta: ${{ steps.delta.outputs.remote_delta_pct }}%)
|
||||
local: ${{ steps.delta.outputs.local_current }}MB (prev: ${{ steps.delta.outputs.local_prev }}MB, delta: ${{ steps.delta.outputs.local_delta_pct }}%)
|
||||
EOF
|
||||
cat /tmp/image-size-report.txt
|
||||
|
||||
- name: Upload size report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: image-size-report
|
||||
path: /tmp/image-size-report.txt
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Phase 3 — E2E tests on built images
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
e2e-api:
|
||||
name: E2E API tests (full scope)
|
||||
needs: [docker-smoke]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
|
||||
- name: Generate test PDFs
|
||||
run: |
|
||||
pip install fpdf2 pypdfium2
|
||||
python e2e/generate-test-data.py
|
||||
|
||||
- name: Start stack
|
||||
run: docker compose up -d --wait --build
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
RATE_LIMIT_RPM: "0"
|
||||
|
||||
- name: Wait for health
|
||||
run: |
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:3000/api/health > /dev/null 2>&1; then
|
||||
echo "Backend healthy"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for backend... ($i/30)"
|
||||
sleep 5
|
||||
done
|
||||
echo "Backend failed to start"
|
||||
docker compose logs
|
||||
exit 1
|
||||
|
||||
- name: Run E2E API tests (full scope)
|
||||
run: |
|
||||
mvn test -f e2e/api/pom.xml \
|
||||
-DbaseUrl=http://localhost:3000 \
|
||||
-Dkarate.options="--tags @smoke,@regression,@e2e"
|
||||
|
||||
- name: Upload Karate reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: karate-api-reports
|
||||
path: e2e/api/target/karate-reports/
|
||||
|
||||
- name: Tear down
|
||||
if: always()
|
||||
run: docker compose down
|
||||
|
||||
e2e-ui:
|
||||
name: E2E UI tests (@critical)
|
||||
needs: [docker-smoke]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: temurin
|
||||
cache: maven
|
||||
|
||||
- name: Install Chrome
|
||||
uses: browser-actions/setup-chrome@v1
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
- name: Generate test PDFs
|
||||
run: |
|
||||
pip install fpdf2 pypdfium2
|
||||
python e2e/generate-test-data.py
|
||||
|
||||
- name: Start stack
|
||||
run: docker compose up -d --wait --build
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
RATE_LIMIT_RPM: "0"
|
||||
|
||||
- name: Wait for health
|
||||
run: |
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:3000/api/health > /dev/null 2>&1; then
|
||||
echo "Backend healthy"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for backend... ($i/30)"
|
||||
sleep 5
|
||||
done
|
||||
echo "Backend failed to start"
|
||||
docker compose logs
|
||||
exit 1
|
||||
|
||||
- name: Run critical UI tests
|
||||
run: >
|
||||
mvn test -f e2e/ui/pom.xml
|
||||
-DbaseUrl=http://localhost:3000
|
||||
-DuiBaseUrl=http://localhost:3000
|
||||
-Dkarate.options="--tags @critical"
|
||||
|
||||
- name: Upload Karate UI reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: karate-ui-reports
|
||||
path: e2e/ui/target/karate-reports/
|
||||
|
||||
- name: Tear down
|
||||
if: always()
|
||||
run: docker compose down
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Phase 4 — Release summary (comment on PR)
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
release-summary:
|
||||
name: Release summary
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
needs:
|
||||
- lint-typecheck
|
||||
- unit-tests
|
||||
- dep-audit
|
||||
- audit-checks
|
||||
- docker-build
|
||||
- docker-smoke
|
||||
- image-scan
|
||||
- image-size
|
||||
- e2e-api
|
||||
- e2e-ui
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/artifacts
|
||||
|
||||
- name: Build summary comment
|
||||
id: summary
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Job results from needs context
|
||||
LINT="${{ needs.lint-typecheck.result }}"
|
||||
TESTS="${{ needs.unit-tests.result }}"
|
||||
DEP="${{ needs.dep-audit.result }}"
|
||||
AUDIT="${{ needs.audit-checks.result }}"
|
||||
BUILD="${{ needs.docker-build.result }}"
|
||||
SMOKE="${{ needs.docker-smoke.result }}"
|
||||
SCAN="${{ needs.image-scan.result }}"
|
||||
SIZE="${{ needs.image-size.result }}"
|
||||
E2E_API="${{ needs.e2e-api.result }}"
|
||||
E2E_UI="${{ needs.e2e-ui.result }}"
|
||||
|
||||
# Determine verdict
|
||||
BLOCKING_FAILED="false"
|
||||
for result in "$LINT" "$TESTS" "$BUILD" "$SMOKE" "$SCAN" "$E2E_API" "$E2E_UI"; do
|
||||
if [ "$result" = "failure" ]; then
|
||||
BLOCKING_FAILED="true"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$BLOCKING_FAILED" = "true" ]; then
|
||||
VERDICT="NO-GO"
|
||||
VERDICT_ICON="🔴"
|
||||
elif [ "$DEP" = "failure" ] || [ "$AUDIT" = "failure" ]; then
|
||||
VERDICT="GO CONDITIONAL"
|
||||
VERDICT_ICON="🟡"
|
||||
else
|
||||
VERDICT="GO"
|
||||
VERDICT_ICON="🟢"
|
||||
fi
|
||||
|
||||
# Status emoji helper
|
||||
status_icon() {
|
||||
case "$1" in
|
||||
success) echo "✅" ;;
|
||||
failure) echo "❌" ;;
|
||||
skipped) echo "⏭️" ;;
|
||||
cancelled) echo "🚫" ;;
|
||||
*) echo "❓" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Read image sizes
|
||||
REMOTE_SIZE=$(cat /tmp/artifacts/image-size-remote/image-size-remote.txt 2>/dev/null || echo "?")
|
||||
LOCAL_SIZE=$(cat /tmp/artifacts/image-size-local/image-size-local.txt 2>/dev/null || echo "?")
|
||||
|
||||
# Read size report
|
||||
SIZE_REPORT=$(cat /tmp/artifacts/image-size-report/image-size-report.txt 2>/dev/null || echo "No size data available")
|
||||
|
||||
# Read audit summary
|
||||
AUDIT_PASS=$(grep -c "PASS" /tmp/artifacts/audit-checks-results/audit-checks.txt 2>/dev/null || echo "?")
|
||||
AUDIT_WARN=$(grep -c "WARN" /tmp/artifacts/audit-checks-results/audit-checks.txt 2>/dev/null || echo "?")
|
||||
AUDIT_FAIL=$(grep -c "FAIL" /tmp/artifacts/audit-checks-results/audit-checks.txt 2>/dev/null || echo "?")
|
||||
|
||||
# Build the comment
|
||||
cat > /tmp/summary.md << ENDOFCOMMENT
|
||||
## ${VERDICT_ICON} Release Gate — ${VERDICT}
|
||||
|
||||
### Validation Results
|
||||
|
||||
| Check | Status | Blocking |
|
||||
|-------|--------|----------|
|
||||
| Lint & type-check | $(status_icon "$LINT") | Yes |
|
||||
| Unit tests | $(status_icon "$TESTS") | Yes |
|
||||
| Dependency audit | $(status_icon "$DEP") | Yes (CRITICAL) |
|
||||
| Audit checks | $(status_icon "$AUDIT") | Yes |
|
||||
| Docker build | $(status_icon "$BUILD") | Yes |
|
||||
| Docker smoke test | $(status_icon "$SMOKE") | Yes |
|
||||
| Image security scan | $(status_icon "$SCAN") | Yes (CRITICAL) |
|
||||
| Image size check | $(status_icon "$SIZE") | No |
|
||||
| E2E API (full scope) | $(status_icon "$E2E_API") | Yes |
|
||||
| E2E UI (@critical) | $(status_icon "$E2E_UI") | Yes |
|
||||
|
||||
### Audit Checks Summary
|
||||
|
||||
| PASS | WARN | FAIL |
|
||||
|------|------|------|
|
||||
| ${AUDIT_PASS} | ${AUDIT_WARN} | ${AUDIT_FAIL} |
|
||||
|
||||
### Docker Image Sizes
|
||||
|
||||
| Target | Size |
|
||||
|--------|------|
|
||||
| remote | ${REMOTE_SIZE} MB |
|
||||
| local | ${LOCAL_SIZE} MB |
|
||||
|
||||
<details>
|
||||
<summary>Size delta vs previous release</summary>
|
||||
|
||||
\`\`\`
|
||||
${SIZE_REPORT}
|
||||
\`\`\`
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
<sub>Generated by <b>release-gate.yml</b> — see <a href="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}">workflow run</a> for full details</sub>
|
||||
ENDOFCOMMENT
|
||||
|
||||
- name: Post or update PR comment
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
COMMENT_TAG="<!-- release-gate-summary -->"
|
||||
|
||||
# Check for existing comment
|
||||
EXISTING_COMMENT_ID=$(gh api \
|
||||
"repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||
--jq ".[] | select(.body | contains(\"$COMMENT_TAG\")) | .id" \
|
||||
2>/dev/null | head -1)
|
||||
|
||||
BODY=$(cat /tmp/summary.md)
|
||||
BODY="${COMMENT_TAG}
|
||||
${BODY}"
|
||||
|
||||
if [ -n "$EXISTING_COMMENT_ID" ]; then
|
||||
gh api \
|
||||
"repos/${{ github.repository }}/issues/comments/${EXISTING_COMMENT_ID}" \
|
||||
-X PATCH \
|
||||
-f body="$BODY"
|
||||
echo "Updated existing comment #${EXISTING_COMMENT_ID}"
|
||||
else
|
||||
gh api \
|
||||
"repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||
-f body="$BODY"
|
||||
echo "Posted new comment"
|
||||
fi
|
||||
43
CODE_OF_CONDUCT.md
Normal file
43
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers at **[INSERT CONTACT EMAIL]**.
|
||||
|
||||
All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
|
||||
49
SECURITY.md
Normal file
49
SECURITY.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| 0.3.x | Yes |
|
||||
| < 0.3 | No |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**Do NOT open a public GitHub issue for security vulnerabilities.**
|
||||
|
||||
Instead, please report them privately:
|
||||
|
||||
1. **Email**: Send a detailed report to **[INSERT SECURITY EMAIL]**
|
||||
2. **GitHub Security Advisory**: Use [GitHub's private vulnerability reporting](https://github.com/scub-france/Docling-Studio/security/advisories/new)
|
||||
|
||||
### What to include
|
||||
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Affected component(s): `document-parser/`, `frontend/`, `docker-compose.yml`, etc.
|
||||
- Impact assessment (data exposure, denial of service, privilege escalation, etc.)
|
||||
- Suggested fix (if any)
|
||||
|
||||
### Response timeline
|
||||
|
||||
| Step | SLA |
|
||||
|------|-----|
|
||||
| Acknowledgment | < 48 hours |
|
||||
| Initial assessment | < 7 days |
|
||||
| Fix developed | < 14 days (critical), < 30 days (other) |
|
||||
| Public disclosure | After fix is released |
|
||||
|
||||
### Process
|
||||
|
||||
1. We acknowledge your report and assign a severity level
|
||||
2. We develop a fix in a **private branch** (never pushed publicly before the advisory)
|
||||
3. We release the fix and publish a GitHub Security Advisory
|
||||
4. We credit the reporter (unless they prefer anonymity)
|
||||
|
||||
## Security Best Practices (for contributors)
|
||||
|
||||
- Never commit secrets, API keys, or credentials
|
||||
- Never disable CORS or security middleware without review
|
||||
- Validate all user input at the API boundary
|
||||
- Keep dependencies up to date (`pip audit`, `npm audit`)
|
||||
- Follow the [OWASP Top 10](https://owasp.org/www-project-top-ten/) guidelines
|
||||
179
docs/PROCESSES.md
Normal file
179
docs/PROCESSES.md
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
# Available Processes
|
||||
|
||||
Index of all documented processes in Docling Studio. Each process is a structured, repeatable workflow with a clear trigger and deliverable.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
| # | Process | Trigger / Command | Doc | Output |
|
||||
|---|---------|-------------------|-----|--------|
|
||||
| 1 | **Commit** | Every commit | [commit-conventions.md](git-workflow/commit-conventions.md) | Conventional Commit message |
|
||||
| 2 | **Code Review** | Every PR | [code-review-checklist.md](git-workflow/code-review-checklist.md) | Reviewed PR with checklist |
|
||||
| 3 | **Merge** | PR approved + CI green | [merge-policy.md](git-workflow/merge-policy.md) | Squash/merge commit on target branch |
|
||||
| 4 | **ADR** | Architecture decision needed | [adr-guide.md](architecture/adr-guide.md) + [adr-template.md](architecture/adr-template.md) | `docs/architecture/adrs/ADR-NNN-*.md` |
|
||||
|
||||
### How to invoke
|
||||
|
||||
```
|
||||
# Ask for a code review
|
||||
Review cette PR avec docs/git-workflow/code-review-checklist.md
|
||||
|
||||
# Create an ADR
|
||||
Crée un ADR pour [la décision à documenter]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality & Audit
|
||||
|
||||
| # | Process | Trigger / Command | Doc | Output |
|
||||
|---|---------|-------------------|-----|--------|
|
||||
| 5 | **Full Release Audit** | Before merging `release/*` to `main` | [docs/audit/master.md](audit/master.md) | 12 audit reports + summary in `reports/release-X.Y.Z/` |
|
||||
| 6 | **Single Audit** | Targeted check on one axis | [docs/audit/audits/*.md](audit/audits/) | Single audit report |
|
||||
| 7 | **Re-Audit** | After fixing CRIT/MAJ findings | [docs/audit/master.md](audit/master.md) | Updated report |
|
||||
| 8 | **Automated Checks** | Quick validation before audit | [profiles/fastapi-vue/commands.sh](../profiles/fastapi-vue/commands.sh) | PASS/WARN/FAIL per check |
|
||||
|
||||
### How to invoke
|
||||
|
||||
```
|
||||
# Full audit
|
||||
Audite la branche release/X.Y.Z en suivant docs/audit/master.md
|
||||
|
||||
# Single audit
|
||||
Exécute l'audit docs/audit/audits/08-security.md sur la branche courante
|
||||
|
||||
# Re-audit after fixes
|
||||
Re-audite les écarts CRIT et MAJ du rapport docs/audit/reports/release-X.Y.Z/summary.md
|
||||
|
||||
# Automated checks (shell)
|
||||
bash profiles/fastapi-vue/commands.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Release & Deploy
|
||||
|
||||
| # | Process | Trigger / Command | Doc | Output |
|
||||
|---|---------|-------------------|-----|--------|
|
||||
| 9 | **Release Gate** | Auto on push/PR `release/*` → `main` | [release-gate.yml](../.github/workflows/release-gate.yml) | GO / GO CONDITIONAL / NO-GO comment on PR |
|
||||
| 10 | **Release** | Feature freeze on `release/*` | [CONTRIBUTING.md](../CONTRIBUTING.md#release-process) | Tag `vX.Y.Z`, Docker images on ghcr.io |
|
||||
| 11 | **Deployment** | After release tag | [deployment-checklist.md](release/deployment-checklist.md) | Running instance at new version |
|
||||
| 12 | **Rollback** | Post-deploy failure detected | [rollback-playbook.md](release/rollback-playbook.md) | Reverted to last known good version |
|
||||
| 13 | **Hotfix** | Critical bug on released version | [CONTRIBUTING.md](../CONTRIBUTING.md#hotfix) | Patch release `vX.Y.Z+1` |
|
||||
|
||||
### Release Gate details
|
||||
|
||||
The release gate runs **automatically** on every push to `release/**` and on PRs targeting `main`. It validates 10 checks in 4 phases:
|
||||
|
||||
```
|
||||
Phase 1 (parallel) Phase 2 (Docker) Phase 3 (E2E) Phase 4
|
||||
┌──────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ lint+typecheck│ │ docker build │──────▶│ e2e API │───▶│ release │
|
||||
│ unit tests │ │ docker smoke │──────▶│ e2e UI │ │ summary │
|
||||
│ dep audit │ │ image scan │ └─────────────┘ │ (PR comment)│
|
||||
│ audit checks │ │ image size │ └─────────────┘
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
| Check | Blocks merge? | Details |
|
||||
|-------|---------------|---------|
|
||||
| Lint & type-check | Yes | ruff + ESLint + vue-tsc |
|
||||
| Unit tests | Yes | pytest + Vitest |
|
||||
| Dep audit | Yes (CRITICAL) | pip-audit + npm audit |
|
||||
| Audit checks | Yes | `profiles/fastapi-vue/commands.sh` |
|
||||
| Docker build | Yes | Both targets (remote + local) |
|
||||
| Docker smoke | Yes | Start container, verify `/api/health` |
|
||||
| Image scan (Trivy) | Yes (CRITICAL) | HIGH = warning annotation |
|
||||
| Image size | No (warning) | Delta vs previous release, alert if > 10% |
|
||||
| E2E API | Yes | `@smoke,@regression,@e2e` (full scope) |
|
||||
| E2E UI | Yes | `@critical` |
|
||||
|
||||
**Verdict**: posted as a comment on the release PR:
|
||||
- **GO** — all checks pass
|
||||
- **GO CONDITIONAL** — blocking checks pass, dep audit or audit checks have warnings
|
||||
- **NO-GO** — at least one blocking check failed
|
||||
|
||||
### How to invoke
|
||||
|
||||
```
|
||||
# Release gate runs automatically — no manual trigger needed
|
||||
# Just push to release/* or open a PR release/* → main
|
||||
|
||||
# Prepare a release
|
||||
Prépare la release X.Y.Z en suivant CONTRIBUTING.md#release-process
|
||||
|
||||
# Deploy
|
||||
Déploie en suivant docs/release/deployment-checklist.md
|
||||
|
||||
# Rollback
|
||||
Rollback en suivant docs/release/rollback-playbook.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Operations
|
||||
|
||||
| # | Process | Trigger / Command | Doc | Output |
|
||||
|---|---------|-------------------|-----|--------|
|
||||
| 14 | **Incident Response** | Service down or degraded | [incident-response.md](operations/incident-response.md) | Mitigation + post-mortem |
|
||||
| 15 | **Security Vulnerability** | Vuln reported | [security-response.md](operations/security-response.md) | Fix + advisory |
|
||||
| 16 | **Monitoring Setup** | New deployment | [monitoring-checklist.md](operations/monitoring-checklist.md) | Monitoring configured |
|
||||
|
||||
### How to invoke
|
||||
|
||||
```
|
||||
# Incident
|
||||
Gère l'incident SEV-1 en suivant docs/operations/incident-response.md
|
||||
|
||||
# Security vulnerability
|
||||
Traite la vulnérabilité signalée en suivant docs/operations/security-response.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Community & Governance
|
||||
|
||||
| # | Process | Trigger / Command | Doc | Output |
|
||||
|---|---------|-------------------|-----|--------|
|
||||
| 17 | **Onboarding** | New contributor | [onboarding-guide.md](community/onboarding-guide.md) | First PR merged |
|
||||
| 18 | **Issue Triage** | New issue opened | [issue-triage-process.md](community/issue-triage-process.md) | Labeled + prioritized issue |
|
||||
| 19 | **Roadmap Update** | Release cycle planning | [roadmap-template.md](community/roadmap-template.md) | Updated roadmap |
|
||||
|
||||
### How to invoke
|
||||
|
||||
```
|
||||
# Triage issues
|
||||
Trie les issues ouvertes en suivant docs/community/issue-triage-process.md
|
||||
|
||||
# Update roadmap
|
||||
Mets à jour la roadmap en suivant docs/community/roadmap-template.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Category | Processes | Key doc |
|
||||
|----------|-----------|---------|
|
||||
| **Dev** | Commit, Review, Merge, ADR | `docs/git-workflow/` |
|
||||
| **Quality** | Audit (full/single/re-audit), Auto-checks | `docs/audit/master.md` |
|
||||
| **Release** | Release, Deploy, Rollback, Hotfix | `docs/release/` + `CONTRIBUTING.md` |
|
||||
| **Ops** | Incident, Security, Monitoring | `docs/operations/` |
|
||||
| **Community** | Onboarding, Triage, Roadmap | `docs/community/` |
|
||||
|
||||
---
|
||||
|
||||
## Standards & References
|
||||
|
||||
These are not processes but reference documents used by the processes above:
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [CONTRIBUTING.md](../CONTRIBUTING.md) | Dev setup, branching, release, versioning |
|
||||
| [coding-standards.md](architecture/coding-standards.md) | Naming, style, architecture rules |
|
||||
| [e2e/CONVENTIONS.md](../e2e/CONVENTIONS.md) | Karate / Karate UI test conventions |
|
||||
| [architecture.md](architecture.md) | System architecture (backend + frontend) |
|
||||
| [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) | Community behavior standards |
|
||||
| [SECURITY.md](../SECURITY.md) | Vulnerability reporting policy |
|
||||
| [profiles/fastapi-vue/profile.md](../profiles/fastapi-vue/profile.md) | Stack layer mapping for audits |
|
||||
52
docs/architecture/adr-guide.md
Normal file
52
docs/architecture/adr-guide.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Architecture Decision Records (ADR) — Guide
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An ADR is a short document that captures a significant architectural decision, along with the context and consequences. ADRs create a **decision log** so future contributors understand *why* the codebase looks the way it does.
|
||||
|
||||
## When to Write an ADR
|
||||
|
||||
Write an ADR when:
|
||||
|
||||
- Choosing or replacing a framework, library, or tool
|
||||
- Changing the architecture (new layer, new pattern, new boundary)
|
||||
- Making a trade-off that future developers might question
|
||||
- Deciding NOT to do something (these are often the most valuable)
|
||||
|
||||
Do NOT write an ADR for:
|
||||
|
||||
- Implementation details that are obvious from the code
|
||||
- Formatting or style choices (those go in [coding-standards.md](coding-standards.md))
|
||||
- Bug fixes or minor refactors
|
||||
|
||||
## How to Write an ADR
|
||||
|
||||
1. Copy `adr-template.md` to `docs/architecture/adrs/ADR-NNN-short-title.md`
|
||||
2. Number sequentially (ADR-001, ADR-002, ...)
|
||||
3. Fill in all sections — especially **Context** (the *why*) and **Alternatives Considered**
|
||||
4. Set status to `Proposed`
|
||||
5. Open a PR for team review
|
||||
6. Once merged, update status to `Accepted`
|
||||
|
||||
## Existing Decisions (captured retroactively)
|
||||
|
||||
These decisions were made before the ADR process was introduced. They are documented here for context:
|
||||
|
||||
| Decision | Rationale | Date |
|
||||
|----------|-----------|------|
|
||||
| Clean Architecture (hexagonal) for backend | Decouple domain from framework — enable converter swapping (local/remote) | 2025-01 |
|
||||
| FastAPI over Django | Async-first, lightweight, Pydantic-native — better fit for a single-purpose API | 2025-01 |
|
||||
| Vue 3 + Pinia over React | Composition API + built-in reactivity — smaller bundle for this use case | 2025-01 |
|
||||
| SQLite over PostgreSQL | Single-file DB, zero ops — appropriate for a document processing tool | 2025-01 |
|
||||
| Karate over Playwright for e2e | Team expertise, unified API+UI testing, JVM ecosystem | 2026-03 |
|
||||
| Feature flags via `/api/health` | No external service needed — backend capabilities drive frontend UI | 2026-03 |
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
Proposed → Accepted → [Deprecated | Superseded by ADR-NNN]
|
||||
```
|
||||
|
||||
- **Deprecated**: The decision is no longer relevant (feature removed, context changed)
|
||||
- **Superseded**: A newer ADR replaces this one (link to the new ADR)
|
||||
- Never delete an ADR — mark it as deprecated/superseded instead
|
||||
39
docs/architecture/adr-template.md
Normal file
39
docs/architecture/adr-template.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# ADR-NNN: [Title]
|
||||
|
||||
**Date**: YYYY-MM-DD
|
||||
**Status**: Proposed | Accepted | Deprecated | Superseded by ADR-NNN
|
||||
**Deciders**: [names]
|
||||
|
||||
## Context
|
||||
|
||||
<!-- What is the issue we're seeing that is motivating this decision? What forces are at play? -->
|
||||
|
||||
## Decision
|
||||
|
||||
<!-- What is the change we're proposing and/or doing? -->
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ...
|
||||
|
||||
### Negative
|
||||
|
||||
- ...
|
||||
|
||||
### Neutral
|
||||
|
||||
- ...
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Alternative 1: [Name]
|
||||
|
||||
- **Pros**: ...
|
||||
- **Cons**: ...
|
||||
- **Why rejected**: ...
|
||||
|
||||
## References
|
||||
|
||||
- [Link to issue, RFC, or discussion]
|
||||
88
docs/architecture/coding-standards.md
Normal file
88
docs/architecture/coding-standards.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Coding Standards
|
||||
|
||||
Conventions for writing consistent, readable code across the Docling Studio codebase.
|
||||
|
||||
## Python (Backend — `document-parser/`)
|
||||
|
||||
### Tooling
|
||||
|
||||
| Tool | Purpose | Config |
|
||||
|------|---------|--------|
|
||||
| **Ruff** | Linting + formatting | `ruff.toml` / `pyproject.toml` |
|
||||
| **pytest** | Testing | `pytest.ini` / `pyproject.toml` |
|
||||
| **mypy** (optional) | Type checking | — |
|
||||
|
||||
### Naming
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| Modules | `snake_case` | `analysis_repo.py` |
|
||||
| Classes | `PascalCase` | `AnalysisJob`, `DocumentConverter` |
|
||||
| Functions / methods | `snake_case` | `create_analysis()` |
|
||||
| Constants | `UPPER_SNAKE_CASE` | `MAX_CONCURRENT_ANALYSES` |
|
||||
| Private | `_leading_underscore` | `_build_converter()` |
|
||||
|
||||
### Style Rules
|
||||
|
||||
- Max function length: **30 lines** (soft limit — justify longer ones)
|
||||
- Max file length: **300 lines** (split into modules if exceeded)
|
||||
- Imports: standard library → third-party → local, separated by blank lines
|
||||
- Type hints on all public functions
|
||||
- Docstrings only on non-obvious public APIs (don't state the obvious)
|
||||
- No `# type: ignore` without a comment explaining why
|
||||
|
||||
### Architecture Rules
|
||||
|
||||
- **Domain layer** (`domain/`): zero imports from `api/`, `persistence/`, `infra/`
|
||||
- **Persistence layer** (`persistence/`): only imports from `domain/`
|
||||
- **API layer** (`api/`): never imports from `persistence/` directly — goes through `services/`
|
||||
- **Services** (`services/`): orchestrate, don't implement — delegate to domain and infra
|
||||
|
||||
## TypeScript / Vue (Frontend — `frontend/src/`)
|
||||
|
||||
### Tooling
|
||||
|
||||
| Tool | Purpose | Config |
|
||||
|------|---------|--------|
|
||||
| **ESLint** | Linting | `.eslintrc.*` |
|
||||
| **Prettier** | Formatting | `.prettierrc` |
|
||||
| **vue-tsc** | Type checking | `tsconfig.json` |
|
||||
| **Vitest** | Testing | `vitest.config.ts` |
|
||||
|
||||
### Naming
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| Components | `PascalCase.vue` | `BboxOverlay.vue` |
|
||||
| Composables | `useCamelCase.ts` | `usePagination.ts` |
|
||||
| Stores | `camelCase.ts` | `analysisStore.ts` |
|
||||
| Types / Interfaces | `PascalCase` | `AnalysisJob`, `BboxRect` |
|
||||
| Constants | `UPPER_SNAKE_CASE` | `DEFAULT_PAGE_SIZE` |
|
||||
| CSS classes | `kebab-case` | `.bbox-overlay` |
|
||||
| `data-e2e` attributes | `kebab-case` | `data-e2e="upload-zone"` |
|
||||
|
||||
### Style Rules
|
||||
|
||||
- **Composition API** only (`<script setup lang="ts">`) — no Options API
|
||||
- One component per file
|
||||
- Props defined with `defineProps<T>()` (type-based, not runtime)
|
||||
- Emits defined with `defineEmits<T>()`
|
||||
- Pinia stores: one per feature, in the feature directory
|
||||
- No global state outside Pinia
|
||||
- API calls only in `api.ts` files (never in components or stores directly)
|
||||
|
||||
### API Contract
|
||||
|
||||
- Frontend sends/receives **camelCase** (Pydantic `alias_generator`)
|
||||
- Backend uses **snake_case** internally
|
||||
- `pages_json` is an exception — contains raw snake_case from `dataclasses.asdict()`
|
||||
|
||||
## Karate (E2E — `e2e/`)
|
||||
|
||||
See [e2e/CONVENTIONS.md](../../e2e/CONVENTIONS.md) for detailed rules.
|
||||
|
||||
Key points:
|
||||
- Use `data-e2e` selectors, never CSS classes
|
||||
- Use `retry()`/`waitFor()`, never `Thread.sleep()` or `delay()`
|
||||
- Setup via API, verify via UI, cleanup via API
|
||||
- Tag tests: `@critical`, `@ui`, `@smoke`, `@regression`, `@e2e`
|
||||
85
docs/community/issue-triage-process.md
Normal file
85
docs/community/issue-triage-process.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Issue Triage Process
|
||||
|
||||
How we classify, prioritize, and manage GitHub issues.
|
||||
|
||||
## Labels
|
||||
|
||||
### Type
|
||||
|
||||
| Label | Description |
|
||||
|-------|-------------|
|
||||
| `bug` | Something is broken |
|
||||
| `feature` | New functionality |
|
||||
| `enhancement` | Improvement to existing functionality |
|
||||
| `docs` | Documentation only |
|
||||
| `question` | Support / how-to question |
|
||||
| `chore` | Tooling, CI, dependencies |
|
||||
|
||||
### Priority
|
||||
|
||||
| Label | Response SLA | Fix SLA | Description |
|
||||
|-------|-------------|---------|-------------|
|
||||
| `priority: P0` | Same day | < 3 days | Critical — service down, data loss, security |
|
||||
| `priority: P1` | < 3 days | < 2 weeks | High — major feature broken, no workaround |
|
||||
| `priority: P2` | < 1 week | Next release | Medium — feature degraded, workaround exists |
|
||||
| `priority: P3` | < 2 weeks | Backlog | Low — minor, cosmetic, nice-to-have |
|
||||
|
||||
### Status
|
||||
|
||||
| Label | Description |
|
||||
|-------|-------------|
|
||||
| `needs-info` | Waiting for reporter to provide more details |
|
||||
| `confirmed` | Bug reproduced or feature accepted |
|
||||
| `good-first-issue` | Suitable for new contributors |
|
||||
| `help-wanted` | Open for community contribution |
|
||||
| `wont-fix` | Intentional behavior, out of scope, or won't be addressed |
|
||||
| `duplicate` | Already tracked in another issue |
|
||||
| `stale` | No activity for 30 days |
|
||||
|
||||
### Component
|
||||
|
||||
| Label | Maps to |
|
||||
|-------|---------|
|
||||
| `component: backend` | `document-parser/` |
|
||||
| `component: frontend` | `frontend/` |
|
||||
| `component: e2e` | `e2e/` |
|
||||
| `component: docker` | Docker / docker-compose |
|
||||
| `component: ci` | `.github/workflows/` |
|
||||
|
||||
## Triage Workflow
|
||||
|
||||
```
|
||||
New issue
|
||||
│
|
||||
├─ Missing info? → label `needs-info`, comment asking for details
|
||||
│ (auto-close after 14 days if no response)
|
||||
│
|
||||
├─ Duplicate? → label `duplicate`, link to original, close
|
||||
│
|
||||
├─ Out of scope? → label `wont-fix`, explain why, close
|
||||
│
|
||||
└─ Valid issue
|
||||
│
|
||||
├─ Add type label (bug / feature / enhancement / ...)
|
||||
├─ Add component label
|
||||
├─ Assess priority (P0 / P1 / P2 / P3)
|
||||
├─ If simple → add `good-first-issue`
|
||||
└─ Assign to milestone (if applicable)
|
||||
```
|
||||
|
||||
## Stale Policy
|
||||
|
||||
| Condition | Action |
|
||||
|-----------|--------|
|
||||
| No activity for **30 days** | Bot labels `stale` + comment |
|
||||
| No activity for **14 more days** | Bot closes the issue |
|
||||
| Reporter responds | `stale` label removed, timer resets |
|
||||
|
||||
Issues labeled `priority: P0` or `priority: P1` are exempt from the stale policy.
|
||||
|
||||
## Response Expectations
|
||||
|
||||
- **Every issue gets a response** (even if it's "thanks, we'll look at this next week")
|
||||
- Acknowledge within the SLA for the priority level
|
||||
- If you can't fix it soon, say so — don't leave reporters hanging
|
||||
- Close issues with a comment explaining the resolution
|
||||
120
docs/community/onboarding-guide.md
Normal file
120
docs/community/onboarding-guide.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Onboarding Guide — First Contribution
|
||||
|
||||
Welcome! This guide helps you go from zero to your first merged PR.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Tool | Version | Check |
|
||||
|------|---------|-------|
|
||||
| Python | 3.12+ | `python --version` |
|
||||
| Node.js | 20+ | `node --version` |
|
||||
| Docker & Docker Compose | latest | `docker compose version` |
|
||||
| Git | 2.x | `git --version` |
|
||||
| Java (for e2e) | 17+ | `java --version` |
|
||||
| Maven (for e2e) | 3.9+ | `mvn --version` |
|
||||
|
||||
## Step 1 — Fork & Clone
|
||||
|
||||
```bash
|
||||
# Fork on GitHub, then:
|
||||
git clone https://github.com/<your-username>/Docling-Studio.git
|
||||
cd Docling-Studio
|
||||
git remote add upstream https://github.com/scub-france/Docling-Studio.git
|
||||
```
|
||||
|
||||
## Step 2 — Run the Stack
|
||||
|
||||
The fastest way to see the app running:
|
||||
|
||||
```bash
|
||||
docker compose up -d --wait
|
||||
open http://localhost:3000
|
||||
```
|
||||
|
||||
## Step 3 — Set Up for Development
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd document-parser
|
||||
python -m venv .venv && source .venv/bin/activate
|
||||
pip install -r requirements.txt # remote mode (lightweight)
|
||||
pip install ruff pytest pytest-asyncio httpx
|
||||
uvicorn main:app --reload --port 8000
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
## Step 4 — Pick Your First Issue
|
||||
|
||||
Look for issues labeled:
|
||||
|
||||
- `good-first-issue` — small, well-scoped, mentored
|
||||
- `help-wanted` — we need help but it may be larger
|
||||
- `docs` — documentation improvements (great starting point)
|
||||
|
||||
## Step 5 — Create a Branch
|
||||
|
||||
```bash
|
||||
git checkout main && git pull upstream main
|
||||
git checkout -b feature/my-change # or fix/my-fix
|
||||
```
|
||||
|
||||
Follow the [branching strategy](../../CONTRIBUTING.md#branching-strategy).
|
||||
|
||||
## Step 6 — Code
|
||||
|
||||
- Read the [architecture docs](../architecture.md) to understand the codebase
|
||||
- Follow the [coding standards](../architecture/coding-standards.md)
|
||||
- Write tests for your changes
|
||||
|
||||
## Step 7 — Verify
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd document-parser
|
||||
ruff check . && ruff format --check .
|
||||
pytest tests/ -v
|
||||
|
||||
# Frontend
|
||||
cd frontend
|
||||
npm run type-check
|
||||
npx eslint src/
|
||||
npm run test:run
|
||||
```
|
||||
|
||||
## Step 8 — Commit & Push
|
||||
|
||||
Follow [commit conventions](../git-workflow/commit-conventions.md):
|
||||
|
||||
```bash
|
||||
git add <files>
|
||||
git commit -m "feat(scope): short description"
|
||||
git push origin feature/my-change
|
||||
```
|
||||
|
||||
## Step 9 — Open a PR
|
||||
|
||||
- Target: `main` (or `release/*` for pre-release fixes)
|
||||
- Fill in the [PR template](../../.github/PULL_REQUEST_TEMPLATE.md)
|
||||
- Add a line in `CHANGELOG.md` under `[Unreleased]`
|
||||
- Wait for CI to pass, then request a review
|
||||
|
||||
## What to Expect
|
||||
|
||||
- A maintainer will review your PR within **3 business days**
|
||||
- You may get feedback — this is normal and helpful
|
||||
- Once approved, a maintainer will merge your PR
|
||||
- Your contribution appears in the next release's changelog
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Open a [Discussion](https://github.com/scub-france/Docling-Studio/discussions) on GitHub
|
||||
- Check existing issues for similar questions
|
||||
- Read the [CONTRIBUTING guide](../../CONTRIBUTING.md) for detailed rules
|
||||
44
docs/community/roadmap-template.md
Normal file
44
docs/community/roadmap-template.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Roadmap — Docling Studio
|
||||
|
||||
Last updated: YYYY-MM-DD
|
||||
|
||||
## Now (current release cycle)
|
||||
|
||||
<!-- Features actively being worked on -->
|
||||
|
||||
| Feature | Issue | Status |
|
||||
|---------|-------|--------|
|
||||
| ... | #NNN | In progress |
|
||||
|
||||
## Next (next release cycle)
|
||||
|
||||
<!-- Planned for the next release, prioritized -->
|
||||
|
||||
| Feature | Issue | Priority |
|
||||
|---------|-------|----------|
|
||||
| ... | #NNN | P1 |
|
||||
|
||||
## Later (future, no timeline)
|
||||
|
||||
<!-- Ideas accepted but not yet scheduled -->
|
||||
|
||||
| Feature | Issue |
|
||||
|---------|-------|
|
||||
| ... | #NNN |
|
||||
|
||||
## Not Planned
|
||||
|
||||
<!-- Explicitly declined or out of scope — explaining why avoids repeat requests -->
|
||||
|
||||
| Feature | Reason |
|
||||
|---------|--------|
|
||||
| ... | ... |
|
||||
|
||||
---
|
||||
|
||||
## How to Propose a Feature
|
||||
|
||||
1. Check [existing issues](https://github.com/scub-france/Docling-Studio/issues) — it may already be tracked
|
||||
2. Open a new issue using the **Feature** template
|
||||
3. If it's a significant change, consider writing an [ADR](../architecture/adr-guide.md)
|
||||
4. The maintainers will triage and prioritize (see [issue triage](issue-triage-process.md))
|
||||
54
docs/git-workflow/code-review-checklist.md
Normal file
54
docs/git-workflow/code-review-checklist.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Code Review Checklist
|
||||
|
||||
Use this checklist when reviewing a Pull Request. Not every item applies to every PR — skip what is irrelevant, but consciously skip it rather than miss it.
|
||||
|
||||
## Correctness
|
||||
|
||||
- [ ] The code does what the PR description says it does
|
||||
- [ ] Edge cases are handled (empty input, missing data, concurrent access)
|
||||
- [ ] Error paths return meaningful messages, not stack traces
|
||||
- [ ] No regressions on existing behavior
|
||||
|
||||
## Architecture & Design
|
||||
|
||||
- [ ] Dependencies flow inward: `api → services → domain` (never reversed)
|
||||
- [ ] Domain layer has no imports from `api/`, `persistence/`, `infra/`
|
||||
- [ ] No business logic in API routes — delegated to services
|
||||
- [ ] New abstractions are justified (no premature generalization)
|
||||
- [ ] Pinia stores stay within their feature boundary
|
||||
|
||||
## Security
|
||||
|
||||
- [ ] User input is validated at the API boundary (Pydantic schemas)
|
||||
- [ ] No secrets, API keys, or credentials in the code
|
||||
- [ ] No `eval()`, `exec()`, or raw SQL
|
||||
- [ ] File paths are sanitized (no path traversal)
|
||||
- [ ] CORS configuration is unchanged or explicitly justified
|
||||
|
||||
## Tests
|
||||
|
||||
- [ ] New behavior has corresponding tests
|
||||
- [ ] Tests are deterministic (no `sleep`, no random, no network)
|
||||
- [ ] Test names describe the scenario, not the implementation
|
||||
- [ ] E2E tests use `data-e2e` attributes, not CSS classes (see [e2e/CONVENTIONS.md](../../e2e/CONVENTIONS.md))
|
||||
|
||||
## Code Quality
|
||||
|
||||
- [ ] No dead code, no commented-out code
|
||||
- [ ] No `TODO` or `FIXME` without a linked issue
|
||||
- [ ] Functions are < 30 lines (or well-justified)
|
||||
- [ ] Variable names are descriptive and consistent with existing code
|
||||
- [ ] No duplicated logic that should be extracted
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] `CHANGELOG.md` updated under `[Unreleased]` if user-facing change
|
||||
- [ ] API changes reflected in Pydantic schemas (auto-documented)
|
||||
- [ ] Breaking changes are flagged in the commit message
|
||||
|
||||
## Pragmatic Checks
|
||||
|
||||
- [ ] The PR does ONE thing (feature, fix, or refactor — not all at once)
|
||||
- [ ] No unrelated formatting changes mixed in
|
||||
- [ ] The diff is reviewable in < 15 minutes (split large PRs)
|
||||
- [ ] CI passes (tests + lint + type-check + build)
|
||||
73
docs/git-workflow/commit-conventions.md
Normal file
73
docs/git-workflow/commit-conventions.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Commit Conventions
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/) to keep the git history readable and to enable automated changelog generation.
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
| Type | When to use | Example |
|
||||
|------|-------------|---------|
|
||||
| `feat` | New feature | `feat(chunking): add hierarchical chunker support` |
|
||||
| `fix` | Bug fix | `fix(upload): handle empty PDF gracefully` |
|
||||
| `docs` | Documentation only | `docs: update architecture diagram` |
|
||||
| `style` | Formatting, no logic change | `style(api): fix ruff formatting warnings` |
|
||||
| `refactor` | Code restructuring, no behavior change | `refactor(persistence): extract repository base class` |
|
||||
| `test` | Adding or updating tests | `test(analysis): add rechunk edge case tests` |
|
||||
| `chore` | Tooling, CI, dependencies | `chore: bump docling to 2.31` |
|
||||
| `perf` | Performance improvement | `perf(bbox): batch coordinate normalization` |
|
||||
| `ci` | CI/CD pipeline changes | `ci: add multi-arch Docker build` |
|
||||
|
||||
## Scopes (optional)
|
||||
|
||||
Use the feature or component name:
|
||||
|
||||
| Scope | Maps to |
|
||||
|-------|---------|
|
||||
| `api` | `document-parser/api/` |
|
||||
| `domain` | `document-parser/domain/` |
|
||||
| `persistence` | `document-parser/persistence/` |
|
||||
| `infra` | `document-parser/infra/` |
|
||||
| `upload` | Upload feature (front + back) |
|
||||
| `analysis` | Analysis feature (front + back) |
|
||||
| `chunking` | Chunking feature (front + back) |
|
||||
| `bbox` | Bounding box pipeline |
|
||||
| `e2e` | `e2e/` tests |
|
||||
| `docker` | Dockerfile, docker-compose |
|
||||
| `ci` | `.github/workflows/` |
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Subject line** — imperative mood, lowercase, no period, max 72 characters
|
||||
2. **Body** — explain *why*, not *what* (the diff shows what)
|
||||
3. **Breaking changes** — add `BREAKING CHANGE:` in the footer or `!` after the type: `feat(api)!: rename /analyses to /jobs`
|
||||
4. **Issue references** — use `Closes #123` or `Fixes #456` in the footer
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
feat(chunking): add page filtering in Prepare mode
|
||||
|
||||
Users can now select which pages to include in chunking.
|
||||
The filter is persisted in the analysis job metadata.
|
||||
|
||||
Closes #87
|
||||
```
|
||||
|
||||
```
|
||||
fix(upload): reject files exceeding MAX_FILE_SIZE_MB
|
||||
|
||||
Previously, oversized files were accepted and failed silently
|
||||
during Docling conversion. Now the API returns 413 with a
|
||||
clear error message.
|
||||
|
||||
Fixes #102
|
||||
```
|
||||
53
docs/git-workflow/merge-policy.md
Normal file
53
docs/git-workflow/merge-policy.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Merge Policy
|
||||
|
||||
## Merge Requirements
|
||||
|
||||
Every PR must meet these conditions before merging:
|
||||
|
||||
1. **CI green** — all checks pass (tests, lint, type-check, build)
|
||||
2. **At least 1 approval** from a maintainer
|
||||
3. **No unresolved conversations** — all review comments addressed
|
||||
4. **Branch up to date** with target branch (rebase or merge from target)
|
||||
5. **CHANGELOG updated** — if the change is user-facing
|
||||
|
||||
## Merge Strategy
|
||||
|
||||
| Branch type | Merge method | Why |
|
||||
|-------------|-------------|-----|
|
||||
| `feature/*` → `main` | **Squash merge** | Clean history — one commit per feature |
|
||||
| `fix/*` → `main` | **Squash merge** | Clean history — one commit per fix |
|
||||
| `fix/*` → `release/*` | **Squash merge** | Same rationale |
|
||||
| `release/*` → `main` | **Merge commit** | Preserves the release branch history |
|
||||
| `hotfix/*` → `main` | **Squash merge** | One atomic fix |
|
||||
|
||||
### Squash merge commit message
|
||||
|
||||
When squashing, the final commit message should follow [Conventional Commits](commit-conventions.md):
|
||||
|
||||
```
|
||||
feat(chunking): add page filtering in Prepare mode (#87)
|
||||
```
|
||||
|
||||
The PR number is appended automatically by GitHub.
|
||||
|
||||
## Stale PR Policy
|
||||
|
||||
| Condition | Action |
|
||||
|-----------|--------|
|
||||
| No activity for **14 days** | Maintainer pings the author |
|
||||
| No activity for **30 days** | PR labeled `stale` |
|
||||
| No activity for **45 days** | PR closed with comment (can be reopened) |
|
||||
|
||||
Draft PRs are exempt from the stale policy.
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
- **Rebase preferred** over merge commits for feature/fix branches
|
||||
- If conflicts are complex, merge from target branch into the PR branch
|
||||
- Never force-push a branch that has active reviewers without warning them
|
||||
|
||||
## Branch Cleanup
|
||||
|
||||
- Feature and fix branches are **deleted after merge** (GitHub auto-delete enabled)
|
||||
- Release branches are kept until the next minor release
|
||||
- Tags are never deleted
|
||||
93
docs/operations/incident-response.md
Normal file
93
docs/operations/incident-response.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Incident Response
|
||||
|
||||
## Severity Levels
|
||||
|
||||
| Level | Description | Examples | Response time |
|
||||
|-------|-------------|----------|---------------|
|
||||
| **SEV-1** | Service down, data loss, security breach | App unreachable, DB corrupted, credentials leaked | Immediate |
|
||||
| **SEV-2** | Major feature broken, degraded for all users | Upload fails, analysis crashes, blank pages | < 2 hours |
|
||||
| **SEV-3** | Minor feature broken, workaround exists | Bbox overlay misaligned, locale missing a key | Next business day |
|
||||
|
||||
## Response Steps
|
||||
|
||||
### 1. Detect
|
||||
|
||||
- Health endpoint returns error (`/api/health`)
|
||||
- User report via GitHub issue
|
||||
- CI/CD pipeline failure on `main`
|
||||
- Docker container crash loop
|
||||
|
||||
### 2. Assess
|
||||
|
||||
- Determine severity (SEV-1/2/3)
|
||||
- Identify affected component: backend, frontend, Docker, CI
|
||||
- Check recent deployments: `git log --oneline -10 main`
|
||||
|
||||
### 3. Communicate
|
||||
|
||||
- **SEV-1**: Notify all maintainers immediately
|
||||
- **SEV-2**: Open a GitHub issue with `priority: P0` label
|
||||
- **SEV-3**: Open a GitHub issue with `priority: P1` label
|
||||
|
||||
### 4. Mitigate
|
||||
|
||||
**Rollback first, investigate later.**
|
||||
|
||||
- If the issue appeared after a deploy → [rollback](../release/rollback-playbook.md)
|
||||
- If the issue is in a specific endpoint → disable the route or return a maintenance response
|
||||
- If the issue is in Docling itself → switch to remote mode (`CONVERSION_ENGINE=remote`)
|
||||
|
||||
### 5. Fix
|
||||
|
||||
- Create a `hotfix/*` branch from the last stable tag
|
||||
- Fix the root cause, add a regression test
|
||||
- Run the [release audit](../audit/master.md) on the fix
|
||||
- Deploy via the [deployment checklist](../release/deployment-checklist.md)
|
||||
|
||||
### 6. Post-Mortem
|
||||
|
||||
Write a post-mortem for SEV-1 and SEV-2 incidents using this template:
|
||||
|
||||
```markdown
|
||||
# Post-Mortem: [Incident Title]
|
||||
|
||||
**Date**: YYYY-MM-DD
|
||||
**Severity**: SEV-X
|
||||
**Duration**: Xh Xm
|
||||
**Author**: [name]
|
||||
|
||||
## Timeline
|
||||
|
||||
| Time | Event |
|
||||
|------|-------|
|
||||
| HH:MM | Incident detected |
|
||||
| HH:MM | Severity assessed |
|
||||
| HH:MM | Mitigation applied |
|
||||
| HH:MM | Root cause identified |
|
||||
| HH:MM | Fix deployed |
|
||||
| HH:MM | Incident resolved |
|
||||
|
||||
## Root Cause
|
||||
|
||||
[What actually broke and why]
|
||||
|
||||
## Impact
|
||||
|
||||
[Who was affected, for how long, what data was at risk]
|
||||
|
||||
## What Went Well
|
||||
|
||||
- ...
|
||||
|
||||
## What Went Wrong
|
||||
|
||||
- ...
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
|--------|-------|-----|
|
||||
| ... | ... | ... |
|
||||
```
|
||||
|
||||
Store post-mortems in `docs/operations/post-mortems/YYYY-MM-DD-short-title.md`.
|
||||
105
docs/operations/monitoring-checklist.md
Normal file
105
docs/operations/monitoring-checklist.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Monitoring Checklist
|
||||
|
||||
What to monitor in a Docling Studio deployment.
|
||||
|
||||
## Health Endpoint
|
||||
|
||||
The primary monitoring signal is the health endpoint:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"engine": "local",
|
||||
"version": "0.3.0",
|
||||
"deploymentMode": "self-hosted"
|
||||
}
|
||||
```
|
||||
|
||||
**Alert if**: status != "ok", endpoint unreachable, or response time > 5s.
|
||||
|
||||
## Four Golden Signals
|
||||
|
||||
### 1. Latency
|
||||
|
||||
| Endpoint | Expected | Alert threshold |
|
||||
|----------|----------|-----------------|
|
||||
| `GET /api/health` | < 100ms | > 1s |
|
||||
| `POST /api/documents` (upload) | < 2s | > 10s |
|
||||
| `POST /api/analyses` (create) | < 500ms (queuing only) | > 5s |
|
||||
| `GET /api/analyses/:id` (results) | < 500ms | > 3s |
|
||||
|
||||
### 2. Traffic
|
||||
|
||||
| Metric | What to watch |
|
||||
|--------|---------------|
|
||||
| Requests per minute | Baseline for normal usage |
|
||||
| Uploads per hour | Capacity planning |
|
||||
| Concurrent analyses | Should stay <= `MAX_CONCURRENT_ANALYSES` |
|
||||
|
||||
### 3. Errors
|
||||
|
||||
| Signal | Alert threshold |
|
||||
|--------|-----------------|
|
||||
| HTTP 5xx rate | > 1% of requests |
|
||||
| Analysis failure rate | > 10% of analyses |
|
||||
| Rate limit hits (429) | Spike = possible abuse |
|
||||
|
||||
### 4. Saturation
|
||||
|
||||
| Resource | Check command | Alert threshold |
|
||||
|----------|---------------|-----------------|
|
||||
| CPU | `docker stats` | > 90% sustained |
|
||||
| Memory | `docker stats` | > 85% (especially in local mode with PyTorch) |
|
||||
| Disk (SQLite + uploads) | `du -sh data/` | > 80% of volume |
|
||||
| Docker container restarts | `docker inspect --format='{{.RestartCount}}'` | > 0 |
|
||||
|
||||
## Docker Health Check
|
||||
|
||||
The `docker-compose.yml` includes a built-in health check:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
```
|
||||
|
||||
Docker will mark the container as `unhealthy` after 3 consecutive failures.
|
||||
|
||||
## Log Monitoring
|
||||
|
||||
### Backend logs (uvicorn)
|
||||
|
||||
```bash
|
||||
docker compose logs -f backend
|
||||
```
|
||||
|
||||
Watch for:
|
||||
- `ERROR` or `CRITICAL` log levels
|
||||
- `TimeoutError` from Docling processing
|
||||
- `sqlite3.OperationalError` (DB issues)
|
||||
- `429 Too Many Requests` spikes
|
||||
|
||||
### Frontend logs (nginx)
|
||||
|
||||
```bash
|
||||
docker compose logs -f frontend
|
||||
```
|
||||
|
||||
Watch for:
|
||||
- `502 Bad Gateway` (backend down)
|
||||
- `413 Request Entity Too Large` (file size limit)
|
||||
|
||||
## Recommended Setup
|
||||
|
||||
For production deployments, consider:
|
||||
|
||||
1. **Uptime monitor** — ping `/api/health` every 60s (UptimeRobot, Healthchecks.io)
|
||||
2. **Log aggregation** — ship Docker logs to a central service
|
||||
3. **Alerting** — notify on container restart, health check failure, or error spike
|
||||
74
docs/operations/security-response.md
Normal file
74
docs/operations/security-response.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Security Vulnerability Response
|
||||
|
||||
Process for handling reported security vulnerabilities. See also [SECURITY.md](../../SECURITY.md) for the public-facing policy.
|
||||
|
||||
## Response Timeline
|
||||
|
||||
| Step | SLA | Action |
|
||||
|------|-----|--------|
|
||||
| **Acknowledge** | < 48h | Confirm receipt to the reporter |
|
||||
| **Assess** | < 7 days | Determine severity (Critical / High / Medium / Low) |
|
||||
| **Fix** | < 14 days (Critical), < 30 days (other) | Develop and test the fix |
|
||||
| **Release** | Same day as fix | Publish patched version |
|
||||
| **Disclose** | After release | Publish GitHub Security Advisory |
|
||||
|
||||
## Severity Assessment
|
||||
|
||||
| Severity | Criteria | Example |
|
||||
|----------|----------|---------|
|
||||
| **Critical** | Remote exploitation, data breach, no auth required | SQL injection in upload endpoint |
|
||||
| **High** | Significant impact, some conditions required | Path traversal on file download |
|
||||
| **Medium** | Limited impact or requires authenticated access | XSS in analysis results display |
|
||||
| **Low** | Minimal impact, theoretical only | Information disclosure in error messages |
|
||||
|
||||
## Fix Process
|
||||
|
||||
1. **Create a private branch** — never push vulnerability details to a public branch before the fix is released
|
||||
2. **Develop the fix** — include a regression test
|
||||
3. **Run the security audit** — `docs/audit/audits/08-security.md`
|
||||
4. **Review** — at least one maintainer must review the fix
|
||||
5. **Release** — tag, build, deploy (see [deployment checklist](../release/deployment-checklist.md))
|
||||
6. **Publish advisory** — GitHub Security Advisory with CVE if applicable
|
||||
|
||||
## Advisory Template
|
||||
|
||||
```markdown
|
||||
# Security Advisory: [Title]
|
||||
|
||||
**Severity**: Critical | High | Medium | Low
|
||||
**Affected versions**: < X.Y.Z
|
||||
**Fixed in**: X.Y.Z
|
||||
**CVE**: CVE-YYYY-NNNNN (if assigned)
|
||||
|
||||
## Description
|
||||
|
||||
[What the vulnerability is, without providing exploitation details]
|
||||
|
||||
## Impact
|
||||
|
||||
[What an attacker could do]
|
||||
|
||||
## Mitigation
|
||||
|
||||
[Upgrade to X.Y.Z or apply workaround]
|
||||
|
||||
## Credit
|
||||
|
||||
[Reporter name, unless they prefer anonymity]
|
||||
```
|
||||
|
||||
## Dependency Vulnerabilities
|
||||
|
||||
Run regular dependency audits:
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd document-parser && pip audit
|
||||
|
||||
# Frontend
|
||||
cd frontend && npm audit
|
||||
```
|
||||
|
||||
For known vulnerabilities in dependencies:
|
||||
- **Critical/High**: Update immediately, release a patch version
|
||||
- **Medium/Low**: Include in the next planned release
|
||||
70
docs/release/deployment-checklist.md
Normal file
70
docs/release/deployment-checklist.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Deployment Checklist
|
||||
|
||||
Checklist for deploying a new release of Docling Studio. Applies to both self-hosted and Hugging Face Space deployments.
|
||||
|
||||
## Pre-Deploy
|
||||
|
||||
- [ ] Release branch merged to `main` via PR
|
||||
- [ ] Git tag `vX.Y.Z` created on `main`
|
||||
- [ ] Release audit passed (score >= 80, 0 CRITICAL) — see [docs/audit/master.md](../audit/master.md)
|
||||
- [ ] `CHANGELOG.md` section finalized with release date
|
||||
- [ ] `frontend/package.json` version matches the tag
|
||||
- [ ] All CI checks green on the tagged commit
|
||||
- [ ] Docker images built and pushed to `ghcr.io`:
|
||||
- `X.Y.Z-remote`, `X.Y.Z-local`
|
||||
- `X.Y-remote`, `X.Y-local`
|
||||
- `latest-remote`, `latest-local`
|
||||
|
||||
## Deploy — Self-Hosted (Docker Compose)
|
||||
|
||||
- [ ] Pull the new image:
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
- [ ] Check environment variables (`.env` or `docker-compose.override.yml`):
|
||||
- `CONVERSION_ENGINE` (local / remote)
|
||||
- `RATE_LIMIT_RPM`
|
||||
- `MAX_FILE_SIZE_MB`
|
||||
- `MAX_CONCURRENT_ANALYSES`
|
||||
- [ ] Start the stack:
|
||||
```bash
|
||||
docker compose up -d --wait
|
||||
```
|
||||
- [ ] Verify health endpoint:
|
||||
```bash
|
||||
curl -s http://localhost:3000/api/health | jq .
|
||||
# Expected: {"status":"ok","engine":"...","version":"X.Y.Z","deploymentMode":"self-hosted"}
|
||||
```
|
||||
|
||||
## Deploy — Hugging Face Space
|
||||
|
||||
- [ ] Upload to HF Space via `huggingface-cli`:
|
||||
```bash
|
||||
huggingface-cli upload <space-id> . . --repo-type space
|
||||
```
|
||||
- [ ] Set environment variables in HF Space settings
|
||||
- [ ] Wait for build to complete in HF Space logs
|
||||
- [ ] Verify the app loads and health endpoint returns correct version
|
||||
|
||||
## Post-Deploy Smoke Test
|
||||
|
||||
- [ ] Home page loads
|
||||
- [ ] Upload a PDF — document appears in the list
|
||||
- [ ] Run an analysis — completes without error
|
||||
- [ ] View results — markdown, HTML, bbox overlays render correctly
|
||||
- [ ] Download results
|
||||
- [ ] If local mode: test chunking
|
||||
- [ ] Check `/api/health` returns the new version
|
||||
|
||||
## Rollback Triggers
|
||||
|
||||
Rollback immediately if any of these occur:
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| Health endpoint returns error or wrong version | Rollback |
|
||||
| Upload or analysis fails on a previously working PDF | Rollback |
|
||||
| Frontend shows blank page or JS errors | Rollback |
|
||||
| Error rate > 5% in the first 15 minutes | Rollback |
|
||||
|
||||
For rollback procedure, see [rollback-playbook.md](rollback-playbook.md).
|
||||
83
docs/release/rollback-playbook.md
Normal file
83
docs/release/rollback-playbook.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Rollback Playbook
|
||||
|
||||
## Decision: Rollback vs Hotfix?
|
||||
|
||||
| Situation | Strategy |
|
||||
|-----------|----------|
|
||||
| Broken deploy, previous version was stable | **Rollback** |
|
||||
| Bug found but previous version also had issues | **Hotfix** |
|
||||
| Data migration was applied (DB schema changed) | **Hotfix** (rollback may lose data) |
|
||||
| Security vulnerability in the new release | **Rollback** + hotfix in parallel |
|
||||
|
||||
## Rollback Procedure — Docker Compose
|
||||
|
||||
### 1. Identify the last known good version
|
||||
|
||||
```bash
|
||||
# List available tags
|
||||
docker image ls ghcr.io/scub-france/docling-studio --format '{{.Tag}}' | sort -V
|
||||
|
||||
# Or check GitHub releases
|
||||
gh release list --repo scub-france/Docling-Studio
|
||||
```
|
||||
|
||||
### 2. Pin the image to the previous version
|
||||
|
||||
Edit `docker-compose.yml` or `docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
image: ghcr.io/scub-france/docling-studio:0.2.0-remote # pin to last good
|
||||
frontend:
|
||||
image: ghcr.io/scub-france/docling-studio:0.2.0-remote
|
||||
```
|
||||
|
||||
### 3. Restart
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d --wait
|
||||
```
|
||||
|
||||
### 4. Verify
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:3000/api/health | jq .
|
||||
# Confirm version matches the rolled-back version
|
||||
```
|
||||
|
||||
### 5. Communicate
|
||||
|
||||
- Notify the team that a rollback was performed
|
||||
- Open an issue describing the failure
|
||||
- Link the failed release and the rollback commit
|
||||
|
||||
## Rollback Procedure — Hugging Face Space
|
||||
|
||||
1. Use `git revert` on the HF Space repo to revert to the previous commit
|
||||
2. Or re-upload the previous version: `huggingface-cli upload <space-id> . . --repo-type space --revision <previous-commit>`
|
||||
3. Verify the app loads correctly
|
||||
|
||||
## Database Considerations
|
||||
|
||||
Docling Studio uses **SQLite** with a file-based database. Key points:
|
||||
|
||||
- **No schema migrations** are applied automatically — the schema is created on first run
|
||||
- If a new release adds columns, rolling back may cause "unknown column" errors
|
||||
- **Before deploying a release that changes the DB schema**: back up the SQLite file
|
||||
|
||||
```bash
|
||||
# Backup before deploy
|
||||
cp data/docling-studio.db data/docling-studio.db.bak-$(date +%Y%m%d)
|
||||
|
||||
# Restore after rollback
|
||||
cp data/docling-studio.db.bak-YYYYMMDD data/docling-studio.db
|
||||
```
|
||||
|
||||
## Post-Rollback
|
||||
|
||||
1. Keep the rollback in place until the root cause is identified
|
||||
2. Fix the issue on a `hotfix/*` branch
|
||||
3. Re-run the [release audit](../audit/master.md) on the fix
|
||||
4. Re-deploy following the [deployment checklist](deployment-checklist.md)
|
||||
267
profiles/fastapi-vue/commands.sh
Executable file
267
profiles/fastapi-vue/commands.sh
Executable file
|
|
@ -0,0 +1,267 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Docling Studio — Automated Audit Checks (FastAPI + Vue 3 profile)
|
||||
# ============================================================================
|
||||
# Runs verification commands for each of the 12 release audits.
|
||||
# Usage: bash profiles/fastapi-vue/commands.sh
|
||||
# Run from the repository root.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
PASS=0
|
||||
WARN=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo -e " ${GREEN}PASS${NC} $1"; ((PASS++)); }
|
||||
warn() { echo -e " ${YELLOW}WARN${NC} $1"; ((WARN++)); }
|
||||
fail() { echo -e " ${RED}FAIL${NC} $1"; ((FAIL++)); }
|
||||
|
||||
# ── 01. Clean Architecture ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 01. Clean Architecture =="
|
||||
|
||||
# Domain must not import from api, persistence, infra
|
||||
if grep -rn "from api\|from persistence\|from infra\|import fastapi\|import aiosqlite" document-parser/domain/ 2>/dev/null; then
|
||||
fail "Domain layer imports forbidden modules"
|
||||
else
|
||||
pass "Domain layer has no forbidden imports"
|
||||
fi
|
||||
|
||||
# API must not import from persistence directly
|
||||
if grep -rn "from persistence" document-parser/api/ 2>/dev/null; then
|
||||
fail "API layer imports directly from persistence"
|
||||
else
|
||||
pass "API layer does not import from persistence"
|
||||
fi
|
||||
|
||||
# ── 02. DDD ────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 02. DDD =="
|
||||
|
||||
# Domain models exist
|
||||
if [ -f document-parser/domain/models.py ] && [ -f document-parser/domain/ports.py ]; then
|
||||
pass "Domain models and ports exist"
|
||||
else
|
||||
fail "Missing domain/models.py or domain/ports.py"
|
||||
fi
|
||||
|
||||
# Value objects exist
|
||||
if [ -f document-parser/domain/value_objects.py ]; then
|
||||
pass "Value objects defined"
|
||||
else
|
||||
warn "No value_objects.py in domain"
|
||||
fi
|
||||
|
||||
# ── 03. Clean Code ─────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 03. Clean Code =="
|
||||
|
||||
# Check for files > 300 lines (backend)
|
||||
LARGE_FILES=$(find document-parser -name "*.py" ! -path "*/.venv/*" ! -path "*/__pycache__/*" ! -path "*/tests/*" -exec awk 'END{if(NR>300) print FILENAME": "NR" lines"}' {} \;)
|
||||
if [ -n "$LARGE_FILES" ]; then
|
||||
warn "Large Python files (>300 lines):"
|
||||
echo "$LARGE_FILES"
|
||||
else
|
||||
pass "No Python files exceed 300 lines"
|
||||
fi
|
||||
|
||||
# Check for files > 300 lines (frontend)
|
||||
LARGE_VUE=$(find frontend/src -name "*.vue" -o -name "*.ts" | xargs awk 'END{if(NR>300) print FILENAME": "NR" lines"}' 2>/dev/null)
|
||||
if [ -n "$LARGE_VUE" ]; then
|
||||
warn "Large frontend files (>300 lines):"
|
||||
echo "$LARGE_VUE"
|
||||
else
|
||||
pass "No frontend files exceed 300 lines"
|
||||
fi
|
||||
|
||||
# ── 04. KISS ───────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 04. KISS =="
|
||||
|
||||
# Check for overly complex patterns
|
||||
if grep -rn "type:\s*ignore" document-parser/ --include="*.py" ! -path "*/.venv/*" 2>/dev/null; then
|
||||
warn "Found type: ignore comments (review if justified)"
|
||||
else
|
||||
pass "No unjustified type: ignore"
|
||||
fi
|
||||
|
||||
# ── 05. DRY ────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 05. DRY =="
|
||||
|
||||
# Check for magic numbers in backend
|
||||
if grep -rn "[^a-zA-Z_][0-9]\{3,\}[^0-9]" document-parser/ --include="*.py" ! -path "*/.venv/*" ! -path "*/tests/*" 2>/dev/null | grep -v "port\|version\|status\|#\|MAX_\|DEFAULT_\|LIMIT_" | head -5; then
|
||||
warn "Possible magic numbers found (review above)"
|
||||
else
|
||||
pass "No obvious magic numbers"
|
||||
fi
|
||||
|
||||
# ── 06. SOLID ──────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 06. SOLID =="
|
||||
|
||||
# Check that ports (interfaces) exist
|
||||
if grep -l "Protocol\|ABC\|abstractmethod" document-parser/domain/ports.py 2>/dev/null; then
|
||||
pass "Domain ports use Protocol/ABC (Dependency Inversion)"
|
||||
else
|
||||
fail "No abstract ports found in domain"
|
||||
fi
|
||||
|
||||
# ── 07. Decoupling ─────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 07. Decoupling =="
|
||||
|
||||
# Frontend should not hardcode backend URLs (except in config)
|
||||
if grep -rn "localhost:8000\|127.0.0.1:8000" frontend/src/ --include="*.ts" --include="*.vue" 2>/dev/null | grep -v "config\|env\|http.ts"; then
|
||||
fail "Frontend hardcodes backend URL outside config"
|
||||
else
|
||||
pass "Frontend backend URL is configurable"
|
||||
fi
|
||||
|
||||
# ── 08. Security ───────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 08. Security =="
|
||||
|
||||
# Check for hardcoded secrets
|
||||
if grep -rni "password\s*=\s*['\"].\+['\"\|secret\s*=\s*['\"].\+['\"\|api_key\s*=\s*['\"].\+['\"]" document-parser/ --include="*.py" ! -path "*/.venv/*" ! -path "*/tests/*" 2>/dev/null; then
|
||||
fail "Possible hardcoded secrets found"
|
||||
else
|
||||
pass "No hardcoded secrets detected"
|
||||
fi
|
||||
|
||||
# Check for eval/exec
|
||||
if grep -rn "\beval(\|exec(" document-parser/ --include="*.py" ! -path "*/.venv/*" 2>/dev/null; then
|
||||
fail "eval() or exec() found in backend"
|
||||
else
|
||||
pass "No eval/exec in backend"
|
||||
fi
|
||||
|
||||
# Check CORS configuration exists
|
||||
if grep -rn "CORSMiddleware" document-parser/ --include="*.py" ! -path "*/.venv/*" 2>/dev/null > /dev/null; then
|
||||
pass "CORS middleware is configured"
|
||||
else
|
||||
warn "No CORS middleware found"
|
||||
fi
|
||||
|
||||
# ── 09. Tests ──────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 09. Tests =="
|
||||
|
||||
# Backend tests exist
|
||||
BACKEND_TESTS=$(find document-parser/tests -name "test_*.py" 2>/dev/null | wc -l)
|
||||
if [ "$BACKEND_TESTS" -gt 0 ]; then
|
||||
pass "Backend: $BACKEND_TESTS test files found"
|
||||
else
|
||||
fail "No backend test files found"
|
||||
fi
|
||||
|
||||
# Frontend tests exist
|
||||
FRONTEND_TESTS=$(find frontend/src -name "*.test.*" 2>/dev/null | wc -l)
|
||||
if [ "$FRONTEND_TESTS" -gt 0 ]; then
|
||||
pass "Frontend: $FRONTEND_TESTS test files found"
|
||||
else
|
||||
fail "No frontend test files found"
|
||||
fi
|
||||
|
||||
# E2E tests exist
|
||||
E2E_TESTS=$(find e2e -name "*.feature" 2>/dev/null | wc -l)
|
||||
if [ "$E2E_TESTS" -gt 0 ]; then
|
||||
pass "E2E: $E2E_TESTS feature files found"
|
||||
else
|
||||
warn "No e2e feature files found"
|
||||
fi
|
||||
|
||||
# Check for skipped tests
|
||||
if grep -rn "@skip\|@ignore\|xit(\|xdescribe(\|pytest.mark.skip" document-parser/tests/ frontend/src/ 2>/dev/null | grep -v "helpers"; then
|
||||
warn "Skipped tests found (review if intentional)"
|
||||
else
|
||||
pass "No skipped tests"
|
||||
fi
|
||||
|
||||
# ── 10. CI / Build ────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 10. CI / Build =="
|
||||
|
||||
# CI workflow exists
|
||||
if [ -f .github/workflows/ci.yml ]; then
|
||||
pass "CI workflow exists"
|
||||
else
|
||||
fail "No CI workflow found"
|
||||
fi
|
||||
|
||||
# Dockerfile exists
|
||||
if [ -f Dockerfile ]; then
|
||||
pass "Dockerfile exists"
|
||||
else
|
||||
fail "No Dockerfile found"
|
||||
fi
|
||||
|
||||
# Health check in docker-compose
|
||||
if grep -q "healthcheck" docker-compose.yml 2>/dev/null; then
|
||||
pass "Docker Compose has health check"
|
||||
else
|
||||
warn "No health check in docker-compose.yml"
|
||||
fi
|
||||
|
||||
# ── 11. Documentation ─────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 11. Documentation =="
|
||||
|
||||
# CHANGELOG exists and has content
|
||||
if [ -f CHANGELOG.md ] && [ -s CHANGELOG.md ]; then
|
||||
pass "CHANGELOG.md exists and is not empty"
|
||||
else
|
||||
fail "CHANGELOG.md missing or empty"
|
||||
fi
|
||||
|
||||
# README exists
|
||||
if [ -f README.md ]; then
|
||||
pass "README.md exists"
|
||||
else
|
||||
fail "README.md missing"
|
||||
fi
|
||||
|
||||
# Check for TODO/FIXME without issue reference
|
||||
TODOS=$(grep -rn "TODO\|FIXME" document-parser/ frontend/src/ --include="*.py" --include="*.ts" --include="*.vue" ! -path "*/.venv/*" ! -path "*/node_modules/*" 2>/dev/null | grep -v "#[0-9]" | head -5)
|
||||
if [ -n "$TODOS" ]; then
|
||||
warn "TODO/FIXME without issue reference:"
|
||||
echo "$TODOS"
|
||||
else
|
||||
pass "No orphaned TODO/FIXME"
|
||||
fi
|
||||
|
||||
# ── 12. Performance ───────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "== 12. Performance =="
|
||||
|
||||
# Check for synchronous file I/O in async context
|
||||
if grep -rn "open(" document-parser/api/ document-parser/services/ --include="*.py" 2>/dev/null | grep -v "aiofiles\|async\|#"; then
|
||||
warn "Synchronous file I/O in async code (review above)"
|
||||
else
|
||||
pass "No synchronous file I/O in async endpoints"
|
||||
fi
|
||||
|
||||
# Check for N+1 patterns (loop with DB call)
|
||||
if grep -rn "for.*in.*:" document-parser/services/ --include="*.py" -A5 2>/dev/null | grep "await.*repo\|await.*db"; then
|
||||
warn "Possible N+1 query pattern (review above)"
|
||||
else
|
||||
pass "No obvious N+1 patterns"
|
||||
fi
|
||||
|
||||
# ── Summary ────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo -e " ${GREEN}PASS${NC}: $PASS"
|
||||
echo -e " ${YELLOW}WARN${NC}: $WARN"
|
||||
echo -e " ${RED}FAIL${NC}: $FAIL"
|
||||
echo "============================================"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
70
profiles/fastapi-vue/profile.md
Normal file
70
profiles/fastapi-vue/profile.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Stack Profile — FastAPI + Vue 3
|
||||
|
||||
Profile for running the 12 release audits on Docling Studio.
|
||||
|
||||
## Layer Mapping
|
||||
|
||||
| Generic Layer | Docling Studio Path | Language |
|
||||
|---------------|---------------------|----------|
|
||||
| **Domain** | `document-parser/domain/` | Python |
|
||||
| **Services** | `document-parser/services/` | Python |
|
||||
| **API** | `document-parser/api/` | Python |
|
||||
| **Infrastructure** | `document-parser/infra/` | Python |
|
||||
| **Persistence** | `document-parser/persistence/` | Python |
|
||||
| **Frontend** | `frontend/src/` | TypeScript / Vue |
|
||||
| **Tests (backend)** | `document-parser/tests/` | Python |
|
||||
| **Tests (frontend)** | `frontend/src/**/*.test.*` | TypeScript |
|
||||
| **Tests (e2e API)** | `e2e/api/` | Karate (Gherkin) |
|
||||
| **Tests (e2e UI)** | `e2e/ui/` | Karate UI (Gherkin) |
|
||||
| **CI/CD** | `.github/workflows/` | YAML |
|
||||
| **Docker** | `Dockerfile`, `docker-compose.yml`, `nginx.conf` | Docker / Nginx |
|
||||
|
||||
## Excluded Paths
|
||||
|
||||
These paths are excluded from audits:
|
||||
|
||||
- `document-parser/.venv/`
|
||||
- `document-parser/__pycache__/`
|
||||
- `frontend/node_modules/`
|
||||
- `frontend/dist/`
|
||||
- `e2e/**/target/`
|
||||
|
||||
## Framework Detection
|
||||
|
||||
Imports that should NOT appear in the domain layer:
|
||||
|
||||
```python
|
||||
# Forbidden in document-parser/domain/
|
||||
from fastapi import ...
|
||||
from pydantic import ... # except BaseModel for value objects
|
||||
import aiosqlite
|
||||
from infra import ...
|
||||
from persistence import ...
|
||||
from api import ...
|
||||
```
|
||||
|
||||
Imports that should NOT cross feature boundaries in the frontend:
|
||||
|
||||
```typescript
|
||||
// features/analysis/ should NOT import from features/chunking/store
|
||||
// features/document/ should NOT import from features/analysis/store
|
||||
// Cross-feature communication goes through shared/ or events
|
||||
```
|
||||
|
||||
## Tools & Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Backend lint | `cd document-parser && ruff check .` |
|
||||
| Backend format | `cd document-parser && ruff format --check .` |
|
||||
| Backend tests | `cd document-parser && pytest tests/ -v` |
|
||||
| Frontend lint | `cd frontend && npx eslint src/` |
|
||||
| Frontend type-check | `cd frontend && npm run type-check` |
|
||||
| Frontend format | `cd frontend && npx prettier --check src/` |
|
||||
| Frontend tests | `cd frontend && npm run test:run` |
|
||||
| E2E API tests | `mvn test -f e2e/api/pom.xml -Dkarate.options="--tags @smoke"` |
|
||||
| E2E UI tests | `mvn test -f e2e/ui/pom.xml -Dkarate.options="--tags @critical"` |
|
||||
| Docker build | `docker compose build` |
|
||||
| Docker health | `curl -s http://localhost:3000/api/health` |
|
||||
| Dependency audit (Python) | `cd document-parser && pip audit` |
|
||||
| Dependency audit (Node) | `cd frontend && npm audit` |
|
||||
Loading…
Reference in a new issue