Skip to content

CI

CI #171

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
package_only:
description: Run package tests and coverage without application audits or distribution builds
type: boolean
default: false
run-name: ${{ inputs.package_only && 'Package tests' || 'CI' }}
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
runtime-matrix:
name: Select deployed macOS runtimes
runs-on: ubuntu-24.04
timeout-minutes: 2
outputs:
runtimes: ${{ steps.matrix.outputs.runtimes }}
package: ${{ steps.matrix.outputs.package }}
ui: ${{ steps.matrix.outputs.ui }}
application: ${{ steps.matrix.outputs.application }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Test runtime selection
run: Scripts/test-ci-runtime-matrix.sh
- name: Check macOS 27 deployment
id: matrix
shell: bash
env:
GH_TOKEN: ${{ github.token }}
MACOS_14_RUNNER: ${{ vars.MACOS_14_RUNNER || 'macos-14' }}
run: |
gh api repos/actions/runner-images/releases/tags/xcode-27-arm64%2F20260907.0173 | Scripts/ci-runtime-matrix.sh
lint:
name: Lint
runs-on: macos-26
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
version: 2026.9.3
- name: Select Xcode
run: Scripts/select-xcode.sh
- name: Run every hook
run: just lint
- name: Verify native test execution guards
run: Scripts/test-desktop-guard.sh
- name: Verify local runner isolation and resource limits
run: bash Scripts/test-local-ui-runner.sh
- name: Verify UI group coverage
run: |
Scripts/check-ui-test-inventory.sh
Scripts/test-ui-test-inventory.sh
- name: Verify deferred render checks
run: Scripts/test-render-capture.sh
package-compatibility:
name: Package (${{ matrix.name }}, ${{ matrix.mode }})
needs: runtime-matrix
runs-on: ${{ matrix.runner }}
timeout-minutes: 10
env:
TOKEN_MENU_BAR_RENDER_CAPTURE_ONLY: 1
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.runtime-matrix.outputs.package) }}
steps:
- name: Verify runtime
env:
EXPECTED_MACOS_MAJOR: ${{ matrix.runtime-major }}
run: |
version="$(sw_vers -productVersion)"
[[ "${version%%.*}" == "$EXPECTED_MACOS_MAJOR" ]] || { echo "::error::Unexpected macOS $version"; exit 1; }
echo "Runtime: macOS $version" >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
version: 2026.9.3
- name: Select Xcode
id: xcode
env:
MIN_XCODE_MAJOR: ${{ matrix.minimum-xcode }}
EXACT_XCODE_MAJOR: ${{ matrix.minimum-xcode }}
run: |
Scripts/select-xcode.sh
version="$(xcodebuild -version | tr '\n' '-')"
echo "cache=$(printf '%s' "$version" | shasum -a 256 | cut -c 1-12)" >> "$GITHUB_OUTPUT"
- name: Cache SwiftPM
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .build
key: spm-v3-${{ matrix.mode }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.runner }}-${{ steps.xcode.outputs.cache
}}-${{ hashFiles('Package.swift', 'justfile', 'Scripts/coverage.sh') }}-${{ hashFiles('Sources/**', 'Tests/**')
}}
restore-keys: |
spm-v3-${{ matrix.mode }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.runner }}-${{ steps.xcode.outputs.cache }}-${{ hashFiles('Package.swift', 'justfile', 'Scripts/coverage.sh') }}-
- name: Check toolchain
id: toolchain
run: Scripts/check-toolchain.sh
- name: Test without native presentation
if: matrix.mode == 'nonpresenting'
env:
TOKEN_MENU_BAR_RENDER_ARTIFACTS: ${{ runner.temp }}/settings-render
run: just test
- name: Test native presentation
if: matrix.mode == 'native'
env:
TOKEN_MENU_BAR_RENDER_ARTIFACTS: ${{ runner.temp }}/settings-render
run: just test-native
- name: Retain mock Settings renders
if: ${{ !cancelled() && steps.toolchain.outcome == 'success' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rendered-package-${{ matrix.runtime-major }}-${{ matrix.mode }}
path: ${{ runner.temp }}/settings-render
include-hidden-files: true
if-no-files-found: error
retention-days: 1
- name: Retain native test crash reports
if: ${{ failure() && matrix.mode == 'native' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: package-crashes-${{ matrix.runtime-major }}
path: |
~/Library/Logs/DiagnosticReports/TokenMenuBar*.ips
~/Library/Logs/DiagnosticReports/swiftpm-testing-helper*.ips
if-no-files-found: ignore
retention-days: 1
coverage:
name: Tests and coverage
runs-on: macos-26
timeout-minutes: 10
env:
TOKEN_MENU_BAR_RENDER_CAPTURE_ONLY: 1
steps:
- name: Verify runtime
run: |
version="$(sw_vers -productVersion)"
[[ "${version%%.*}" == 26 ]] || { echo "::error::Unexpected macOS $version"; exit 1; }
echo "Runtime: macOS $version" >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
version: 2026.9.3
- name: Select Xcode
id: xcode
run: |
Scripts/select-xcode.sh
version="$(xcodebuild -version | tr '\n' '-')"
echo "cache=$(printf '%s' "$version" | shasum -a 256 | cut -c 1-12)" >> "$GITHUB_OUTPUT"
- name: Cache SwiftPM
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .build
key: spm-v3-coverage-${{ runner.os }}-${{ runner.arch }}-macos-26-${{ steps.xcode.outputs.cache }}-${{ hashFiles('Package.swift',
'justfile', 'Scripts/coverage.sh') }}-${{ hashFiles('Sources/**', 'Tests/**') }}
restore-keys: |
spm-v3-coverage-${{ runner.os }}-${{ runner.arch }}-macos-26-${{ steps.xcode.outputs.cache }}-${{ hashFiles('Package.swift', 'justfile', 'Scripts/coverage.sh') }}-
- name: Check toolchain
id: toolchain
run: Scripts/check-toolchain.sh
- name: Tests with coverage gate
if: ${{ !cancelled() && steps.toolchain.outcome == 'success' }}
env:
TOKEN_MENU_BAR_RENDER_ARTIFACTS: ${{ runner.temp }}/settings-render
run: just coverage
- name: Retain mock Settings renders
if: ${{ !cancelled() && steps.toolchain.outcome == 'success' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rendered-package-26-native
path: ${{ runner.temp }}/settings-render
if-no-files-found: error
retention-days: 1
- name: Retain native test crash reports
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: package-crashes-26
path: |
~/Library/Logs/DiagnosticReports/TokenMenuBar*.ips
~/Library/Logs/DiagnosticReports/swiftpm-testing-helper*.ips
if-no-files-found: ignore
retention-days: 1
application-ui:
name: Application UI (${{ matrix.name }})
permissions:
actions: read
contents: read
needs: runtime-matrix
if: ${{ github.event_name != 'workflow_dispatch' || !inputs.package_only }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.runtime-matrix.outputs.ui) }}
# https://github.com/rhysd/actionlint/blob/v1.7.12/rule_workflow_call.go
uses: ./.github/workflows/ui.yml # zizmor: ignore[self-repository] -- actionlint 1.7.12 rejects $/ workflow calls
with:
runner: ${{ matrix.runner }}
runtime-major: ${{ matrix.runtime-major }}
minimum-xcode: ${{ matrix.minimum-xcode }}
rendered-text:
name: Rendered text
permissions:
actions: read
contents: read
needs: [runtime-matrix, package-compatibility, coverage, application-ui]
if: ${{ !cancelled() && needs.runtime-matrix.result == 'success' }}
runs-on: macos-26
timeout-minutes: 10
env:
RUNTIMES: ${{ join(fromJSON(needs.runtime-matrix.outputs.runtimes), ' ') }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install the pinned OCR engine
run: Scripts/install-ocr.sh
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: rendered-*
github-token: ${{ github.token }}
path: .build/rendered
- name: Check every package render
run: |
for runtime in $RUNTIMES; do
for mode in nonpresenting native; do
directory=".build/rendered/rendered-package-$runtime-$mode"
Scripts/check-rendered-text.sh "$directory" 16
fixtures=("$directory"/run.*/verifier-present.png)
[[ "${#fixtures[@]}" == 1 ]]
Scripts/test-rendered-text.sh "${fixtures[0]}"
done
done
- name: Check every long-text UI group
if: ${{ github.event_name != 'workflow_dispatch' || !inputs.package_only }}
run: |
for runtime in $RUNTIMES; do
while IFS= read -r group; do
Scripts/check-rendered-text.sh ".build/rendered/rendered-ui-$runtime-$group" 1
done < <(jq -r 'keys[] | select(startswith("text-"))' .github/ui-test-groups.json)
done
application:
name: >-
${{ matrix.runtime-major == 26 && format('Build app flavors ({0}, {1})', matrix.scheme, matrix.configuration) || format('Build
{0} ({1})', matrix.scheme, matrix.name) }}
needs: runtime-matrix
if: ${{ github.event_name != 'workflow_dispatch' || !inputs.package_only }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.runtime-matrix.outputs.application) }}
steps:
- name: Verify runtime
env:
EXPECTED_MACOS_MAJOR: ${{ matrix.runtime-major }}
run: |
version="$(sw_vers -productVersion)"
[[ "${version%%.*}" == "$EXPECTED_MACOS_MAJOR" ]] || { echo "::error::Unexpected macOS $version"; exit 1; }
echo "Runtime: macOS $version" >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
version: 2026.9.3
- name: Select Xcode
id: xcode
env:
MIN_XCODE_MAJOR: ${{ matrix.minimum-xcode }}
EXACT_XCODE_MAJOR: ${{ matrix.minimum-xcode }}
run: |
Scripts/select-xcode.sh
version="$(xcodebuild -version | tr '\n' '-')"
echo "cache=$(printf '%s' "$version" | shasum -a 256 | cut -c 1-12)" >> "$GITHUB_OUTPUT"
- name: Cache Xcode packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .build/xcode-packages
key: xcode-spm-${{ runner.os }}-${{ runner.arch }}-${{ matrix.runner }}-${{ steps.xcode.outputs.cache }}-${{ hashFiles('Package.swift',
'App/project.yml') }}
- name: Generate project
run: just xcode
- name: Restore distribution build
id: distribution-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.build/app-derived/Build
.build/app-derived/ModuleCache.noindex
.build/app-derived/SDKStatCaches.noindex
.build/app-derived/CompilationCache.noindex
key: distribution-v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.runner }}-${{ steps.xcode.outputs.cache }}-${{
matrix.configuration }}-${{ hashFiles('Package.swift', 'App/project.yml') }}-${{ hashFiles('Sources/**', 'App/**')
}}
restore-keys: |
distribution-v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.runner }}-${{ steps.xcode.outputs.cache }}-${{ matrix.configuration }}-${{ hashFiles('Package.swift', 'App/project.yml') }}-
- name: Build
env:
SCHEME: ${{ matrix.scheme }}
CONFIGURATION: ${{ matrix.configuration }}
run: |
set -o pipefail
mkdir -p .build
xcodebuild -project App/TokenMenuBar.xcodeproj -scheme "$SCHEME" -configuration "$CONFIGURATION" \
-destination 'platform=macOS' -derivedDataPath .build/app-derived -jobs 2 \
-clonedSourcePackagesDirPath .build/xcode-packages CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY="" \
SPARKLE_PUBLIC_ED_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= build \
2>&1 | tee .build/distribution-build.log | tail -30
- name: Save distribution build
if: steps.distribution-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.build/app-derived/Build
.build/app-derived/ModuleCache.noindex
.build/app-derived/SDKStatCaches.noindex
.build/app-derived/CompilationCache.noindex
key: ${{ steps.distribution-cache.outputs.cache-primary-key }}
- name: Retain distribution build log
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: distribution-${{ matrix.configuration }}-${{ matrix.runner }}
path: .build/distribution-build.log
retention-days: 1
- name: Verify distribution settings
env:
CONFIGURATION: ${{ matrix.configuration }}
DISTRIBUTION: ${{ matrix.distribution }}
CONDITION: ${{ matrix.condition }}
SANDBOX: ${{ matrix.sandbox }}
ENTITLEMENTS: ${{ matrix.entitlements }}
TARGET: ${{ matrix.target }}
run: Scripts/verify-build-settings.sh "$TARGET" "$CONFIGURATION" "$DISTRIBUTION" "$CONDITION" "$SANDBOX" "$ENTITLEMENTS"
- name: Verify every shipped deployment target
env:
DISTRIBUTION: ${{ matrix.distribution }}
UPDATER: ${{ matrix.updater }}
run: |
app="$(find .build/app-derived/Build/Products -path '*/Token Menu Bar.app' -type d | head -1)"
test -n "$app"
Scripts/verify-deployment-targets.sh "$app" 14.0
Scripts/verify-app-bundle.sh "$app" "$DISTRIBUTION" "$UPDATER"
- name: Package the macOS 14 smoke artifact
if: matrix.runner == 'macos-26' && matrix.scheme == 'TokenMenuBar-Direct'
run: |
app="$(find .build/app-derived/Build/Products -path '*/Token Menu Bar.app' -type d | head -1)"
codesign --force --deep --sign - "$app"
ditto -c -k --keepParent "$app" .build/token-menu-bar-macos-14-smoke.zip
- name: Upload the macOS 14 smoke artifact
if: matrix.runner == 'macos-26' && matrix.scheme == 'TokenMenuBar-Direct'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: token-menu-bar-macos-14-smoke
path: .build/token-menu-bar-macos-14-smoke.zip
if-no-files-found: error
retention-days: 1
macos-14-runtime:
name: Release smoke (macOS 14 runtime)
permissions:
actions: read
contents: read
needs: [runtime-matrix, application]
if: ${{ !cancelled() && needs.runtime-matrix.result == 'success' && (github.event_name != 'workflow_dispatch' || !inputs.package_only)
}}
runs-on: ${{ vars.MACOS_14_RUNTIME_RUNNER || 'macos-14' }}
steps:
- name: Verify runtime
run: |
version="$(sw_vers -productVersion)"
[[ "${version%%.*}" == 14 ]] || { echo "::error::Unexpected macOS $version"; exit 1; }
echo "Runtime: macOS $version" >> "$GITHUB_STEP_SUMMARY"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: token-menu-bar-macos-14-smoke
path: smoke
github-token: ${{ github.token }}
- name: Launch with isolated data
run: |
ditto -x -k smoke/token-menu-bar-macos-14-smoke.zip smoke/app
executable="smoke/app/Token Menu Bar.app/Contents/MacOS/Token Menu Bar"
runtime="$PWD/smoke/runtime"
session="macos-14-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
support="$runtime/token-menu-bar-verify-$session"
ready="$support/snapshots-demo.json"
mkdir -p "$support"
TOKEN_MENU_BAR_VERIFY_SESSION="$session" TOKEN_MENU_BAR_VERIFY_SUPPORT_DIRECTORY="$support" \
"$executable" --verify-ui >smoke/launch.log 2>&1 &
pid=$!
for _ in {1..80}; do
if grep -Fq '"claude"' "$ready" 2>/dev/null && grep -Fq '"codex"' "$ready" 2>/dev/null; then
break
fi
if ! kill -0 "$pid" 2>/dev/null; then
cat smoke/launch.log
exit 1
fi
sleep 0.25
done
if ! grep -Fq '"claude"' "$ready" 2>/dev/null || ! grep -Fq '"codex"' "$ready" 2>/dev/null; then
cat smoke/launch.log
test ! -f "$support/log.txt" || cat "$support/log.txt"
kill "$pid" 2>/dev/null || true
wait "$pid" || true
exit 1
fi
kill "$pid" 2>/dev/null || true
wait "$pid" || true