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

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

Upload GitHub release assets safely with gh

You will upload one or more files to an existing GitHub Release with gh release upload, optionally give an asset a friendlier display label, and check that the command is aimed at the intended repository. 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

  • 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 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.
  • Each file argument is local input. A URL is not a local asset path for this command.
  • Do not use sudo. Authentication and GitHub permissions belong to your user account, and elevated local privileges do not grant release access.
  • Do not treat a successful HTTP response as proof of the right bytes. Verify the asset name, release tag and checksum.

Done means

  • The installed gh version and authenticated account were confirmed.
  • The release tag and repository were checked before the upload.
  • Every intended asset uploaded, with no accidental glob expansion.
  • Any display label was attached to the correct local file.
  • --clobber was avoided unless replacement was deliberate and recoverable.
  • The release asset was downloaded or inspected and its checksum matched the local build.