Release/0.6.1 #62
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Gate | |
| on: | |
| pull_request: | |
| branches: [main] | |
| 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.3.1 | |
| - 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.3.1 | |
| - 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.6.2 | |
| with: | |
| name: unit-test-results | |
| path: /tmp/*-tests.txt | |
| dep-audit: | |
| name: Dependency audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4.3.1 | |
| - 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.6.2 | |
| 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.3.1 | |
| - 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: | | |
| # Strip ANSI codes before counting | |
| CLEAN=$(sed 's/\x1b\[[0-9;]*m//g' /tmp/audit-checks.txt) | |
| PASS=$(echo "$CLEAN" | grep -c "PASS" || true) | |
| WARN=$(echo "$CLEAN" | grep -c "WARN" || true) | |
| FAIL=$(echo "$CLEAN" | grep -c "FAIL" || true) | |
| echo "pass=${PASS:-0}" >> "$GITHUB_OUTPUT" | |
| echo "warn=${WARN:-0}" >> "$GITHUB_OUTPUT" | |
| echo "fail=${FAIL:-0}" >> "$GITHUB_OUTPUT" | |
| - name: Upload audit results | |
| if: always() | |
| uses: actions/upload-artifact@v4.6.2 | |
| 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.3.1 | |
| - 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.6.2 | |
| 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.6.2 | |
| 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.3.1 | |
| - name: Download remote image | |
| uses: actions/download-artifact@v4.3.0 | |
| 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.3.1 | |
| - name: Download image | |
| uses: actions/download-artifact@v4.3.0 | |
| 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@v0.35.0 | |
| with: | |
| image-ref: docling-studio:${{ matrix.target }} | |
| format: table | |
| exit-code: 1 | |
| severity: CRITICAL | |
| output: /tmp/trivy-critical-${{ matrix.target }}.txt | |
| trivyignores: .trivyignore.yaml | |
| # `latest` instead of the action default — the previously hardcoded | |
| # v0.69.3 was yanked from GitHub releases mid-run (2026-04-29) and | |
| # broke the gate. Following Trivy stable is safer than chasing a | |
| # specific tag that can vanish. | |
| version: latest | |
| - name: Run Trivy — HIGH (informational) | |
| if: always() | |
| uses: aquasecurity/trivy-action@v0.35.0 | |
| with: | |
| image-ref: docling-studio:${{ matrix.target }} | |
| format: table | |
| exit-code: 0 | |
| severity: HIGH | |
| output: /tmp/trivy-high-${{ matrix.target }}.txt | |
| trivyignores: .trivyignore.yaml | |
| version: latest | |
| - 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.6.2 | |
| 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.3.1 | |
| - name: Download size artifacts | |
| uses: actions/download-artifact@v4.3.0 | |
| 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.6.2 | |
| 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.3.1 | |
| - 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.6.2 | |
| 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.3.1 | |
| - 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.6.2 | |
| 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.3.1 | |
| - name: Download image-size artifacts | |
| uses: actions/download-artifact@v4.3.0 | |
| with: | |
| pattern: image-size-* | |
| path: /tmp/artifacts | |
| - name: Download audit-checks artifact | |
| uses: actions/download-artifact@v4.3.0 | |
| with: | |
| name: audit-checks-results | |
| path: /tmp/artifacts/audit-checks-results | |
| - 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 |