For code-level conventions (branch names, key handling, AOT gotchas, test framework), see AGENTS.md. This doc covers the higher-level workflows — chiefly how releases happen.
- Features:
milestone-N-<slug>(whereNmatches the milestone issue). - Cleanup:
chore/<slug>. - Bug fixes:
fix/<slug>. - One branch per change. (Agents must additionally use a dedicated worktree per branch — see AGENTS.md § Conventions.)
- Open PRs as drafts until you've verified them locally; mark ready when you're confident.
- Commit subjects: short imperative ("Add X", not "Added X"). Body explains why, not what.
A release is a v* git tag, a GitHub Release attaching native single-file binaries for every supported runtime, and a Homebrew formula and Scoop manifest pointing at them. It all happens from the Actions tab — nothing is done locally, and no tag is pushed by hand.
Version-picking and publishing are a-team's reusable workflows, pinned to a released tag. release.yml keeps TuiCode's own build matrix — five RIDs, AOT, macOS signing and notarisation — and calls the shared ones on either side of it, so there's one release flow to maintain rather than two that drift.
Actions → Release → Run workflow → Run workflow. That's it. It publishes only from main: run on another branch, it builds and stops there.
The Bump dropdown defaults to auto, which reads the labels of PRs merged since the last release: breaking → major, enhancement → minor, otherwise patch. Choose patch, minor or major to override it.
flowchart TD
Click["Actions → Release → Run workflow<br/>bump: auto | patch | minor | major"]
Click -->|workflow_dispatch| Version["version job<br/>a-team/release-version.yml"]
Version -->|"next version, e.g. 0.1.0"| Matrix{{"matrix per RID"}}
Matrix --> A["osx-arm64<br/>(macos-14)"]
Matrix --> B["linux-x64<br/>(ubuntu-22.04)"]
Matrix --> C["linux-arm64<br/>(ubuntu-22.04-arm)"]
Matrix --> D["win-x64<br/>(windows-latest)"]
Matrix --> E["win-arm64<br/>(windows-11-arm)"]
A --> Publish["dotnet publish<br/>-c Release --PublishAot<br/>-p:MinVerVersionOverride"]
B --> Publish
C --> Publish
D --> Publish
E --> Publish
Publish --> Sign["macOS: codesign + notarize<br/>(if APPLE_* secrets present)"]
Sign --> Archive["tar.gz / zip<br/>+ sha256 sidecar"]
Archive --> Upload["actions/upload-artifact"]
Upload --> Pub["publish job<br/>a-team/release-publish.yml"]
Pub --> Tag["gh release create v0.1.0<br/>--target $GITHUB_SHA --generate-notes"]
Tag --> Public[("Published release<br/>10 assets, tag created")]
Public --> Render["render packaging/tuicode.rb + tuicode.json<br/>version + 5 SHA256s"]
Render --> TapPR["PR to homebrew-tap<br/>auto-merge on"]
TapPR -->|tap CI goes green| Brew[("brew install<br/>mentaldesk/tap/tuicode")]
Render --> Bucket["commit to scoop-bucket<br/>bucket/tuicode.json"]
Bucket --> Scoop[("scoop install<br/>mentaldesk/tuicode")]
style Public fill:#e0ffe0
style Brew fill:#e0ffe0
style Scoop fill:#e0ffe0
The shared publish job creates the tag, so a build that fails on any platform (the matrix is fail-fast) leaves no tag behind and nothing to clean up. concurrency: release keeps two dispatches from racing for the same version. Re-running the workflow after a successful publish picks the next version — tags are immutable; never delete and re-push one.
The release is published outright rather than as a draft: draft assets 404 for unauthenticated clients, which is what brew install is. The gate that used to be "publish the draft" is now the tap's own CI (brew style, brew audit --strict --online, and a full install/test/uninstall cycle on macOS arm64, Linux x64 and Linux arm64), which has to go green before auto-merge lands the formula.
packaging/tuicode.rb is the tap's formula and packaging/tuicode.json the bucket's Scoop manifest, each with the version and its SHA256s replaced by {{version}} and {{sha_<rid>}}. The shared publish job renders both from the archives it downloaded and fails the job on a placeholder nothing filled — PackagingTemplateTests catches a typo'd one before the release rather than during it, and checks the manifest still parses as JSON once rendered. The formula has no version line: brew audit --strict rejects one that duplicates the URL.
Both are pushed with PACKAGES_TOKEN, a fine-grained PAT with Contents + Pull requests write on mentaldesk/homebrew-tap and Contents write on mentaldesk/scoop-bucket, stored in the release environment, which only main can deploy to. Without that secret the release still publishes and the job warns.
The tap gets a PR its own CI gates. The bucket has no CI, so the manifest is committed straight to its default branch — a mistake in the template is live for Windows users immediately, which is what the placeholder and JSON checks are there to prevent.
flowchart LR
Release[("v0.1.0 release<br/>5× tarballs/zips<br/>+ .sha256")]
Release -->|"rendered packaging/tuicode.rb"| Tap["mentaldesk/homebrew-tap<br/>Formula/tuicode.rb"]
Tap -->|brew install| User["User's machine<br/>/opt/homebrew/bin/tuicode"]
Release -->|"rendered packaging/tuicode.json"| Bucket["mentaldesk/scoop-bucket<br/>bucket/tuicode.json"]
Bucket -->|scoop install| Win["Windows machine<br/>~/scoop/shims/tuicode.exe"]
Release -.->|future: #75| Winget["winget"]
Release -.->|future: #44| Linux["apt / AUR / Flatpak"]
The Homebrew formula points at the release tarball URL with a pinned SHA256. Both the PR and its merge are automatic; brew upgrade tuicode picks up the new version once it lands, and tuicode's About dialog reports the same number.
The Scoop manifest is the same idea for Windows: 64bit and arm64 entries pointing at the release .zips with pinned hashes, so scoop update tuicode gets the version the release just cut.
For the iTerm2 dynamic profile that ships alongside tuicode on macOS, see the formula's caveats block and #40 for the umbrella story across other terminal emulators.