8000 Publish a notarized release from a tag by dominion525 · Pull Request #4 · dominion525/blip · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Release

on:
push:
tags: ['v*']

permissions:
contents: write

jobs:
release:
# macos-15 is the arm64 image; the project builds arm64 only.
runs-on: macos-15
timeout-minutes: 45
env:
# Pinned to the Xcode the project is developed with. Bump when the image drops this version.
DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# The version lives in project.yml. The tag has to agree with it, or the release would
# carry a version the app does not report.
- name: Check the tag against the project version
id: version
run: |
tag="${GITHUB_REF_NAME#v}"
project="$(awk '/MARKETING_VERSION:/ { gsub(/"/, "", $2); print $2 }' project.yml)"
if [ "${tag}" != "${project}" ]; then
echo "Tag ${GITHUB_REF_NAME} does not match MARKETING_VERSION ${project} in project.yml" >&2
exit 1
fi
echo "version=${tag}" >> "${GITHUB_OUTPUT}"

- name: Install XcodeGen
run: brew install xcodegen
env:
HOMEBREW_NO_AUTO_UPDATE: 1
HOMEBREW_NO_INSTALL_CLEANUP: 1

# build.sh picks up a Developer ID certificate from the keychain on its own, so the
# certificate only has to be present in the search list.
- name: Import the signing certificate
env:
CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
keychain="${RUNNER_TEMP}/build.keychain-db"
certificate="${RUNNER_TEMP}/certificate.p12"
echo -n "${CERTIFICATE_BASE64}" | base64 --decode > "${certificate}"
security create-keychain -p "${KEYCHAIN_PASSWORD}" "${keychain}"
security set-keychain-settings -lut 21600 "${keychain}"
security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${keychain}"
security import "${certificate}" -k "${keychain}" -P "${P12_PASSWORD}" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${KEYCHAIN_PASSWORD}" "${keychain}"
security list-keychain -d user -s "${keychain}" login.keychain-db
rm -f "${certificate}"

- name: Build
run: ./build.sh

- name: Notarize
env:
NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }}
NOTARY_ISSUER_ID: ${{ secrets.NOTARY_ISSUER_ID }}
NOTARY_KEY_P8_BASE64: ${{ secrets.NOTARY_KEY_P8_BASE64 }}
run: |
key="${RUNNER_TEMP}/notary.p8"
echo -n "${NOTARY_KEY_P8_BASE64}" | base64 --decode > "${key}"
NOTARY_KEY_P8="${key}" ./Scripts/notarize.sh
rm -f "${key}"

# Packaged after stapling, so the archive carries the notarization ticket.
- name: Package the stapled app
run: ditto -c -k --keepParent Blip.app "Blip-${{ steps.version.outputs.version }}.zip"

- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${GITHUB_REF_NAME}" "Blip-${{ steps.version.outputs.version }}.zip" --generate-notes
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,36 @@ xcrun llvm-cov report .build/debug/BlipCorePackageTests.xctest/Contents/MacOS/Bl
-instr-profile .build/debug/codecov/ 8000 default.profdata -ignore-filename-regex='\.build/|Tests/'
```

## Release

Pushing a `v*` tag builds, notarizes, and publishes the app.

```
git tag v0.1.0
git push origin v0.1.0
```

The tag has to match `MARKETING_VERSION` in project.yml, or the workflow stops before building. The release carries `Blip-<version>.zip`, packaged after the notarization ticket is stapled, so it opens on another Mac without Gatekeeper blocking it.

The workflow reads six repository secrets.

```
Secret Contents
BUILD_CERTIFICATE_BASE64 Developer ID Application certificate exported as .p12, base64 encoded
P12_PASSWORD Export password of that .p12
KEYCHAIN_PASSWORD Any string; unlocks the temporary keychain on the runner
NOTARY_KEY_ID App Store Connect API key ID
NOTARY_ISSUER_ID App Store Connect issuer ID
NOTARY_KEY_P8_BASE64 The .p8 API key file, base64 encoded
```

The same notarization runs locally against a profile stored by `xcrun notarytool store-credentials`.

```
./build.sh
NOTARY_KEYCHAIN_PROFILE=blip ./Scripts/notarize.sh
```

## Usage

Blip puts a cursor icon in the menu bar and does not appear in the Dock.
Expand Down Expand Up @@ -145,6 +175,9 @@ build.sh Assembles and signs Blip.app
test.sh Runs both test suites
Scripts/make-icon.swift App icon artwork (--dark for the white variant)
Scripts/make-icns.sh Converts a PNG into an icns
Scripts/notarize.sh Submits the app to the notary service and staples the ticket
.github/workflows/ci.yml Runs both test suites and build.sh on pull requests
.github/workflows/release.yml Builds, notarizes, and publishes a release on a v* tag
```

## Limitations
Expand Down
43 changes: 43 additions & 0 deletions Scripts/notarize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
# Submits Blip.app to the Apple notary service and staples the ticket to it.
# Run build.sh first; the app has to be signed with a Developer ID certificate.
#
# Credentials come from the environment:
# NOTARY_KEYCHAIN_PROFILE a profile stored by `xcrun notarytool store-credentials`
# NOTARY_KEY_P8 path to an App Store Connect API key, with NOTARY_KEY_ID and NOTARY_ISSUER_ID
set -euo pipefail

DIR="$(cd "$(dirname "$0")/.." && pwd)"
APP_DIR="${DIR}/Blip.app"
ZIP="${DIR}/.build/notarize/Blip.zip"

if [ ! -d "${APP_DIR}" ]; then
echo "Blip.app not found. Run build.sh first." >&2
exit 1
fi

if [ -n "${NOTARY_KEYCHAIN_PROFILE:-}" ]; then
CREDENTIALS=(--keychain-profile "${NOTARY_KEYCHAIN_PROFILE}")
elif [ -n "${NOTARY_KEY_P8:-}" ]; then
CREDENTIALS=(--key "${NOTARY_KEY_P8}" --key-id "${NOTARY_KEY_ID}" --issuer "${NOTARY_ISSUER_ID}")
else
echo "No notary credentials. Set NOTARY_KEYCHAIN_PROFILE, or NOTARY_KEY_P8 with NOTARY_KEY_ID and NOTARY_ISSUER_ID." >&2
exit 1
fi

# The notary service takes an archive, not a bundle. The ticket is stapled to the app itself,
# so this archive is only for the submission; package the stapled app separately.
echo "==> archive for submission"
mkdir -p "$(dirname "${ZIP}")"
rm -f "${ZIP}"
ditto -c -k --keepParent "${APP_DIR}" "${ZIP}"

echo "==> notarytool submit"
xcrun notarytool submit "${ZIP}" "${CREDENTIALS[@]}" --wait

echo "==> stapler staple"
xcrun stapler staple "${APP_DIR}"
xcrun stapler validate "${APP_DIR}"
spctl -a -vvv -t exec "${APP_DIR}"

echo "==> done: ${APP_DIR}"
Loading
0