Skip to content

Commit 9a9f955

Browse files
marcstraubeclaude
andauthored
chore(ci): add path-based job filtering to CI pipeline (#67)
## Summary Closes #64 - Add `dorny/paths-filter@v3` to detect changed file categories (`source`, `tooling`, `deps`) - Conditionally skip expensive jobs (mutation, build, security, sbom) for docs-only and release-please PRs - Add always-running `format` job as lightweight baseline check - Add **CI Gate** job (pattern from `marcstraube/common` collection) as single required check for branch protection ## Job trigger matrix | Changed files | format | quality | build | security | mutation | sbom | |---|---|---|---|---|---|---| | `src/**`, `tests/**` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `**/*.md`, docs only | ✅ | ⏭️ | ⏭️ | ⏭️ | ⏭️ | ⏭️ | | `eslint.config.js`, `tsconfig.json` | ✅ | ✅ | ✅ | ⏭️ | ⏭️ | ⏭️ | | `package.json`, `pnpm-lock.yaml` | ✅ | ✅ | ✅ | ✅ | ⏭️ | ✅ | | `release-please--*` branch | ✅ | ⏭️ | ⏭️ | ⏭️ | ⏭️ | ⏭️ | ## Follow-up After merge, update Branch Protection required checks: replace individual job checks with **`CI Gate`**. ## Test plan - [ ] Verify CI runs all jobs on this PR (source files not changed, but workflow file is new) - [ ] After merge, test with a docs-only PR to confirm jobs are skipped - [ ] Update branch protection to require only `CI Gate` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d3a1e40 commit 9a9f955

1 file changed

Lines changed: 110 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,45 @@ permissions:
1010
contents: read
1111

1212
jobs:
13-
quality:
13+
changes:
1414
runs-on: ubuntu-latest
15+
outputs:
16+
source: ${{ steps.filter.outputs.source }}
17+
tooling: ${{ steps.filter.outputs.tooling }}
18+
deps: ${{ steps.filter.outputs.deps }}
19+
release-please: ${{ steps.release.outputs.match }}
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v6
1523

24+
- name: Detect changed paths
25+
uses: dorny/paths-filter@v3
26+
id: filter
27+
with:
28+
filters: |
29+
source:
30+
- 'src/**'
31+
- 'tests/**'
32+
tooling:
33+
- 'eslint.config.js'
34+
- 'tsconfig.json'
35+
- 'vitest.config.ts'
36+
- 'stryker.config.js'
37+
deps:
38+
- 'package.json'
39+
- 'pnpm-lock.yaml'
40+
41+
- name: Check for release-please branch
42+
id: release
43+
run: |
44+
if [[ "${{ github.head_ref }}" == release-please--* ]]; then
45+
echo "match=true" >> "$GITHUB_OUTPUT"
46+
else
47+
echo "match=false" >> "$GITHUB_OUTPUT"
48+
fi
49+
50+
format:
51+
runs-on: ubuntu-latest
1652
steps:
1753
- name: Checkout repository
1854
uses: actions/checkout@v6
@@ -34,6 +70,32 @@ jobs:
3470
- name: Check formatting
3571
run: pnpm run format:check
3672

73+
quality:
74+
needs: changes
75+
if: >-
76+
needs.changes.outputs.release-please != 'true' &&
77+
(needs.changes.outputs.source == 'true' ||
78+
needs.changes.outputs.tooling == 'true' ||
79+
needs.changes.outputs.deps == 'true')
80+
runs-on: ubuntu-latest
81+
steps:
82+
- name: Checkout repository
83+
uses: actions/checkout@v6
84+
85+
- name: Setup pnpm
86+
uses: pnpm/action-setup@v5
87+
with:
88+
version: 10
89+
90+
- name: Setup Node.js
91+
uses: actions/setup-node@v6
92+
with:
93+
node-version: '20'
94+
cache: 'pnpm'
95+
96+
- name: Install dependencies
97+
run: pnpm install --frozen-lockfile
98+
3799
- name: Run linter
38100
run: pnpm run lint
39101

@@ -52,9 +114,13 @@ jobs:
52114
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
53115

54116
build:
117+
needs: [changes, quality]
118+
if: >-
119+
needs.changes.outputs.release-please != 'true' &&
120+
(needs.changes.outputs.source == 'true' ||
121+
needs.changes.outputs.tooling == 'true' ||
122+
needs.changes.outputs.deps == 'true')
55123
runs-on: ubuntu-latest
56-
needs: quality
57-
58124
steps:
59125
- name: Checkout repository
60126
uses: actions/checkout@v6
@@ -87,8 +153,12 @@ jobs:
87153
run: pnpm run size
88154

89155
security:
156+
needs: changes
157+
if: >-
158+
needs.changes.outputs.release-please != 'true' &&
159+
(needs.changes.outputs.source == 'true' ||
160+
needs.changes.outputs.deps == 'true')
90161
runs-on: ubuntu-latest
91-
92162
steps:
93163
- name: Checkout repository
94164
uses: actions/checkout@v6
@@ -111,9 +181,11 @@ jobs:
111181
run: pnpm audit --prod --audit-level=moderate
112182

113183
mutation:
184+
needs: [changes, quality]
185+
if: >-
186+
needs.changes.outputs.release-please != 'true' &&
187+
needs.changes.outputs.source == 'true'
114188
runs-on: ubuntu-latest
115-
needs: quality
116-
117189
steps:
118190
- name: Checkout repository
119191
uses: actions/checkout@v6
@@ -143,9 +215,12 @@ jobs:
143215
path: reports/mutation/
144216

145217
sbom:
218+
needs: [changes, quality]
219+
if: >-
220+
needs.changes.outputs.release-please != 'true' &&
221+
(needs.changes.outputs.source == 'true' ||
222+
needs.changes.outputs.deps == 'true')
146223
runs-on: ubuntu-latest
147-
needs: quality
148-
149224
steps:
150225
- name: Checkout repository
151226
uses: actions/checkout@v6
@@ -172,3 +247,30 @@ jobs:
172247
with:
173248
name: sbom
174249
path: sbom.cdx.json
250+
251+
# =============================================================================
252+
# CI GATE (single required check for branch protection)
253+
# =============================================================================
254+
ci-gate:
255+
name: CI Gate
256+
runs-on: ubuntu-latest
257+
if: always()
258+
needs: [format, quality, build, security, mutation, sbom]
259+
steps:
260+
- name: Check required job results
261+
run: |
262+
results=(
263+
"${{ needs.format.result }}"
264+
"${{ needs.quality.result }}"
265+
"${{ needs.build.result }}"
266+
"${{ needs.security.result }}"
267+
"${{ needs.mutation.result }}"
268+
"${{ needs.sbom.result }}"
269+
)
270+
for r in "${results[@]}"; do
271+
if [ "$r" = "failure" ] || [ "$r" = "cancelled" ]; then
272+
echo "::error::Required job failed or was cancelled: $r"
273+
exit 1
274+
fi
275+
done
276+
echo "All required jobs passed (or were skipped)."

0 commit comments

Comments
 (0)