--- 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/.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 --desc "" --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/ git init -b main git add -A && git commit -m "" git remote add origin https://gitea.tzgyn.dev/TZGyn/.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//*.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/.rb`**; **CLI tools → `Formula/.rb`**. - After pushing, sync Homebrew's own clone: ```bash git -C ~/.cache/homebrew-tap add -A git -C ~/.cache/homebrew-tap commit -m "Add cask" git -C ~/.cache/homebrew-tap push git -C /opt/homebrew/Library/Taps/tzgyn/homebrew-tap pull --ff-only brew audit --cask tzgyn/tap/ # or: brew audit --formula tzgyn/tap/ ``` - 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 "" do version "X.Y.Z" sha256 "" url "https://gitea.tzgyn.dev/TZGyn//releases/download/v#{version}/.zip" name "" desc "" homepage "https://gitea.tzgyn.dev/TZGyn/" app ".app" end ``` ### Formula (CLI) ```ruby class < Formula desc "" homepage "https://gitea.tzgyn.dev/TZGyn/" url "https://gitea.tzgyn.dev/TZGyn//releases/download/v0.1.0/-0.1.0-darwin-arm64.tar.gz" sha256 "" version "0.1.0" def install bin.install "" end end ``` ## 3. Releasing a macOS GUI app Two scripts per repo: `build-app.sh ` and `release.sh `. 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.`. 2. **Notarize + staple + zip** (`release.sh`): build → `ditto -c -k --keepParent .app dist/.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 --title " " \ --note "" --asset dist/.zip \ --repo TZGyn/ --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` → `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 ``` ## 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" ``` Submit and verify: ```bash xcrun notarytool submit dist/.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 && xcrun stapler validate .app spctl -a -vv .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` 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 # 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 "" 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.