Skip to content

Commit ed06a78

Browse files
authored
🤖 ci: gate CI jobs by changed paths (#12)
## Summary Introduce path-aware CI orchestration so expensive Go jobs only run when Go-relevant files change. ## Background Docs-only pull requests were still triggering full Go lint/test/build workflows. This change preserves CI signal while reducing unnecessary job execution. ## Implementation - Added a `changes` job using pinned `dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36`. - Added `go`, `workflows`, and `publish` outputs from that job. - Gated `lint` and `test` on `needs.changes.outputs.go == 'true'`. - Gated `lint-actions` on `needs.changes.outputs.workflows == 'true'`. - Updated `publish-main` to depend on `changes` and to allow upstream `success` or `skipped` outcomes while still requiring `publish == 'true'`. ## Validation - `make verify-vendor` - `make test` - `make build` - `go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.10` ## Risks Low. Scope is limited to workflow orchestration. Main risk is incorrect path classification causing a job to skip unexpectedly; mitigated by explicit filters and preserving required job dependency/result checks for publish. --- <details> <summary>📋 Implementation Plan</summary> # Plan: Option 2 — Per-job path filtering in CI ## Context / Why The current `.github/workflows/ci.yaml` runs Go-heavy jobs (`lint`, `test`) for every pull request, including docs-only changes (for example, markdown edits under `.mux/` and `AGENTS.md`). The goal is to keep CI signal quality while avoiding unnecessary Go setup/lint/test/build work when no Go-relevant files changed. ## Evidence - `.github/workflows/ci.yaml` currently has no `paths`/`paths-ignore` filters at trigger or job level. - `lint` and `test` always run on `pull_request` and `push` to `main`. - `publish-main` currently depends on `[test, lint, lint-actions]`, so any job-level skipping logic must avoid accidentally blocking publish when dependencies are skipped by design. These sources are sufficient to plan the workflow-only change because the optimization is isolated to CI orchestration in `.github/workflows/ci.yaml`. ## Implementation details 1. **Add a `changes` classifier job in `.github/workflows/ci.yaml`** - Insert a first job that computes file-change categories with `dorny/paths-filter` (SHA-pinned). - Emit outputs for at least: - `go` (Go source/build/vendor/tooling inputs) - `workflows` (`.github/workflows/**`) - `publish` (files that should trigger `publish-main`, usually superset of `go` plus container build files) ```yaml jobs: changes: runs-on: ubuntu-latest outputs: go: ${{ steps.filter.outputs.go }} workflows: ${{ steps.filter.outputs.workflows }} publish: ${{ steps.filter.outputs.publish }} steps: - name: Checkout uses: actions/checkout@34e1148 # v4.3.1 with: persist-credentials: false - name: Detect changed paths id: filter uses: dorny/paths-filter@<PINNED_SHA> # v3 with: filters: | go: - '**/*.go' - 'go.mod' - 'go.sum' - 'vendor/**' - 'Makefile' workflows: - '.github/workflows/**' publish: - '**/*.go' - 'go.mod' - 'go.sum' - 'vendor/**' - 'Dockerfile*' - 'Dockerfile.goreleaser' ``` 2. **Gate expensive jobs using `needs: changes` + `if:` guards** - `lint` and `test` should run only when `needs.changes.outputs.go == 'true'`. - `lint-actions` should run only when `needs.changes.outputs.workflows == 'true'`. - Keep `codex-comments` behavior unchanged (PR-only, cheap, policy-oriented). ```yaml lint: needs: changes if: needs.changes.outputs.go == 'true' ... test: needs: changes if: needs.changes.outputs.go == 'true' ... lint-actions: needs: changes if: needs.changes.outputs.workflows == 'true' ... ``` 3. **Update `publish-main` so intentionally skipped dependencies do not block it** - Keep existing `push`/`main` gating. - Add `needs: [changes, test, lint, lint-actions]`. - Use an `if:` expression with `always()` and explicit dependency result checks (`success` or `skipped`) plus `publish == 'true'`. ```yaml publish-main: needs: [changes, test, lint, lint-actions] if: | always() && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.publish == 'true' && (needs.test.result == 'success' || needs.test.result == 'skipped') && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.lint-actions.result == 'success' || needs.lint-actions.result == 'skipped') ... ``` 4. **Validate behavior with targeted scenarios** - Run `go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.10` locally after YAML edits. - Open/refresh a PR with markdown-only changes; confirm `lint`/`test` are skipped and required checks remain mergeable. - Open/refresh a PR with Go file changes; confirm `lint`/`test` execute. - Confirm push to `main` with docs-only diff does not publish image (`publish == false`), while code-related push still publishes. <details> <summary>Why this structure (brief)</summary> - Job-level filtering (instead of top-level `paths-ignore`) preserves visibility of all jobs/check names while avoiding unnecessary work. - A dedicated `changes` job centralizes path logic so future categories can be added in one place. - Explicit `success || skipped` checks avoid accidental publish blockage when upstream jobs are intentionally skipped. </details> </details> --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$0.16`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=0.16 -->
1 parent 8ae6328 commit ed06a78

1 file changed

Lines changed: 54 additions & 2 deletions

File tree

‎.github/workflows/ci.yaml‎

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,48 @@ permissions:
1010
contents: read
1111

1212
jobs:
13+
changes:
14+
name: Detect changed paths
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
pull-requests: read
19+
outputs:
20+
go: ${{ steps.filter.outputs.go }}
21+
workflows: ${{ steps.filter.outputs.workflows }}
22+
publish: ${{ steps.filter.outputs.publish }}
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
26+
with:
27+
persist-credentials: false
28+
29+
- name: Detect changed paths
30+
id: filter
31+
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
32+
with:
33+
filters: |
34+
go:
35+
- '**/*.go'
36+
- 'go.mod'
37+
- 'go.sum'
38+
- 'vendor/**'
39+
- 'Makefile'
40+
- '.golangci.yml'
41+
- '.golangci.yaml'
42+
workflows:
43+
- '.github/workflows/**'
44+
publish:
45+
- '**/*.go'
46+
- 'go.mod'
47+
- 'go.sum'
48+
- 'vendor/**'
49+
- 'Dockerfile*'
50+
- 'Dockerfile.goreleaser'
51+
1352
lint:
53+
needs: changes
54+
if: needs.changes.outputs.go == 'true'
1455
runs-on: ubuntu-latest
1556
steps:
1657
- name: Checkout
@@ -46,6 +87,8 @@ jobs:
4687
go-package: ./...
4788

4889
test:
90+
needs: changes
91+
if: needs.changes.outputs.go == 'true'
4992
runs-on: ubuntu-latest
5093
steps:
5194
- name: Checkout
@@ -77,6 +120,8 @@ jobs:
77120

78121
lint-actions:
79122
name: Lint GitHub Actions
123+
needs: changes
124+
if: needs.changes.outputs.workflows == 'true'
80125
runs-on: ubuntu-latest
81126
permissions:
82127
contents: read
@@ -122,8 +167,15 @@ jobs:
122167

123168
publish-main:
124169
name: Publish GHCR :main
125-
needs: [test, lint, lint-actions]
126-
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
170+
needs: [changes, test, lint, lint-actions]
171+
if: |
172+
always() &&
173+
github.event_name == 'push' &&
174+
github.ref == 'refs/heads/main' &&
175+
needs.changes.outputs.publish == 'true' &&
176+
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
177+
(needs.lint.result == 'success' || needs.lint.result == 'skipped') &&
178+
(needs.lint-actions.result == 'success' || needs.lint-actions.result == 'skipped')
127179
runs-on: ubuntu-latest
128180
permissions:
129181
contents: read

0 commit comments

Comments
 (0)