Skip to content

Add Buildkite pipeline to build the app#13

Merged
mokagio merged 16 commits into
mainfrom
ainfra-2373-workspace-app-buildkite-to-build-sign-notarize-and-track-as
May 14, 2026
Merged

Add Buildkite pipeline to build the app#13
mokagio merged 16 commits into
mainfrom
ainfra-2373-workspace-app-buildkite-to-build-sign-notarize-and-track-as

Conversation

@mokagio
Copy link
Copy Markdown
Contributor

@mokagio mokagio commented May 9, 2026

First-pass CI to prove the pipeline scaffolding works end-to-end.
Signing and notarization are intentionally out of scope.

How to test

Check the artifact from https://buildkite.com/automattic/workspace/builds/7:

Screenshot 2026-05-09 at 8 39 52 PM

Posted by Claude Code (Opus 4.7) on behalf of @mokagio with approval.

mokagio and others added 2 commits May 9, 2026 14:43
AINFRA-2373 — wire up code signing for WP Workspace ahead of CI.

Mirrors the prototype on the `platform-imessage` `ainfra-2351-add-buildkite-pipeline` worktree:
single `set_up_signing` lane that pulls the Developer ID Application cert for team `PZYM8XX95Q` from the shared `a8c-fastlane-match` S3 bucket (us-east-2).

Lane is readonly by default — only sync, never mutate the bucket — so it does not require an App Store Connect API key for routine local use.
The optional read-write path (passing `readonly: false`) requires the ASC API key trio and is reserved for cert rotation.

Local verification:
- `bundle exec fastlane lanes` — Fastfile parses, lane is listed.
- `bundle exec fastlane set_up_signing` — cert installed in login keychain.
- `security find-identity -v -p codesigning` shows `Developer ID Application: Automattic, Inc. (PZYM8XX95Q)`.

`fastlane/Matchfile` is intentionally empty: the Fastfile passes all match parameters explicitly so callers always see the configuration in one place.

`.gitignore` adds `vendor/bundle/`, `.bundle/`, and the auto-generated `fastlane/report.xml` and `fastlane/README.md`.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 (1M context) <noreply@anthropic.com>
A first CI step to prove the pipeline scaffolding works end-to-end:
install gems, fetch the Developer ID cert via fastlane match, run a
universal `make` build with ad-hoc signing, and upload the `.app` zip
as an artifact.

Signing and notarization are intentionally out of scope here so we
can iterate on those next without doubting the plumbing.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
@mokagio mokagio self-assigned this May 9, 2026
mokagio and others added 7 commits May 9, 2026 17:03
`make release` now produces both a notarized+stapled ZIP and a notarized+stapled DMG from a single Developer ID-signed `.app`.

`Tools/notarize.sh` is a generic notarize-and-staple helper that takes any `.app` or `.dmg` path, reads the App Store Connect API key from env (canonical `APP_STORE_CONNECT_API_KEY_{KEY_ID,ISSUER_ID,KEY}` first, falls back to team-suffixed names), submits to `xcrun notarytool --wait`, fetches the log on failure, and staples the artifact in place.
PEM handling matches the platform-imessage script: `printf '%b\n'` to decode `\n` escapes from the env var into real newlines and supply the trailing newline that `notarytool` requires (without it, submissions fail as `invalidPEMDocument`).

The `Makefile` change moves WPCOM OAuth secret injection out of the `all` target and into the `$(APP_EXECUTABLE_TARGET)` recipe before the codesign step.
The previous `all` recipe re-injected and re-signed on every invocation, which would strip a staple ticket from any subsequently notarized `.app`.
With the injection inside the file recipe, `make all` becomes a phony alias that only rebuilds when sources change, leaving stapled bundles intact across iterative invocations.

New targets:
- `notarize-app` — submits the `.app`, staples in place.
- `zip` — `ditto` packages the (stapled) `.app` to `$(ZIP_PATH)`.
- `notarize-dmg` — submits the codesigned DMG, staples in place.
- `release` — full chain: ZIP and DMG built from one signed and notarized `.app`.

The old `notarize` target (which used `--keychain-profile`, local-only) is removed; the new flow uses an API key and works the same locally and in CI.

Verified end-to-end on this machine with team `PZYM8XX95Q`:
- ZIP: `Notarized Developer ID` per `spctl --assess`, inner `.app` stapled.
- DMG: `Notarized Developer ID` per `spctl --assess --type install`, stapled.

Requires `fileicon` on the build host for the `dmg` target's Applications-folder icon step (`brew install fileicon`).

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 (1M context) <noreply@anthropic.com>
`install_gems` in CI relies on `BUNDLE_PATH=vendor/bundle` so the
toolkit's gem cache restore puts gems where bundler expects them.
The previous `.gitignore` swallowed all of `.bundle/`, so the config
never reached CI and gems would install into the system path.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
Now that the local `make release` chain produces signed and notarized
`.zip` and `.dmg` artifacts, point the Buildkite Build step at it
instead of the ad-hoc smoke build. Runs on every build for now;
PR/branch filtering can come once the secrets and timing are settled.

Adds a defensive `brew install create-dmg fileicon` so the agent has
the tools the `dmg` recipe needs, regardless of base image state.

Build agent must provide: `MATCH_S3_ACCESS_KEY`,
`MATCH_S3_SECRET_ACCESS_KEY`, `MATCH_PASSWORD`,
`APP_STORE_CONNECT_API_KEY_KEY_ID`,
`APP_STORE_CONNECT_API_KEY_ISSUER_ID`,
`APP_STORE_CONNECT_API_KEY_KEY`, and `WPCOM_OAUTH_CLIENT_SECRET`.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
The Makefile `dmg` recipe runs `osascript` to ask Finder to create
an Applications alias inside the staging folder. On a headless
Buildkite agent there's no Finder session, the AppleEvent times out
after ~120s, and the build fails after the (already successful)
notarization+zip of the `.app`.

Switch the CI invocation to `make zip`, which still notarizes and
staples the `.app` and produces the same `.zip` artifact. DMG
support on CI can come later, alongside a Makefile change to drop
the AppleScript step in favor of a plain `ln -s /Applications`.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
`APP_NAME="WP Workspace"` produced `build/WP Workspace.zip` containing
`WP Workspace.app`, which is awkward to handle in shells, URLs, and
download paths. CI overrides `APP_NAME=WPWorkspace` so the bundle and
zip on disk are space-free; `PRODUCT_NAME` stays "WP Workspace" so
the user-visible app name (CFBundleDisplayName, menu bar) is unchanged.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
Hyphen reads better than the run-together `WPWorkspace` while still
keeping shell paths and download URLs space-free.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
Pushes a `Build, sign, notarize` commit status to GitHub from the
Build step so the outcome is visible on the PR checks UI rather than
only inside Buildkite.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
Comment thread .buildkite/commands/build.sh Outdated
Comment on lines +8 to +9
echo "--- :package: Install brew dependencies"
brew install create-dmg fileicon
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently unused. See code comment below before the make zip call.

@artpi we have other apps that do DMG so we should be to sort it out, but wanted to check with you first before going ahead on whether that's a distribution method you want.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dmg would be perfect!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds initial Buildkite CI scaffolding for building/package-signing the macOS app, along with supporting Makefile/Fastlane tooling for code signing and notarization.

Changes:

  • Add a Buildkite pipeline + build command to run a CI build and upload a ZIP artifact.
  • Introduce a notarization helper script and new Makefile targets for notarizing/zipping (and a fuller “release” flow).
  • Add Bundler/Fastlane setup (Gemfile/lock, Fastfile) and CI-related config (.xcode-version, bundler config, gitignore).

Reviewed changes

Copilot reviewed 9 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Tools/notarize.sh Adds a script to submit/staple notarization via notarytool.
Makefile Adds paths/targets for notarize/zip/release and updates DMG handling.
Gemfile Adds Bundler dependencies for Fastlane + release toolkit plugin.
Gemfile.lock Locks Ruby dependencies for CI/release tooling.
fastlane/Fastfile Adds lane to fetch Developer ID cert via match (S3-backed).
.xcode-version Declares Xcode version used to select the CI image.
.gitignore Ignores bundler artifacts and Fastlane generated outputs.
.bundle/config Configures Bundler install path to vendor/bundle.
.buildkite/shared-pipeline-vars Exports IMAGE_ID/plugin version used for pipeline upload interpolation.
.buildkite/pipeline.yml Defines the Buildkite step to run the build and publish artifacts.
.buildkite/commands/build.sh CI command script: install gems, set up signing, run make zip.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .buildkite/pipeline.yml
Comment thread .buildkite/commands/build.sh Outdated
The `APP_STORE_CONNECT_API_KEY_<TEAM>_*` fallback chain existed for the
hypothetical case of a shell holding API keys for multiple Apple Dev
teams at once. CI never hits it and devs rarely need it; meanwhile it
adds extra branches, docs, and an indirect message about how creds are
managed that isn't actually true here. Stick to the canonical names.

Removes the fallback resolution, the corresponding `--team-id` flag
that only existed to drive it, and the related docstring sections.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
@artpi
Copy link
Copy Markdown
Contributor

artpi commented May 11, 2026

@mokagio The built artifact works perfectly and if it can package .dmg it would be perfect.

mokagio and others added 4 commits May 12, 2026 12:00
…-sign-notarize-and-track-as

Conflict in `Makefile` resolved by keeping HEAD's structure (secret
injection + codesign live inside `$(APP_EXECUTABLE_TARGET)` so re-running
`make all` doesn't strip notarization staples) and porting main's new
`WPCOM_OAUTH_CLIENT_ID` injection into the same recipe block.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`create-dmg` runs `osascript` against Finder to set the window
background and position icons on the mounted DMG. The headless
Buildkite Apple Silicon agents have no Finder session, so the
AppleEvent times out after ~120s and the build dies (which is why
6e8c9bf disabled the step in CI).

`appdmg` writes `.DS_Store` directly via `hdiutil` plus the `ds-store`
library, so it works on a headless agent. Matches what Studio does in
`apps/studio` for the same reason. The Applications shortcut is now a
plain `/Applications` symlink rather than a Finder alias file.

CI runs `make release` again, capturing both the `.zip` (stapled
`.app` inside) and the signed+notarized+stapled `.dmg`.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
Build #43 failed with `/bin/sh: npx: command not found`. The
`xcode-26.4.1` image doesn't ship Node, so install it on demand
before `make release` reaches the `dmg` target. `brew install` is
idempotent — a no-op when node is already present, so future image
revisions that include Node out of the box won't pay the cost.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.7 <noreply@anthropic.com>
The previous `brew install node` step pulled the full Homebrew node bottle
on every build (~1 min on a clean image) and silently drifted with whatever
version Homebrew happened to ship.
Switching to `automattic/nvm#0.6.0` reads `.nvmrc` and reuses a cached nvm
install, so the version is pinned and reproducible across CI and local
machines that already run nvm.

`24.15.0` is the current Node LTS (Krypton).

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mokagio mokagio marked this pull request as ready for review May 12, 2026 23:29
Copilot AI and others added 2 commits May 13, 2026 05:12
Comment thread Makefile
@cp $(MENU_BAR_LOGO) "$(RESOURCES)/"
@rm -rf "$(RESOURCES)/Fonts"
@cp -R Resources/Fonts "$(RESOURCES)/Fonts"
@secret="$${WPCOM_OAUTH_CLIENT_SECRET:-}"; \
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe good to validate WPCOM_OAUTH_CLIENT_SECRET isn't empty just in case? I think the way it is, it might generate artifacts with a missing key.

Copy link
Copy Markdown
Contributor

@artpi artpi May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont know how to check it but I downloaded artifact 50, re-signed-in in the app and its seems to be using proper oauth secret and app id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I meant just to make sure this var isn't empty / inexistent due to a mistake in the setup, as a safety net.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — you were right, the recipe wrote whatever $secret ended up being, even if empty, and the inline check in manual-release.sh had a latent bug where plutil's trailing newline made wc -c return 1 for the empty case so it never tripped.

Addressed in #40 — extracted the post-build verification into a shared Tools/verify-oauth-secret.sh and wired it into notarize-app, so make release (CI) and manual-release.sh both run the same check. Thanks for raising it!

Posted by Claude (Opus 4.7) on behalf of @mokagio with approval.

Comment thread .buildkite/pipeline.yml
IMAGE_ID: $IMAGE_ID

steps:
- label: ":package: Build, sign, notarize"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step will then sign/notarizes on PR builds, right?

In theory, an author with write access (or a contributor PR allowed by us) could modify .buildkite/shared-pipeline-vars or build.sh and access release secrets... unlikely, but maybe worth it to cover it? 🤔

Perhaps a safer strategy would be to run signing / notarization using trusted refs only (trunk, tags or manually triggered release builds in a separate pipeline), then make PRs run an unsigned/no-secret build?

Comment thread Makefile
# .DS_Store layout directly via `hdiutil` + the `ds-store` library, no AppleScript
# or Finder session required. That matters on headless CI agents, where any tool
# that drives Finder via osascript times out after ~120s.
dmg: $(DMG_PATH)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh won't this break if there are spaces in APP_NAME? The default is WP Workspace Dev but we use WP-Workspace in CI (which should work).

Copy link
Copy Markdown
Contributor

@artpi artpi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked build 50 and its working well!

https://buildkite.com/automattic/workspace/builds/50/

@mokagio mokagio merged commit f60055a into main May 14, 2026
1 check passed
@mokagio mokagio deleted the ainfra-2373-workspace-app-buildkite-to-build-sign-notarize-and-track-as branch May 14, 2026 04:32
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.

5 participants