Files
TZGyn-skill/SKILL.md
T
TZGyn e141187460 Require notarization for GUI releases
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
2026-10-08 10:04:08 +08:00

230 lines
9.2 KiB
Markdown

---
name: TZGyn-skill
description: 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`, owner `TZGyn`, `tea` login name
`gitea.tzgyn.dev`. SSH: `git@gitea.tzgyn.dev:TZGyn/<repo>.git`.
- **Always print the upgrade script** (the `brew` commands 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.quarantine` or `brew install --cask --no-quarantine`.
If notarization fails, fix the cause (see
[Troubleshooting](#notarization-troubleshooting)) before publishing.
- Never commit build artifacts. `.gitignore` covers `.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
```bash
# 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`**
(origin `https://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:
```bash
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):
```bash
brew tap TZGyn/tap https://gitea.tzgyn.dev/TZGyn/homebrew-tap.git
brew trust TZGyn/tap
```
### Cask (GUI app)
```ruby
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)
```ruby
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>`.
1. **Build + sign** (`build-app.sh`): `swift build -c release` → handwritten
`Info.plist` (NOT generated from `Package.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 set `LSUIElement = true` and
`CFBundleIdentifier = dev.tzgyn.<name>`.
2. **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](#4-notarization) for the credentials.
3. **Publish the release** and attach the zip:
```bash
tea releases create --tag v<version> --title "<App> <version>" \
--note "<what changed>" --asset dist/<App>.zip \
--repo TZGyn/<repo> --login gitea.tzgyn.dev
```
4. **Bump the cask** (`version` + `sha256`), push the tap, pull Homebrew's
clone, `brew audit`. Verify by downloading the asset back and comparing the
sha256, and `spctl -a -vv <App>.app` → `accepted` /
`source=Notarized Developer ID`. If `spctl` does not say
`source=Notarized Developer ID`, stop and fix notarization first — do not
publish the release or the cask.
5. **Print the upgrade script**:
```bash
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:
```bash
codesign --force --options runtime --timestamp --sign "$IDENT" "<App>.app"
```
Submit and verify:
```bash
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:
```bash
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:
1. 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.
2. Keep it truthful — only document what has actually worked; delete steps that
became stale (e.g. a changed path or flag).
3. Commit and push:
```bash
cd ~/Git/TZGyn-skill
git add -A && git commit -m "<what behaviour was added>"
git push
```
4. 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.