Release signing and hosting
Sign and notarize customer builds, and publish verified native distributions to object storage.
Signed application builds
The Signed macOS release GitHub Actions workflow publishes automatically when a stable v<major>.<minor>.<patch> tag is pushed. The tagged commit must already be on main, and its version must match Resources/Info.plist. You can also run Actions → Signed macOS release → Run workflow on main, enter the matching version, and choose whether to publish or only produce a verified workflow artifact. It uses Xcode 27 and its current macOS SDK, while keeping macOS 14 as the minimum supported version. App releases target Apple silicon, including the updater helpers.
Configure these repository secrets through GitHub's encrypted secret storage:
| Secret | Value |
|---|---|
MORROW_CERTIFICATE_P12_BASE64 |
Base64 of the Developer ID Application certificate and private key, exported as an encrypted .p12 |
MORROW_CERTIFICATE_PASSWORD |
Password protecting that .p12 |
MORROW_NOTARY_KEY_BASE64 |
Base64 of the dedicated App Store Connect team API key's .p8 file |
MORROW_NOTARY_KEY_ID |
That API key's Key ID |
MORROW_NOTARY_ISSUER_ID |
The team's Issuer ID |
MORROW_SPARKLE_ED_PRIVATE_KEY |
Base64-encoded 32-byte Ed25519 seed for signing Morrow update archives |
Set repository variable MORROW_APPLE_TEAM_ID to the certificate's Apple Team ID. Set MORROW_BINARY_CATALOG_URL to the public HTTPS catalog to include it as the default source in releases. A user-selected source overrides this default.
The workflow runs the tests, imports the certificate into a temporary keychain, checks its team, signs the app, bundled CLI, and all Sparkle helpers with hardened runtime and timestamps, and submits an archive to Apple. Only an accepted submission is stapled and packaged. It verifies the ticket and Gatekeeper assessment before producing Morrow-<version>-macos-arm64.zip, SHA256SUMS, and the notarization reports. On failure, available reports are retained for diagnosis. Temporary credentials and the keychain are removed even after a failed build.
After notarization, the workflow generates appcast.xml using the same pinned Sparkle version as the app. The archive's Ed25519 signature is independently verified against SUPublicEDKey in the bundled Info.plist. The signed ZIP, checksum, and feed are uploaded to a draft release before it is published as the latest release. Published assets are immutable; use a newer version for fixes. Older versions cannot replace the stable feed. The feed URL is the latest release's appcast.
Keep the update private key backed up securely outside the repository, and retain the matching public key in Resources/Info.plist across releases. This key is separate from the Apple Developer ID certificate. Do not regenerate it for each build. CFBundleVersion comes from the release workflow's increasing run number; preserve that ordering if the workflow is replaced. See Sparkle's distribution documentation for update signing and key rotation.
Signed release builds enable the native updater. Local ad-hoc builds leave it disabled, so a development app cannot replace itself with a public release. Updating Morrow does not install database or runtime upgrades, or change their data folders; those remain explicit service-management actions.
Never put private keys, certificate passwords, or storage credentials in source files, workflow text, documentation, or chat. Apple Developer membership and a valid Developer ID Application certificate are required. See Apple's notarization workflow.
Contabo publisher setup
AWS CLI is an administrator publishing tool. Customers download public HTTPS archives directly through Morrow and do not install it. Install AWS CLI v2 using AWS's macOS installer; a current-user installation works without Homebrew. This development machine uses ~/.local/bin/aws.
Configure a dedicated profile locally. Enter access keys only in your own terminal:
aws configure --profile contabo
aws configure set endpoint_url https://sin1.contabostorage.com --profile contabo
aws configure set s3.addressing_style path --profile contabo
Use region default for the signing region and output json. Choose the endpoint matching your bucket's location; the example is Singapore. Copy distributions/storage.example.json to the ignored distributions/storage.local.json, then enter your bucket, public download base, endpoint, and profile. The public base uses Contabo's documented tenant-and-bucket URL; it is distinct from the endpoint used by AWS CLI. See Contabo's object storage documentation.
Publish a prepared native archive
New releases target Apple silicon (arm64) only. Recipes and publication reject new Intel packages. Historical Intel objects remain immutable so existing download URLs continue to work.
First prepare a complete relocatable distribution and a release descriptor using the catalog format. Include all required libraries, resources, executable paths, redistribution notices, architecture, minimum macOS version, and the archive's SHA-256. Optional license, sourceURL, sourceSHA256, and notarizationID fields preserve provenance. Do not upload Homebrew bottles with external Cellar dependencies.
python3 scripts/publish-binary.py \
--archive /path/to/distribution.tar.gz \
--release /path/to/release.json \
--dry-run
python3 scripts/publish-binary.py \
--archive /path/to/distribution.tar.gz \
--release /path/to/release.json \
--public-read
--dry-run shows the planned public URLs and descriptor without contacting storage. Publishing checks the local checksum, uploads to a content-addressed key, downloads the public object to verify its bytes, then merges the release into morrow/catalog.json. Existing releases with conflicting content are refused. Conditional catalog writes prevent silently overwriting a concurrent publisher; unsupported conditional writes stop publication.
--public-read sets the ACL only on the archive and catalog objects being published. It does not make the entire bucket public or change unrelated objects. Omit it when public access is already configured separately. The publisher delegates authentication to AWS CLI's profile and never reads or prints access keys.
For complete source-build recipes and recipes declaring corresponding sources, the publisher also retains their pinned source archives and build dependencies under immutable sources/ keys. Public source URLs and checksums are attached as sourceArchives metadata. This keeps redistribution sources available alongside the binary without adding compiler tools to customer installations. License notices stay inside the package.
After publishing, set the repository's MORROW_BINARY_CATALOG_URL variable to the printed catalog URL and build a signed release. Customer releases use this source automatically. For an existing development Mac, set an override with morrow binary source <url>. New catalog patch versions become available through Morrow's existing update checks; previous archives remain immutable.
Large archives use multipart uploads and bounded parallel public byte-range downloads. Interrupted byte-range requests retry with a bounded delay. Every public byte is hashed in archive order before the catalog changes; size and byte-range mismatches stop publication. Smaller archives and servers without byte-range support use a normal complete download.
Publication merges the current catalog and uses conditional writes. Returned ETags are normalized for Ceph/Contabo compatibility; concurrent catalog changes still stop publication. Storage failures name the operation and error category without printing credentials.
The hosted catalog covers all 17 supported service and runtime families on Apple silicon (arm64). The published version table lists exact releases. Newly prepared packages pass native dependency and version checks, Developer ID signing, and Apple notarization before publication. The original checksum-verified official Go archive for Apple silicon remains immutable. Existing services and data continue using their current binaries during this transition.
Portable PHP preparation
distributions/php/8.0.30.json pins the upstream StaticPHP CLI/FPM archives and SHA-256 digests for Apple silicon. Prepare a fresh signed package directory with:
python3 scripts/package-php.py --recipe distributions/php/8.0.30.json --output .build/php-release --identity "$MORROW_SIGNING_IDENTITY" --team "$MORROW_EXPECTED_TEAM"
Preparation validates archive layout, architecture, minimum macOS, and dependencies restricted to system libraries. It includes upstream license notices, source provenance, and version-specific configuration, then signs each executable with Developer ID. Run scripts/notarize-php.sh .build/php-release to notarize its submission ZIP and record the accepted ID in the descriptor before publishing the archive with scripts/publish-binary.py. No Homebrew installation or global PHP linking occurs during preparation.
The Signed PHP distributions GitHub Actions workflow runs this preparation and notarization with the existing release credentials in a temporary keychain. It uploads signed archives, descriptors, and the Apple acceptance report; download these artifacts to publish through the locally configured object-storage profile. Storage access keys are not needed by this workflow.
Node and service preparation
The Complete portable distributions workflow builds Apple silicon packages on a native arm64 Mac runner. Its recipes cover PostgreSQL, MySQL, MariaDB, MongoDB, Redis, Valkey, Memcached, Python, Ruby, Flutter, Go, and MinIO. Native source builds use pinned dependencies; complete upstream SDKs retain their libraries, resources, and notices. The publisher rewrites references only to libraries already inside that package, rejects unresolved host dependencies, and signs the relocated code before notarization. Recipes and accepted build artifacts are published before their versions are advertised to customers.
Current PHP source recipes include CLI/FPM and pinned OpenSSL, Oniguruma, ICU, and libzip builds. They supply a trusted certificate bundle and a version-specific INI file; Morrow resolves its own configuration placeholder after download without running an installation script. The older PHP 8.0 StaticPHP workflow remains available for that legacy release.
Ruby's native-extension configuration and PHP's phpize/php-config helpers use the installed package location rather than the publisher's SDK paths. Building native extensions can still require Xcode and the extension's own development headers; those are development prerequisites rather than a Homebrew requirement for running the packaged runtime.
Signed Go packages retain the compiler, tools, standard library, headers, and source, but omit the compiler suite's testdata directories containing deliberately malformed or foreign executables. These fixtures are not required to build Go applications; use the upstream source archive for developing or running Go's own compiler suite.
The Signed service distributions workflow uses reviewed recipes in distributions/<package>/<version>.json. Recipes cover Node 22.23.3 and 24.22.0 LTS, Caddy 2.11.7, Mailpit 1.31.6, and dnsmasq 2.93 for Apple silicon. Node includes npm, npx, headers, and its bundled licenses. Caddy and Mailpit use checksum-pinned official release archives. dnsmasq is built from pinned source using Xcode, with optional DHCP, TFTP, scripting, and external library integrations disabled for Morrow's loopback DNS use.
Run the workflow with the package name and exact recipe version. It prepares the arm64 package, rejects external library dependencies or incorrect minimum macOS declarations, signs all native executables with the release identity, checks the native executable's version, and submits the packages to Apple. Only accepted submissions produce publishable artifacts. Build tools are confined to the publisher's build machine; customers do not need Homebrew, nvm, Git, AWS CLI, or Xcode for these downloads.
Download the workflow artifact and publish the arm64 archive with its corresponding *-release.json through scripts/publish-binary.py. Storage credentials stay in the locally configured profile. Node descriptors may include lts: true; this records the upstream release designation instead of inferring it from a version number. An immutable package is never rebuilt over an existing published version.
Preserve running services during deployment
Stage a new app with MORROW_APP_OUTPUT; do not overwrite executable files used by active service runners. Deploy the entire staged bundle atomically, preserving the previous bundle until its processes have exited. Launch Services registration and scoped attribution cleanup can update association metadata without stopping servers. Already running legacy workers and historical macOS records may remain separate until macOS refreshes or those workers next restart normally. Never reset the global background-task database to force a cosmetic cleanup.