TaskMenu releases are published from GitHub Actions when a vX.Y.Z tag is pushed. The release workflow builds a signed archive, packages it into a DMG, notarizes the DMG with Apple, and uploads the DMG plus a SHA-256 checksum to the GitHub release.
Homebrew cask distribution is maintained in the public crazytan/homebrew-tap repository.
Add these repository secrets in GitHub under Settings -> Secrets and variables -> Actions -> Secrets:
| Secret | Value |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Base64-encoded .p12 export of your Developer ID Application certificate |
BUILD_CERTIFICATE_PASSWORD |
Password used when exporting the .p12 file |
APP_STORE_CONNECT_API_KEY_BASE64 |
Base64-encoded App Store Connect API private key (.p8) |
GOOGLE_CLIENT_ID |
Google OAuth client ID used by release builds |
APP_STORE_CONNECT_KEY_ID |
App Store Connect API key ID |
APP_STORE_CONNECT_ISSUER_ID |
App Store Connect issuer ID |
Add these repository variables under Settings -> Secrets and variables -> Actions -> Variables:
| Variable | Value |
|---|---|
GOOGLE_REDIRECT_SCHEME |
Google OAuth redirect scheme, for example com.googleusercontent.apps.<client-id-prefix> |
APPLE_TEAM_ID |
Apple Developer Team ID, for example V82M9YX8BR |
The release workflow also accepts GOOGLE_REDIRECT_SCHEME and APPLE_TEAM_ID as secrets for compatibility, but variables are preferred when the values are not sensitive.
TaskMenu uses a native OAuth client with PKCE, so release builds do not embed a Google client secret.
- Open Keychain Access on the Mac that has your Developer ID certificate.
- Find Developer ID Application: Your Name (TEAMID).
- Expand the certificate row and make sure the private key is included.
- Select the certificate and private key together, then choose File -> Export Items....
- Save as
DeveloperIDApplication.p12and set a strong export password. - Copy the base64 form into the GitHub secret:
base64 -i DeveloperIDApplication.p12 | tr -d '\n' | pbcopyPaste the clipboard into BUILD_CERTIFICATE_BASE64. Put the export password in BUILD_CERTIFICATE_PASSWORD.
Create an App Store Connect API key in App Store Connect -> Users and Access -> Integrations -> App Store Connect API. A key with Developer access is enough for notarization.
Download the .p8 key once, then copy the base64 form into APP_STORE_CONNECT_API_KEY_BASE64:
base64 -i AuthKey_XXXXXXXXXX.p8 | tr -d '\n' | pbcopyStore the key ID in APP_STORE_CONNECT_KEY_ID and the issuer ID in APP_STORE_CONNECT_ISSUER_ID.
If Apple rejects the submission (status Invalid), the release job prints the full notarization log from notarytool log before failing. The per-file issues in that log are the place to start.
If the release job fails with:
HTTP status code: 401. Unauthenticated.
then Apple rejected the App Store Connect API credentials. Check these values first:
APP_STORE_CONNECT_API_KEY_BASE64must be the base64 text of the downloaded.p8private key file, not the JSON key metadata.APP_STORE_CONNECT_KEY_IDmust match the key ID shown for that same.p8file.APP_STORE_CONNECT_ISSUER_IDmust be the issuer ID from App Store Connect API access.- The API key must be active, team-scoped, and have at least Developer access.
You can validate the same credentials locally before pushing a tag:
xcrun notarytool history \
--key "$HOME/private_keys/AuthKey_XXXXXXXXXX.p8" \
--key-id "XXXXXXXXXX" \
--issuer "00000000-0000-0000-0000-000000000000"- Move changelog entries from
Unreleasedto a version heading:
## vX.Y.Z (YYYY-MM-DD)- Update
MARKETING_VERSIONandCURRENT_PROJECT_VERSIONinproject.yml. - Regenerate the project if you want the checked-in
.xcodeprojto reflect the new version:
xcodegen generate- Run tests:
xcodebuild test -project TaskMenu.xcodeproj -scheme TaskMenu \
-configuration Debug \
-destination "platform=macOS"- Commit and push to
main. - Create and push the release tag:
git tag -a vX.Y.Z -m "TaskMenu vX.Y.Z"
git push origin main vX.Y.ZThe release workflow validates that the tag version matches MARKETING_VERSION and that CHANGELOG.md has a matching section before it publishes anything.
- After the GitHub release publishes, update the Homebrew cask in
crazytan/homebrew-tap:
brew tap crazytan/tap
cd "$(brew --repository crazytan/tap)"
version="${VERSION:-$(gh release view --repo crazytan/TaskMenu --json tagName --jq '.tagName | sub("^v"; "")')}"
sha256="$(curl -LfsS "https://github.com/crazytan/TaskMenu/releases/download/v${version}/TaskMenu-${version}.dmg.sha256" | awk '{print $1}')"
perl -0pi -e "s/version \"[^\"]+\"/version \"${version}\"/" Casks/taskmenu.rb
perl -0pi -e "s/sha256 \"[0-9a-f]+\"/sha256 \"${sha256}\"/" Casks/taskmenu.rb
brew audit --cask --strict crazytan/tap/taskmenu
brew livecheck --cask crazytan/tap/taskmenu
git add Casks/taskmenu.rb
git commit -m "Update TaskMenu to ${version}"
git pushYou can also run Release manually from GitHub Actions. Enter the version without the leading v and choose whether the GitHub release should be marked as a prerelease. The workflow still expects project.yml and CHANGELOG.md to match that version.
Two guards apply to manual runs:
- If the tag
vX.Y.Zalready exists, the workflow must be dispatched from that tag (or a ref pointing at the same commit); it fails if the checked-out commit differs from the tagged commit. If the tag does not exist yet, publishing creates it at the dispatched commit. - Release assets are immutable: if the release already has a DMG or checksum with the same name, the upload fails instead of replacing it. Delete the bad asset (or the release) manually first if you really need to republish.
After producing a signed app bundle, you can package it locally:
VERSION="X.Y.Z"
SIGNING_IDENTITY="Developer ID Application" \
APP_STORE_CONNECT_KEY_ID="XXXXXXXXXX" \
APP_STORE_CONNECT_ISSUER_ID="00000000-0000-0000-0000-000000000000" \
APP_STORE_CONNECT_KEY_PATH="$HOME/private_keys/AuthKey_XXXXXXXXXX.p8" \
./scripts/make_dmg.sh \
--app build/TaskMenu.xcarchive/Products/Applications/TaskMenu.app \
--version "$VERSION" \
--output-dir build/artifactsWhen the APP_STORE_CONNECT_* variables are set, the script notarizes the DMG, staples it, and runs a Gatekeeper assessment (spctl --assess) that must pass. Without them it skips notarization and prints skipping Gatekeeper assessment (no notarization credentials); such a DMG is fine for local testing but is not distributable.