236 lines
9.6 KiB
Markdown
236 lines
9.6 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
|
|
```
|
|
|
|
Push `main` **before** this: `tea` tags the remote default branch, so an
|
|
unpushed commit means the tag points at source that is not what shipped
|
|
(the binary would contain changes absent from the tag). If it already
|
|
happened, re-point the tag: `git push origin :refs/tags/v<version>` then
|
|
`git tag -f v<version> <commit> && git push origin v<version>`.
|
|
|
|
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.
|