Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
44c3f42
ci: Bump the github-actions group with 2 updates
dependabot[bot] Sep 24, 2026
058791a
Build BlurtEngine for iOS: fence the macOS-only files, make the seams…
nbisht-aai Sep 24, 2026
b6f4a0c
Add the iPhone app and its keyboard
nbisht-aai Sep 24, 2026
c351de0
Fix the check job: recorder file length, an unused import, redundant …
nbisht-aai Sep 24, 2026
7d08578
docs: on-device testing steps for the iPhone app; tidy a doc comment
nbisht-aai Sep 24, 2026
58698bb
iOS: opt the shared payloads out of main-actor isolation; lexicon rea…
nbisht-aai Sep 24, 2026
bd7424c
iOS: nonisolated delegate conformance, unchecked buffer handoff; add …
nbisht-aai Sep 24, 2026
d626c5c
Keyboard: insert a result once and only while fresh; presence means o…
nbisht-aai Sep 24, 2026
d88bb58
iOS: the keyboard trusts only a live app and a fresh phase
nbisht-aai Sep 24, 2026
e22b580
Keyboard: the mic key opens the app before Full Access too
nbisht-aai Sep 24, 2026
884a2d8
iOS: signing team comes from BLURT_TEAM; phone-testing steps spelled out
nbisht-aai Sep 24, 2026
e4a44bf
docs: Xcode 26.6 is enough for a phone on iOS 26; 27 only for iOS 27
nbisht-aai Sep 24, 2026
b2be01f
iOS: build under Xcode 27's compiler
nbisht-aai Sep 24, 2026
e852a55
iOS: a simulator microphone, and the keyboard no longer crashes on ap…
nbisht-aai Sep 25, 2026
fb31aef
docs: scripts/ios-sim.sh — the simulator loop in one command; getting…
nbisht-aai Sep 25, 2026
aa64c2b
Keyboard: the Mac's design on the phone, and a gallery to render it
nbisht-aai Sep 25, 2026
673e779
Merge pull request #2 from nbisht-aai/dependabot/github_actions/githu…
nbisht-aai Sep 25, 2026
849263d
Merge pull request #1 from nbisht-aai/feat/blurtengine-multiplatform
nbisht-aai Sep 25, 2026
44d1373
Keyboard: an iPhone look, hands-free start, auto-capitalisation, the …
nbisht-aai Sep 25, 2026
2269492
docs: DESIGN.md — the two looks, the iPhone geometry, hands-free
nbisht-aai Sep 25, 2026
93421f1
Keyboard: a notice settles back to idle after its dwell
nbisht-aai Sep 25, 2026
c217f57
App: the Mac's ready screen on a phone, and Settings behind the gear
nbisht-aai Sep 25, 2026
cf919d5
Keyboard: the mic panel is a two-page carousel with the full keyboard
nbisht-aai Sep 25, 2026
cc9faad
Keyboard: one look — Blurt's colours at the iPhone's exact spacing; n…
nbisht-aai Sep 25, 2026
ec20a12
Keyboard: no words, the orb is the whole voice control; the mic leave…
nbisht-aai Sep 25, 2026
f99ea06
docs: DESIGN.md — no pill on the keyboard
nbisht-aai Sep 25, 2026
bbc3cff
docs: DESIGN.md accessibility section matches the wordless keyboard
nbisht-aai Sep 25, 2026
b471056
Keyboard: the orb, centred and bare, spins then grows into the wave
nbisht-aai Sep 25, 2026
a5bc0da
Keyboard: quick-add a key term in seconds, without leaving the app yo…
nbisht-aai Sep 25, 2026
b277b63
Keyboard: split the model's typing half into its own file; return lab…
nbisht-aai Sep 25, 2026
7e82235
Keyboard: drop the brace the split left behind
nbisht-aai Sep 25, 2026
fed15b7
Keyboard: the model's typing half reaches its state across the file s…
nbisht-aai Sep 25, 2026
484f068
App: share key terms with friends, and take theirs
nbisht-aai Sep 25, 2026
1bc0f7f
Keyboard: six curated themes, picked from live previews
nbisht-aai Sep 25, 2026
16b06d5
Keyboard: the term field claims typing that reaches the host field
nbisht-aai Sep 25, 2026
53e09df
iOS: a unit-test target for the iPhone code, run locally and in CI
nbisht-aai Sep 25, 2026
7febbc3
iOS foundation: the audit's fixes, the two-process contract pinned, t…
nbisht-aai Sep 25, 2026
a039f0e
Flat: a matte two-tone orb with a circling perimeter, the wave bare, …
nbisht-aai Sep 25, 2026
485b1cd
Revert "Flat: a matte two-tone orb with a circling perimeter, the wav…
nbisht-aai Sep 25, 2026
b63201a
App: the home screen's half of the audit fixes
nbisht-aai Sep 25, 2026
c1d8fee
Prototype: the prism orb — green in, violet out, sparkles when the wo…
nbisht-aai Sep 25, 2026
d8f426a
App: the home screen's half of the audit fixes
nbisht-aai Sep 25, 2026
584dc02
Prototype: the orb lives green; a violet drop pulses in when the word…
nbisht-aai Sep 25, 2026
d4e581b
Keyboard: the iPhone's own look is the default theme, light or dark w…
nbisht-aai Sep 25, 2026
b103073
Merge branch 'feat/blurtengine-multiplatform' into proto/orb-prism
nbisht-aai Sep 25, 2026
dcd6595
Prototype: no pill — the wave passes through the orb; soft, slow fades
nbisht-aai Sep 29, 2026
863b36c
Prototype: the orb dissipates into a thin wave; flat, contemporary ch…
nbisht-aai Sep 29, 2026
5ee7a2b
iOS: design tokens as the one source, and deterministic gallery captures
nbisht-aai Sep 29, 2026
782635f
iOS: the Figma file as scripts (Design/figma)
nbisht-aai Sep 29, 2026
c6bb50f
Design/figma: split the builders per call, carry tokens once, plugin …
nbisht-aai Sep 29, 2026
0f2cf47
iOS keyboard: design pass 1 — surface grain, a proportional orb light…
nbisht-aai Sep 29, 2026
71fdd86
iOS: the brand's faces as bundled fonts
nbisht-aai Sep 29, 2026
5f96337
Design: tokens for the brand's two faces, its fonts and its curve
nbisht-aai Sep 29, 2026
b32cc99
iOS: measure the iPhone's keyboard, and pin the keys to it
nbisht-aai Sep 29, 2026
a9c9b9b
iOS: the rules out of the views, no visual change
nbisht-aai Sep 29, 2026
203644c
Keyboard: three mic concepts behind one seam, for the pick
nbisht-aai Sep 29, 2026
cb6db3a
Keyboard: the chrome on the brand
nbisht-aai Sep 29, 2026
e478ba2
App: the screens on the brand, and DESIGN.md rewritten
nbisht-aai Sep 29, 2026
6c1d2cd
Keyboard: a temporary switch between the three mic concepts
nbisht-aai Sep 29, 2026
63de3b1
Keyboard: draw the candidates synchronously — they never showed in th…
nbisht-aai Sep 29, 2026
545527d
Keyboard: the surface is the host's — clear by default
nbisht-aai Sep 30, 2026
6c9129d
Keyboard: the return key says return; the element and the + sit centred
nbisht-aai Sep 30, 2026
1c3e2e7
Keyboard: the carousel swipes from anywhere; the bar pairs the + with…
nbisht-aai Sep 30, 2026
294198e
Probe: the keyboard's flows in the real extension, face × mic × layout
nbisht-aai Sep 30, 2026
734c4bb
Probe: the flows test on the main actor; a sheet per run
nbisht-aai Sep 30, 2026
6aca9d3
Keyboard: the floor in the material's own colour, not black
nbisht-aai Sep 30, 2026
7ca5edc
Keyboard: the carousel is a strip the finger slides; the pages never …
nbisht-aai Sep 30, 2026
2c9f3e9
Keyboard: highlight a word and the + is a chip that adds it
nbisht-aai Sep 30, 2026
8b4848c
Keyboard: the mic at the left or right edge, for one hand
nbisht-aai Sep 30, 2026
3e37780
Keyboard: the mic itself is centred; the + hangs beside it
nbisht-aai Sep 30, 2026
c3b632a
Probe: the panel's carousel inside Messages, every way, on video
nbisht-aai Sep 30, 2026
cf2f9cc
Probe: every way a keyboard comes back, ending with the mic drawn
nbisht-aai Sep 30, 2026
eb5ed06
Keyboard: one placement rule, a path-drawn grille, a gate that settle…
nbisht-aai Oct 1, 2026
d5c32e3
Merge remote-tracking branch 'origin/main' into redesign/brand-faces
nbisht-aai Oct 1, 2026
706d53e
Merge pull request #3 from nbisht-aai/feat/blurtengine-multiplatform
nbisht-aai Oct 1, 2026
86fde4e
Merge pull request #5 from nbisht-aai/redesign/brand-faces
nbisht-aai Oct 1, 2026
bb3757e
Merge remote-tracking branch 'upstream/main' into ios-keyboard
nbisht-aai Oct 1, 2026
cbbb503
Engine: fence the read-aloud files for iOS; HostIdentity without rele…
nbisht-aai Oct 1, 2026
4584e64
Keyboard: drop two declarations the iOS periphery scan found unused
nbisht-aai Oct 1, 2026
87a109d
CI: the mutation target list follows InsertionSeparator; ios-build ge…
nbisht-aai Oct 1, 2026
8363e50
Keyboard: fix five review findings in the mic and app hand-off
alexkroman-assembly Oct 2, 2026
938c28a
iPhone: split the logic into BlurtiOSCore and gate it like the engine
alexkroman-assembly Oct 2, 2026
c41ec3a
iPhone: hold it to the Mac's lint gates; drop Figma and the bundled f…
alexkroman-assembly Oct 2, 2026
1a2cd8c
Keyboard: never cancel words behind the user's back; build the probe
alexkroman-assembly Oct 2, 2026
ca9aa7a
Docs: bring CLAUDE.md, the agents and the guides up to the iPhone work
alexkroman-assembly Oct 2, 2026
72494f7
Simplify: one unanswered press, one rig, scripts on their shared helpers
alexkroman-assembly Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/agents/cleanup-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ design. Note also that `check.sh` already runs `periphery scan --strict` with
`retain_public: false`, so plainly-unreferenced symbols are caught — spend your
effort on what periphery cannot see.

On the iPhone (`App/BlurtiOS/`), equally deliberate:

- **The two losing mic concepts are on purpose.** `VoiceRibs`, `VoiceStreak`,
`VoiceElementKind`'s `.ribs`/`.streak`, `SharedStore.voiceElementKind` and the
`-BlurtGalleryVoice` switch are marked TEMPORARY: three concepts are built side
by side until one is picked (`DESIGN.md` › The voice element), then the losers go
together. Don't propose deleting one piecemeal.
- **The gallery and the probe screens are entry points**, reached by launch
arguments (`-BlurtGallery…`, `-BlurtProbeField`) from `scripts/design-capture.sh`,
`ios-keyboard-shot.sh`, `ios-keyboard-flows.sh` and `apple-geometry.sh` — not
from any view. Likewise the `BlurtiOSProbe` XCUITest helpers.
- **`BlurtiOSCore` uses `package` access, not `public`,** and its explicit
`package init`s exist because a memberwise initializer is only ever internal.
Seams with one production use (`KeyboardModel.transport`, `.urlOpener`,
`.proxyOverride`, `SharedStore.override`) are there for the tests.
- **`SimulatorAudioSource` is the simulator's microphone**, not a duplicate of
`WindowedAudioSource`: `AVCaptureSession` carries no audio in the simulator.
- The iPhone periphery scan is CI's and `ios-check.sh`'s, with `--retain-public`,
so it reports the iPhone code's own dead declarations — `package` ones included.

## Method

Verify every claim by reading the real code; never report a suspicion. For an
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/macos-hig-reviewer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: macos-hig-reviewer
description: Reviews SwiftUI/AppKit UI changes against macOS Human Interface Guidelines — modality, alerts vs inline validation, grouped-form structure, controls, and native idioms. Use after editing views in App/Blurt (wizard, settings, overlay).
description: Reviews SwiftUI/AppKit UI changes against macOS Human Interface Guidelines — modality, alerts vs inline validation, grouped-form structure, controls, and native idioms. Use after editing views in App/Blurt (wizard, settings, overlay). Mac only — not for App/BlurtiOS, whose views follow App/BlurtiOS/DESIGN.md.
tools: Read, Grep, Glob
---

Expand Down
37 changes: 32 additions & 5 deletions .claude/agents/swift6-concurrency-reviewer.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
---
name: swift6-concurrency-reviewer
description: Reviews Swift changes for Swift 6 strict-concurrency correctness (actor isolation, Sendable, @MainActor, data races) and Blurt's documented audio/pipeline invariants. Use after editing engine or app code that touches actors, async, the mic capture path, or the dictation pipeline.
description: Reviews Swift changes for Swift 6 strict-concurrency correctness (actor isolation, Sendable, @MainActor, data races) and Blurt's documented audio/pipeline invariants. Use after editing engine, Mac app or iPhone (App/BlurtiOS) code that touches actors, async, the mic capture path, the dictation pipeline, or the keyboard's hand-off with the app.
tools: Read, Grep, Glob, Bash
---

You are a Swift 6 strict-concurrency reviewer for Blurt, a macOS dictation
app. The engine (`Sources/BlurtEngine/`) is a `swift-tools-version:6.2`
package with **no external dependencies**; the app shell (`App/Blurt/`) is
AppKit/SwiftUI.
You are a Swift 6 strict-concurrency reviewer for Blurt, a dictation app for
macOS and iPhone. The engine (`Sources/BlurtEngine/`) is a `swift-tools-version:6.2`
package with **no external dependencies**; the Mac shell (`App/Blurt/`) is
AppKit/SwiftUI. The iPhone app (`App/BlurtiOS/`) is two processes over one App
Group — the app (listens and transcribes) and a keyboard extension (inserts the
words) — with their shared logic in the `BlurtiOSCore` static framework. Every
iPhone target defaults to the main actor (`SWIFT_DEFAULT_ACTOR_ISOLATION`), so
anything that runs elsewhere is spelled `nonisolated`.

## What to review

Expand Down Expand Up @@ -51,6 +55,29 @@ Look at the changes (default to the working diff via `git diff` and
`BlurtUITests` XCUITest bundle is the exception), and must never touch the real
Keychain (`APIKeyStore`) — use an isolated service.

### On the iPhone (`App/BlurtiOS/`)

- **The keyboard never hears anything:** no `AVFoundation`/`AVFAudio` in
`BlurtKeyboard/`, `Shared/` or `BlurtiOSCore/` (the keyboard links the core).
Capture lives only in the app (`WindowedAudioSource`, `SimulatorAudioSource`).
- **Payloads that cross processes or the capture path are `nonisolated`** —
`SharedStore`, `PhaseSnapshot`, `KeyboardCommand`, `DictationResult` and the
rest in `SharedState.swift`. Flag one that drifts back to the main-actor default.
- **System callbacks arrive off the main actor:** a `DarwinObserver` handler and
`requestSupplementaryLexicon`'s callback (an XPC queue) are `@Sendable` and hop
with `Task { @MainActor in … }`; a main-actor closure there traps at runtime.
- **The keyboard never cancels words behind the user's back:** a `.cancel` over a
recording or a transcription comes only from the × key or the term field opened
over a highlighted word. Ordering bugs between the keyboard's picture (the last
phase it read) and the app's real state are where this breaks — flag a cancel
sent on a stale `.recording`/`.processing` snapshot.
- **An automatic window close ends the session before the feed:** the release has
to find the audio, so `ListeningWindow.onClosingOnItsOwn` is awaited before
`source.close()`.
- **`BlurtiOSTests` run serially** — they swap the global `SharedStore.override`;
an async test suspended mid-await must not see another suite's. Flag a change
that re-enables parallel testing for that bundle.

## How to report

Verify before asserting: read the surrounding code, and if useful run
Expand Down
4 changes: 4 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"Bash(scripts/check.sh --portable)",
"Bash(./scripts/check.sh --portable)",
"Bash(scripts/dev-build.sh)",
"Bash(scripts/ios-sim.sh*)",
"Bash(scripts/ios-test.sh*)",
"Bash(scripts/ios-check.sh*)",
"Bash(scripts/ios-typecheck.sh*)",
"Bash(prettier *)",
"Bash(actionlint)",
"Bash(actionlint *)",
Expand Down
13 changes: 13 additions & 0 deletions .claude/skills/check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,16 @@ hides every lint finding behind it — expect to fix a batch, not a queue.
missing tool. A local run that ends `ok (UI suite + leak scan NOT run …)` is
green for everything it covered — just don't claim the integration suite
passed. That answer comes from `check.yml` on the PR.

## The iPhone app

`check.sh` covers `App/BlurtiOS` with the dependency guard, the invariants and the linters, and —
off CI, when an iPhone simulator on this Xcode's iOS runtime exists — `scripts/ios-check.sh`: the
tests with `BlurtiOSCore`'s coverage gate, `swiftlint analyze`, the probe build and periphery over
the iPhone project. Without that runtime it is skipped with a note (the closing `ok` line says so)
and CI's `ios-build` job, which runs the same script, is the authority; `check.sh` then runs
`scripts/ios-typecheck.sh` instead, which typechecks the iPhone targets against the Mac Catalyst
frameworks — the stand-in for a Mac with only the Command Line Tools. Locally, `scripts/ios-test.sh` runs `BlurtiOSTests` on a
simulator when Xcode is installed and enforces `BlurtiOSCore`'s line-coverage floor (`MIN_IOS_COVERAGE`, 88%;
`BLURT_IOS_SANITIZER=thread` or `address` runs the TSan / ASan pass CI's `ios-sanitizers` job
runs), and `scripts/ios-sim.sh` builds and launches the app there.
28 changes: 26 additions & 2 deletions .claude/skills/project-guardrails/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ genuinely correct, and reaching for it means it's time to stop and ask.
a second time inside the capture lock and held a duplicate of the whole
utterance (~3.7 MB at the cap) until release. Don't reintroduce a buffered
copy "just in case" — there is no retry-from-buffer path, by design.
- **Don't pre-open the mic to make presses feel faster.** `MicCapture.warmUp()`
- **(macOS shell) Don't pre-open the mic to make presses feel faster.** `MicCapture.warmUp()`
is stateless on purpose — build a session, drop it — and there is no warm or
prepared recorder to reuse. Measured on hardware: building a session (or the
retired `prepareToRecord()`) leaves the device closed and costs ~15 ms, while
Expand Down Expand Up @@ -129,7 +129,7 @@ only one of stt_prompt or prompt; they are the same field`, before the audio is
default does not describe the behaviour. Note the plural is the documented
spelling, but the singular `language_code` is accepted too (a 200, not an
unknown-key 400), so both are live and `KeytermsWireTests` pins both absent.
- **Injection is always a clipboard paste** (save → write → ⌘V → settle →
- **(macOS shell) Injection is always a clipboard paste** (save → write → ⌘V → settle →
restore), degrading to "left it on the clipboard" when the target is lost. No
keystroke-by-keystroke typing path, no length threshold.

Expand Down Expand Up @@ -179,3 +179,27 @@ only one of stt_prompt or prompt; they are the same field`, before the audio is
hardened runtime and a **secure timestamp** (`--options runtime --timestamp`),
or notarization rejects the build. `release-build.sh` re-signs frameworks for
this — don't remove it.

## iPhone (App/BlurtiOS)

- **The keyboard never hears anything.** iOS lets no keyboard use the microphone; the app
listens and transcribes, the keyboard is a remote control over the App Group that inserts
the words. No `AVFoundation` in `BlurtKeyboard/`, `Shared/` or `BlurtiOSCore/` (the keyboard
links it) (mechanized).
- The app's microphone is a _listening window_, opened once while the app is in front and
kept open, not a per-press capture — the two rows marked "(macOS shell)" above do not apply.
- Insertion is the keyboard's `textDocumentProxy`; the clipboard is only the fallback when no
keyboard is there to take the words, and the phase then says "Copied", never "Pasted".
- **Logic lives in `BlurtiOSCore`, UI in the targets** — the Mac's engine/shell split. The
core has no SwiftUI, no views and no audio; its API is `package`, not `public` (periphery
still sees dead code); it is static, linked by both targets. It alone is coverage-gated at
88%. A UI hook the model needs is a shell-side extension, never SwiftUI in the core.
- **The keyboard never cancels words behind the user's back.** A cancel over a recording or a
transcription comes only from the × key or the term field opened over a highlighted word —
not from leaving the screen, a swipe across the orb, or the plain +. The fuzz test enforces it.
- **No private Settings URLs** (`App-Prefs:`): `openSettingsURLString` only, and the copy names
the whole path (Apps › Blurt › Keyboards).
- **No bundled brand fonts** — removed over their licence; `BlurtType`'s roles use system faces.
- **`BlurtiOSTests` run serially** (scheme + `-parallel-testing-enabled NO`): they swap a global
`SharedStore.override`, which `.serialized` alone doesn't protect across suites.
- The full list, with every number, is `App/BlurtiOS/DESIGN.md`.
130 changes: 115 additions & 15 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,96 @@ jobs:
if-no-files-found: error
retention-days: 14

# The engine is a two-platform package, and `check` only ever compiles it for
# macOS: `swift test` builds for the host, and every job above runs on one.
# A macOS-only symbol that escapes its `#if os(macOS)` fence therefore passes
# `check` and breaks the iOS app, which is why this job exists and why `gate`
# treats it as required. It builds the library alone, for the simulator SDK —
# `xcodebuild` rather than `swift build`, because cross-compiling needs the
# SDK/destination handling xcodebuild owns, and it resolves the package
# straight from the checkout, no project needed. `generic/platform=iOS
# Simulator` builds against the runner's default (latest installed) simulator
# SDK without naming a device or pinning an iOS version. The scheme is the
# package's own `BlurtEngine` product scheme, so only the library builds; the
# test target stays a macOS (`swift test`) concern.
ios-build:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: macos-26
# The simulator build plus the unit tests take ~12 minutes on macos-26 and
# the periphery scan rebuilds after them; 15 cut the scan off mid-run.
timeout-minutes: 30

steps:
- name: Checkout blurt
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: blurt
persist-credentials: false

- name: Build BlurtEngine for iOS
working-directory: blurt
run: |
xcodebuild -scheme BlurtEngine \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$RUNNER_TEMP/BlurtEngine-iOS" \
build

- name: Install XcodeGen
# Just the one formula, not `brew bundle --file=Brewfile`: the iPhone
# app's project is the only thing this job generates, and installing the
# other eleven tools would cost more than the build it precedes. xcodegen
# is in the Brewfile too, so this is the version check.sh uses.
run: brew install xcodegen

- name: Install the iPhone gates' tools
# swiftlint for `swiftlint analyze`, periphery for the unused-code scan,
# xcbeautify for the build output; all in the Brewfile too, so these are
# the versions check.sh uses. The project itself is generated, not
# committed (people open App/Blurt's; this one is regenerated a second
# before every build by ios-test.sh, so it can never go stale).
run: brew install swiftlint periphery xcbeautify

- name: Build the iPhone app and its keyboard, and run its quality gates
working-directory: blurt
# scripts/ios-check.sh — the iPhone's check.sh: BlurtiOSTests (warnings
# as errors, BlurtiOSCore's coverage gate) on whichever iPhone simulator
# the runner has (scripts/ios-lib.sh picks), then `swiftlint analyze`
# over that from-scratch build's log, then periphery. Ad hoc signing: the
# simulator accepts it and, unlike no signing, it embeds the App Group
# entitlement.
run: BLURT_DERIVED_DATA="$RUNNER_TEMP/BlurtiOS" scripts/ios-check.sh

# The iPhone suite under each sanitizer, as check.sh runs the engine's. A job
# of its own, one runner per sanitizer, rather than more steps in ios-build:
# each is a full instrumented rebuild, and ios-build is already near its
# timeout. The coverage gate runs in ios-build's plain pass only.
ios-sanitizers:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: macos-26
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
sanitizer: [thread, address]

steps:
- name: Checkout blurt
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: blurt
persist-credentials: false

- name: Install XcodeGen and xcbeautify
run: brew install xcodegen xcbeautify

- name: Run the iPhone unit tests under the ${{ matrix.sanitizer }} sanitizer
working-directory: blurt
env:
BLURT_IOS_SANITIZER: ${{ matrix.sanitizer }}
run: BLURT_DERIVED_DATA="$RUNNER_TEMP/BlurtiOS" scripts/ios-test.sh

# Always-runs summary job, so a *skipped* `check` can't be mistaken for a pass.
# GitHub reports a skipped required check to branch protection as successful, so
# anything that makes `check` skip — the `changes` filter erroring, a `needs`
Expand All @@ -223,30 +313,40 @@ jobs:
# NOTE: this only protects the repo once branch protection requires `gate`
# instead of (or as well as) `check` — that's a repo settings change.
gate:
needs: [changes, check]
needs: [changes, check, ios-build, ios-sanitizers]
if: always()
runs-on: ubuntu-latest
steps:
- name: Assert the macOS gate ran or was intentionally skipped
- name: Assert the macOS, iOS and iOS-sanitizer gates ran or were intentionally skipped
env:
CHECK_RESULT: ${{ needs.check.result }}
IOS_RESULT: ${{ needs.ios-build.result }}
IOS_SANITIZERS_RESULT: ${{ needs.ios-sanitizers.result }}
CHANGES_RESULT: ${{ needs.changes.result }}
CODE_CHANGED: ${{ needs.changes.outputs.code }}
run: |
echo "changes=$CHANGES_RESULT code=$CODE_CHANGED check=$CHECK_RESULT"
echo "changes=$CHANGES_RESULT code=$CODE_CHANGED check=$CHECK_RESULT ios-build=$IOS_RESULT ios-sanitizers=$IOS_SANITIZERS_RESULT"
if [ "$CHANGES_RESULT" != "success" ]; then
echo "error: the changes filter did not succeed, so the gate cannot be trusted" >&2
exit 1
fi
case "$CHECK_RESULT" in
success) echo "macOS gate passed" ;;
skipped)
if [ "$CODE_CHANGED" = "false" ]; then
echo "docs-only change; macOS gate intentionally skipped"
else
echo "error: check was skipped but code changed" >&2
exit 1
fi
;;
*) echo "error: macOS gate result was '$CHECK_RESULT'" >&2; exit 1 ;;
esac
# Same rule for both platform gates: green, or skipped only because the
# change was docs-only. Written once so the iOS gate can't drift into
# a looser reading than the macOS one.
assert_gate() {
case "$2" in
success) echo "$1 gate passed" ;;
skipped)
if [ "$CODE_CHANGED" = "false" ]; then
echo "docs-only change; $1 gate intentionally skipped"
else
echo "error: $1 gate was skipped but code changed" >&2
exit 1
fi
;;
*) echo "error: $1 gate result was '$2'" >&2; exit 1 ;;
esac
}
assert_gate macOS "$CHECK_RESULT"
assert_gate iOS "$IOS_RESULT"
assert_gate "iOS sanitizers" "$IOS_SANITIZERS_RESULT"
30 changes: 30 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ jobs:
-derivedDataPath "$DERIVED" \
-resolvePackageDependencies

# The iPhone app's project is generated, not committed (see check.yml's
# ios-build), so generate it before tracing starts. It has no remote
# packages to resolve — only the local engine.
- name: Generate the iPhone app's project (untraced)
working-directory: App/BlurtiOS
run: |
brew install xcodegen
xcodegen generate --quiet

- name: Initialize CodeQL
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
with:
Expand Down Expand Up @@ -122,6 +131,27 @@ jobs:
CODE_SIGNING_ALLOWED=NO \
build

# The iPhone app, its keyboard and BlurtiOSCore, traced in the same
# database: CodeQL analyses whatever the job compiles between init and
# analyze, so the Mac build alone would leave App/BlurtiOS unscanned.
# Simulator, signing off — nothing here needs a device or a team. One
# architecture: CodeQL extracts every compile, and a second slice would
# compile and extract every file again for nothing new.
- name: Build Swift (iPhone)
working-directory: App/BlurtiOS
run: |
xcodebuild \
-project BlurtiOS.xcodeproj \
-scheme BlurtiOS \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$DERIVED-ios" \
ARCHS=arm64 ONLY_ACTIVE_ARCH=YES \
CODE_SIGN_IDENTITY="-" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
build

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
with:
Expand Down
Loading
Loading