SPEC-0015: Safari Web Extension — Apple Distribution and CI Pipeline
Overview
This spec defines the requirements for packaging the existing Manifest V3 Web Extension (SPEC-0008) as a native iOS and macOS Safari Web Extension, distributing it via TestFlight, and automating that distribution through the existing GitHub Actions CI pipeline. See 📝 ADR-0015 for the distribution strategy decision and 📝 ADR-0012 for the extension architecture decision.
Requirements
Requirement: Xcode Project Generation
The integrations/extension/ source directory SHALL be convertible to a valid Xcode project using
xcrun safari-web-extension-converter without requiring modifications to the existing
JavaScript or manifest files beyond what the converter applies automatically. The generated
Xcode project MUST be committed to the repository under integrations/apple/ and MUST target both iOS 15+
and macOS 12+ from a single project with two targets (iOS app and macOS app).
Scenario: Converter runs without errors
- WHEN a developer runs
xcrun safari-web-extension-converter integrations/extension/ --app-name "joe-links" --bundle-identifier net.joestump.joe-links --swift --macos-only false - THEN an Xcode project is created at
integrations/apple/with no conversion errors and the existingbackground.js,popup.html,popup.js,options.html,options.js, andmanifest.jsonare referenced intact
Scenario: Both platform targets present
- WHEN the generated Xcode project is opened
- THEN it contains an iOS app target (
joe-links (iOS)) and a macOS app target (joe-links (macOS)) sharing the same extension bundle
Requirement: iOS Safari Compatibility
The extension MUST function correctly in iOS 15+ Safari. Capabilities that are unavailable in
iOS Safari service workers MUST degrade gracefully without breaking extension startup or keyword
interception. Specifically, OffscreenCanvas (used for programmatic icon drawing) MUST NOT
cause a fatal error when unavailable; the extension MUST fall back to the PNG icons declared
in manifest.json.
Scenario: Extension loads on iOS Safari
- WHEN the TestFlight app is installed on an iPhone running iOS 15 or later and the extension is enabled in Settings → Safari → Extensions
- THEN the extension activates without errors and keyword interception is operational
Scenario: OffscreenCanvas unavailable
- WHEN the service worker attempts to call
setActionIcon()on iOS whereOffscreenCanvasis not supported - THEN the
try/catchinsetActionIcon()silently swallows the error and the extension continues operating with the PNG fallback icons
Scenario: declarativeNetRequest rules applied on iOS
- WHEN the extension starts on iOS 16 or later
- THEN
updateRedirectRules()successfully registers dynamicdeclarativeNetRequestrules for each keyword host; on iOS 15, the call is silently skipped if the API is unavailable
Requirement: macOS Safari Compatibility
The extension MUST function correctly in macOS 12+ Safari. The macOS app wrapper MUST be notarized using the Apple Developer Program distribution certificate so that Gatekeeper accepts it on any Mac without requiring the user to bypass security settings.
Scenario: Extension installs on macOS without Gatekeeper bypass
- WHEN a user downloads the macOS app from TestFlight or a direct link and opens it
- THEN macOS Gatekeeper accepts the app without displaying a security warning, and the extension appears in Safari → Settings → Extensions
Scenario: Extension enables in macOS Safari
- WHEN the user enables the extension in Safari → Settings → Extensions and grants "Allow on all websites"
- THEN keyword interception is active and
go/foonavigations redirect correctly in Safari
Requirement: Code Signing Configuration
The Xcode project MUST be configured for automatic signing using an Apple Developer Program
team. Signing assets — the distribution certificate (.p12 + passphrase) and provisioning
profiles — MUST be stored as GitHub Actions secrets and MUST NOT be committed to the repository.
The CI signing setup SHOULD use fastlane match or equivalent certificate management to support
reproducible signing across machines.
Scenario: CI builds and signs the app without local Xcode
- WHEN a GitHub Actions runner executes the CI release job for a tagged commit
- THEN the runner installs the signing certificate from GitHub Actions secrets, builds the Xcode archive, and produces a signed
.xcarchivewithout requiring any interactive Xcode session
Scenario: Signing secrets are missing
- WHEN the required signing secrets (
APPLE_CERT_P12,APPLE_CERT_PASSWORD,APP_STORE_CONNECT_API_KEY) are not present in the GitHub Actions environment - THEN the CI job MUST fail with a clear error message identifying the missing secret rather than producing an unsigned or improperly signed build
Requirement: TestFlight Upload
On every tagged release, the CI pipeline MUST build an .xcarchive for both iOS and macOS
targets and upload the resulting .ipa (iOS) and .pkg or .app (macOS) to TestFlight via
the App Store Connect API. The upload MUST use the App Store Connect API key (Issuer ID, Key ID,
and .p8 private key) stored as GitHub Actions secrets. The CI job MUST fail if the upload
does not succeed.
Scenario: Tag triggers TestFlight upload
- WHEN a git tag matching
v*is pushed and the GitHub Actions CI pipeline runs - THEN the pipeline builds the Xcode archive, exports the iOS
.ipaand macOS app, uploads both to TestFlight, and the build appears in App Store Connect under the joe-links app within 15 minutes
Scenario: Upload fails due to invalid API key
- WHEN the App Store Connect API key secret is expired or invalid
- THEN the CI upload step fails with a non-zero exit code and a descriptive error message; no partial build is uploaded