Skip to content

Commit 05f513c

Browse files
sirtimidclaude
andauthored
ci: skip unnecessary jobs based on change detection (#892)
## Description Adds a `detect-changes` job to the main CI workflow that classifies changed files and conditionally skips expensive jobs when they aren't needed. This reduces CI time and resource usage for documentation, config, and CI-only PRs. ## Changes - Added a `detect-changes` job that categorizes files into: docs, config/tooling, CI, and code - **Docs-only PRs** (`.md`, `.txt`, `docs/`, `.claude/`, `LICENSE`): run lint only, skip build, test, integration, e2e, and security scan - This could be further optimized in the future by not running eslint unnecessarily - **Config-only PRs** (`.eslintrc`, `.prettierrc`, `.editorconfig`, etc.): run lint only, skip build/test/e2e - **CI-only PRs** (`.github/workflows/`, `.github/actions/`): run actionlint only, skip everything else - **Code PRs**: run everything as before (no change) - Added a `lint-only` job for config-only changes that don't need the full build/test pipeline - Updated `all-jobs-complete` gate to dynamically verify success based on what was expected to run - Pushes to `main` always run the full pipeline regardless of file types - Added a step summary with change detection results for easy debugging ## Testing This is a CI workflow change. It can be validated by observing the GitHub Actions run on this PR itself — since the only changed file is `.github/workflows/main.yml`, the `detect-changes` job should classify it as CI-only and skip lint/build/test jobs. The workflow is also self-testing through its own merge queue integration. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes CI gating logic and could unintentionally skip required checks or misclassify changes, affecting merge protection and security scanning coverage. > > **Overview** > Adds a new `detect-changes` job that diffs against the PR/merge-queue base SHA and classifies changes into `has-code`, `has-ci`, and `has-lint-targets`, publishing a step summary for debugging. > > Updates the main workflow to **conditionally run** `check-workflows`, `analyse-code`, and `lint-build-test` only when relevant (while always running the full pipeline on pushes to `main`), and introduces a `lint-only` job for non-code changes. > > Refactors `all-jobs-complete` to dynamically enforce success based on the detected change category (including requiring `detect-changes` to succeed) so the final gate accurately reflects what was expected to run. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6ac945e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2b97357 commit 05f513c

1 file changed

Lines changed: 166 additions & 5 deletions

File tree

.github/workflows/main.yml

Lines changed: 166 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,100 @@ jobs:
2626
if: github.event_name == 'merge_group'
2727
uses: MetaMask/github-tools/.github/actions/check-skip-merge-queue@v1
2828

29+
detect-changes:
30+
name: Detect changes
31+
runs-on: ubuntu-latest
32+
outputs:
33+
has-code: ${{ steps.changes.outputs.has-code }}
34+
has-ci: ${{ steps.changes.outputs.has-ci }}
35+
has-lint-targets: ${{ steps.changes.outputs.has-lint-targets }}
36+
steps:
37+
- uses: actions/checkout@v6
38+
with:
39+
fetch-depth: 0
40+
- name: Detect change categories
41+
id: changes
42+
run: |
43+
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
44+
# On pushes to main, always run everything
45+
{
46+
echo "has-code=true"
47+
echo "has-ci=true"
48+
echo "has-lint-targets=true"
49+
} >> "$GITHUB_OUTPUT"
50+
exit 0
51+
fi
52+
53+
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
54+
BASE="${{ github.event.pull_request.base.sha }}"
55+
elif [[ "${{ github.event_name }}" == "merge_group" ]]; then
56+
BASE="${{ github.event.merge_group.base_sha }}"
57+
else
58+
echo "::error::Unexpected event type: ${{ github.event_name }}"
59+
exit 1
60+
fi
61+
62+
FILES=$(git diff --name-only "$BASE" HEAD)
63+
64+
HAS_CODE=false
65+
HAS_CI=false
66+
HAS_LINT_TARGETS=false
67+
68+
while IFS= read -r file; do
69+
[[ -z "$file" ]] && continue
70+
71+
case "$file" in
72+
# CI workflow files (also lint-worthy for prettier on .yml)
73+
.github/workflows/*)
74+
HAS_CI=true
75+
HAS_LINT_TARGETS=true
76+
;;
77+
# Custom actions are test infrastructure — treat as code
78+
.github/actions/*)
79+
HAS_CI=true
80+
HAS_CODE=true
81+
HAS_LINT_TARGETS=true
82+
;;
83+
# Documentation (lint-worthy for prettier, but not code)
84+
*.md|*.txt|docs/*|LICENSE*)
85+
HAS_LINT_TARGETS=true
86+
;;
87+
# Config/tooling (lint-worthy but not code)
88+
.eslintrc*|.prettierrc*|.editorconfig|.gitignore|.gitattributes|.nvmrc|.yarnrc*)
89+
HAS_LINT_TARGETS=true
90+
;;
91+
# Everything else is code (source, tests, package.json, tsconfig, lockfile, etc.)
92+
*)
93+
HAS_CODE=true
94+
HAS_LINT_TARGETS=true
95+
;;
96+
esac
97+
done <<< "$FILES"
98+
99+
{
100+
echo "has-code=$HAS_CODE"
101+
echo "has-ci=$HAS_CI"
102+
echo "has-lint-targets=$HAS_LINT_TARGETS"
103+
} >> "$GITHUB_OUTPUT"
104+
105+
{
106+
echo "## Change detection results"
107+
echo "- has-code: $HAS_CODE"
108+
echo "- has-ci: $HAS_CI"
109+
echo "- has-lint-targets: $HAS_LINT_TARGETS"
110+
echo "### Changed files"
111+
echo '```'
112+
echo "$FILES"
113+
echo '```'
114+
} >> "$GITHUB_STEP_SUMMARY"
115+
29116
check-workflows:
30117
name: Check workflows
31118
runs-on: ubuntu-latest
32-
needs: check-skip-merge-queue
33-
if: github.event_name != 'merge_group' || needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true'
119+
needs: [check-skip-merge-queue, detect-changes]
120+
if: |
121+
(github.event_name != 'merge_group' || needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true')
122+
&& (needs.detect-changes.outputs.has-ci == 'true' || needs.detect-changes.outputs.has-code == 'true')
34123
steps:
35124
- uses: actions/checkout@v6
36125
- name: Download actionlint
@@ -43,7 +132,12 @@ jobs:
43132

44133
analyse-code:
45134
name: Code scanner
46-
needs: check-workflows
135+
needs: [check-skip-merge-queue, check-workflows, detect-changes]
136+
if: |
137+
always()
138+
&& needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true'
139+
&& needs.detect-changes.outputs.has-code == 'true'
140+
&& needs.check-workflows.result != 'failure'
47141
uses: ./.github/workflows/security-code-scanner.yml
48142
permissions:
49143
actions: read
@@ -55,9 +149,37 @@ jobs:
55149

56150
lint-build-test:
57151
name: Lint, build, and test
58-
needs: check-workflows
152+
needs: [check-skip-merge-queue, check-workflows, detect-changes]
153+
if: |
154+
always()
155+
&& needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true'
156+
&& needs.detect-changes.outputs.has-code == 'true'
157+
&& needs.check-workflows.result != 'failure'
59158
uses: ./.github/workflows/lint-build-test.yml
60159

160+
lint-only:
161+
name: Lint (no code changes)
162+
runs-on: ubuntu-latest
163+
needs: [detect-changes]
164+
if: needs.detect-changes.outputs.has-code != 'true' && needs.detect-changes.outputs.has-lint-targets == 'true'
165+
strategy:
166+
matrix:
167+
node-version: [24.x]
168+
steps:
169+
- name: Checkout and setup environment
170+
uses: MetaMask/action-checkout-and-setup@v3
171+
with:
172+
is-high-risk-environment: false
173+
node-version: ${{ matrix.node-version }}
174+
- run: yarn lint
175+
- name: Require clean working directory
176+
shell: bash
177+
run: |
178+
if ! git diff --exit-code; then
179+
echo "Working tree dirty at end of job"
180+
exit 1
181+
fi
182+
61183
coverage-report:
62184
name: Coverage report
63185
needs: lint-build-test
@@ -107,15 +229,54 @@ jobs:
107229
all-jobs-complete:
108230
name: All jobs complete
109231
runs-on: ubuntu-latest
232+
if: ${{ always() }}
110233
needs:
234+
- detect-changes
235+
- check-workflows
111236
- analyse-code
112237
- lint-build-test
238+
- lint-only
113239
outputs:
114240
passed: ${{ steps.set-output.outputs.passed }}
115241
steps:
116242
- name: Set passed output
117243
id: set-output
118-
run: echo "passed=true" >> "$GITHUB_OUTPUT"
244+
run: |
245+
# detect-changes must always succeed — without it we can't trust any outputs
246+
if [[ "${{ needs.detect-changes.result }}" != "success" ]]; then
247+
echo "detect-changes did not succeed"
248+
exit 1
249+
fi
250+
251+
HAS_CODE="${{ needs.detect-changes.outputs.has-code }}"
252+
HAS_CI="${{ needs.detect-changes.outputs.has-ci }}"
253+
HAS_LINT="${{ needs.detect-changes.outputs.has-lint-targets }}"
254+
255+
# If code changed, analyse-code and lint-build-test must have succeeded
256+
if [[ "$HAS_CODE" == "true" ]]; then
257+
if [[ "${{ needs.analyse-code.result }}" != "success" || "${{ needs.lint-build-test.result }}" != "success" ]]; then
258+
echo "Code jobs did not succeed"
259+
exit 1
260+
fi
261+
fi
262+
263+
# check-workflows runs for both CI and code changes; require it when either is present
264+
if [[ "$HAS_CI" == "true" || "$HAS_CODE" == "true" ]]; then
265+
if [[ "${{ needs.check-workflows.result }}" != "success" ]]; then
266+
echo "check-workflows did not succeed"
267+
exit 1
268+
fi
269+
fi
270+
271+
# If only lint targets changed (no code), lint-only must have succeeded
272+
if [[ "$HAS_CODE" != "true" && "$HAS_LINT" == "true" ]]; then
273+
if [[ "${{ needs.lint-only.result }}" != "success" ]]; then
274+
echo "Lint-only job did not succeed"
275+
exit 1
276+
fi
277+
fi
278+
279+
echo "passed=true" >> "$GITHUB_OUTPUT"
119280
120281
all-jobs-pass:
121282
name: All jobs pass

0 commit comments

Comments
 (0)