Skip to content

feat(ci): Implement aggregate job pattern for path-based CI checks#82

Merged
eyelock merged 1 commit into
mainfrom
feat/aggregate-ci-checks
Jan 20, 2026
Merged

feat(ci): Implement aggregate job pattern for path-based CI checks#82
eyelock merged 1 commit into
mainfrom
feat/aggregate-ci-checks

Conversation

@eyelock
Copy link
Copy Markdown
Owner

@eyelock eyelock commented Jan 20, 2026

Summary

Implements the aggregate job pattern to enable path-based CI checking without blocking automation. This solves the critical issue where the appcast workflow cannot push documentation updates to main.

The Problem We're Solving

Current Issue:

remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: - 4 of 4 required status checks are expected.

When the appcast workflow pushes Docs/appcast.xml updates:

  1. CI workflow has paths: filters (only runs on code changes)
  2. Docs-only commits → workflow doesn't run
  3. Branch protection requires: Build, Test, Lint, Format
  4. Checks stuck in "Pending" state forever
  5. Push blocked ❌

Why GitHub Rulesets Didn't Work:

After extensive research, we discovered:

  • GitHub Rulesets patterns match branch names, not file paths in commits
  • Pattern Sources/** means "branches starting with Sources/" (like Sources/feature-foo)
  • NOT "commits that touch files in Sources/ directory"
  • Path-based protection requires a different approach

The Solution: Aggregate Job Pattern

This is the battle-tested, industry-standard pattern for monorepos and path-filtered CI.

How It Works

Before (Blocked):

on:
  push:
    paths: ['Sources/**', 'Tests/**', ...]  # Doesn't run for docs
jobs:
  build: ...  # Required in branch protection
  test: ...   # Required in branch protection

→ Docs commit → Workflow skipped → Checks "Pending" → Blocked

After (Fixed):

on:
  push:  # Always runs (no path filter)
jobs:
  changes:  # Detect what changed
    outputs: { code: 'true/false' }
  
  build:
    needs: changes
    if: needs.changes.outputs.code == 'true'  # Conditional
  
  all-clear:  # Aggregate - ALWAYS runs
    needs: [changes, build, test, lint, format]
    if: always()
    # Passes if jobs succeeded OR were skipped

→ Docs commit → changes detects no code → Jobs skip (report Success) → all-clear passes ✅

Key Technical Details

  1. Skipped jobs report "Success" - When a job has if: condition that evaluates false, GitHub treats it as succeeded, NOT pending

  2. all-clear always runs - Uses if: always() to run regardless of upstream job results

  3. Checks for failures - Uses contains(needs.*.result, 'failure') to detect if any required job failed

  4. One required check - Branch protection only needs to require "All Clear", not individual jobs

Changes

Modified: .github/workflows/ci.yml

Removed:

  • paths: filters from workflow triggers (lines 6-14, 17-25)

Added:

  • changes job - Detects file changes using dorny/paths-filter@v3
  • Conditional logic on build, test, lint, format jobs
  • all-clear aggregate job with failure detection

Unchanged:

  • All existing job functionality preserved
  • Same checks run when code changes
  • build-release job unchanged

Impact

Immediate Benefits

  • ✅ Appcast workflow can push to Docs/ without CI blocks
  • ✅ Docs-only commits pass instantly (no wasted CI time)
  • ✅ Code changes still require all 4 checks to pass
  • ✅ No false "Pending" states blocking merges

What Still Works

  • ✅ All CI checks run on code changes
  • ✅ Pull requests validated before merge
  • ✅ Manual workflow dispatch still works
  • ✅ Hotfix branches protected

Testing This PR

This PR will demonstrate the fix:

  1. This PR only changes .github/workflows/ci.yml (workflow file)
  2. Workflow file is in the "code" path filter
  3. CI should run all checks (build, test, lint, format)
  4. All checks should pass
  5. all-clear check should appear and pass

After merge, test with docs-only commit:

echo "test" >> Docs/test.txt
git commit -m "test: docs only"
git push origin main

Should succeed without running full CI.

Branch Protection Setup (After Merge)

Create one simple ruleset or use legacy branch protection:

GitHub Rulesets (Recommended):

  1. Go to: Repository → Settings → Rules → Rulesets
  2. Create "Main Branch Protection"
  3. Target: Default branch (main)
  4. Required status checks: "All Clear" (ONLY this one)
  5. Block force pushes: ✅
  6. Restrict deletions: ✅

OR Legacy Branch Protection:

  1. Go to: Settings → Branches → Branch protection rules
  2. Branch: main
  3. Required status checks: "All Clear"
  4. ✅ Require branches to be up to date

CRITICAL: Do NOT require Build/Test/Lint/Format individually - only require "All Clear"

References

This pattern is widely documented and used by major engineering teams:

Rollback Plan

If issues arise:

  1. Revert this PR (one commit to revert)
  2. Re-enable old branch protection
  3. Appcast workflow will be blocked again, but repo is functional

🤖 Generated with Claude Code

Replaces path-filtered workflow triggers with conditional jobs and an
aggregate status check to solve the branch protection blocking issue.

**Problem:**
- Path filters at workflow level cause checks to not run for docs-only commits
- Branch protection requires these checks
- Checks stuck in "Pending" state forever
- Blocks appcast workflow from pushing to Docs/

**Solution:**
- Remove paths from workflow trigger (workflow always runs)
- Add "changes" detection job using dorny/paths-filter
- Make build/test/lint/format conditional on code changes
- Add "all-clear" aggregate job that always runs
- Jobs that skip report "Success" (not "Pending")

**How It Works:**
1. Docs-only commit → changes detects no code changes
2. Build/test/lint/format jobs skip (report Success)
3. all-clear job checks results, sees no failures, passes ✅
4. Push succeeds without running full CI

**Branch Protection Setup:**
- Require ONLY the "All Clear" check
- Do NOT require individual Build/Test/Lint/Format checks
- This single check aggregates all results

**Benefits:**
- Appcast workflow can push docs without CI
- Code changes still require full CI validation
- No false "Pending" blocks
- Battle-tested pattern used by major teams

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@eyelock eyelock merged commit a1b0285 into main Jan 20, 2026
7 checks passed
@eyelock eyelock deleted the feat/aggregate-ci-checks branch January 20, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant