Skip to content

An ordinal, not an offset — the outline already knows which row it drew #41

An ordinal, not an offset — the outline already knows which row it drew

An ordinal, not an offset — the outline already knows which row it drew #41

name: Build & test
# The gap this closes, from unimplemented.md §8c: the Release build, both
# platform builds and the test suites were all run by hand, so nothing caught a
# break until someone happened to run the right command.
#
# Two real failures from one session make the case:
#
# * `xcodebuild build` does **not** compile the test target. A breaking change
# to a package API left `HelloNotesTests` uncompilable while every build
# said BUILD SUCCEEDED — twice.
# * Building only the platform you are looking at hid two iOS breaks (a shared
# view referencing an AppKit-only type, and a layout living inside a
# `#if os(macOS)` file) for days.
#
# So: build **both** platforms, build-for-testing (which is what compiles the
# tests), and run them.
#
# No step interpolates event data — no issue titles, branch names or commit
# messages reach a `run:`. The only `${{ }}` inside a shell command is this
# workflow's own matrix, and it goes through `env:` regardless.
on:
push:
branches: [main]
paths-ignore:
- 'website/**'
- 'docs/**'
- '**/*.md'
pull_request:
paths-ignore:
- 'website/**'
- 'docs/**'
- '**/*.md'
workflow_dispatch:
permissions:
contents: read
concurrency:
# A push that supersedes an in-flight run cancels it — macOS runners are the
# expensive kind, and only the tip's answer matters.
group: build-${{ github.ref }}
cancel-in-progress: true
# Why macos-26 and no explicit Xcode selection: `setup-xcode: latest-stable`
# put Xcode 26.3 (macOS 26.2 SDK) on a macos-15 host, and that mismatch failed
# exactly one job — `platform=macOS`, the only destination that resolves the
# *host* as a run destination. `generic/platform=macOS` and the iOS Simulator
# both built fine on the same runner, which is what made it look like a flake.
# The image's default toolchain is already Xcode 26, so aligning host and SDK
# needs no action at all. (Same shape as the FinvestLens CI, which passes.)
env:
SPM_CACHE: ~/.cache/hellonotes-spm
jobs:
editor-package:
# Fast and hermetic: no Xcode project, no simulator, no test host. Runs
# first so an editor regression fails in ~2 minutes rather than ~40.
name: Editor package tests
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: swift test
run: swift test --package-path Packages/NotesEditor
app:
name: App — ${{ matrix.platform }}
runs-on: macos-26
# Cold builds are 30–47 minutes across 74 targets (CLAUDE.md). A long
# xcodebuild here is a cold build, not a hang.
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- platform: macOS
destination: 'platform=macOS'
test: true
- platform: iOS
destination: 'generic/platform=iOS Simulator'
test: false
steps:
- uses: actions/checkout@v7
- name: Cache resolved packages
uses: actions/cache@v6
with:
path: ~/.cache/hellonotes-spm
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved', '**/project.pbxproj') }}
restore-keys: spm-${{ runner.os }}-
# The direct-API cloud providers read their keys from a git-ignored
# xcconfig. The app builds and runs without it — those providers just
# report "not configured" — so CI uses the committed template.
- name: Provide the secrets template
run: cp Config/Secrets.example.xcconfig Config/Secrets.xcconfig
# The runner has no Developer ID certificate and no provisioning
# profiles, and cannot be given them: signing needs a private key. CI is
# here to prove the code *compiles* and the tests pass, so signing is
# switched off for every invocation below. The signed, notarised artefact
# is produced on a developer machine by the release runbook
# (docs/production.md Appendix A2), which is the only place it can be.
- name: Build
env:
DESTINATION: ${{ matrix.destination }}
run: |
xcodebuild build \
-project HelloNotes.xcodeproj -scheme HelloNotes \
-destination "$DESTINATION" -configuration Debug \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-derivedDataPath "$PWD/DerivedData" \
-clonedSourcePackagesDirPath "$SPM_CACHE"
# Separate from `test` on purpose: this is the step that compiles the test
# target, and it is the one that would have caught the break above. It
# needs no test host, so it fails fast and for an unambiguous reason.
- name: Compile the tests
if: matrix.test
env:
DESTINATION: ${{ matrix.destination }}
run: |
xcodebuild build-for-testing \
-project HelloNotes.xcodeproj -scheme HelloNotes \
-destination "$DESTINATION" -configuration Debug \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-derivedDataPath "$PWD/DerivedData" \
-clonedSourcePackagesDirPath "$SPM_CACHE"
- name: Test
if: matrix.test
env:
DESTINATION: ${{ matrix.destination }}
run: |
xcodebuild test-without-building \
-project HelloNotes.xcodeproj -scheme HelloNotes \
-destination "$DESTINATION" -configuration Debug \
-only-testing:HelloNotesTests \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-derivedDataPath "$PWD/DerivedData" \
-clonedSourcePackagesDirPath "$SPM_CACHE"
release:
# "A green Debug build does not imply Release compiles" — a Swift SIL-inliner
# crash once broke every Release build (and so every archive and DMG) while
# Debug stayed clean, unnoticed for ~6,400 lines. See implemented.md §13.
# Runs only on main, because it is the slowest job here.
name: Release build (macOS)
if: github.event_name != 'pull_request'
runs-on: macos-26
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
- name: Cache resolved packages
uses: actions/cache@v6
with:
path: ~/.cache/hellonotes-spm
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved', '**/project.pbxproj') }}
restore-keys: spm-${{ runner.os }}-
- name: Provide the secrets template
run: cp Config/Secrets.example.xcconfig Config/Secrets.xcconfig
- name: Build (Release)
run: |
xcodebuild build \
-project HelloNotes.xcodeproj -scheme HelloNotes \
-destination 'generic/platform=macOS' -configuration Release \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
-derivedDataPath "$PWD/DerivedData" \
-clonedSourcePackagesDirPath "$SPM_CACHE"