Open-source project
tcnksm/ghr avatar
tcnksm/ghr

ghr: a release uploader that reads your repository URL from .git/config

Upload multiple artifacts to GitHub Release in parallel

1,262 stars83 forksGoMIT

At a glance

What is it?
ghr creates a GitHub Release and uploads artifacts to it in parallel, and it guesses the repository from the .git/config of the directory you run it in, so the usual invocation is a tag and a path. It is MIT licensed, it works as a GitHub Action, and it is deliberately small next to github-release.
Who is it for?
ghr fits a team that cross compiles binaries, writes checksums and needs all of them attached to a GitHub Release in one step, and it fits a project that wants a release step in GitHub Actions without writing a curl script. It does not fit a team that needs the wider feature set of a full release management tool, and it does not fit a repository where the release and the git remote live in different places without you overriding the defaults.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two arguments, and the repository is inferred

The usage is one line.

bash
$ ghr [option] TAG [PATH]

You must provide TAG, a git tag, and optionally a PATH to the artifacts you want to upload. The path can be a file or a directory, and if you give a directory all files in it are uploaded.

The interesting part is the assumption underneath. ghr assumes that you are in a git repository when executed, because normally the artifacts you want to upload to a GitHub Release page are in that repository or generated there. With that assumption, ghr implicitly reads the repository URL from the .git/config file. You can change that information through the options.

So the typical invocation in a release script is the shortest possible thing.

bash
$ ghr v0.1.0 pkg/
--> Uploading: pkg/0.1.0_SHASUMS
--> Uploading: pkg/ghr_0.1.0_darwin_386.zip
--> Uploading: pkg/ghr_0.1.0_darwin_amd64.zip
--> Uploading: pkg/ghr_0.1.0_linux_386.zip
--> Uploading: pkg/ghr_0.1.0_linux_amd64.zip
--> Uploading: pkg/ghr_0.1.0_windows_386.zip
--> Uploading: pkg/ghr_0.1.0_windows_amd64.zip

That output is worth reading for what it implies about the use case. The example uploads a SHASUMS file and one archive per platform, six of them for a Go project supporting 386 and amd64 on Darwin, Linux and Windows. Parallelism is the reason the tool exists, and the example is the case it exists for.

The project also publishes a demo repository showing a release page with a v1.0.0 tag and cross compiled Go binaries uploaded, so you can see the finished artefact rather than only reading about it.

Four flags for the same problem, and knowing which one you need

The options list is where the tool's real behaviour lives, and four of the flags are about the same situation, the release already existing.

-delete deletes the release and its git tag in advance if it exists, and the documentation says it is the same as -recreate. -replace replaces artifacts if they are already uploaded. -soft stops uploading if the same tag already exists. And -draft releases as draft, which the description annotates as unpublish.

Those are four different answers to one question and they are not interchangeable. -delete is destructive, it removes the tag as well as the release, which is the right choice for a release script that reruns from scratch on every tag. -replace is the one most people want in CI, since it uploads new artifacts over old ones without touching the tag. -soft is the safe option, it refuses rather than destroys. -draft is not about the conflict at all, it is about publishing later.

The rest of the flags set the release metadata. -t sets the GitHub API token, -u the GitHub username, -r the repository name, -c the target commitish which can be a branch, a commit or a SHA, -n the release title and -b the body text describing the contents of the release.

One flag is worth singling out. -p NUM sets the amount of parallelism, and the default is the number of CPU. That default is the right one for a workstation uploading a handful of archives from a local disk, and it is the wrong one for a CI runner with a slow network and a small number of large files, where a high concurrency can saturate the link without finishing any upload faster. Nothing in the documentation says that, so it is the kind of thing you learn by watching the timings.

There is also -generatenotes, which the project ties to a GitHub feature added in October 2021, the ability to generate the body of a Release automatically from a format specified in .github/release.yml. The flag turns that on so the body is added programmatically instead of being supplied manually.

Tokens, precedence, and pointing at a different GitHub

The token handling is the part to get right, because the failure without a token is silent in the sense that nothing happens at all.

You need a token from an account with enough permissions to create releases. The instructions are to visit the GitHub account settings, then Applications for the user, where you can create a token in the Personal access tokens section. For a private repository you need the repo scope and for a public repository you need public_repo.

There are three places to set it, and they have a precedence order. The GITHUB_TOKEN environment variable, the -token command line option, or a github.token property in the .gitconfig file.

bash
$ export GITHUB_TOKEN="....."
bash
$ git config --global github.token "....."

The note is that the environment variable takes precedence over the gitconfig value. That ordering is worth remembering, because a CI job that exports GITHUB_TOKEN will silently override a developer's global gitconfig token, which is usually the desired behaviour in CI and occasionally surprising on a laptop.

The gitconfig route is the interesting one, because it keeps the token out of the shell history and out of the process environment, and it uses a key namespace that does not collide with git's own settings. It is also the reason ghr reads .git/config at all, since it is already parsing that file for the repository URL.

For GitHub Enterprise, the same environment variable mechanism redirects the API endpoint.

bash
$ export GITHUB_API=http://github.company.com/api/v3/

So the enterprise story is a single environment variable rather than a separate build or a configuration file, which is a good sign for a tool that has to work inside an existing corporate release pipeline.

The GitHub Action, and the version default you should look at

ghr can be used as a GitHub Action in your workflows. The action downloads and runs ghr to create a GitHub Release and upload artifacts, and the basic usage is three lines.

yaml
- uses: tcnksm/ghr@v0
  with:
    tag: ${{ github.ref_name }}
    path: dist/

The inputs table is long and the defaults are the interesting part. The version of ghr to install defaults to v0.17.2, which is behind the v0.18.5 release listed in the repository, and it is a defaulted value rather than a required one, so a workflow that does not set it silently gets an older binary. The tag input is the only required one, and the path defaults to the current directory.

The other defaults follow the CLI flags one for one, which is the right design, because a workflow author who knows the command line already knows the action. The token input defaults to ${{ github.token }}, the workflow scoped token, so authentication needs no configuration. The boolean inputs for draft, prerelease, replace, delete, soft and generatenotes all default to false, and the latest input takes true, false or auto.

The cross compiled release example shows the full shape, and it is a good template because it is the standard Go release layout.

yaml
name: Release
on:
  push:
    tags:
      - "v*"
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
      - name: Build
        run: make crossbuild
      - uses: tcnksm/ghr@v0
        with:
          tag: ${{ github.ref_name }}
          path: dist/
          replace: "true"

Three details in it are worth copying. The permissions block grants contents: write, which is the minimum needed to create a release, and stating it explicitly is better than relying on the default token permissions. The go-version-file input points setup-go at go.mod rather than pinning a version, so the build matches the module. And replace is set to true, which is what you want in a tag push workflow that may rerun.

Where a small uploader stops being enough

Two limits, and the project names the first one itself.

The comparison section of the README is short. aktau/github-release can also create and edit releases and upload artifacts, and it has many options, while ghr is described as a simple alternative, and the sentence that follows is that ghr will parallelize upload artifacts.

So the honest framing is that ghr is not trying to be a release management platform. It does the one thing, uploads files to a release, and does it concurrently. If you need release editing, drafting workflows, changelog generation beyond the GitHub native feature, matrix builds, signing, attestations, or anything that involves deciding what should go into a release based on what changed, you need the larger tool, and the README is telling you so rather than pretending otherwise.

The second limit is the inference. ghr assumes it is run inside a git repository and reads the remote from .git/config. That is convenient in the overwhelmingly common case of releasing from the repository itself. It is wrong in the case of a release pipeline that builds in one directory and publishes from another, or a monorepo where the tag belongs to the root but the artifacts live in a subdirectory, or a CI job with a shallow or unusual checkout. In those cases you override with -u and -r, which is easy, but the tool will not work it out for you and the error will be a release created against the wrong repository.

A third, smaller thing. The tool is a Go program that talks to the GitHub API through github.com/google/go-github, and the module requires Go 1.26.0, which is a recent floor. That is not a problem for a release tool that is run by a build you control, but it is worth knowing before you drop it into an older CI image.

Editorial conclusion

ghr fits a team that cross compiles binaries, writes checksums and needs all of them attached to a GitHub Release in one step, and it fits a project that wants a release step in GitHub Actions without writing a curl script. It does not fit a team that needs the wider feature set of a full release management tool, and it does not fit a repository where the release and the git remote live in different places without you overriding the defaults. Check three things before you commit. Check the token scope, because a private repository needs the repo scope and a public one needs public_repo, and the token is read from the GITHUB_TOKEN environment variable, the -token flag or a github.token property in .gitconfig, with the environment variable taking precedence. Check how it behaves when the tag already exists, because the -delete, -replace, -soft and -draft flags are four different answers to that question and picking the wrong one either destroys a release or silently does nothing. And check the parallelism, since -p sets the amount of parallelism and defaults to the number of CPUs. The last push was on 2026-10-14, the newest release listed is v0.18.5 from 2026-09-22, and the license is MIT.

Frequently asked questions

How do I use ghr to create a GitHub Release?

Set a GitHub API token, change into your repository root directory, then run ghr with a tag and optionally a path. The path can be a file or a directory, and if you give a directory all files in it are uploaded, with the repository URL read implicitly from .git/config.

Where does ghr get the GitHub token from?

From the GITHUB_TOKEN environment variable, the -token command line option, or a github.token property in the .gitconfig file, with the environment variable taking precedence over the gitconfig value. A private repository needs the repo scope and a public one needs public_repo.

How do I make ghr work with GitHub Enterprise?

Set the GITHUB_API environment variable to your instance's API endpoint, for example http://github.company.com/api/v3/, and ghr will talk to that endpoint instead of the public one.

What happens if the release tag already exists when using ghr?

It depends on the flag. -delete deletes the release and its git tag first, the same as -recreate, -replace replaces artifacts that were already uploaded, -soft stops uploading if the same tag already exists, and -draft creates the release as a draft instead.

How do I use ghr in a GitHub Actions workflow?

Use the tcnksm/ghr@v0 action with a tag input, which defaults to the current ref name, and a path to the artifacts. The version input defaults to v0.17.2, so pin it explicitly if you need a newer binary, and the token input defaults to the workflow's github.token.

What license is ghr released under?

MIT, with a LICENSE file at the root of the repository. The newest listed release is v0.18.5 from 2026-09-22, and the last push was on 2026-10-14.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. tcnksm/ghr on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tcnksm-ghr.svg)](https://hysenlabs.com/projects/tcnksm-ghr)