Skip to content

Release pipeline#85

Draft
graeme wants to merge 21 commits into
mainfrom
worktree-release-pipeline
Draft

Release pipeline#85
graeme wants to merge 21 commits into
mainfrom
worktree-release-pipeline

Conversation

@graeme

@graeme graeme commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

PR: Automated build, notarise and release pipeline

Summary

Adds a two-stage macOS release pipeline. Every merge to main builds, signs and notarises the app; a manual "Run workflow" step promotes the latest build to a GitHub Release and opens the downstream PRs. With the version now at 0.2.0, the first tag will be v0.2.0.

Changes

  • build.yml (new) — on merge to main: archive with Developer ID signing (secure timestamp + hardened runtime), notarise and staple both the app and the .pkg, then upload two fixed-name artifacts:
    • Homebrew-<version>.pkg — website download; bundles the official signed Homebrew.pkg and runs it from a postinstall (offline, no curl | bash).
    • Homebrew-<version>.zip — the bare, stapled Homebrew.app for the cask.
  • release.yml (rewritten) — manual workflow_dispatch promote: reads the version from Version.xcconfig, tags locally, downloads the latest main artifacts, pushes the tag only on success, creates the Release with both assets, opens a version-bump PR, and opens a homebrew-app cask PR via bump-cask-pr.
  • docs/RELEASING.md (new) — process, required secrets, and caveats.

Why this split

Building and notarising on merge means nothing ships that hasn't already built successfully — the promote step never rebuilds, so a late failure can't leave a broken tag.

Testing

  • Both workflows validated as YAML; the generated postinstall syntax-checked with bash -n.
  • Build → promote exercised on-branch via temporary triggers (reverted before review) to produce a real notarised build, a Release with both assets, and the version-bump PR.
  • Cask PR step needs the homebrew-app cask to exist and the HOMEBREW_GITHUB_API_TOKEN secret.

PR checklist

  • Have you followed this repository's contribution and workflow guidance?
  • Have you explained what changed and why this should land now?
  • Have you run relevant local checks for the changed scope?
  • Are changes scoped and free of unrelated modifications?

  • AI was used to generate or assist with generating this PR.
  • Claude drafted the workflows and docs; I reviewed every step, verified the signing/notarisation flags, and drove the pipeline on-branch to confirm behaviour.

Follow-ups

  • Set the new HOMEBREW_GITHUB_API_TOKEN secret (a PAT that can fork homebrew-cask).
  • The first homebrew-app cask submission is manual; bump-cask-pr automates every bump after that.

graeme and others added 7 commits July 7, 2026 22:38
Split the build/sign/package machinery out of release.yml into a
dedicated Build workflow that runs on every push to main (and as a
signed-but-not-notarised dry run on pull requests).

On main it produces a fully signed, notarised and stapled .pkg and
uploads it as a fixed-name `BrewUI-pkg` artifact so the promote
workflow can attach the most recent successful build to a release,
without ever rebuilding at release time.

The marketing version is read from the project build settings
(MARKETING_VERSION, sourced from Configurations/Version.xcconfig)
rather than from a tag input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the fragile "create tag then build" release with the robust
pattern from the blogpost: the binary is already built and notarised
by build.yml on merge to main, and Release just promotes it.

On the manual "Run workflow" trigger it reads the marketing version
from Configurations/Version.xcconfig, creates the tag locally, downloads
the most recent successful main build artifact, verifies it matches the
version, then pushes the tag and creates the GitHub Release with the
notarised .pkg attached. Pushing the tag happens last, so a failed run
never leaves a dangling tag on GitHub.

Pushing a change to this workflow triggers a dry run that exercises
permissions without creating a tag or release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a successful release, bump the marketing version in
Configurations/Version.xcconfig (minor by default, e.g. 1.0 -> 1.1) and
open a PR against main. main is branch-protected, so GITHUB_TOKEN cannot
push to it directly; a PR is the supported path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the GitHub Release exists, run `brew bump-cask-pr` to open a PR
against Homebrew/homebrew-cask bumping the brewui cask to the released
version, pinning the notarised package's sha256. Requires a
HOMEBREW_GITHUB_API_TOKEN PAT that can fork homebrew-cask and open a PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/RELEASING.md covering the build-on-merge + manual-promote
pipeline: how to cut a release, the required secrets (including the new
HOMEBREW_GITHUB_API_TOKEN), immutable-tag and first-time-cask caveats,
and a stub for the future patch-release workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pkgbuild --root'd the entire xcodebuild export directory, which also
contains the exporter's own DistributionSummary.plist, ExportOptions.plist
and Packaging.log — so the installed .pkg dropped those stray files into
/Applications next to Brew.app.

Use --component on the app bundle instead, so only Brew.app is packaged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The postinstall script installed Homebrew by curling install.sh at
install time: it needs network access during installation, runs as the
console user via su, and fails silently on headless/MDM installs.

Instead, download the official Homebrew.pkg and ship it as a nested
sub-package in the distribution: productbuild --synthesize builds a
distribution that installs Homebrew first, then Brew.app. This drops the
--scripts postinstall from pkgbuild and deletes scripts/postinstall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +23 to +27
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

# Creates a short-lived keychain isolated to this job. The 6-hour
# timeout (21600 s) exceeds any reasonable build duration.
graeme and others added 4 commits July 8, 2026 08:50
Version.xcconfig is a repo source file, not part of the published docs
site, so the relative link failed htmlproofer's internal link check.
Reference it as inline code instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app target is named Homebrew, so PRODUCT_NAME builds Homebrew.app,
not Brew.app. The packaging steps inherited the stale Brew.app paths from
the old workflow, which would have failed the signature check and
pkgbuild. Point --component and the signature verification at
export/Homebrew.app, and name the archive Homebrew.xcarchive to match.

The scheme name (Brew) and bundle identifier (sh.brew.app) are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the Homebrew.app rename across the deliverables: the component and
distribution packages become Homebrew-component.pkg / Homebrew-<version>.pkg,
the build artifact becomes Homebrew-pkg, and the cask bumped by
bump-cask-pr becomes homebrew.

Left unchanged: the BrewUITests target and the Tools/BrewUILint linter
(unrelated to the app bundle), and the repo/project name in docs prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace `ls dist/Homebrew-*.pkg | head` with a nullglob array so the
package is located without parsing ls output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@graeme graeme changed the title Worktree release pipeline Release pipeline improvements Jul 8, 2026
@graeme graeme changed the title Release pipeline improvements Release pipeline Jul 8, 2026
graeme and others added 10 commits July 8, 2026 19:08
productbuild cannot nest Homebrew.pkg: it is a product archive (a finished
installer with its own scripts), and productbuild only assembles component
packages, so --synthesize failed with "is a product archive, not a
component package".

Instead, stage the official signed Homebrew.pkg into the pkgbuild scripts
directory and run it from a postinstall via `installer -pkg` when Homebrew
is not already present. This preserves Homebrew's own install logic and
signature, and is still offline (no curl | bash). The distribution package
goes back to wrapping the single app component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cask install path drops a bare Homebrew.app, which needs its own
stapled notarisation ticket — the pkg's ticket does not travel with the
extracted app. Notarise and staple the app bundle before packaging (so
the copy embedded in the pkg is stapled too), then ditto the stapled app
into Homebrew-<version>.zip and upload it as a second fixed-name artifact
(Homebrew-app) alongside the pkg. Attestation now covers both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The promote workflow now downloads both build artifacts, checks each
matches the version, and attaches both to the GitHub Release: the .pkg
(website download) and the .zip (bare app, for the future cask PR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cask installs the bare app (the zip), so bump-cask-pr must pin the
zip's sha256, not the pkg's. Hash $APP_ZIP instead of $PKG_PATH.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmed with a Homebrew lead maintainer that the cask will be named
homebrew-app. Update the bump-cask-pr token and the docs to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Do not merge — revert this commit before opening for review.

- build.yml: notarise/staple/zip/attest/upload also fire on this branch,
  so it produces real artifacts here (normally main-only).
- release.yml: triggers on pushes to this branch, downloads this branch's
  build artifacts, and runs the tag/release/version-bump steps for real.
- The cask PR step stays dispatch-only, so it is skipped here.

Every change is tagged with a THROWAWAY comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The archive step failed with "A timestamp was expected but was not found"
during codesign of Homebrew.app. Developer ID signing requires a secure
Apple timestamp, and notarisation requires the hardened runtime; neither
was pinned, so codesign behaviour on CI was non-deterministic.

Add --timestamp --options runtime to OTHER_CODE_SIGN_FLAGS, and verify the
archived app carries a Timestamp= line so this fails fast with a clear
message instead of surfacing later at notarisation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The notarise/staple/upload steps are guarded by event_name == 'push',
but build.yml's push trigger was scoped to main, so feature-branch runs
came through the pull_request event and the guards were skipped. Add this
branch to the push trigger so it fires a real push event and the existing
guards run — the same push-event path main uses.

Revert before review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants