Home / Alt manpages / gh-release-upload(1)

  • gh-release-upload(1)
  • User command
  • linux

Upload GitHub Release Assets Safely with gh

The build finished and someone is waiting for the binary: gh release upload attaches files to an existing GitHub release. This guide checks the target first. Allow about ten minutes for a single upload, plus the time needed for GitHub to receive a large file.

This guide uses GitHub CLI 2.87.3, installed on this machine. The command talks to GitHub, so the examples below are ready to adapt but deliberately use placeholder repository names and files. Uploading an asset changes remote release state. There is no undo built into this command: recovery means deleting the uploaded asset or uploading a corrected replacement with a deliberate decision about its name.

1. Confirm the installed command and authentication

Start by checking which executable will run and which version it provides:

$ command -v gh
/home/linuxbrew/.linuxbrew/bin/gh
$ gh --version
gh version 2.87.3 (2026-02-23)

Check your account before selecting a release. This is read-only and does not upload anything:

$ gh auth status
Logged in to github.com account YOUR_ACCOUNT

The exact status text depends on the host and account. If authentication is missing or the reported account is wrong, stop here and fix that context with your normal gh auth workflow. Do not compensate for an uncertain account by adding --clobber or by guessing a repository.

Checkpoint

You have the expected gh version and the expected GitHub account is active.

2. Identify the release tag and asset files

The first argument after upload is the release tag, not a release title. The remaining arguments are paths to files. Check both before sending anything:

$ gh release list --repo OWNER/REPOSITORY
$ test -r dist/PROJECT-VERSION.tar.gz && echo "archive is readable"
archive is readable
$ sha256sum dist/PROJECT-VERSION.tar.gz

Replace OWNER/REPOSITORY, PROJECT-VERSION.tar.gz and v1.2.3 with real values. If the repository is already the current directory's remote, you can omit --repo; using it explicitly makes a copy-and-paste command easier to audit. A path containing spaces must be quoted. A missing or unreadable file should be fixed before the upload is attempted.

3. Upload an asset without replacing an existing one

Use the tag and file path together:

$ gh release upload v1.2.3 dist/PROJECT-VERSION.tar.gz --repo OWNER/REPOSITORY
https://github.com/OWNER/REPOSITORY/releases/download/v1.2.3/PROJECT-VERSION.tar.gz

The URL is representative of the successful output shape. The owner, repository, tag and file name will be yours. The command uploads the file as an asset on that release. It does not create the release or publish a new tag.

To upload several assets in one command, list each path after the tag:

$ gh release upload v1.2.3 \
    dist/PROJECT-VERSION.tar.gz \
    dist/PROJECT-VERSION.sha256 \
    --repo OWNER/REPOSITORY

Keep the paths explicit when a release contains files from different build directories. Shell expansion such as dist/* can include debug packages, temporary files or a second architecture that was not meant for publication.

Checkpoint

The command returned an asset URL, and the uploaded file's checksum matches the value you checked locally.

4. Set a display label when the file name is not reader-friendly

GitHub CLI lets you append text beginning with # to a file argument. The part before the marker is the local path; the part after it becomes the asset's display label. Quote the complete argument so the shell does not treat the label as separate syntax:

$ gh release upload v1.2.3 \
    'dist/PROJECT-VERSION-linux-amd64.tar.gz#Linux x86-64 binary' \
    --repo OWNER/REPOSITORY

The uploaded content still comes from the local file. The label does not rename the file on disk. Choose a label that describes the platform or format without hiding what the asset is. If a label contains a shell-sensitive character, quoting the whole argument is still the safest habit.

5. Handle an asset name that already exists

Without --clobber, an existing asset name is a useful guard against accidentally replacing a published build. Treat a conflict as a review point: inspect the release and compare checksums before deciding whether a replacement is correct.

$ gh release view v1.2.3 --repo OWNER/REPOSITORY
$ sha256sum dist/PROJECT-VERSION.tar.gz
$ gh release upload v1.2.3 dist/PROJECT-VERSION.tar.gz --repo OWNER/REPOSITORY
HTTP 422: already_exists

The final error text can vary with the installed version and API response. The important result is a non-zero status and an existing asset that was not silently replaced.

Warning

--clobber deletes the existing asset before uploading the new one. The installed manual and command help warn that the original is lost if the upload fails. Use it only after checking the local file, the release tag and the target repository:

$ sha256sum dist/PROJECT-VERSION.tar.gz
$ gh release upload v1.2.3 dist/PROJECT-VERSION.tar.gz \
    --clobber --repo OWNER/REPOSITORY

This is not an atomic replacement. If the command fails after deletion, recover by rerunning the upload with a verified file. If the new file is wrong, upload the known-good copy again with --clobber. Keep a local copy until the release page and checksum have been checked.

6. Verify the release after uploading

Read the release and list its assets after every upload, especially after a clobber operation:

$ gh release view v1.2.3 --repo OWNER/REPOSITORY
$ gh release download v1.2.3 \
    --pattern 'PROJECT-VERSION.tar.gz' \
    --dir /tmp/gh-release-check
$ sha256sum /tmp/gh-release-check/PROJECT-VERSION.tar.gz
LOCAL_CHECKSUM  dist/PROJECT-VERSION.tar.gz
DOWNLOADED_CHECKSUM  /tmp/gh-release-check/PROJECT-VERSION.tar.gz

Replace LOCAL_CHECKSUM and DOWNLOADED_CHECKSUM with the actual values from the two commands; they should match. The download directory is a temporary verification location. Remove only that directory when you have finished checking it, and do not remove your source build or the release asset as part of cleanup.

Common traps

  • Title used as tag: a tag is required. A release title such as Project 1.2.3 is not a substitute for the actual tag, often v1.2.3.
  • Repo flag misused as a path: --repo uses [HOST/]OWNER/REPO. It is the right option for an upload from outside the repository or to GitHub Enterprise, not a local directory path.
  • URL passed as a file: each file argument is local input. A URL is not a local asset path for this command.
  • Reaching for sudo: authentication and GitHub permissions belong to your user account, and elevated local privileges do not grant release access.
  • Trusting the HTTP status alone: a successful response is not proof of the right bytes. Verify the asset name, release tag and checksum.

Done means

  • Version and account confirmed: the installed gh version and authenticated account were confirmed.
  • Target checked: the release tag and repository were checked before the upload.
  • No stray files: every intended asset uploaded, with no accidental glob expansion.
  • Labels attached correctly: any display label was attached to the correct local file.
  • Clobber used deliberately: --clobber was avoided unless replacement was deliberate and recoverable.
  • Checksum matched: the release asset was downloaded or inspected and its checksum matched the local build.