add buildkite pipeline for cli release#71
Conversation
Wires the `platform-imessage` Buildkite pipeline (provisioned via [Automattic/buildkite-ci#848](Automattic/buildkite-ci#848)) to actually do something, so the macOS CLI release can move off GitHub Actions and onto Automattic Buildkite — keeping the Developer ID p12 and ASC API key alongside the rest of the org's Apple distribution material in `a8c-secrets` rather than as GitHub-side secrets. Trigger model: - PR / `main` push → compile-check only (`swift build -c release`). - `v*` tag push → `.buildkite/commands/release-cli.sh` runs the local `scripts/sign-and-notarize-cli` (universal binary, hardened runtime, Apple-notarized) and publishes the tarball + sha256 to a GitHub Release via `gh`. The release script is idempotent: if the release already exists for the tag it just uploads with `--clobber`, so re-runs from a Buildkite retry don't error. `shared-pipeline-vars` mirrors the convention from `pocket-casts-desktop` / `Automattic-Tracks-iOS`: pin the `a8c-ci-toolkit` plugin and read the Xcode image id from `.xcode-version`. Follow-up: once a tag build proves green end-to-end on Buildkite, remove `.github/workflows/release-imessage-cli.yml` so we don't have two workflows publishing different assets to the same release. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
`artifact_paths` runs at end-of-step regardless of step exit code, so an explicit `buildkite-agent artifact upload` in the script only fires on success — exactly when we need the artifact least. Switching to the pipeline DSL means we still get the tarball + sha256 stashed even when codesign, notarize, or `gh release` fails partway through. The script now writes to `dist/` (already gitignored) instead of a mktemp dir, so the glob in `artifact_paths` has a stable target. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse the two pipeline steps into one that runs on every CI build
(PR, `main`, tag) and produces a signed+notarized universal-binary
tarball, uploaded as a Buildkite artifact via `artifact_paths`. Non-tag
builds skip the `gh release` step; tag builds do the full publish.
Lets reviewers grab a fully usable CLI from any PR's BK build instead
of having to clone and run the script themselves.
Non-tag asset names are versioned `${package.json version}-${shortsha}`
so each build's artifact is uniquely named, matching the BK artifact
naming convention.
Tradeoff: every PR/`main` push hits Apple's notary service (~30-90s
each). Well under the 75/hr/team cap, but worth knowing if traffic
spikes.
---
Generated with the help of Claude Code, https://claude.ai/code
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Buildkite Mac agent doesn't carry our Developer ID cert in its keychain by default, so without this the script fails with `no Developer ID Application identity for team PZYM8XX95Q in keychain`. `set_up_signing` lane mirrors the `snelectron` / `matticspace-mobile` pattern: `sync_code_signing(type: 'developer_id', ...)` against the shared `a8c-fastlane-match` S3 bucket. Cert is already provisioned there for our team. `app_identifier: []` works for `developer_id` since the cert isn't tied to a specific app bundle. Env-var presence is enforced via release-toolkit's `EnvManager` so we fail fast with a clear message if `MATCH_S3_*` / `MATCH_PASSWORD` are missing on the agent, rather than getting an opaque `match` error. `Gemfile` carries only the two top-level gems (fastlane, wpmreleasetoolkit); everything else is transitive. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI expects it.
Buildkite now builds, signs (Developer ID, hardened runtime), notarizes, and publishes the universal CLI to GitHub Releases on every `v*` tag — verified end to end on the BK pipeline branch (build #7 produced a notarized artifact with CDHashes matching Apple's notary ticket). Keeping the GHA workflow live alongside it would race the BK publish on every tag and attach two assets — one signed and notarized, one unsigned and arm64-only — to the same release. Asset names line up: GHA produced `imessage-cli-${version}-macos-arm64.tar.gz`, BK produces `imessage-cli-${version}-macos-universal.tar.gz`. Same shape, just the arch suffix differs. `.sha256` companion file and tar contents (a bare `imessage-cli` at the root) are byte-compatible. `.github/workflows/ci.yml` (tests + npm publish to GH Packages) is intentionally untouched — that path serves a different purpose and isn't replaced by Buildkite. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`.buildkite/commands/ci.sh` mirrors what `.github/workflows/ci.yml` did: inject `BEEPER_DEPS_TOKEN` for private deps, install JS deps, run JS + Swift tests, then build (or `npm publish` to GH Packages on `main` when `package.json`'s version is new). A `PUBLISHING=true` build env var is the equivalent of GHA's `workflow_dispatch publishing=true` override. Slotted next to the existing CLI release step in `pipeline.yml` — the two run in parallel, hit different `.build/` paths, and report independent GitHub statuses. The release step's `key` is renamed `build` → `release-cli` so the two lanes don't clash. GHA's `Run TypeScript tests` step ran `yarn test`, which per `package.json` is `yarn test:swift && yarn test:js` — meaning the follow-up `Run Swift tests` step ran the Swift suite a second time. The port runs each suite once; net behavior is unchanged. `actions/cache` for `.turbo` isn't ported — the BK Mac queue uses persistent VMs, so incremental Turbo state stays on disk between builds on the same agent. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The only `github.com/beeper/*` dependency in the tree is `BetterSwiftAX` (`Package.swift:22`), which is public — verified locally that `yarn install --immutable` resolves without the rewrite and SwiftPM clones it anonymously. The rewrite was inherited from the GHA workflow as defensive plumbing; re-add it (or a case-insensitive variant covering `Beeper/`) the moment a private beeper-org dep gets introduced. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The BK Mac agents ship Node with corepack disabled, so the bare `yarn` invocations in `ci.sh` exit 127. Enabling corepack activates the version pinned by `packageManager` in `package.json` (yarn 4.14.1). --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Buildkite Mac agents don't ship Node on the non-interactive PATH, so `corepack enable` (and therefore `yarn`) failed with command-not-found. The a8c convention is to activate Node via the `automattic/nvm` plugin, which reads `.nvmrc` for the version. Pin to Node 20 to match the direct `@types/node@20.17.19` dep in `package.json`. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Gio Lodi <giovanni.lodi42@gmail.com>
|
There was a problem hiding this comment.
Pull request overview
This PR moves CI/CD responsibilities (including macOS CLI signing/notarization + release publishing) from GitHub Actions to an Automattic Buildkite pipeline, adding the scripts/config needed to run on macOS agents and manage signing assets via a8c-secrets-backed tooling.
Changes:
- Add Buildkite pipeline config plus
ci.shandrelease-cli.shcommand scripts to test/build and publish CLI release assets. - Introduce a Bundler-managed Fastlane setup (Gemfile/Gemfile.lock + Fastfile) to fetch Developer ID signing credentials (via match/S3).
- Add local toolchain pins/ignores (.xcode-version, .nvmrc, vendor/bundle ignore) and remove GitHub Actions workflows previously handling CI/release.
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.buildkite/pipeline.yml |
Defines Buildkite steps for test/build and CLI release flow. |
.buildkite/shared-pipeline-vars |
Centralizes plugin pins and image id derived from .xcode-version. |
.buildkite/commands/ci.sh |
Runs yarn install, JS+Swift tests, then builds and optionally publishes to GH Packages. |
.buildkite/commands/release-cli.sh |
Builds/signs/notarizes CLI and uploads artifacts; publishes to GitHub Releases on tags. |
Gemfile |
Adds fastlane + wpmreleasetoolkit dependencies for signing credential setup. |
Gemfile.lock |
Locks Ruby dependencies for CI reproducibility. |
fastlane/Fastfile |
Adds a lane to fetch Developer ID certs into CI keychain via match (S3). |
fastlane/.gitignore |
Ignores Fastlane-generated files in fastlane/. |
.bundle/config |
Pins bundler install path under vendor/bundle. |
.gitignore |
Ignores vendor/bundle/ (bundler install output). |
.xcode-version |
Pins Xcode image version for CI. |
.nvmrc |
Pins Node major version for CI. |
.github/workflows/ci.yml |
Removed GitHub Actions CI workflow in favor of Buildkite. |
.github/workflows/release-imessage-cli.yml |
Removed GitHub Actions CLI release workflow in favor of Buildkite. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Tests + library build, plus `npm publish` to GH Packages on main when | ||
| # package.json's version is new. | ||
| - label: ":jest: :swift: Test & build" | ||
| key: test-build | ||
| command: .buildkite/commands/ci.sh | ||
| plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] |
| # Runs on every build (PR, main, tag). The script signs and notarizes the | ||
| # CLI, stashes the tarball as a Buildkite artifact, and only publishes to | ||
| # GitHub Releases when triggered by a `v*` tag. | ||
| - label: ":apple: Build, sign, notarize CLI" | ||
| key: release-cli |
| echo "--- :key: install Developer ID cert into the agent keychain" | ||
| install_gems | ||
| bundle exec fastlane set_up_signing | ||
|
|
||
| echo "--- :hammer_and_wrench: build, sign, notarize" | ||
| ./scripts/sign-and-notarize-cli | ||
|
|
||
| binary=".build/universal/release/imessage-cli" | ||
| if [ ! -f "$binary" ]; then | ||
| printf >&2 "expected signed binary at %s\n" "$binary" |
There was a problem hiding this comment.
The env vars should be in CI at all times. If the build fails because it can't find them, even though it's not a release (publish=false) that's a good signal.
| # Manual override. | ||
| # Set via build env var (e.g. via the "New Build" dialog or BK API). | ||
| if [ "${PUBLISHING:-false}" = "true" ]; then | ||
| should_publish=true |
There was a problem hiding this comment.
PUBLISHING=true override drops the main branch guard.
The deleted GHA ci.yml had if: ${{ github.ref_name == 'main' && (needs.check-published.outputs.version-exists == 'false' || inputs.publishing == 'true') }} — inputs.publishing only took effect on main. Here, should_publish=true is set unconditionally if PUBLISHING=true, regardless of BUILDKITE_BRANCH/BUILDKITE_TAG. Triggering a BK build on any feature branch with the env var checked will run CI_PUBLISHING=true npm publish, which (assuming package.json's version isn't already on the registry) will publish @beeper/platform-imessage from a non-main commit.
The commit message describes this as "the equivalent of GHA's workflow_dispatch publishing=true override", so the broader scope looks unintended. Suggest folding the override into the existing BUILDKITE_BRANCH=main && -z BUILDKITE_TAG block.
| # main) still produce a signed+notarized tarball — uploaded as a | ||
| # Buildkite artifact — for download/testing. | ||
| if [ -n "${BUILDKITE_TAG:-}" ]; then | ||
| tag="$BUILDKITE_TAG" |
There was a problem hiding this comment.
Tag-only release is a behavior change vs the retired GHA workflow.
The deleted .github/workflows/release-imessage-cli.yml triggered on either a v* tag or a push to main that touched package.json (its metadata step computed should_release by diffing package.json's version against the prior commit). The new BK script publishes only when BUILDKITE_TAG is non-empty.
The PR body's "Trigger model" section calls out the new model, but the workflow-retirement commit (378166) doesn't, so flagging in case the team's current release flow is just "merge a version bump to main" — if so, no CLI release will be published until they also push a tag.
Not necessarily a bug; just worth confirming intent before merge.
|
|
||
| echo "--- :rocket: publish to GitHub release" | ||
| # Idempotent: create the release if missing, then upload with --clobber so re-runs replace | ||
| if ! gh release view "$tag" --repo beeper/platform-imessage >/dev/null 2>&1; then |
There was a problem hiding this comment.
Validate gh auth before the build, not after.
gh release view/create/upload need GH_TOKEN (or GITHUB_TOKEN). Right now a missing token surfaces only at line 52+, after swift build (universal, both arches), lipo, strip, codesign, and a notarytool round-trip — ~10 min wasted plus a notary submission burned.
fastlane/Fastfile uses EnvManager.get_required_env! for the match secrets specifically to avoid this; mirroring it here for the gh token (only when publish=true) would close the loop.
Latent for now — triggers on any fresh BK Mac agent that hasn't been provisioned with the token yet, or after a creds rotation.
| # Publish to GitHub Releases only on tag builds. Non-tag builds (PRs, | ||
| # main) still produce a signed+notarized tarball — uploaded as a | ||
| # Buildkite artifact — for download/testing. | ||
| if [ -n "${BUILDKITE_TAG:-}" ]; then |
There was a problem hiding this comment.
Tag-prefix check missing.
The deleted GHA release-imessage-cli.yml listened on tags: ['v*']. Here the publish branch is gated solely on [ -n "${BUILDKITE_TAG:-}" ], so a non-v tag (e.g. staging-1) would set version="staging-1", build imessage-cli-staging-1-macos-universal.tar.gz, and call gh release create staging-1.
The PR body and every per-commit message describes the trigger as "v* tag push → release", so the missing prefix check looks like an oversight rather than a relaxation. Suggest:
case "${BUILDKITE_TAG:-}" in
v*) publish=true; tag="$BUILDKITE_TAG"; version="${tag#v}" ;;
*) publish=false; ... ;;
esac|
|
||
| echo "--- :hammer_and_wrench: build, sign, notarize" | ||
| ./scripts/sign-and-notarize-cli | ||
|
|
There was a problem hiding this comment.
Pin the arch flag to keep the path contract local.
.build/universal/release/imessage-cli only exists when scripts/sign-and-notarize-cli runs with --arch universal. That's the script's current default but it's not a contract — someone changing the default to e.g. arm64 would silently break this BK step (the [ ! -f "$binary" ] check at L23 would fire after the full build).
Cheap fix: invoke ./scripts/sign-and-notarize-cli --arch universal so the BK pipeline owns the arch decision rather than inheriting it from the script's default.
| setup_ci | ||
|
|
||
| # No `.env` file in this repo today — env vars come from the Buildkite agent. | ||
| # `set_up` still needed so EnvManager has a configured instance. |
There was a problem hiding this comment.
platform-imessage.env doesn't exist in the repo.
The inline comment acknowledges this ("No .env file in this repo today — env vars come from the Buildkite agent"), but EnvManager.set_up(env_file_name: 'platform-imessage.env') still names a file that isn't there. Whether set_up no-ops or errors when the file is missing is implementation-specific to wpmreleasetoolkit's version — a future minor bump could turn this into a setup failure.
Safer to either drop the env_file_name: argument (use the default) or commit an empty placeholder.
Rationale
Stacked on top of #70 (the local sign+notarize automation).
Moves both GHA workflows —
ci.yml(tests +npm publishto GH Packages) andrelease-imessage-cli.yml(CLI release) — onto Automattic Buildkite (provisioned in Automattic/buildkite-ci#848).This lets the Developer ID p12 and ASC API key live in
a8c-secretsrather than as GitHub-side secrets, and consolidates the two workflows into a single pipeline.Two parallel steps on every build:
Test & build(ci.sh) — JS + Swift tests, library build,npm publishto GH Packages onmainwhenpackage.json's version isn't on the registry.Build, sign, notarize CLI(release-cli.sh) — universal hardened+notarized binary viascripts/sign-and-notarize-cli, tarball + sha256 as a BK artifact every build,gh release uploadto GitHub Releases onv*tag.Both GHA workflows are deleted in this PR.
Tradeoffs
Catches signing/notarization regressions on PRs and leaves a downloadable signed artifact for ad-hoc testing.
Costs ~6m per PR build (universal Swift release build is the dominant cost; notarytool is ~23s).
Gating the CLI step on
main/tag is one config line away if it becomes painful.Gemfile+ vendored gems) so the Developer ID cert install usesbundle exec fastlane set_up_signing— Automattic's standard cert-fetch pattern viaa8c-secrets.The notarytool call itself is still plain
xcrun notarytool submit --wait; Fastlane only handles cert provisioning.gh release upload --clobberkeeps the publish step idempotent so a BK retry doesn't error when the asset already exists.Gotchas
PATH.The pipeline uses
automattic/nvm#0.6.0with.nvmrc=20(matches the direct@types/node@20.17.19dep) socorepack/yarnresolve correctly inside the step.ghis expected to be on the BK Mac agents and authenticated forbeeper/platform-imessage— confirm on the first tag build.How to test
main.Test & buildandBuild, sign, notarize CLIgo green;download the
imessage-cli-{version}-macos-universal.tar.gzBK artifact, untar, run on a Mac, verify Gatekeeper accepts it.v0.x.y-testtag —Build, sign, notarize CLIadditionally uploads the tarball + sha256 to the matching GH Release.Posted by Claude Code (Opus 4.7, 1M context) on behalf of @mokagio with approval.