mirror of
https://github.com/LearningCircuit/local-deep-research.git
synced 2026-06-15 19:46:56 +03:00
feat(ci): add CI gate to release pipeline for PR-quality checks (#2371)
Add a companion CI gate alongside the existing security gate in the release pipeline. This ensures all PR-quality checks (linting, type checking, tests, validation) run and pass before any release proceeds, closing the gap where PR tests could be skipped at release time.
This commit is contained in:
7
.github/scripts/file-whitelist-check.sh
vendored
7
.github/scripts/file-whitelist-check.sh
vendored
@@ -21,7 +21,12 @@ while IFS= read -r line; do
|
||||
done < "$WHITELIST_FILE"
|
||||
|
||||
# Get list of files to check
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
||||
if [ "${CHECK_ALL_FILES:-}" = "true" ]; then
|
||||
echo "🔍 Checking ALL tracked files (release gate mode)..."
|
||||
CHANGED_FILES=$(git ls-files)
|
||||
TOTAL_FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l)
|
||||
echo "📋 Found $TOTAL_FILE_COUNT tracked files to check"
|
||||
elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
||||
# For PRs: check all files that would be added/modified in the entire PR
|
||||
echo "🔍 Checking files in PR from $GITHUB_BASE_REF to HEAD..."
|
||||
|
||||
|
||||
1
.github/workflows/check-env-vars.yml
vendored
1
.github/workflows/check-env-vars.yml
vendored
@@ -2,6 +2,7 @@ name: Check Environment Variables
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_call: # Called by ci-gate.yml for release pipeline
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
259
.github/workflows/ci-gate.yml
vendored
Normal file
259
.github/workflows/ci-gate.yml
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
name: CI Gate
|
||||
|
||||
# CI quality gate for the release pipeline.
|
||||
#
|
||||
# Ensures all PR-quality checks (linting, type checking, tests, validation)
|
||||
# run and pass before any release proceeds. Complements the security-focused
|
||||
# release-gate.yml with code quality and correctness checks.
|
||||
#
|
||||
# Architecture:
|
||||
# release.yml → ci-gate.yml → {pre-commit, mypy, docker-tests, ...}
|
||||
# Nesting depth: release.yml(1) → ci-gate.yml(2) → workflow(3) — safe limit.
|
||||
|
||||
on:
|
||||
workflow_call: # Called by release.yml
|
||||
secrets:
|
||||
OPENROUTER_API_KEY:
|
||||
required: false
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
permissions: {} # Minimal top-level for OSSF Scorecard
|
||||
|
||||
jobs:
|
||||
# ============================================
|
||||
# Code Quality
|
||||
# ============================================
|
||||
pre-commit:
|
||||
uses: ./.github/workflows/pre-commit.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
mypy-type-check:
|
||||
uses: ./.github/workflows/mypy-type-check.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# ============================================
|
||||
# Test Suites
|
||||
# ============================================
|
||||
docker-tests:
|
||||
uses: ./.github/workflows/docker-tests.yml
|
||||
with:
|
||||
strict-mode: true
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write # Needed by pytest-tests for PR comments (no-ops in release context)
|
||||
secrets:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
|
||||
# ============================================
|
||||
# Validation
|
||||
# ============================================
|
||||
validate-image-pinning:
|
||||
uses: ./.github/workflows/validate-image-pinning.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
file-whitelist-check:
|
||||
uses: ./.github/workflows/file-whitelist-check.yml
|
||||
with:
|
||||
check-all-files: true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
check-env-vars:
|
||||
uses: ./.github/workflows/check-env-vars.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
security-file-write-check:
|
||||
uses: ./.github/workflows/security-file-write-check.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# ============================================
|
||||
# Summary job that reports overall status
|
||||
# ============================================
|
||||
ci-gate-summary:
|
||||
name: CI Gate Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
# Code Quality
|
||||
- pre-commit
|
||||
- mypy-type-check
|
||||
# Test Suites
|
||||
- docker-tests
|
||||
# Validation
|
||||
- validate-image-pinning
|
||||
- file-whitelist-check
|
||||
- check-env-vars
|
||||
- security-file-write-check
|
||||
if: always()
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check CI scan results
|
||||
env:
|
||||
PRE_COMMIT_RESULT: ${{ needs.pre-commit.result }}
|
||||
MYPY_RESULT: ${{ needs.mypy-type-check.result }}
|
||||
DOCKER_TESTS_RESULT: ${{ needs.docker-tests.result }}
|
||||
IMAGE_PINNING_RESULT: ${{ needs.validate-image-pinning.result }}
|
||||
FILE_WHITELIST_RESULT: ${{ needs.file-whitelist-check.result }}
|
||||
ENV_VARS_RESULT: ${{ needs.check-env-vars.result }}
|
||||
FILE_WRITE_RESULT: ${{ needs.security-file-write-check.result }}
|
||||
run: |
|
||||
# Redirect all output to GITHUB_STEP_SUMMARY (fixes SC2129)
|
||||
exec >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Count results first
|
||||
FAILED=""
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
check_result() {
|
||||
local result="$1"
|
||||
if [ "$result" = "success" ]; then
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
return 0
|
||||
else
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
FAILED="true"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check all results silently first
|
||||
check_result "$PRE_COMMIT_RESULT" || true
|
||||
check_result "$MYPY_RESULT" || true
|
||||
check_result "$DOCKER_TESTS_RESULT" || true
|
||||
check_result "$IMAGE_PINNING_RESULT" || true
|
||||
check_result "$FILE_WHITELIST_RESULT" || true
|
||||
check_result "$ENV_VARS_RESULT" || true
|
||||
check_result "$FILE_WRITE_RESULT" || true
|
||||
|
||||
TOTAL=$((PASS_COUNT + FAIL_COUNT))
|
||||
|
||||
# ============================================
|
||||
# BIG STATUS BANNER
|
||||
# ============================================
|
||||
if [ -z "$FAILED" ]; then
|
||||
echo "# :white_check_mark: CI GATE: PASSED"
|
||||
echo ""
|
||||
echo "> **All $TOTAL CI checks passed successfully.**"
|
||||
echo "> This release is approved from a code quality perspective."
|
||||
else
|
||||
echo "# :x: CI GATE: FAILED"
|
||||
echo ""
|
||||
echo "> **$FAIL_COUNT of $TOTAL checks failed.** Release is blocked."
|
||||
echo "> Review the failures below and fix before releasing."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "## Detailed Results"
|
||||
echo ""
|
||||
|
||||
# Reset for detailed output
|
||||
FAILED=""
|
||||
|
||||
# ============================================
|
||||
# Code Quality
|
||||
# ============================================
|
||||
echo "### Code Quality"
|
||||
|
||||
if [ "$PRE_COMMIT_RESULT" = "success" ]; then
|
||||
echo ":white_check_mark: **Pre-commit (linting, formatting)**: Passed"
|
||||
else
|
||||
echo ":x: **Pre-commit (linting, formatting)**: $PRE_COMMIT_RESULT"
|
||||
FAILED="true"
|
||||
fi
|
||||
|
||||
if [ "$MYPY_RESULT" = "success" ]; then
|
||||
echo ":white_check_mark: **Mypy Type Check**: Passed"
|
||||
else
|
||||
echo ":x: **Mypy Type Check**: $MYPY_RESULT"
|
||||
FAILED="true"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# Test Suites
|
||||
# ============================================
|
||||
echo ""
|
||||
echo "### Test Suites"
|
||||
|
||||
if [ "$DOCKER_TESTS_RESULT" = "success" ]; then
|
||||
echo ":white_check_mark: **Docker Tests (pytest + UI + LLM + infra + smoke)**: Passed"
|
||||
else
|
||||
echo ":x: **Docker Tests (pytest + UI + LLM + infra + smoke)**: $DOCKER_TESTS_RESULT"
|
||||
FAILED="true"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# Validation
|
||||
# ============================================
|
||||
echo ""
|
||||
echo "### Validation"
|
||||
|
||||
if [ "$IMAGE_PINNING_RESULT" = "success" ]; then
|
||||
echo ":white_check_mark: **Docker Image Pinning**: Passed"
|
||||
else
|
||||
echo ":x: **Docker Image Pinning**: $IMAGE_PINNING_RESULT"
|
||||
FAILED="true"
|
||||
fi
|
||||
|
||||
if [ "$FILE_WHITELIST_RESULT" = "success" ]; then
|
||||
echo ":white_check_mark: **File Whitelist Security**: Passed"
|
||||
else
|
||||
echo ":x: **File Whitelist Security**: $FILE_WHITELIST_RESULT"
|
||||
FAILED="true"
|
||||
fi
|
||||
|
||||
if [ "$ENV_VARS_RESULT" = "success" ]; then
|
||||
echo ":white_check_mark: **Environment Variables**: Passed"
|
||||
else
|
||||
echo ":x: **Environment Variables**: $ENV_VARS_RESULT"
|
||||
FAILED="true"
|
||||
fi
|
||||
|
||||
if [ "$FILE_WRITE_RESULT" = "success" ]; then
|
||||
echo ":white_check_mark: **Security File Writes**: Passed"
|
||||
else
|
||||
echo ":x: **Security File Writes**: $FILE_WRITE_RESULT"
|
||||
FAILED="true"
|
||||
fi
|
||||
|
||||
# ============================================
|
||||
# Final result with prominent summary
|
||||
# ============================================
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
|
||||
if [ -n "$FAILED" ]; then
|
||||
echo "## :rotating_light: Action Required"
|
||||
echo ""
|
||||
echo "| Status | Result |"
|
||||
echo "|--------|--------|"
|
||||
echo "| **Gate** | :x: **BLOCKED** |"
|
||||
echo "| **Passed** | $PASS_COUNT |"
|
||||
echo "| **Failed** | $FAIL_COUNT |"
|
||||
echo ""
|
||||
echo "_Fix the failing checks above before releasing._"
|
||||
exit 1
|
||||
else
|
||||
echo "## :tada: Ready for Release"
|
||||
echo ""
|
||||
echo "| Status | Result |"
|
||||
echo "|--------|--------|"
|
||||
echo "| **Gate** | :white_check_mark: **APPROVED** |"
|
||||
echo "| **Passed** | $PASS_COUNT / $TOTAL |"
|
||||
echo ""
|
||||
echo "_All CI checks passed. Security scans run as separate gate in release pipeline._"
|
||||
fi
|
||||
30
.github/workflows/docker-tests.yml
vendored
30
.github/workflows/docker-tests.yml
vendored
@@ -6,6 +6,20 @@ on:
|
||||
branches: [ main, dev ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_call: # Called by ci-gate.yml for release pipeline
|
||||
inputs:
|
||||
strict-mode:
|
||||
description: 'Run in strict mode (all tests blocking, no continue-on-error)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
secrets:
|
||||
OPENROUTER_API_KEY:
|
||||
required: false
|
||||
GIST_TOKEN:
|
||||
required: false
|
||||
COVERAGE_GIST_ID:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
|
||||
# Top-level permissions set to minimum (OSSF Scorecard Token-Permissions)
|
||||
@@ -66,7 +80,8 @@ jobs:
|
||||
if: |
|
||||
github.event_name == 'pull_request' ||
|
||||
github.ref == 'refs/heads/main' ||
|
||||
github.ref == 'refs/heads/dev'
|
||||
github.ref == 'refs/heads/dev' ||
|
||||
inputs.strict-mode == true
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
@@ -140,8 +155,9 @@ jobs:
|
||||
cd tests/infrastructure_tests && npm ci
|
||||
|
||||
- name: Run all pytest tests with coverage
|
||||
# Continue even if some tests fail - we still want the coverage report
|
||||
continue-on-error: true
|
||||
# In strict mode (release pipeline): tests are blocking
|
||||
# In normal mode (PR/push): continue even if some tests fail for coverage report
|
||||
continue-on-error: ${{ !inputs.strict-mode }}
|
||||
env:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
run: |
|
||||
@@ -587,7 +603,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: LLM Unit Tests
|
||||
needs: [build-test-image, detect-changes]
|
||||
if: needs.detect-changes.outputs.llm == 'true' || github.event_name == 'workflow_dispatch'
|
||||
if: needs.detect-changes.outputs.llm == 'true' || github.event_name == 'workflow_dispatch' || inputs.strict-mode == true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -662,7 +678,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: LLM Example Tests
|
||||
needs: [build-test-image, detect-changes]
|
||||
if: needs.detect-changes.outputs.llm == 'true' || github.event_name == 'workflow_dispatch'
|
||||
if: needs.detect-changes.outputs.llm == 'true' || github.event_name == 'workflow_dispatch' || inputs.strict-mode == true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -730,7 +746,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Production Image Smoke Test
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.docker == 'true' || github.event_name == 'workflow_dispatch'
|
||||
if: needs.detect-changes.outputs.docker == 'true' || github.event_name == 'workflow_dispatch' || inputs.strict-mode == true
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -860,7 +876,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Infrastructure Tests
|
||||
needs: [build-test-image, detect-changes]
|
||||
if: needs.detect-changes.outputs.infrastructure == 'true' || github.event_name == 'workflow_dispatch'
|
||||
if: needs.detect-changes.outputs.infrastructure == 'true' || github.event_name == 'workflow_dispatch' || inputs.strict-mode == true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
8
.github/workflows/file-whitelist-check.yml
vendored
8
.github/workflows/file-whitelist-check.yml
vendored
@@ -4,6 +4,13 @@ name: File Whitelist Security Check
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
workflow_call: # Called by ci-gate.yml for release pipeline
|
||||
inputs:
|
||||
check-all-files:
|
||||
description: 'Check ALL tracked files (not just changed files)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -29,5 +36,6 @@ jobs:
|
||||
env:
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
CHECK_ALL_FILES: ${{ inputs.check-all-files }}
|
||||
run: |
|
||||
.github/scripts/file-whitelist-check.sh
|
||||
|
||||
1
.github/workflows/mypy-type-check.yml
vendored
1
.github/workflows/mypy-type-check.yml
vendored
@@ -3,6 +3,7 @@ name: Mypy Type Checking
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
workflow_call: # Called by ci-gate.yml for release pipeline
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
1
.github/workflows/pre-commit.yml
vendored
1
.github/workflows/pre-commit.yml
vendored
@@ -3,6 +3,7 @@ name: Pre-commit Checks
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
workflow_call: # Called by ci-gate.yml for release pipeline
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -100,6 +100,22 @@ jobs:
|
||||
actions: read
|
||||
packages: read # needed by codeql-scan
|
||||
|
||||
# ============================================================================
|
||||
# CI GATE - All CI checks must pass before release proceeds
|
||||
# ============================================================================
|
||||
# This gate runs all PR-quality checks (linting, type checking, tests,
|
||||
# validation) that complement the security-focused release-gate.
|
||||
# ============================================================================
|
||||
ci-gate:
|
||||
needs: [version-check]
|
||||
if: needs.version-check.outputs.should_release == 'true'
|
||||
uses: ./.github/workflows/ci-gate.yml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write # Needed by docker-tests for PR comments (no-ops in release context)
|
||||
secrets:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
|
||||
# ============================================================================
|
||||
# TEST GATE (Advisory) - WebKit tests run but don't block releases
|
||||
# ============================================================================
|
||||
@@ -162,9 +178,10 @@ jobs:
|
||||
contents: read
|
||||
|
||||
build:
|
||||
needs: [version-check, release-gate, test-gate, e2e-test-gate, responsive-test-gate, compat-test-gate]
|
||||
# test-gate (Playwright WebKit) and responsive-test-gate are advisory; release-gate, e2e-test-gate, and compat-test-gate are required
|
||||
if: ${{ !cancelled() && needs.release-gate.result == 'success' && needs.e2e-test-gate.result == 'success' && needs.compat-test-gate.result == 'success' }}
|
||||
needs: [version-check, release-gate, ci-gate, test-gate, e2e-test-gate, responsive-test-gate, compat-test-gate]
|
||||
# test-gate (Playwright WebKit) and responsive-test-gate are advisory
|
||||
# release-gate, ci-gate, e2e-test-gate, and compat-test-gate are required
|
||||
if: ${{ !cancelled() && needs.release-gate.result == 'success' && needs.ci-gate.result == 'success' && needs.e2e-test-gate.result == 'success' && needs.compat-test-gate.result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
|
||||
@@ -3,6 +3,7 @@ name: Security File Write Check
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_call: # Called by ci-gate.yml for release pipeline
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
1
.github/workflows/validate-image-pinning.yml
vendored
1
.github/workflows/validate-image-pinning.yml
vendored
@@ -12,6 +12,7 @@ on:
|
||||
- '.github/workflows/validate-image-pinning.yml'
|
||||
- '.github/scripts/validate-docker-compose-images.sh'
|
||||
- '.github/scripts/validate-workflow-images.py'
|
||||
workflow_call: # Called by ci-gate.yml for release pipeline
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Minimal permissions for OSSF Scorecard
|
||||
|
||||
Reference in New Issue
Block a user