codeburn/.github/workflows/release-menubar.yml
Resham Joshi da5311ab5e
ci(mac): make the signed menubar and desktop workflows import the Developer ID cert (#1731)
Keychain Access exports the Developer ID p12 with RC2-40 encryption and a 1-iteration sha1 MAC, which the runner's security import rejects as a wrong password. Both workflows rewrap it with openssl@3 -legacy (3DES, 2048 iterations) under a masked one-off password; the desktop workflow now signs from its own keychain because electron-builder's temporary keychain failed to unlock. Verified: both workflows green on dispatch, signed, notarized and stapled.
2026-10-10 18:31:06 +02:00

178 lines
8.3 KiB
YAML

name: Release macOS Menubar
# Triggers on a `mac-v*` tag push (e.g. `git tag mac-v0.8.0 && git push origin mac-v0.8.0`),
# or manually via the Actions tab. Builds a universal arm64+x86_64 bundle, signs it with the
# Developer ID Application cert (hardened runtime, secure timestamp), notarizes and staples it,
# zips via `ditto`, and uploads the zip to the GitHub Release. Tag builds then point the
# rolling `update-feeds` prerelease at it. Without the signing secrets the job fails.
# The installer verifies the checksum, bundle id, team signature and Gatekeeper before
# replacing the local app.
on:
push:
tags:
- 'mac-v*'
workflow_dispatch:
inputs:
version:
description: 'Version label for the bundle (e.g. v0.8.0 or dev-preview)'
required: true
default: 'dev-preview'
permissions:
contents: write # Needed to create the release + upload assets.
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Resolve version label
id: version
run: |
if [[ "${GITHUB_REF}" == refs/tags/mac-v* ]]; then
echo "value=${GITHUB_REF#refs/tags/mac-}" >> "$GITHUB_OUTPUT"
else
echo "value=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
fi
- name: Show Swift toolchain
run: swift --version
- name: Import signing certificate and notary key
env:
MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
APPSTORE_API_KEY_P8_BASE64: ${{ secrets.APPSTORE_API_KEY_P8_BASE64 }}
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
run: |
missing=()
for name in MACOS_CERT_P12_BASE64 MACOS_CERT_PASSWORD MACOS_KEYCHAIN_PASSWORD \
APPSTORE_API_KEY_P8_BASE64 APPSTORE_API_KEY_ID APPSTORE_API_ISSUER_ID; do
[[ -n "${!name}" ]] || missing+=("$name")
done
if (( ${#missing[@]} )); then
echo "::error::Missing signing secrets: ${missing[*]}. Refusing to build an unsigned menubar release (see RELEASING.md)."
exit 1
fi
keychain="$RUNNER_TEMP/codeburn-signing.keychain-db"
cert="$RUNNER_TEMP/codeburn-signing.p12"
# Keychain Access exports a legacy p12 (RC2-40, sha1 MAC, 1 iteration) that the
# runner's `security import` rejects as a wrong password; rewrap it in a form it reads.
umask 077
openssl3="$(brew --prefix openssl@3)/bin/openssl"
base64 --decode <<< "$MACOS_CERT_P12_BASE64" > "$cert.in"
"$openssl3" pkcs12 -legacy -in "$cert.in" -passin env:MACOS_CERT_PASSWORD -nodes -out "$cert.pem"
export cert_password="$(openssl rand -hex 24)"
echo "::add-mask::$cert_password"
"$openssl3" pkcs12 -export -legacy -in "$cert.pem" -out "$cert" -passout env:cert_password \
-keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 -iter 2048
rm -f "$cert.in" "$cert.pem"
security create-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$keychain"
security import "$cert" -k "$keychain" -P "$cert_password" -f pkcs12 -T /usr/bin/codesign -T /usr/bin/security
rm -f "$cert"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_KEYCHAIN_PASSWORD" "$keychain" > /dev/null
# shellcheck disable=SC2046
security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"')
identity=$(security find-identity -v -p codesigning "$keychain" | awk '/Developer ID Application: .*\(XRVP7P7F9M\)/ { print $2; exit }')
if [[ -z "$identity" ]]; then
echo "::error::No Developer ID Application identity for team XRVP7P7F9M in MACOS_CERT_P12_BASE64."
exit 1
fi
key="$RUNNER_TEMP/AuthKey_${APPSTORE_API_KEY_ID}.p8"
base64 --decode <<< "$APPSTORE_API_KEY_P8_BASE64" > "$key"
{
echo "CODESIGN_IDENTITY=$identity"
echo "NOTARY_KEY_PATH=$key"
echo "NOTARY_KEY_ID=$APPSTORE_API_KEY_ID"
echo "NOTARY_ISSUER_ID=$APPSTORE_API_ISSUER_ID"
} >> "$GITHUB_ENV"
- name: Build + sign + notarize + zip
run: mac/Scripts/package-app.sh "${{ steps.version.outputs.value }}"
- name: Verify the zip the way the installer will
env:
ZIP: mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip
run: |
(cd "$(dirname "$ZIP")" && shasum -a 256 -c "$(basename "$ZIP").sha256")
check="$RUNNER_TEMP/verify-zip"
rm -rf "$check" && mkdir -p "$check"
ditto -x -k "$ZIP" "$check"
codesign --verify --deep --strict -R='anchor apple generic and certificate leaf[subject.OU] = "XRVP7P7F9M"' "$check/CodeBurnMenubar.app"
spctl --assess --type execute -vv "$check/CodeBurnMenubar.app"
- name: Upload artifact (for manual runs)
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v7
with:
name: CodeBurnMenubar-${{ steps.version.outputs.value }}
path: |
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip.sha256
if-no-files-found: error
- name: Create / update GitHub Release
if: startsWith(github.ref, 'refs/tags/mac-v')
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: Menubar ${{ steps.version.outputs.value }}
body: |
Install with:
```
npm install -g codeburn
codeburn menubar
```
That command drops the app into `~/Applications`, records the persistent
`codeburn` CLI path used by the menubar, verifies the checksum and the
AgentSeal Developer ID signature, and launches it. The app is notarized, so
the zip from this page also opens directly.
files: |
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip
mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip.sha256
fail_on_unmatched_files: true
# Last on purpose: the feed only moves once the published asset is known to match the
# verified build. It lives on a prerelease so it never becomes GitHub's Latest release.
- name: Point the update feed at this release
if: startsWith(github.ref, 'refs/tags/mac-v')
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
VERSION: ${{ steps.version.outputs.value }}
run: |
zip_name="CodeBurnMenubar-${VERSION}.zip"
url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${zip_name}"
built=$(cut -d' ' -f1 "mac/.build/dist/${zip_name}.sha256")
published=$(curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 "$url" | shasum -a 256 | cut -d' ' -f1)
if [[ "$published" != "$built" ]]; then
echo "::error::Published ${zip_name} (${published}) does not match the verified build (${built})."
exit 1
fi
feed="$RUNNER_TEMP/menubar-latest.json"
jq -n --arg version "${VERSION#v}" --arg url "$url" --arg sha256 "$built" \
'{version: $version, url: $url, sha256: $sha256}' > "$feed"
cat "$feed"
if ! gh release view update-feeds > /dev/null 2>&1; then
gh release create update-feeds --prerelease --latest=false \
--title "Update feeds" --notes "Update metadata for the apps. Not a release to download."
fi
gh release upload update-feeds "$feed" --clobber
- name: Remove signing material
if: always()
run: |
security delete-keychain "$RUNNER_TEMP/codeburn-signing.keychain-db" 2>/dev/null || true
rm -f "$RUNNER_TEMP"/AuthKey_*.p8 "$RUNNER_TEMP/codeburn-signing.p12"