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:
LearningCircuit
2026-02-22 17:25:09 +01:00
committed by GitHub
parent 98e86450b8
commit d07ff2bdf7
10 changed files with 321 additions and 11 deletions

View File

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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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 }}

View File

@@ -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:

View File

@@ -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