Creating a CLI tool is easy, compared to distributing it
Goreleaser makes publishing almost a cake walk, but it comes with a steep learning curve. Still, distribution is just as important as the product itself. Shipping your tool as standalone binaries isn’t enough. reaching users through established channels matters It increses your tool’s reputation and gives a free visibility. Like for macOS, there’s Homebrew, now a days many Linux distributions are promoting it, brew is backed by a large community. Similarly, Winget and Scoop are important distribution channels. Being available through these channels brings significantly more visibility and ease of use.
By the way, GoReleaser is a publishing automation tool, and it’s not exclusive to Go, one can build and distribute apps written in many languages like Go, Zig, Rust, Typescript and more.
Let’s first learn about different types of package managers before publishing there.
- homebrew.
Brew is generally maintained by the community and formulas are mostly written in Ruby :).
So do you have to learn ruby to publish on homebrew?
The answer is no, you don’t. goreleaser covers everything for you.
You can create an empty GitHub repo named homebrew-tap or any other name with a homebrew- prefix. the prefix is recommended
gh repo create user1/homebrew-tap --public
brew new-tap user1/tap
brew install gh git
What is tap?
A tap is a repository with some recipes that brew needs to follow to install a tool. recipe is, in the sense, a formula to install a tool the repo user1/homebrew-tap will be reffered as user1/tap while using.
On the other side you can make a repo like user1/homebrew-cli –> user1/cli (tap).
- scoop?
Scoop is a command line installer for Windows. Think of it as the Windows equivalent of Homebrew, but with its own flavor. To distribute your tool via Scoop, you’ll need to create a “bucket.” A bucket is basically Scoop’s version of a tap, a repo with the name scoop-bucket is suggested.
gh repo create user1/scoop-bucket --public
scoop bucket add bucket-name <https://github.com/user1/bucket-name> ## adding a bucket to scoop
scoop install bucket-name/your-tool ## installing your tool from the bucket
Creating a Scoop bucket is straightforward:
- Winget
Now, Winget is Microsoft’s official package manager for Windows built into the operating system.
The process here is a bit different. Instead of creating a new repo you have to fork the main repo,
and then create a new branch for your tool you don’t have to do this thing manually goreleaser handles that for you.
Winget do’s and don’ts
There are a few things to keep in mind:
one tool = one branch,
you can’t submit multiple tools in one branch as the pr is for tracking the changes of a single tool, goreleaser automates this process too.
Generally, Linux users are techy so they are more likely to install .deb and .rpm packages, as well as tools like eget, And the thing with go as a language is that it includes all the necessary C libraries in the binaries so it can be runned by just putting in $PATH on any machine.
the process generally is like you have to create/modify a file in other repo. But github-actions only gives your workflow access to that repo only
create a PAT (personal access token) with scopes read, write, releasing, files, repos, actions, action-metadata etc, and add it to your repo secrets. then you can use that token in your workflow to access other repo.
Remember you can’t create any variable with GITHUB_ prefix in the secrets, so you have to use a different name for your secret like PAT.
to create a release, you have to create a numeric tag only as winget refuses to accept alphanumeric tags.
like,
1.1.1
0.1.2-1
tags can be created using
git tag 1.1.1
git push -u origin 1.1.1
## if you use jj then
jj new # stage changes
jj desc # describe changes
jj bookmark advance main # advance bookmark main to the new staged state.
jj tag set 1.1.1
jj git push --all #or
jj git push --tag 1.1.1
If you have followed my previous post on doing ci with mise
then read that post first as I am going to use the same approach here. Also I am assuming you are already using mise on your machine. goreleaser is available on mise, so you can install it using mise use goreleaser@latest.
To getting started, you have to generate a goreleaser config file using goreleaser init command. It will create a .goreleaser.yml file in your repo. You can modify it according to your needs. For detailed info please refer to the official documentation.
name: build and release
on:
push:
tags:
- '*'
workflow_dispatch:
permissions:
contents: write
pages: write
id-token: write
jobs:
ci:
runs-on: ubuntu-latest
container:
image: ghcr.io/jdx/mise:2026.8.16
steps:
- name: checkout code
uses: actions/checkout@main
with:
fetch-depth: 0
- name: Trust git directory in container
run: git config --global --add safe.directory '*'
- name: Install dependencies & building
run: |
mise deps
mise fetch
mise run release
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
MISE_EXPERIMENTAL: true
mise.toml
[tools]
go = "latest"
goreleaser = "latest"
[deps]
go = { auto = true }
[tasks]
fetch = { description = "Fetch Go module dependencies", run = "go mod download" }
testcase = { description = "Run Go testcase", run = "go run ./test/test.go" }
build = { description = "Build Go binary", run = ["mise deps", "mise run fetch", "goreleaser build --snapshot --clean"] }
test = { description = "test binary", run = ["mise deps", "mise run testcase", "mise run build"] }
testcases = { description = "Run Go test cases", run = "go test ./..." }
release = { description = "Build and release using Goreleaser", run = "goreleaser release --clean" }
So I had composed the .goreleaser.yml file for ensor it should look like this.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: ensor
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- id: ensor
main: .
binary: ensor
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
- openbsd
goarch:
- amd64
- arm64
- "386"
ignore:
- goos: darwin
goarch: "386"
flags:
- -trimpath
ldflags:
- -s -w -X github.com/Pratyay360/ensor/cmd.version={{.Version}}
archives:
- id: default
ids:
- ensor
formats:
- tar.gz
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
formats:
- zip
checksum:
name_template: checksums.txt
changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Features
regexp: "^feat"
order: 0
- title: Bug fixes
regexp: "^fix"
order: 1
- title: Others
order: 999
nfpms:
- id: packages
package_name: ensor
description: "Censor secrets from your codebase"
homepage: "https://github.com/Pratyay360/ensor"
maintainer: "Pratyay360 <pratyaymustafi@outlook.com>"
license: Apache-2.0
formats:
- deb
- rpm
- apk
homebrew_casks:
- name: ensor
homepage: "https://github.com/Pratyay360/ensor"
description: "Censor secrets from your codebase"
license: Apache-2.0
binaries:
- ensor
generate_completions_from_executable:
executable: ensor
args:
- completion
base_name: ensor
shell_parameter_format: cobra
shells:
- bash
- zsh
- fish
directory: Casks
commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}"
repository:
owner: Pratyay360
name: homebrew-tap
branch: main
scoops:
- name: ensor
homepage: "https://github.com/Pratyay360/ensor"
description: "Censor secrets from your codebase"
license: Apache-2.0
commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
repository:
owner: Pratyay360
name: scoop-bucket
branch: main
winget:
- name: winget
publisher: Pratyay360
publisher_url: "https://github.com/Pratyay360"
publisher_support_url: "https://github.com/Pratyay360/ensor/issues"
package_identifier: pratyay360.ensor
short_description: "Censor secrets from your codebase"
description: "A unified CLI for censoring secrets from your codebase"
license: Apache-2.0
homepage: "https://github.com/Pratyay360/ensor"
commit_msg_template: "{{ .PackageIdentifier }}: {{ .Tag }}"
repository:
owner: Pratyay360
name: winget-pkgs
branch: ensor # branch name should be your tool name. for easy navigation later on.
release:
github:
owner: Pratyay360
name: ensor
snapshot:
version_template: "{{ incpatch .Version }}-next"
for other tech stack you can use make or just or mise.
I am yet to explore aur as they have stopped registration and haven’t seen any literature on conda forge so have to try this later on (maybe content for a new post), Also creating a custom coppr repo or ppa to distribute tool is tbd.


Top comments (0)