Releasing tools on different channels with goreleaser

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.

  1. 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).

  1. 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:

  1. 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 <[email protected]>"
    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.

Subscribe on GitHub