authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-21 01:12:19-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-20 22:12:19-07:00
log8f7da3b99e48ffb678d3426a1b5fb602ecdd50f7
tree25d71bbb841ace4166d65cdb636ea74900dc747f
parentd7b9a689fe8fd68a87cb34f32d67de8695e61ee4
signature Signed by PGP key B5690EEEBB952194

ci: only boot the macOS native lint runner when native sources change (#7283)

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>

2 files changed, 91 insertions(+), 3 deletions(-)

.github/workflows/ci.yml+82
...@@ -175,8 +175,90 @@ jobs:...@@ -175,8 +175,90 @@ jobs:
175 - name: Test resource monitor175 - name: Test resource monitor
176 run: cargo test --locked --manifest-path native/resource-monitor/Cargo.toml176 run: cargo test --locked --manifest-path native/resource-monitor/Cargo.toml
177177
178 # The static analysis below needs a macOS runner, which bills ~6.7x a Linux
179 # minute, so gate it on the native sources it actually lints instead of paying
180 # for it on every push. Detection is API-only (no checkout) and fails open: if
181 # the diff cannot be resolved, the lint runs.
182 mobile_native_changes:
183 name: Mobile Native Changes
184 runs-on: blacksmith-2vcpu-ubuntu-2404
185 timeout-minutes: 5
186 permissions:
187 contents: read
188 pull-requests: read
189 outputs:
190 changed: ${{ steps.detect.outputs.changed }}
191 steps:
192 - name: Detect mobile native changes
193 id: detect
194 env:
195 GH_TOKEN: ${{ github.token }}
196 PR_NUMBER: ${{ github.event.pull_request.number }}
197 BEFORE_SHA: ${{ github.event.before }}
198 run: |
199 set -uo pipefail
200
201 fail_open() {
202 echo "$* Running native static analysis."
203 echo "changed=true" >> "$GITHUB_OUTPUT"
204 exit 0
205 }
206
207 count_rows() {
208 printf '%s\n' "$1" | grep -c . || true
209 }
210
211 # One row per changed file, holding the new path and, for a rename,
212 # the path it replaced: renaming a matched file out of the matched
213 # paths removes a lint input just like editing it.
214 row='[.filename, (.previous_filename // empty)] | @tsv'
215
216 if [[ -n "${PR_NUMBER}" ]]; then
217 # The PR files endpoint stops at 3000 files and pagination cannot
218 # extend it, so cross-check against the count the PR itself reports.
219 expected=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.changed_files') \
220 || fail_open "Could not read the pull request."
221 rows=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq ".[] | ${row}") \
222 || fail_open "Could not resolve changed files."
223
224 listed=$(count_rows "$rows")
225 if [[ "$listed" -lt "$expected" ]]; then
226 fail_open "GitHub listed only ${listed} of ${expected} changed files."
227 fi
228 else
229 rows=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${BEFORE_SHA}...${GITHUB_SHA}" --jq ".files[]? | ${row}") \
230 || fail_open "Could not resolve changed files."
231
232 # The compare endpoint reports at most 300 files and pagination does
233 # not extend that list, so a full list may be hiding native changes.
234 listed=$(count_rows "$rows")
235 if [[ "$listed" -ge 300 ]]; then
236 fail_open "GitHub listed ${listed} changed files, the compare endpoint maximum."
237 fi
238 fi
239
240 paths=$(tr '\t' '\n' <<< "$rows")
241
242 # Sources scripts/mobile-native-static-check.ts lints, plus the tool
243 # and rule configuration that decides how it lints them, plus the
244 # root package.json that defines the lint:mobile command.
245 pattern='^apps/mobile/.*\.(swift|kt|kts)$|^apps/mobile/(\.swiftlint\.yml|detekt\.yml|\.editorconfig|Brewfile)$|^scripts/mobile-native-static-check\.ts$|^package\.json$|^\.github/workflows/ci\.yml$'
246
247 if grep -qE "$pattern" <<< "$paths"; then
248 echo "Native sources or lint configuration changed:"
249 grep -E "$pattern" <<< "$paths"
250 echo "changed=true" >> "$GITHUB_OUTPUT"
251 else
252 echo "No mobile native sources or lint configuration changed."
253 echo "changed=false" >> "$GITHUB_OUTPUT"
254 fi
255
178 mobile_native_static_analysis:256 mobile_native_static_analysis:
179 name: Mobile Native Static Analysis257 name: Mobile Native Static Analysis
258 needs: mobile_native_changes
259 # Skip only on an explicit "no": a gate job that failed or errored leaves the
260 # output empty, and that must run the lint rather than silently skip it.
261 if: ${{ !cancelled() && needs.mobile_native_changes.outputs.changed != 'false' }}
180 runs-on: blacksmith-6vcpu-macos-26262 runs-on: blacksmith-6vcpu-macos-26
181 timeout-minutes: 10263 timeout-minutes: 10
182 steps:264 steps:
docs/internals/ci.md+9-3
...@@ -2,8 +2,8 @@...@@ -2,8 +2,8 @@
22
3> For maintainers. Using T3 Code? See [docs/user](../user/).3> For maintainers. Using T3 Code? See [docs/user](../user/).
44
5[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs four jobs on pull requests and5[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs these quality gates on pull requests
6pushes to `main`:6and pushes to `main`:
77
8- **Check**: `vp check` (format and lint; this repo sets `typeCheck: false` in its lint options),8- **Check**: `vp check` (format and lint; this repo sets `typeCheck: false` in its lint options),
9 then `vpr typecheck` for the workspace type check. The same job9 then `vpr typecheck` for the workspace type check. The same job
...@@ -11,7 +11,13 @@ pushes to `main`:...@@ -11,7 +11,13 @@ pushes to `main`:
11 still exports its expected symbols.11 still exports its expected symbols.
12- **Test**: `vp run test` across the workspace.12- **Test**: `vp run test` across the workspace.
13- **Mobile Native Static Analysis**: `vp run lint:mobile` on macOS, wrapping13- **Mobile Native Static Analysis**: `vp run lint:mobile` on macOS, wrapping
14 `scripts/mobile-native-static-check.ts`.14 `scripts/mobile-native-static-check.ts`. A cheap Linux **Mobile Native Changes** job gates it:
15 the macOS runner only boots when the diff touches `apps/mobile` Swift/Kotlin sources, the
16 SwiftLint/detekt/ktlint configuration, the `Brewfile`, the check script, the root `package.json`
17 that defines `lint:mobile`, or `ci.yml`. Otherwise the job is skipped, which GitHub reports as
18 success for the required check. Renames are matched on both their old and new path. The gate fails
19 open in every other case: if the changed-file list cannot be resolved, GitHub truncates it, or the
20 gate job itself fails, the lint runs.
15- **Release Smoke**: exercises release-only workflow steps through `scripts/release-smoke.ts`, so21- **Release Smoke**: exercises release-only workflow steps through `scripts/release-smoke.ts`, so
16 release breakage surfaces on PRs rather than at tag time.22 release breakage surfaces on PRs rather than at tag time.
1723