Stage 1 · Code
Capstone — Ship a Real Tool
Publish & Distribute
Ship the binaries you built to a GitHub Release, wire up `go install`, and publish a Homebrew tap so anyone can install your tool with one command.
Why Publishing Matters
A cross-compiled dist/ folder sitting on your laptop helps nobody. Publishing is the step that turns "binaries I built" into "a tool someone else can install" — a tagged, checksummed, discoverable release that a stranger can run without cloning your repo or owning a Go toolchain.
- GitHub Releases — the baseline. Every other distribution method links back to a release.
go install** — zero-friction for anyone who already has Go installed.- Homebrew tap — the most familiar install path for macOS and Linux users:
brew install. - Package managers (apt/scoop/AUR) — worth it only once a tool has real external users; skip for now.
Tag & Release on GitHub
Releases are anchored to Git tags. Use semantic versioning (vMAJOR.MINOR.PATCH) so go install and Homebrew can resolve versions predictably.
# 1. Tag the commit you want to release
git tag -a v1.0.0 -m "tasker v1.0.0"
git push origin v1.0.0
# 2. Build the release artifacts (from the previous lesson)
make release
./scripts/package.sh v1.0.0
# 3. Publish the release with the GitHub CLI
gh release create v1.0.0 \
dist/*.tar.gz dist/*.zip dist/checksums.txt \
--title "tasker v1.0.0" \
--notes "First tagged release: add, list, done, delete commands."gh (https://cli.github.com) authenticates with gh auth login and can create releases, open PRs, and watch CI runs without leaving your terminal — worth installing before your first release.
Automate Releases With Actions
Manual releases work, but they drift — someone forgets a platform, or the checksums file goes stale. Wire the same steps into a GitHub Actions workflow that fires whenever you push a version tag.
name: release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Build release binaries
run: make release VERSION=${{ github.ref_name }}
- name: Package archives and checksums
run: ./scripts/package.sh ${{ github.ref_name }}
- name: Publish GitHub release
run: |
gh release create ${{ github.ref_name }} \
dist/*.tar.gz dist/*.zip dist/checksums.txt \
--title "tasker ${{ github.ref_name }}" \
--generate-notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}GitHub can generate release notes from merged pull requests automatically. It's not a substitute for a curated CHANGELOG on major versions, but it removes the excuse to skip notes on routine patch releases.
go install Distribution
If your module path is public (a real GitHub repo, go.mod with the correct module path), anyone with Go installed can fetch and build your tool directly from source — no release artifacts required.
# Installs the latest tagged version into $GOBIN (usually ~/go/bin)
go install github.com/thesyscoder/tasker/cmd/tasker@latest
# Pin to an exact version instead of latest
go install github.com/thesyscoder/tasker/cmd/tasker@v1.0.0
# Make sure $GOBIN is on PATH
echo 'export PATH="$PATH:$(go env GOPATH)/bin"' >> ~/.zshrcgo install resolves @latest against your repo's Git tags — it does nothing useful until go.mod's module line matches your actual repo URL and you've pushed at least one vX.Y.Z tag. Untagged commits install as pseudo-versions that are hard for users to reason about.
Homebrew Tap
A tap is just a GitHub repo (conventionally named homebrew-<name>) containing Ruby formula files that tell Homebrew which release archive to download and how to install it.
class Tasker < Formula
desc "Fast command-line task tracker"
homepage "https://github.com/thesyscoder/tasker"
version "1.0.0"
if OS.mac? && Hardware::CPU.arm?
url "https://github.com/thesyscoder/tasker/releases/download/v1.0.0/tasker_darwin_arm64.tar.gz"
sha256 "REPLACE_WITH_CHECKSUM_FROM_checksums.txt"
elsif OS.mac?
url "https://github.com/thesyscoder/tasker/releases/download/v1.0.0/tasker_darwin_amd64.tar.gz"
sha256 "REPLACE_WITH_CHECKSUM_FROM_checksums.txt"
elsif OS.linux?
url "https://github.com/thesyscoder/tasker/releases/download/v1.0.0/tasker_linux_amd64.tar.gz"
sha256 "REPLACE_WITH_CHECKSUM_FROM_checksums.txt"
end
def install
bin.install "tasker"
end
test do
system "#{bin}/tasker", "--version"
end
end
brew tap thesyscoder/tasker
brew install tasker
tasker --versionHand-writing formulas works for one release. Past that, tools like goreleaser (https://goreleaser.com) generate the cross-compiled archives, checksums, GitHub release, and Homebrew formula update from a single YAML config and one git tag — worth adopting once you're cutting releases regularly.
Choosing a Distribution Path
| Method | Best for | User needs |
|---|---|---|
| GitHub Releases | Everyone — the required baseline | A browser or curl |
| go install | Other Go developers, internal tools | A Go toolchain |
| Homebrew tap | macOS/Linux users who expect brew install | Homebrew |
| Package manager (apt/AUR/scoop) | Tools with real external adoption | Nothing — it's just there |
For your capstone, shipping a tagged GitHub release with cross-compiled archives and checksums is the required bar — that alone makes your tool installable by anyone. Add go install support for free by keeping your module path correct, and treat a Homebrew tap as the optional next step once you want a friend to install it without touching a terminal-unfriendly curl command.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.