From 941028e705a19d5a522664b0b5a2450d4f96a26d Mon Sep 17 00:00:00 2001 From: Pier-Jean Malandrino Date: Thu, 9 Apr 2026 13:20:43 +0200 Subject: [PATCH] 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 --- .github/PULL_REQUEST_TEMPLATE.md | 32 + .github/workflows/ci.yml | 2 +- .github/workflows/release-gate.yml | 776 +++++++++++++++++++++ CODE_OF_CONDUCT.md | 43 ++ SECURITY.md | 49 ++ docs/PROCESSES.md | 179 +++++ docs/architecture/adr-guide.md | 52 ++ docs/architecture/adr-template.md | 39 ++ docs/architecture/coding-standards.md | 88 +++ docs/community/issue-triage-process.md | 85 +++ docs/community/onboarding-guide.md | 120 ++++ docs/community/roadmap-template.md | 44 ++ docs/git-workflow/code-review-checklist.md | 54 ++ docs/git-workflow/commit-conventions.md | 73 ++ docs/git-workflow/merge-policy.md | 53 ++ docs/operations/incident-response.md | 93 +++ docs/operations/monitoring-checklist.md | 105 +++ docs/operations/security-response.md | 74 ++ docs/release/deployment-checklist.md | 70 ++ docs/release/rollback-playbook.md | 83 +++ profiles/fastapi-vue/commands.sh | 267 +++++++ profiles/fastapi-vue/profile.md | 70 ++ 22 files changed, 2450 insertions(+), 1 deletion(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/release-gate.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 SECURITY.md create mode 100644 docs/PROCESSES.md create mode 100644 docs/architecture/adr-guide.md create mode 100644 docs/architecture/adr-template.md create mode 100644 docs/architecture/coding-standards.md create mode 100644 docs/community/issue-triage-process.md create mode 100644 docs/community/onboarding-guide.md create mode 100644 docs/community/roadmap-template.md create mode 100644 docs/git-workflow/code-review-checklist.md create mode 100644 docs/git-workflow/commit-conventions.md create mode 100644 docs/git-workflow/merge-policy.md create mode 100644 docs/operations/incident-response.md create mode 100644 docs/operations/monitoring-checklist.md create mode 100644 docs/operations/security-response.md create mode 100644 docs/release/deployment-checklist.md create mode 100644 docs/release/rollback-playbook.md create mode 100755 profiles/fastapi-vue/commands.sh create mode 100644 profiles/fastapi-vue/profile.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3a0471b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +## Type + +- [ ] Feature (`feature/*`) +- [ ] Bug fix (`fix/*`) +- [ ] Hotfix (`hotfix/*`) +- [ ] Documentation +- [ ] Refactoring +- [ ] CI/CD +- [ ] Other: ___ + +## Summary + + + +## Related issues + + + +## 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 + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4361f4d..a7bed60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, 'release/**'] + branches: [main] pull_request: branches: [main, 'release/**'] diff --git a/.github/workflows/release-gate.yml b/.github/workflows/release-gate.yml new file mode 100644 index 0000000..2ad2915 --- /dev/null +++ b/.github/workflows/release-gate.yml @@ -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 < /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 | + +
+ Size delta vs previous release + + \`\`\` + ${SIZE_REPORT} + \`\`\` +
+ + --- + + Generated by release-gate.yml — see workflow run for full details + ENDOFCOMMENT + + - name: Post or update PR comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + COMMENT_TAG="" + + # 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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2e3e89d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..13cddd7 --- /dev/null +++ b/SECURITY.md @@ -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 diff --git a/docs/PROCESSES.md b/docs/PROCESSES.md new file mode 100644 index 0000000..9a77c18 --- /dev/null +++ b/docs/PROCESSES.md @@ -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 | diff --git a/docs/architecture/adr-guide.md b/docs/architecture/adr-guide.md new file mode 100644 index 0000000..b428a86 --- /dev/null +++ b/docs/architecture/adr-guide.md @@ -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 diff --git a/docs/architecture/adr-template.md b/docs/architecture/adr-template.md new file mode 100644 index 0000000..a58bea4 --- /dev/null +++ b/docs/architecture/adr-template.md @@ -0,0 +1,39 @@ +# ADR-NNN: [Title] + +**Date**: YYYY-MM-DD +**Status**: Proposed | Accepted | Deprecated | Superseded by ADR-NNN +**Deciders**: [names] + +## Context + + + +## Decision + + + +## Consequences + +### Positive + +- ... + +### Negative + +- ... + +### Neutral + +- ... + +## Alternatives Considered + +### Alternative 1: [Name] + +- **Pros**: ... +- **Cons**: ... +- **Why rejected**: ... + +## References + +- [Link to issue, RFC, or discussion] diff --git a/docs/architecture/coding-standards.md b/docs/architecture/coding-standards.md new file mode 100644 index 0000000..7afc9df --- /dev/null +++ b/docs/architecture/coding-standards.md @@ -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 (`