Shipping an unnotarized build left the user with an app Gatekeeper
rejects, and the skill previously suggested quarantine workarounds that
hid the problem. Notarization is now a hard gate, with the account-level
agreement failure documented so it is diagnosed instead of bypassed.
💘 Generated with Crush
Assisted-by: Crush:kimi-k3
9.2 KiB
name, description
| name | description |
|---|---|
| TZGyn-skill | Use for any TZGyn development workflow — starting a new project on Gitea, publishing to the TZGyn Homebrew tap (casks for GUI apps, formulae for CLIs), signing and notarizing a desktop/mobile build, cutting a release, or extending/updating this skill itself. |
TZGyn dev skill
Personal conventions for TZGyn projects. Source of truth is the Git repo
https://gitea.tzgyn.dev/TZGyn/TZGyn-skill; it is installed as a global Crush
skill at ~/.agents/skills/TZGyn-skill (a symlink to the checkout, so editing
the repo updates the live skill).
The reference implementations to copy from are ~/Git/kapture and
~/Git/konference (macOS menu-bar apps).
Ground rules
- Host: Gitea at
https://gitea.tzgyn.dev, ownerTZGyn,tealogin namegitea.tzgyn.dev. SSH:git@gitea.tzgyn.dev:TZGyn/<repo>.git. - Always print the upgrade script (the
brewcommands below) in the final answer after any release or version bump. The user wants it every time. - Every GUI release must be notarized and stapled. An unnotarized build is
not a release: never publish one, and never paper over Gatekeeper with
xattr -dr com.apple.quarantineorbrew install --cask --no-quarantine. If notarization fails, fix the cause (see Troubleshooting) before publishing. - Never commit build artifacts.
.gitignorecovers.build/,*.app/,dist/,.swiftpm/,xcuserdata/,DerivedData/,.DS_Store. (go build ./...drops a binary next to the source — ignore it explicitly.) - Never commit secrets (
.p8, API keys) or push to a remote unless asked. - Commit messages: concise, why-focused, first line < 72 chars; add a body only when it helps a future reader.
- Prefer SwiftPM (Swift/SwiftUI) for macOS apps; no external dependencies.
1. New project on Gitea
# 1. Create the repo (as the authenticated user).
tea repos create --name <repo> --desc "<one-line description>" --login gitea.tzgyn.dev
# Do NOT pass --owner TZGyn: that hits /orgs/TZGyn/repos and 404s for a user.
# 2. Initialise and push.
cd ~/Git/<repo>
git init -b main
git add -A && git commit -m "<why>"
git remote add origin https://gitea.tzgyn.dev/TZGyn/<repo>.git
git push -u origin main
Scaffold every repo with README.md, AGENTS.md (architecture + commands +
gotchas), and .gitignore. See ~/Git/simpletopbar for the minimal shape:
Package.swift, Sources/<Name>/*.swift, build-app.sh, release.sh,
scripts/.
2. Homebrew tap
-
The tap clone lives at
~/.cache/homebrew-tap(originhttps://gitea.tzgyn.dev/TZGyn/homebrew-tap.git). -
Layout: GUI apps →
Casks/<name>.rb; CLI tools →Formula/<name>.rb. -
After pushing, sync Homebrew's own clone:
git -C ~/.cache/homebrew-tap add -A git -C ~/.cache/homebrew-tap commit -m "Add <name> cask" git -C ~/.cache/homebrew-tap push git -C /opt/homebrew/Library/Taps/tzgyn/homebrew-tap pull --ff-only brew audit --cask tzgyn/tap/<name> # or: brew audit --formula tzgyn/tap/<name> -
Users tap once (non-GitHub taps need the full URL):
brew tap TZGyn/tap https://gitea.tzgyn.dev/TZGyn/homebrew-tap.git brew trust TZGyn/tap
Cask (GUI app)
cask "<name>" do
version "X.Y.Z"
sha256 "<sha256 of the release zip>"
url "https://gitea.tzgyn.dev/TZGyn/<repo>/releases/download/v#{version}/<Asset>.zip"
name "<Display Name>"
desc "<one line>"
homepage "https://gitea.tzgyn.dev/TZGyn/<repo>"
app "<Name>.app"
end
Formula (CLI)
class <Name> < Formula
desc "<one line>"
homepage "https://gitea.tzgyn.dev/TZGyn/<repo>"
url "https://gitea.tzgyn.dev/TZGyn/<repo>/releases/download/v0.1.0/<asset>-0.1.0-darwin-arm64.tar.gz"
sha256 "<sha256 of the tarball>"
version "0.1.0"
def install
bin.install "<binary>"
end
end
3. Releasing a macOS GUI app
Two scripts per repo: build-app.sh <version> and release.sh <version>.
-
Build + sign (
build-app.sh):swift build -c release→ handwrittenInfo.plist(NOT generated fromPackage.swift) →make-icon.sh→ sign with the Developer ID identity resolved from the login keychain (security find-identity -v -p codesigning | awk '/Developer ID Application:/{print $2;exit}'), falling back to ad-hoc (-). GUI apps setLSUIElement = trueandCFBundleIdentifier = dev.tzgyn.<name>. -
Notarize + staple + zip (
release.sh): build →ditto -c -k --keepParent <App>.app dist/<App>.zip→ submit to Apple → staple → re-zip → print sha256. See Notarization for the credentials. -
Publish the release and attach the zip:
tea releases create --tag v<version> --title "<App> <version>" \ --note "<what changed>" --asset dist/<App>.zip \ --repo TZGyn/<repo> --login gitea.tzgyn.dev -
Bump the cask (
version+sha256), push the tap, pull Homebrew's clone,brew audit. Verify by downloading the asset back and comparing the sha256, andspctl -a -vv <App>.app→accepted/source=Notarized Developer ID. Ifspctldoes not saysource=Notarized Developer ID, stop and fix notarization first — do not publish the release or the cask. -
Print the upgrade script:
brew update brew upgrade --cask <name>
4. Notarization
Signing identity: Developer ID Application: Teh Zi Gyn (RX6767S6BW).
App Store Connect API key (shared with Kapture):
| Value | Where |
|---|---|
| Key file | ~/.config/kapture/AuthKey_K5KXMMSB28.p8 |
| Key ID | K5KXMMSB28 |
| Issuer ID | 3e25e5dc-8b75-453f-a71c-c7f3feaac628 (not secret) |
| Team ID | RX6767S6BW |
Signing must use a hardened runtime and a secure timestamp:
codesign --force --options runtime --timestamp --sign "$IDENT" "<App>.app"
Submit and verify:
xcrun notarytool submit dist/<App>.zip \
--key "$HOME/.config/kapture/AuthKey_K5KXMMSB28.p8" \
--key-id K5KXMMSB28 --issuer 3e25e5dc-8b75-453f-a71c-c7f3feaac628 \
--wait --output-format json
xcrun stapler staple <App>.app && xcrun stapler validate <App>.app
spctl -a -vv <App>.app # want: accepted, source=Notarized Developer ID
release.sh must notarize. Treat any skip or failure as a failed release:
do not publish the tag, the release, or the cask, and do not suggest
quarantine workarounds (xattr -dr com.apple.quarantine,
brew install --cask --no-quarantine) — those hand the user a build macOS
rejects. Only publish once spctl -a -vv <App>.app reports accepted with
source=Notarized Developer ID.
Notarization troubleshooting
| Symptom | Cause / fix |
|---|---|
HTTP status code: 403. A required agreement is missing or has expired. |
The Apple Developer Program License Agreement for the team is unsigned or expired. The Account Holder must accept it in App Store Connect → Business → Agreements and at developer.apple.com/account → Agreements. Account-wide: every call fails, including read-only notarytool history, until it is accepted; allow a few minutes to propagate. |
| The same 403 after accepting | The wrong agreement was accepted (App Store Connect "Paid Apps" instead of the Program License Agreement) or it was accepted under a different team's account. |
401 Authentication credentials are incorrect |
Wrong key id / issuer, or the key belongs to another team. |
Confirm the team before blaming the credentials — do not "fix" this by switching teams or re-signing:
security find-identity -v -p codesigning # the Developer ID team must match the key's team
spctl -a -vv /Applications/<App>.app # an already-notarized app proves the team/key pair works
xcrun notarytool history --key ... --key-id ... --issuer ... # 403 here = account agreement, not the app
notarytool history failing the same way as the submit is the tell: the
problem is the account, not the bundle, so there is nothing to change in the
app or the scripts.
5. Mobile
For mobile (iOS/Android), notarization does not apply; the equivalent is store signing (App Store Connect / Play Console) or EAS. Ask for the specific stack before assuming; this skill currently only codifies the macOS path.
6. Extending this skill (self-improvement)
This skill is meant to grow. When the user asks for a new behaviour, preference, or convention:
-
Edit the relevant section of
SKILL.md(add a new section if it is a new topic). Keep it concrete: exact commands, paths, and templates. -
Keep it truthful — only document what has actually worked; delete steps that became stale (e.g. a changed path or flag).
-
Commit and push:
cd ~/Git/TZGyn-skill git add -A && git commit -m "<what behaviour was added>" git push -
The installed copy is a symlink, so it updates immediately; a new Crush session picks it up. Confirm with
crush_info(the skill should appear under[skills]).
Prefer updating this skill over repeating ad-hoc instructions in a project: if a convention applies to more than one repo, it belongs here.