1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
name: CI
on:
push:
branches:
tags:
pull_request:
branches:
env:
CARGO_TERM_COLOR: always
jobs:
# Path-split gate (#609). Docs/skills-only PRs skip the heavy jobs while still
# publishing every required status context, so they merge in seconds without
# deadlocking branch protection. A single gate is the source of truth for the
# split — no twin `ci-skip.yml` whose stub names must be hand-mirrored against
# this matrix (which would silently rot when the matrix or protection set
# changes). Non-PR events (main pushes, release tags) always take the full
# path. `src/templates/*.md` are include_str!-compiled and unit-tested
# (ADR-0030), so the doc set scopes `*.md` to the repo root only — never
# `**/*.md` — and anything not explicitly a doc runs full CI (fail-safe).
gate:
name: Gate
runs-on: ubuntu-latest
outputs:
code: ${{ steps.decide.outputs.code }}
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v3
id: filter
if: github.event_name == 'pull_request'
with:
filters: |
code:
- '**'
- '!*.md'
- '!docs/**'
- '!.claude/skills/**'
- id: decide
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=${{ steps.filter.outputs.code }}" >> "$GITHUB_OUTPUT"
fi
# Required-context jobs (Test, Clippy, Rustfmt, Docs) always run so their
# contexts always report `success`; on the fast path every step is skipped,
# which is a genuine passing job — not a `skipped` conclusion whose
# branch-protection semantics vary. Non-required jobs below skip wholesale.
test:
name: Test
needs: gate
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
- nightly
features:
- ""
- "mcp"
steps:
- uses: actions/checkout@v7
if: needs.gate.outputs.code == 'true'
- uses: dtolnay/rust-toolchain@master
if: needs.gate.outputs.code == 'true'
with:
toolchain: ${{ matrix.rust }}
components: rustfmt
- uses: Swatinem/rust-cache@v2
if: needs.gate.outputs.code == 'true'
- name: Run tests
if: needs.gate.outputs.code == 'true'
run: |
if [ -z "${{ matrix.features }}" ]; then
cargo test --verbose
else
cargo test --features "${{ matrix.features }}" --verbose
fi
mcp-build:
name: MCP Release Build
needs: gate
if: needs.gate.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build omni-dev-mcp (release)
run: cargo build --release --features mcp --bin omni-dev-mcp
windows-build:
name: Windows Build
needs: gate
if: needs.gate.outputs.code == 'true'
runs-on: windows-latest
# Mirrors the release.yml win-msvc binary build so platform-portability
# regressions (e.g. the Unix-only daemon, #1041) are caught on every PR
# rather than only when a release tag is pushed.
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build binaries (release, mcp)
run: cargo build --release --features mcp --bin omni-dev --bin omni-dev-mcp
fmt:
name: Rustfmt
needs: gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
if: needs.gate.outputs.code == 'true'
- uses: dtolnay/rust-toolchain@stable
if: needs.gate.outputs.code == 'true'
with:
components: rustfmt
- name: Check formatting
if: needs.gate.outputs.code == 'true'
run: cargo fmt --all -- --check
clippy:
name: Clippy
needs: gate
runs-on: ubuntu-latest
strategy:
matrix:
features:
- ""
- "mcp"
steps:
- uses: actions/checkout@v7
if: needs.gate.outputs.code == 'true'
- uses: dtolnay/rust-toolchain@stable
if: needs.gate.outputs.code == 'true'
with:
components: clippy
- uses: Swatinem/rust-cache@v2
if: needs.gate.outputs.code == 'true'
- name: Run clippy
if: needs.gate.outputs.code == 'true'
run: |
if [ -z "${{ matrix.features }}" ]; then
cargo clippy --all-targets -- -D warnings
else
cargo clippy --all-targets --features "${{ matrix.features }}" -- -D warnings
fi
docs:
name: Docs
needs: gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
if: needs.gate.outputs.code == 'true'
- uses: dtolnay/rust-toolchain@stable
if: needs.gate.outputs.code == 'true'
- uses: Swatinem/rust-cache@v2
if: needs.gate.outputs.code == 'true'
- name: Build docs
if: needs.gate.outputs.code == 'true'
run: cargo doc --no-deps --document-private-items
coverage:
name: Coverage
needs: gate
if: needs.gate.outputs.code == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # required to post the coverage comment
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # full history so `git merge-base` can resolve the PR's fork point
# The coverage pipeline — cargo-llvm-cov run, merge-base baseline (download
# + git-worktree recompute fallback), `omni-dev coverage diff` sticky PR
# comment, summary + artifacts, baseline publish on main, and the line gate
# — is packaged in action-works/omni-dev-coverage-check (issue #1024).
# worktree-system-deps installs the ALSA headers only on the merge-base
# recompute path, for fork points that predate the cpal/voice removal
# (#980); a no-op once the merge-base is itself cpal-free.
- uses: action-works/omni-dev-coverage-check@v1
with:
fail-under-lines: 30
worktree-system-deps: libasound2-dev
audit:
name: Security Audit
needs: gate
if: needs.gate.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Pull a prebuilt cargo-audit binary instead of `cargo install` (which
# compiles from source every run — rust-cache can't help since it doesn't
# cache ~/.cargo/bin). Mirrors the prebuilt cargo-deny-action below.
- uses: taiki-e/install-action@v2
with:
tool: cargo-audit
- name: Run cargo-audit
run: cargo audit
deny:
name: Dependency Policy
needs: gate
if: needs.gate.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: EmbarkStudios/cargo-deny-action@v2.0.20
with:
command: check all
secrets:
name: Secret Scanning
needs: gate
if: needs.gate.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: TruffleHog scan
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified