Phase 82: Counts & Pending-Set Cutover (#231) #902
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: CI | |
| on: | |
| push: | |
| # Only main + release tags. Feature-branch CI runs via the pull_request | |
| # event below, so pushing a branch that also has an open PR no longer | |
| # triggers two competing runs that cancel each other on the concurrency | |
| # group. Also stops docker-publish (push: github.event_name != 'pull_request') | |
| # from publishing images off feature-branch pushes -- publishing now only | |
| # happens on main and bare CalVer [0-9]+.[0-9]+.[0-9]+ tags. | |
| branches: ["main"] | |
| tags: ["[0-9]+.[0-9]+.[0-9]+"] | |
| pull_request: | |
| branches: ["**"] | |
| env: | |
| CI: true | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| security-events: write | |
| jobs: | |
| # ============================================================================ | |
| # CHANGE DETECTION - Skip heavy jobs for docs-only changes | |
| # ============================================================================ | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| code-changed: ${{ steps.filter.outputs.code-changed }} | |
| steps: | |
| - name: π Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: π§ Setup Just | |
| uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0 | |
| - name: π Detect changed file types | |
| id: filter | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF_TYPE: ${{ github.ref_type }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.sha }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| run: | | |
| if [[ "${EVENT_NAME}" == "schedule" ]] || \ | |
| [[ "${EVENT_NAME}" == "workflow_dispatch" ]] || \ | |
| [[ "${REF_TYPE}" == "tag" ]]; then | |
| echo "code-changed=true" >> "${GITHUB_OUTPUT}" | |
| echo "βοΈ Scheduled/manual/tag build β running full pipeline" | |
| exit 0 | |
| fi | |
| if [[ "${EVENT_NAME}" == "pull_request" ]]; then | |
| CHANGED_FILES=$(git diff --name-only "${BASE_SHA}" "${HEAD_SHA}") | |
| elif [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" ]] || ! git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then | |
| # New branch (zero SHA) or force-pushed branch whose before-SHA is gone β compare against default branch | |
| CHANGED_FILES=$(git diff --name-only "origin/main...${HEAD_SHA}") | |
| else | |
| CHANGED_FILES=$(git diff --name-only "${BEFORE_SHA}" "${HEAD_SHA}") | |
| fi | |
| echo "π Changed files:" | |
| echo "${CHANGED_FILES}" | |
| # Delegate the doc-vs-code classification to the versioned, unit-tested | |
| # scripts/classify-changed-files.sh via `just` (D-10). The script prints | |
| # `code-changed=false` when EVERY changed path is documentation | |
| # (*.md, .planning/**, LICENSE, docs/**, *.txt) and `code-changed=true` | |
| # otherwise β conservative by construction, so any code/test/workflow | |
| # change still runs the full pipeline. Its stdout is already in | |
| # GitHub Actions name=value form, appended straight to the job output. | |
| CODE_CHANGED=$(echo "${CHANGED_FILES}" | just detect-code-changes) | |
| echo "${CODE_CHANGED}" >> "${GITHUB_OUTPUT}" | |
| if [[ "${CODE_CHANGED}" == "code-changed=false" ]]; then | |
| echo "π Only documentation/planning files changed β skipping tests, security, and docker jobs" | |
| else | |
| echo "π» Code changes detected β running full pipeline" | |
| fi | |
| # ============================================================================ | |
| # QUALITY GATES - Run in parallel after change detection | |
| # ============================================================================ | |
| quality: | |
| uses: ./.github/workflows/code-quality.yml | |
| test: | |
| needs: [detect-changes, quality] | |
| if: needs.detect-changes.outputs.code-changed == 'true' | |
| uses: ./.github/workflows/tests.yml | |
| # nosemgrep: yaml.github-actions.security.secrets-inherit.secrets-inherit -- in-repo reusable workflow; inherit is intentional. | |
| secrets: inherit | |
| security: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.code-changed == 'true' | |
| uses: ./.github/workflows/security.yml | |
| permissions: | |
| contents: read | |
| security-events: write | |
| docker: | |
| needs: [detect-changes, quality] | |
| if: needs.detect-changes.outputs.code-changed == 'true' | |
| uses: ./.github/workflows/docker-validate.yml | |
| # ============================================================================ | |
| # AGGREGATE RESULTS - Enforce quality gates | |
| # ============================================================================ | |
| aggregate-results: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - detect-changes | |
| - quality | |
| - test | |
| - security | |
| - docker | |
| if: always() | |
| steps: | |
| - name: π Check all pipeline results | |
| env: | |
| CODE_CHANGED: ${{ needs.detect-changes.outputs.code-changed }} | |
| DETECT_RESULT: ${{ needs.detect-changes.result }} | |
| QUALITY_RESULT: ${{ needs.quality.result }} | |
| TEST_RESULT: ${{ needs.test.result }} | |
| SECURITY_RESULT: ${{ needs.security.result }} | |
| DOCKER_RESULT: ${{ needs.docker.result }} | |
| run: | | |
| echo "Pipeline Results Summary:" | |
| echo " Detect Changes: ${DETECT_RESULT}" | |
| echo " Code Changed: ${CODE_CHANGED}" | |
| echo " Code Quality: ${QUALITY_RESULT}" | |
| echo " Tests: ${TEST_RESULT}" | |
| echo " Security: ${SECURITY_RESULT}" | |
| echo " Docker: ${DOCKER_RESULT}" | |
| # DENY-LIST gate: a job counts as passing ONLY when it explicitly | |
| # succeeded (docs-only skip is the one sanctioned exception below). | |
| # Allow-listing just "failure" (the old logic) let a failed or | |
| # cancelled detect-changes cascade into skipped test/security/docker | |
| # and slip through green with nothing run (CR-01). | |
| # The change gate itself must have run cleanly; otherwise code-changed | |
| # is untrustworthy and the heavy jobs were skipped on a false signal. | |
| if [[ "${DETECT_RESULT}" != "success" ]]; then | |
| echo "β Change detection did not succeed (${DETECT_RESULT}) β cannot trust the skip gate" | |
| exit 1 | |
| fi | |
| # Code quality always runs and must always pass. | |
| if [[ "${QUALITY_RESULT}" != "success" ]]; then | |
| echo "β Code quality did not pass (${QUALITY_RESULT})" | |
| exit 1 | |
| fi | |
| # Docs-only change: test/security/docker are intentionally skipped. | |
| # Accept 'skipped' (skip-with-success) but reject failure/cancelled. | |
| if [[ "${CODE_CHANGED}" == "false" ]]; then | |
| for result in "${TEST_RESULT}" "${SECURITY_RESULT}" "${DOCKER_RESULT}"; do | |
| if [[ "${result}" != "skipped" && "${result}" != "success" ]]; then | |
| echo "β Docs-only change but a gated job ended '${result}' (expected skipped)" | |
| exit 1 | |
| fi | |
| done | |
| echo "π Docs-only change β gated jobs skipped as expected" | |
| echo "β All required pipeline workflows passed!" | |
| exit 0 | |
| fi | |
| # Code change: every gated job must have explicitly succeeded. | |
| for pair in "Tests:${TEST_RESULT}" "Security:${SECURITY_RESULT}" "Docker:${DOCKER_RESULT}"; do | |
| name="${pair%%:*}" | |
| result="${pair##*:}" | |
| if [[ "${result}" != "success" ]]; then | |
| echo "β ${name} did not succeed (${result})" | |
| exit 1 | |
| fi | |
| done | |
| echo "β All pipeline workflows passed!" | |
| # ============================================================================ | |
| # DOCKER PUBLISH - Build and push images to GHCR after all gates pass | |
| # ============================================================================ | |
| docker-publish: | |
| needs: [detect-changes, aggregate-results] | |
| if: needs.detect-changes.outputs.code-changed == 'true' | |
| uses: ./.github/workflows/docker-publish.yml | |
| # nosemgrep: yaml.github-actions.security.secrets-inherit.secrets-inherit -- in-repo reusable workflow; inherit is intentional. | |
| secrets: inherit | |
| permissions: | |
| contents: read | |
| packages: write |