From dd64ee4b7dc6e00586c68be8c708a29186da409b Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 12:35:11 +1000 Subject: [PATCH 01/21] Scaffold match cert delivery for the desktop app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the `set_up_signing` fastlane lane (match, readonly, `developer_id`, team `PZYM8XX95Q`, S3 `a8c-fastlane-match`) under `apps/desktop`, so a build agent fetches the Developer ID Application cert into its keychain before electron-builder runs. Cert delivery is `match` for every Automattic signing path, electron-builder included. Locked against Ruby 3.3.4 (the agent ceiling) with fastlane `2.236`, which re-bundles `multi_json` — so the earlier `<= 2.235` workaround gem is unneeded. Verified by running `bundle exec fastlane set_up_signing`: it pulled the match repo from S3 and installed the real Developer ID cert. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/.bundle/config | 4 + apps/desktop/Gemfile | 6 + apps/desktop/Gemfile.lock | 303 +++++++++++++++++++++++++++++++ apps/desktop/fastlane/.gitignore | 2 + apps/desktop/fastlane/Fastfile | 52 ++++++ 5 files changed, 367 insertions(+) create mode 100644 apps/desktop/.bundle/config create mode 100644 apps/desktop/Gemfile create mode 100644 apps/desktop/Gemfile.lock create mode 100644 apps/desktop/fastlane/.gitignore create mode 100644 apps/desktop/fastlane/Fastfile diff --git a/apps/desktop/.bundle/config b/apps/desktop/.bundle/config new file mode 100644 index 00000000..54298662 --- /dev/null +++ b/apps/desktop/.bundle/config @@ -0,0 +1,4 @@ +--- +BUNDLE_PATH: "vendor/bundle" +BUNDLE_SPECIFIC_PLATFORM: "false" +BUNDLE_FORCE_RUBY_PLATFORM: "true" diff --git a/apps/desktop/Gemfile b/apps/desktop/Gemfile new file mode 100644 index 00000000..29ed5e0e --- /dev/null +++ b/apps/desktop/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'fastlane', '~> 2.236' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 14.6' diff --git a/apps/desktop/Gemfile.lock b/apps/desktop/Gemfile.lock new file mode 100644 index 00000000..99fbe5da --- /dev/null +++ b/apps/desktop/Gemfile.lock @@ -0,0 +1,303 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1259.0) + aws-sdk-core (3.251.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.129.0) + aws-sdk-core (~> 3, >= 3.248.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.225.0) + aws-sdk-core (~> 3, >= 3.248.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + buildkit (1.6.1) + sawyer (>= 0.6) + chroma (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.5) + declarative (0.0.20) + diffy (3.4.4) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + erubi (1.13.1) + excon (0.112.0) + faraday (1.10.5) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.1) + fastlane (2.236.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-wpmreleasetoolkit (14.6.0) + buildkit (~> 1.5) + chroma (= 0.2.0) + diffy (~> 3.3) + dotenv (~> 2.8) + fastlane (~> 2.231) + gettext (~> 3.5) + git (~> 1.3) + google-cloud-storage (~> 1.31) + java-properties (~> 0.3.0) + nokogiri (~> 1.19, >= 1.19.3) + octokit (~> 6.1) + parallel (~> 1.14) + plist (~> 3.1) + progress_bar (~> 1.3) + rake (>= 12.3, < 14.0) + rake-compiler (~> 1.0) + xcodeproj (~> 1.22) + fastlane-sirp (1.1.0) + fiddle (1.1.8) + forwardable (1.4.0) + gettext (3.5.2) + erubi + locale (>= 2.0.5) + prime + racc + text (>= 1.3.0) + gh_inspector (1.1.3) + git (1.19.1) + addressable (~> 2.8) + rchardet (~> 1.8) + google-apis-androidpublisher_v3 (0.102.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) + mini_mime (~> 1.0) + mutex_m + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + google-apis-iamcredentials_v1 (0.27.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.63.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.6.0) + google-cloud-storage (1.60.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.0) + googleauth (1.17.0) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + java-properties (0.3.0) + jmespath (1.6.2) + json (2.19.8) + jwt (3.2.0) + base64 + locale (2.1.5) + fiddle + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + mini_portile2 (2.8.9) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + nokogiri (1.19.3) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + octokit (6.1.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + options (2.3.2) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + parallel (1.28.0) + plist (3.7.2) + prime (0.1.4) + forwardable + singleton + progress_bar (1.3.4) + highline (>= 1.6) + options (~> 2.3.0) + pstore (0.2.1) + public_suffix (7.0.5) + racc (1.8.1) + rake (13.4.2) + rake-compiler (1.3.1) + rake + rchardet (1.10.2) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.8.0) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + sawyer (0.9.3) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + security (0.1.5) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + singleton (0.3.0) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + text (1.3.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane (~> 2.236) + fastlane-plugin-wpmreleasetoolkit (~> 14.6) + +BUNDLED WITH + 2.5.11 diff --git a/apps/desktop/fastlane/.gitignore b/apps/desktop/fastlane/.gitignore new file mode 100644 index 00000000..56e5e3de --- /dev/null +++ b/apps/desktop/fastlane/.gitignore @@ -0,0 +1,2 @@ +README.md +report.xml diff --git a/apps/desktop/fastlane/Fastfile b/apps/desktop/fastlane/Fastfile new file mode 100644 index 00000000..fecc1c29 --- /dev/null +++ b/apps/desktop/fastlane/Fastfile @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? + +APPLE_TEAM_ID = 'PZYM8XX95Q' + +CODE_SIGNING_STORAGE_OPTIONS = { + storage_mode: 's3', + s3_bucket: 'a8c-fastlane-match', + s3_region: 'us-east-2' +}.freeze + +CODE_SIGNING_ENV_VARS = %w[ + MATCH_S3_ACCESS_KEY + MATCH_S3_SECRET_ACCESS_KEY + MATCH_PASSWORD +].freeze + +ASC_API_KEY_ENV_VARS = %w[ + APP_STORE_CONNECT_API_KEY_KEY_ID + APP_STORE_CONNECT_API_KEY_ISSUER_ID + APP_STORE_CONNECT_API_KEY_KEY +].freeze + +require 'fastlane/plugin/wpmreleasetoolkit' + +EnvManager = Fastlane::Wpmreleasetoolkit::EnvManager + +before_all do + # Sets up a temporary keychain so match works in CI. No-op locally. + setup_ci + + # `set_up` is needed even with no `.env` file so EnvManager has a configured + # instance; on CI the vars come from the Buildkite agent. + EnvManager.set_up(env_file_name: 'cortext-desktop.env') +end + +desc 'Fetch the Developer ID Application certificate into the keychain' +lane :set_up_signing do |readonly: true| + CODE_SIGNING_ENV_VARS.each { |k| EnvManager.get_required_env!(k) } + ASC_API_KEY_ENV_VARS.each { |k| EnvManager.get_required_env!(k) } unless readonly + + sync_code_signing( + type: 'developer_id', + platform: 'macos', + team_id: APPLE_TEAM_ID, + app_identifier: [], + api_key: readonly ? nil : app_store_connect_api_key, + readonly: readonly, + **CODE_SIGNING_STORAGE_OPTIONS + ) +end From 61f0f8661604414aa7d99b268ba4e81cbb5460f7 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 12:35:58 +1000 Subject: [PATCH 02/21] Sign and notarize the desktop DMG with Developer ID Replaces `mac.identity: null` with the Automattic Developer ID Application identity, enables hardened runtime, and turns on electron-builder's built-in notarization (`mac.notarize.teamId`, driven by `APPLE_API_*` at build time). The bundled `runtime/bin/php` is listed in `mac.binaries`: electron-builder does not auto-sign Mach-O files dropped into `Contents/Resources` via `extraResources`, so an unsigned `php` would otherwise fail notarization. Entitlements grant only what hardened runtime needs here: JIT and unsigned-executable-memory for Electron's V8, and disabled library validation so the app can spawn the separately-signed bundled `php`. They live in `signing/` because the repo gitignores `build/`. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/package.json | 13 +++++++++++-- apps/desktop/signing/entitlements.mac.plist | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/signing/entitlements.mac.plist diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8390ddd1..aa24eb94 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -54,9 +54,18 @@ "arch": "arm64" } ], - "identity": null, + "identity": "Developer ID Application: Automattic, Inc. (PZYM8XX95Q)", "category": "public.app-category.productivity", - "minimumSystemVersion": "12.0.0" + "minimumSystemVersion": "12.0.0", + "hardenedRuntime": true, + "entitlements": "signing/entitlements.mac.plist", + "entitlementsInherit": "signing/entitlements.mac.plist", + "binaries": [ + "Contents/Resources/runtime/bin/php" + ], + "notarize": { + "teamId": "PZYM8XX95Q" + } } } } diff --git a/apps/desktop/signing/entitlements.mac.plist b/apps/desktop/signing/entitlements.mac.plist new file mode 100644 index 00000000..b62aca4a --- /dev/null +++ b/apps/desktop/signing/entitlements.mac.plist @@ -0,0 +1,15 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + + com.apple.security.cs.disable-library-validation + + + From b51f3f03ca4e881982beded818159a426bcd732e Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 12:36:09 +1000 Subject: [PATCH 03/21] Add Buildkite pipeline to build the signed desktop DMG Moves the release build to the Buildkite macOS queue, where the signing secrets live (GitHub Actions cannot reach them). Mirrors the GHA `release-desktop.yml` build sequence, then runs `set_up_signing` (match) before `npm run dist` so electron-builder signs from the keychain cert and notarizes, and verifies the result with `codesign` + `stapler`. The existing GHA workflow stays as the unsigned/dry path; cutting the release over to this pipeline is a separate human decision. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/commands/release-desktop.sh | 72 ++++++++++++++++++++++++++ .buildkite/pipeline.yml | 23 ++++++++ 2 files changed, 95 insertions(+) create mode 100755 .buildkite/commands/release-desktop.sh create mode 100644 .buildkite/pipeline.yml diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh new file mode 100755 index 00000000..0facf5c9 --- /dev/null +++ b/.buildkite/commands/release-desktop.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Build, sign, and notarize the Cortext desktop DMG on the macOS signing queue. +# Mirrors .github/workflows/release-desktop.yml; the release build moves here +# because GitHub Actions cannot reach the signing secrets. + +# Only v* tag builds publish to the GitHub Release. Other builds still produce a +# signed+notarized DMG, uploaded as a Buildkite artifact. +if [[ "${BUILDKITE_TAG:-}" == v* ]]; then + version="${BUILDKITE_TAG#v}" + publish=true +else + base_version="$(python3 -c 'import json; print(json.load(open("apps/desktop/package.json"))["version"])')" + version="${base_version}-${BUILDKITE_COMMIT:0:7}" + publish=false +fi + +echo "--- :package: install JS + PHP dependencies" +corepack enable +corepack prepare "pnpm@$(node -p "require('./package.json').packageManager.split('@')[1]")" --activate +composer install --no-dev --optimize-autoloader --no-interaction +pnpm install --frozen-lockfile +npm --prefix apps/desktop ci + +# Electron 42 skips the binary download during `npm ci`; fetch it so +# electron-builder packages a complete app. +( cd apps/desktop && npx install-electron ) + +echo "--- :php: build bundled arm64 PHP runtime" +npm --prefix apps/desktop run runtime:php + +echo "--- :card_index_dividers: build distribution snapshot" +CORTEXT_DESKTOP_DISTRIBUTION=1 npm --prefix apps/desktop run snapshot + +echo "--- :key: install Developer ID cert into the agent keychain" +( cd apps/desktop && install_gems && bundle exec fastlane set_up_signing ) + +echo "--- :apple: build, sign, notarize DMG" +# electron-builder signs from the match-installed keychain cert (mac.identity) +# and notarizes via its built-in @electron/notarize, driven by APPLE_API_*. +# APPLE_API_KEY must be a path to the .p8, so materialize the key the agent +# carries as APP_STORE_CONNECT_API_KEY_KEY into a temp file. +apple_api_key_path="$(mktemp -t cortext_asc).p8" +trap 'rm -f "$apple_api_key_path"' EXIT +printf '%s' "$APP_STORE_CONNECT_API_KEY_KEY" > "$apple_api_key_path" +export APPLE_API_KEY="$apple_api_key_path" +export APPLE_API_KEY_ID="$APP_STORE_CONNECT_API_KEY_KEY_ID" +export APPLE_API_ISSUER="$APP_STORE_CONNECT_API_KEY_ISSUER_ID" + +npm --prefix apps/desktop run dist -- -c.extraMetadata.version="$version" + +echo "--- :white_check_mark: verify signature + notarization" +dmg="$(ls apps/desktop/dist/*.dmg)" +codesign --verify --strict --verbose=2 "$dmg" +xcrun stapler validate "$dmg" + +if ! "$publish"; then + echo "--- :information_source: no v* tag; signed DMG stashed as a Buildkite artifact" + exit 0 +fi + +echo "--- :rocket: attach DMG to draft GitHub Release" +if ! gh release view "$version" --repo Automattic/cortext >/dev/null 2>&1; then + gh release create "$version" \ + --repo Automattic/cortext \ + --draft \ + --title "Cortext $version" \ + --notes "Cortext $version" +fi +gh release upload "$version" "$dmg" --repo Automattic/cortext --clobber diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml new file mode 100644 index 00000000..95a3e792 --- /dev/null +++ b/.buildkite/pipeline.yml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +agents: + queue: mac + +env: + IMAGE_ID: $IMAGE_ID + +steps: + # Build, sign, and notarize the desktop DMG. Runs on every build; only a v* + # tag publishes to the GitHub Release. The signed DMG is always a Buildkite + # artifact for download/testing. + - label: ":apple: Build, sign, notarize desktop DMG" + key: release-desktop + command: .buildkite/commands/release-desktop.sh + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + artifact_paths: + - "apps/desktop/dist/*.dmg" + - "apps/desktop/dist/*.dmg.blockmap" + notify: + - github_commit_status: + context: Build, Sign & Notarize Desktop From e0ad9c05a2fa85b08174970d9085218f68a0d2fc Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 14:54:52 +1000 Subject: [PATCH 04/21] Tighten verbose AI comments Co-authored-by: Gio Lodi --- .buildkite/commands/release-desktop.sh | 7 ++----- .buildkite/pipeline.yml | 3 --- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index 0facf5c9..0c9ce3ab 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -2,12 +2,9 @@ set -euo pipefail -# Build, sign, and notarize the Cortext desktop DMG on the macOS signing queue. -# Mirrors .github/workflows/release-desktop.yml; the release build moves here -# because GitHub Actions cannot reach the signing secrets. +# Build, sign, and notarize the Cortext desktop DMG. -# Only v* tag builds publish to the GitHub Release. Other builds still produce a -# signed+notarized DMG, uploaded as a Buildkite artifact. +# Only v* tag builds publish to the GitHub Release. if [[ "${BUILDKITE_TAG:-}" == v* ]]; then version="${BUILDKITE_TAG#v}" publish=true diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 95a3e792..3a44735b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -8,9 +8,6 @@ env: IMAGE_ID: $IMAGE_ID steps: - # Build, sign, and notarize the desktop DMG. Runs on every build; only a v* - # tag publishes to the GitHub Release. The signed DMG is always a Buildkite - # artifact for download/testing. - label: ":apple: Build, sign, notarize desktop DMG" key: release-desktop command: .buildkite/commands/release-desktop.sh From 4f8ad519ac5a55004394d784a394991059a12af5 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 15:05:39 +1000 Subject: [PATCH 05/21] Add Buildkite agent and plugin config `shared-pipeline-vars` defines `CI_TOOLKIT_PLUGIN`, `NVM_PLUGIN`, and `IMAGE_ID`, which `pipeline.yml` references; without it the plugin entries render blank and the pipeline upload is rejected. `.xcode-version`/`.nvmrc`/`.ruby-version` pin the agent toolchain. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/shared-pipeline-vars | 11 +++++++++++ .nvmrc | 1 + .ruby-version | 1 + .xcode-version | 1 + 4 files changed, 14 insertions(+) create mode 100755 .buildkite/shared-pipeline-vars create mode 100644 .nvmrc create mode 100644 .ruby-version create mode 100644 .xcode-version diff --git a/.buildkite/shared-pipeline-vars b/.buildkite/shared-pipeline-vars new file mode 100755 index 00000000..e4d87ca4 --- /dev/null +++ b/.buildkite/shared-pipeline-vars @@ -0,0 +1,11 @@ +#!/bin/sh + +# Sourced by the pipeline-upload step so these values land in the rendered pipeline.yml. + +CI_TOOLKIT_PLUGIN_VERSION='6.0.1' +NVM_PLUGIN_VERSION='0.6.0' +XCODE_VERSION=$(sed -E 's/^~> ?//' .xcode-version) + +export IMAGE_ID="xcode-$XCODE_VERSION" +export CI_TOOLKIT_PLUGIN="automattic/a8c-ci-toolkit#$CI_TOOLKIT_PLUGIN_VERSION" +export NVM_PLUGIN="automattic/nvm#$NVM_PLUGIN_VERSION" diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a2e33f6e --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.15 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..a0891f56 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.4 diff --git a/.xcode-version b/.xcode-version new file mode 100644 index 00000000..59c8fb19 --- /dev/null +++ b/.xcode-version @@ -0,0 +1 @@ +26.3 From c58fb2bb517c0c8b63d7c1e66750efd89c445301 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 15:16:44 +1000 Subject: [PATCH 06/21] Read fastlane team id from electron-builder config electron-builder's `mac.notarize.teamId` is the source of truth for the team. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/fastlane/Fastfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/fastlane/Fastfile b/apps/desktop/fastlane/Fastfile index fecc1c29..56fdd64d 100644 --- a/apps/desktop/fastlane/Fastfile +++ b/apps/desktop/fastlane/Fastfile @@ -1,8 +1,10 @@ # frozen_string_literal: true +require 'json' + UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? -APPLE_TEAM_ID = 'PZYM8XX95Q' +APPLE_TEAM_ID = JSON.parse(File.read(File.expand_path('../package.json', __dir__))).dig('build', 'mac', 'notarize', 'teamId') CODE_SIGNING_STORAGE_OPTIONS = { storage_mode: 's3', From 07b2f4db5ba162da63ad1f4d977ea4c73a02d388 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 15:40:19 +1000 Subject: [PATCH 07/21] Install composer on the agent before the build The xcode-* image has no composer; without it the build dies at status 127. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/commands/release-desktop.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index 0c9ce3ab..5f9d2f3a 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -17,6 +17,8 @@ fi echo "--- :package: install JS + PHP dependencies" corepack enable corepack prepare "pnpm@$(node -p "require('./package.json').packageManager.split('@')[1]")" --activate +# The xcode-* agent image ships no composer; brew pulls php in as a dependency. +command -v composer >/dev/null || brew install composer composer install --no-dev --optimize-autoloader --no-interaction pnpm install --frozen-lockfile npm --prefix apps/desktop ci From cacc933bdefe1b2868b19d35130c01742e68adfe Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 15:47:28 +1000 Subject: [PATCH 08/21] Install static-php-cli build toolchain on the agent `spc` source-builds libraries without a pre-built binary (libxml2 hit `cmake` not found); the minimal agent image carries none of the toolchain. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/commands/release-desktop.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index 5f9d2f3a..63f986d7 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -27,6 +27,12 @@ npm --prefix apps/desktop ci # electron-builder packages a complete app. ( cd apps/desktop && npx install-electron ) +echo "--- :hammer_and_wrench: install static-php-cli build toolchain" +# static-php-cli source-builds any library without a pre-built binary (e.g. +# libxml2), which the minimal agent image can't do unaided. +brew install cmake autoconf automake libtool bison re2c +export PATH="$(brew --prefix bison)/bin:$PATH" + echo "--- :php: build bundled arm64 PHP runtime" npm --prefix apps/desktop run runtime:php From 9579a244b7e3bef33143b76720130a0e2c2e2d12 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 15:56:49 +1000 Subject: [PATCH 09/21] Set mac.notarize to a boolean This electron-builder version rejects the `{ teamId }` object form; it takes a boolean and reads the credentials from `APPLE_API_*` at build time. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index aa24eb94..377e6f43 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -63,9 +63,7 @@ "binaries": [ "Contents/Resources/runtime/bin/php" ], - "notarize": { - "teamId": "PZYM8XX95Q" - } + "notarize": true } } } From b113fb0c3e0b82fa0eb73db0b77e1fcd673925a9 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 15:56:50 +1000 Subject: [PATCH 10/21] Decode literal newlines when writing the notarization key `APPLE_API_KEY` must point at a valid PEM; the secret stores it with newlines as literal `\n`. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/commands/release-desktop.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index 63f986d7..c02ab029 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -49,7 +49,9 @@ echo "--- :apple: build, sign, notarize DMG" # carries as APP_STORE_CONNECT_API_KEY_KEY into a temp file. apple_api_key_path="$(mktemp -t cortext_asc).p8" trap 'rm -f "$apple_api_key_path"' EXIT -printf '%s' "$APP_STORE_CONNECT_API_KEY_KEY" > "$apple_api_key_path" +# The secret stores the .p8 with newlines as literal \n; %b turns them back into +# real newlines so the file is a valid PEM (a no-op if they are already real). +printf '%b' "$APP_STORE_CONNECT_API_KEY_KEY" > "$apple_api_key_path" export APPLE_API_KEY="$apple_api_key_path" export APPLE_API_KEY_ID="$APP_STORE_CONNECT_API_KEY_KEY_ID" export APPLE_API_ISSUER="$APP_STORE_CONNECT_API_KEY_ISSUER_ID" From 339a5731fa530ffd39b8b3d46c0300924890166a Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 16:06:53 +1000 Subject: [PATCH 11/21] Read team id from the signing identity, not notarize `mac.notarize` is now a boolean, so it no longer carries `teamId`; the identity string is the field in electron-builder's config that still embeds it. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/fastlane/Fastfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/fastlane/Fastfile b/apps/desktop/fastlane/Fastfile index 56fdd64d..0daba91e 100644 --- a/apps/desktop/fastlane/Fastfile +++ b/apps/desktop/fastlane/Fastfile @@ -4,7 +4,8 @@ require 'json' UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? -APPLE_TEAM_ID = JSON.parse(File.read(File.expand_path('../package.json', __dir__))).dig('build', 'mac', 'notarize', 'teamId') +# The team id is the trailing `(XXXXXXXXXX)` of electron-builder's signing identity. +APPLE_TEAM_ID = JSON.parse(File.read(File.expand_path('../package.json', __dir__))).dig('build', 'mac', 'identity')[/\(([A-Z0-9]+)\)\z/, 1] CODE_SIGNING_STORAGE_OPTIONS = { storage_mode: 's3', From 6489ee2aff9a91caa89b7f5cd976d7ba9c54ef03 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 16:14:35 +1000 Subject: [PATCH 12/21] Drop the cert-type prefix from mac.identity electron-builder picks the certificate type itself and rejects an identity that carries the `Developer ID Application:` prefix. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 377e6f43..d0333fd8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -54,7 +54,7 @@ "arch": "arm64" } ], - "identity": "Developer ID Application: Automattic, Inc. (PZYM8XX95Q)", + "identity": "Automattic, Inc. (PZYM8XX95Q)", "category": "public.app-category.productivity", "minimumSystemVersion": "12.0.0", "hardenedRuntime": true, From 2d39f8c2fe53992807dfd7a934ee3a197ac866d1 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 10 Jun 2026 17:07:50 +1000 Subject: [PATCH 13/21] Verify the notarized app, not the unsigned dmg electron-builder signs, notarizes and staples `Cortext.app`, then wraps it in an unsigned `.dmg`; verifying the dmg's signature failed the build even though the app was correctly notarized. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .buildkite/commands/release-desktop.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index c02ab029..333ac49d 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -60,8 +60,12 @@ npm --prefix apps/desktop run dist -- -c.extraMetadata.version="$version" echo "--- :white_check_mark: verify signature + notarization" dmg="$(ls apps/desktop/dist/*.dmg)" -codesign --verify --strict --verbose=2 "$dmg" -xcrun stapler validate "$dmg" +# electron-builder signs, notarizes and staples the .app, then wraps it in an +# unsigned .dmg — so the notarized artifact to verify is the app, not the dmg. +app="apps/desktop/dist/mac-arm64/Cortext.app" +codesign --verify --strict --deep --verbose=2 "$app" +spctl --assess --type exec --verbose=2 "$app" +xcrun stapler validate "$app" if ! "$publish"; then echo "--- :information_source: no v* tag; signed DMG stashed as a Buildkite artifact" From 706431b9ef897888b5558348f31809dcf8846fdc Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 11 Jun 2026 15:42:10 +1000 Subject: [PATCH 14/21] Fail on unexpectedly finding multiple DMGs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .buildkite/commands/release-desktop.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index 333ac49d..6c0175a4 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -59,7 +59,9 @@ export APPLE_API_ISSUER="$APP_STORE_CONNECT_API_KEY_ISSUER_ID" npm --prefix apps/desktop run dist -- -c.extraMetadata.version="$version" echo "--- :white_check_mark: verify signature + notarization" -dmg="$(ls apps/desktop/dist/*.dmg)" +dmg=(apps/desktop/dist/*.dmg) +[[ ${#dmg[@]} -eq 1 ]] || { echo "Expected exactly one DMG, found ${#dmg[@]}"; exit 1; } +dmg="${dmg[0]}" # electron-builder signs, notarizes and staples the .app, then wraps it in an # unsigned .dmg — so the notarized artifact to verify is the app, not the dmg. app="apps/desktop/dist/mac-arm64/Cortext.app" From 3c83d786143c37420f5022650bcb2069e36acc13 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Thu, 11 Jun 2026 15:43:14 +1000 Subject: [PATCH 15/21] Fail if team ID cannot be computed from JSON Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/desktop/fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/fastlane/Fastfile b/apps/desktop/fastlane/Fastfile index 0daba91e..1e842ce1 100644 --- a/apps/desktop/fastlane/Fastfile +++ b/apps/desktop/fastlane/Fastfile @@ -6,7 +6,7 @@ UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Hel # The team id is the trailing `(XXXXXXXXXX)` of electron-builder's signing identity. APPLE_TEAM_ID = JSON.parse(File.read(File.expand_path('../package.json', __dir__))).dig('build', 'mac', 'identity')[/\(([A-Z0-9]+)\)\z/, 1] - +UI.user_error!("Couldn't parse Apple Team ID from build.mac.identity in apps/desktop/package.json") unless APPLE_TEAM_ID CODE_SIGNING_STORAGE_OPTIONS = { storage_mode: 's3', s3_bucket: 'a8c-fastlane-match', From b5bf5d214cf43e7c2ec2fe058566b8a2ca969cd6 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 12 Jun 2026 10:10:47 +1000 Subject: [PATCH 16/21] Accept plain semver release tags The desktop release workflow should publish for the repo's existing tag convention, which omits the leading v. Keep accepting v-prefixed tags so existing release inputs remain compatible. --- Generated with the help of Codex, https://openai.com/codex Co-Authored-By: Codex GPT-5 --- .buildkite/commands/release-desktop.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index 6c0175a4..8c3a091d 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -4,8 +4,8 @@ set -euo pipefail # Build, sign, and notarize the Cortext desktop DMG. -# Only v* tag builds publish to the GitHub Release. -if [[ "${BUILDKITE_TAG:-}" == v* ]]; then +# Release tag builds publish to the GitHub Release. +if [[ "${BUILDKITE_TAG:-}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then version="${BUILDKITE_TAG#v}" publish=true else @@ -70,7 +70,7 @@ spctl --assess --type exec --verbose=2 "$app" xcrun stapler validate "$app" if ! "$publish"; then - echo "--- :information_source: no v* tag; signed DMG stashed as a Buildkite artifact" + echo "--- :information_source: no release tag; signed DMG stashed as a Buildkite artifact" exit 0 fi From a369ef1a62872b39c270cccf667bb74dc8bc8078 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 12 Jun 2026 10:25:32 +1000 Subject: [PATCH 17/21] Require plain semver release tags The desktop release workflow should match the repo's tag convention exactly. Tags with a leading v should stay on the artifact-only path. --- Generated with the help of Codex, https://openai.com/codex Co-Authored-By: Codex GPT-5 --- .buildkite/commands/release-desktop.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.buildkite/commands/release-desktop.sh b/.buildkite/commands/release-desktop.sh index 8c3a091d..28ca02db 100755 --- a/.buildkite/commands/release-desktop.sh +++ b/.buildkite/commands/release-desktop.sh @@ -5,8 +5,8 @@ set -euo pipefail # Build, sign, and notarize the Cortext desktop DMG. # Release tag builds publish to the GitHub Release. -if [[ "${BUILDKITE_TAG:-}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - version="${BUILDKITE_TAG#v}" +if [[ "${BUILDKITE_TAG:-}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + version="$BUILDKITE_TAG" publish=true else base_version="$(python3 -c 'import json; print(json.load(open("apps/desktop/package.json"))["version"])')" From 3c8e19cb87977e7b897b818e6e4f3d1798b0dc48 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 16 Jun 2026 15:49:23 +1000 Subject: [PATCH 18/21] =?UTF-8?q?Delete=20GHA=20workflow=20for=20macOS=20D?= =?UTF-8?q?MG=20release=20=E2=80=94=20Use=20Buildkite=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-desktop.yml | 136 -------------------------- 1 file changed, 136 deletions(-) delete mode 100644 .github/workflows/release-desktop.yml diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml deleted file mode 100644 index 3b6b78a6..00000000 --- a/.github/workflows/release-desktop.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: Release desktop DMG - -on: - workflow_call: - inputs: - version: - required: true - type: string - prerelease: - required: false - default: true - type: boolean - dry_run: - required: false - default: true - type: boolean - workflow_dispatch: - inputs: - version: - description: 'Version' - required: true - default: '0.1.0' - prerelease: - description: 'Prerelease' - required: true - default: true - type: boolean - dry_run: - description: 'Dry run (uploads files only; no release)' - required: true - default: true - type: boolean - -permissions: - contents: write - -concurrency: - group: cortext-release-desktop-${{ inputs.version }} - cancel-in-progress: true - -jobs: - desktop: - name: Build desktop DMG (macOS arm64) - runs-on: macos-14 - steps: - - uses: actions/checkout@v6.0.2 - - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - with: - version: 11.0.8 - standalone: true - - - uses: actions/setup-node@v6 - with: - node-version: '24.15' - cache: 'pnpm' - - - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 - with: - php-version: '8.1' - tools: composer:v2 - coverage: none - - - uses: actions/cache@v5 - with: - path: ~/.composer/cache - key: composer-${{ hashFiles('composer.lock') }} - - - name: Install PHP dependencies - run: composer install --no-dev --optimize-autoloader --no-interaction - - - name: Install JS dependencies - run: pnpm install --frozen-lockfile - - - name: Install desktop dependencies - working-directory: apps/desktop - run: npm ci - - # Electron 42 skips the binary download during npm ci. Fetch it here - # so electron-builder packages a complete app. - - name: Install Electron binary - working-directory: apps/desktop - run: npx install-electron - - # static-php-cli compiles natively; a macos-14 runner yields arm64. - # The build is the slow part, so cache the result across runs. - - name: Cache bundled PHP runtime - uses: actions/cache@v5 - with: - path: | - apps/desktop/.runtime-cache - apps/desktop/runtime/bin - key: desktop-runtime-php-${{ hashFiles('apps/desktop/scripts/install-runtime.mjs') }} - - - name: Build bundled arm64 PHP - working-directory: apps/desktop - env: - GITHUB_TOKEN: ${{ github.token }} - run: npm run runtime:php - - - name: Cache snapshot downloads (WP core + wp-cli + SQLite plugin) - uses: actions/cache@v5 - with: - path: apps/desktop/.snapshot-cache - key: desktop-snapshot-cache-${{ hashFiles('apps/desktop/scripts/build-snapshot.mjs') }} - - - name: Build distribution snapshot - working-directory: apps/desktop - env: - CORTEXT_DESKTOP_DISTRIBUTION: '1' - run: npm run snapshot - - - name: Build unsigned DMG - working-directory: apps/desktop - env: - VERSION: ${{ inputs.version }} - run: npm run dist -- -c.extraMetadata.version="$VERSION" - - - name: Upload dry-run DMG - if: ${{ inputs.dry_run }} - uses: actions/upload-artifact@v7 - with: - name: cortext-desktop-dmg-${{ inputs.version }}-dry-run - path: apps/desktop/dist/*.dmg - if-no-files-found: error - - # Attach the DMG to the Release for this tag. Only files are set, so - # the plugin workflow stays the owner of the Release title and notes. - - name: Attach DMG to draft GitHub Release - if: ${{ ! inputs.dry_run }} - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 - with: - tag_name: ${{ inputs.version }} - draft: true - prerelease: ${{ inputs.prerelease }} - files: apps/desktop/dist/*.dmg From 5cc6c74e46d122dde65f380cb8d15055154c8519 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 16 Jun 2026 16:00:13 +1000 Subject: [PATCH 19/21] Remove dead desktop release workflow call The desktop reusable workflow was deleted so GitHub Actions should no longer reference it. Buildkite owns the signed DMG release path from the release tag build. --- Generated with the help of Codex CLI, https://openai.com/codex Co-Authored-By: Codex GPT-5 --- .github/workflows/release.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a11b929d..7e9c1a83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,9 +27,8 @@ concurrency: group: cortext-release-${{ inputs.version }} cancel-in-progress: true -# Orchestrator: builds the plugin ZIP and the desktop DMG and lets each attach -# to the same tagged Release. Either child can also be run on its own from the -# Actions tab; the first run creates the draft Release, later runs attach to it. +# GitHub Actions prepares the plugin ZIP and draft Release. Buildkite owns the +# signed desktop DMG and attaches it from the release tag build. jobs: plugin: uses: ./.github/workflows/release-plugin.yml @@ -38,11 +37,3 @@ jobs: prerelease: ${{ inputs.prerelease }} dry_run: ${{ inputs.dry_run }} secrets: inherit - - desktop: - uses: ./.github/workflows/release-desktop.yml - with: - version: ${{ inputs.version }} - prerelease: ${{ inputs.prerelease }} - dry_run: ${{ inputs.dry_run }} - secrets: inherit From 8a592d497524a52b06b5bd69273f5568739d110c Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 16 Jun 2026 16:02:01 +1000 Subject: [PATCH 20/21] Document Buildkite desktop release ownership Release operators need GitHub Actions and Buildkite responsibilities to be explicit now that the desktop DMG has a single publishing path. --- Generated with the help of Codex CLI, https://openai.com/codex Co-Authored-By: Codex GPT-5 --- apps/desktop/README.md | 12 +++++++----- docs/desktop-decisions.md | 10 +++++++++- docs/release.md | 32 ++++++++++++++------------------ 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/apps/desktop/README.md b/apps/desktop/README.md index d330e13d..a951895e 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -127,15 +127,17 @@ through wp-admin. The snapshot disables core, plugin, and theme updates, and each launch refreshes the update-lock mu-plugin in the extracted site. WordPress changes come through new Cortext desktop releases. -For now the build is arm64 only and unsigned. +Release builds are arm64-only, signed, and notarized on Buildkite. Local builds +remain unsigned unless you provide a signing environment. ## Releasing `.github/workflows/release.yml` is the entry point; run it from the Actions tab. -It calls two reusable workflows: `release-plugin.yml` builds the plugin ZIP and -owns the GitHub Release notes, and `release-desktop.yml` builds the arm64 PHP, -builds a distribution snapshot, runs electron-builder, and uploads the DMG. Both -write to the same Release by tag, and you can also run either one on its own. +It builds the plugin ZIP, validates the milestone, writes release notes, and +creates or updates the draft GitHub Release. Buildkite owns the macOS desktop +DMG: the release tag build builds the arm64 PHP runtime, builds the distribution +snapshot, runs electron-builder, signs and notarizes the app, and uploads the +DMG to the same Release. ## Performance diff --git a/docs/desktop-decisions.md b/docs/desktop-decisions.md index da6dc9d5..d720c056 100644 --- a/docs/desktop-decisions.md +++ b/docs/desktop-decisions.md @@ -2,11 +2,19 @@ Running log for desktop-specific runtime and packaging decisions. Keep the detailed benchmark numbers in PR comments or artifacts unless they become stable product guidance. +## 2026-06-16 — Buildkite owns the signed macOS DMG + +**Decision.** GitHub Actions prepares the plugin ZIP, validates the release milestone, writes release notes, and creates or updates the draft GitHub Release. Buildkite is the only macOS DMG publisher: the release tag build builds the bundled PHP runtime and distribution snapshot, runs electron-builder, signs and notarizes the app, verifies the signed app, and uploads the DMG to the same draft Release. + +**Why.** Keeping the desktop artifact in one CI system avoids two automation paths racing to build or upload the same DMG. Buildkite also has the macOS signing and notarization environment needed for the release artifact. + +**Revisit when.** Desktop releases need additional architectures, auto-update artifacts, or a different release owner. + ## 2026-06-02 — Package the desktop app as an unsigned arm64 DMG **Decision.** `apps/desktop` packages to a macOS DMG with electron-builder. `npm --prefix apps/desktop run dist` runs the build against a `build` block in `package.json`: appId `com.automattic.cortext`, product name `Cortext`, a single arm64 `dmg` target, `identity: null` so it is unsigned, and `snapshot.zip` plus `runtime/bin/php` listed as `extraResources`. The output lands in `apps/desktop/dist`. `main.js` reads the bundled snapshot and PHP from `process.resourcesPath` when `app.isPackaged`, and DevTools stay closed in the packaged build. The distribution snapshot is built with `CORTEXT_DESKTOP_DISTRIBUTION=1`, which ships only the autologin mu-plugin and drops the timing and runtime-probe ones; the autologin mu-plugin is now inert unless `CORTEXT_DESKTOP` is defined. -**Release flow.** `release.yml` is a `workflow_dispatch` orchestrator that calls two reusable workflows: `release-plugin.yml` (plugin ZIP, milestone, notes) and `release-desktop.yml` (builds the arm64 PHP, builds the distribution snapshot, runs electron-builder, attaches the DMG). Both write to the same Release by tag, so the first run creates the draft and later runs add to it. Each can also run on its own from the Actions tab. The shipped app checks GitHub Releases on launch and links to the download when a newer version exists; installing updates in place is not done, since that needs a signed app and Squirrel.Mac. +**Release flow.** This original GitHub Actions desktop release flow was superseded by the 2026-06-16 Buildkite release flow. The shipped app checks GitHub Releases on launch and links to the download when a newer version exists; installing updates in place is not done, since that needs a signed app and Squirrel.Mac. **Why unsigned and arm64 only.** This is an alpha for technical testers, so we skip the Apple Developer ID, signing, and notarization for now and live with the Open Anyway step on first launch. arm64 is the only target because static-php-cli does not cross-compile on macOS, so a second architecture means a second native runner. diff --git a/docs/release.md b/docs/release.md index 7602086f..08d2ede4 100644 --- a/docs/release.md +++ b/docs/release.md @@ -106,30 +106,26 @@ missing a `type:*` label or has more than one. Releases run from the Actions tab, through the "Prepare release" workflow (`.github/workflows/release.yml`). It takes a version, a `prerelease` flag, and -a `dry_run` flag. Run it with `dry_run` on first: that builds everything and -uploads the artifacts without creating a tag or a Release. Turn `dry_run` off to -publish a draft Release. +a `dry_run` flag. Run it with `dry_run` on first: that builds the plugin ZIP and +uploads the release notes without creating a tag or a Release. Turn `dry_run` +off to publish a draft Release. -"Prepare release" only orchestrates. It calls two reusable workflows: +The GitHub Actions release workflow owns the plugin ZIP, milestone validation, +release notes, and draft GitHub Release. Buildkite owns the macOS desktop DMG. +The release tag build signs, notarizes, and staples the app, then uploads the +DMG to the same draft Release. -- `release-plugin.yml` resolves the milestone, builds the changelog and the - plugin ZIP, and creates the draft Release. -- `release-desktop.yml` builds the bundled PHP and the snapshot, runs - electron-builder, and attaches the macOS DMG. +If the DMG needs to be rebuilt, rerun the Buildkite release tag build rather +than starting a GitHub Actions workflow. -Both write to the same Release by tag. Whichever runs first creates the draft, -and the other adds its artifact. The plugin run owns the title and notes; the -desktop run only uploads the DMG. You can also run either workflow on its own -from the Actions tab, which is handy for rebuilding just the DMG against a draft -that already exists. +Both systems write to the same Release by tag, but only Buildkite uploads the +desktop artifact. The GitHub Actions run owns the title, notes, and plugin ZIP. ## Desktop app The desktop release builds a macOS DMG with electron-builder and attaches it to -the same Release as the plugin ZIP. The build is arm64-only and unsigned for -now, so macOS may show a "Cortext is damaged" warning the first time it opens. -Release notes should tell people to move Cortext to Applications. If the warning -appears, they should click Cancel, open Terminal, and run -`xattr -dr com.apple.quarantine /Applications/Cortext.app && open /Applications/Cortext.app`. +the same Release as the plugin ZIP. The release build is arm64-only, signed, and +notarized by Buildkite. Release notes should tell people to move Cortext to +Applications. The installed app checks GitHub Releases on launch and links to the download when a newer version exists, but it does not update itself. From b5aa9978e771e218ea47b0efe6cc063a273f3120 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 16 Jun 2026 16:02:32 +1000 Subject: [PATCH 21/21] Fix desktop release install note The release notes should match the Buildkite-signed DMG that ships to users. --- Generated with the help of Codex CLI, https://openai.com/codex Co-Authored-By: Codex GPT-5 --- .github/workflows/release-plugin.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-plugin.yml b/.github/workflows/release-plugin.yml index 8d167c20..78ea277d 100644 --- a/.github/workflows/release-plugin.yml +++ b/.github/workflows/release-plugin.yml @@ -119,9 +119,9 @@ jobs: run: | cat >> release-notes.md <<'EOF' - ## Opening the macOS app + ## Installing the macOS app - This DMG is unsigned, so macOS may show a "Cortext is damaged" warning the first time you open it. Move Cortext to Applications. If that warning appears, click Cancel, open Terminal, and run `xattr -dr com.apple.quarantine /Applications/Cortext.app && open /Applications/Cortext.app`. + The macOS DMG is signed and notarized. Move Cortext to Applications before opening it. EOF - name: Build plugin ZIP