Skip to content

Phase 82: Counts & Pending-Set Cutover (#231) #902

Phase 82: Counts & Pending-Set Cutover (#231)

Phase 82: Counts & Pending-Set Cutover (#231) #902

Workflow file for this run

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