Publish a GitHub Release with gh release create

A release you cannot reproduce is a release nobody trusts six months later. This guide creates a GitHub Release from a known tag using gh release create, with controlled notes and optional assets, checked at every step. The examples use GitHub CLI 2.87.3, installed on the machine used for this guide. Allow about 10 minutes for the checks and upload, longer if you still need to write release notes or build artefacts.

Before you start

You need a local Git checkout of the repository, an authenticated gh installation with permission to create releases, and a working tree whose commit and tag you have reviewed. Run these ordinary, non-privileged checks from the repository directory:

gh --version
git status --short
git branch --show-current
git log -1 --oneline
git tag --list 'v*' --sort=-version:refname | head

For this guide, replace OWNER/REPO with the repository you actually intend to change and choose a tag such as v1.2.3. The gh release create command uses the current repository by default. Use --repo OWNER/REPO when the checkout is not the repository you mean, or when you want the target to be unambiguous.

Checkpoint: Stop if the latest commit, branch, repository, or tag is not the one you expect. A release is a public repository change. It does not require sudo, and using elevated privileges will not fix a GitHub permission problem.

1. Choose how the tag is supplied

The positional <tag> is optional. Omit it and the command opens an interactive flow. For a repeatable release, provide the tag explicitly:

TAG='v1.2.3'
gh release create "$TAG" --repo OWNER/REPO

If that tag does not exist, GitHub CLI creates it from the latest state of the repository's default branch. That default is easy to miss: the commit you inspected locally is not necessarily the commit used for the new tag. To point automatic tag creation at a specific branch or full commit SHA, add --target:

gh release create v1.2.3 --repo OWNER/REPO --target release-candidate

A tag that already exists locally is not, by itself, proof that the remote has it. If the release must use an existing remote tag, add --verify-tag; the command then aborts instead of creating a missing tag:

gh release create v1.2.3 --repo OWNER/REPO --verify-tag

Checkpoint: Use --verify-tag for a release process that must never create a tag implicitly. Without it, a spelling error in the tag can create a different release line from the default branch.

2. Prepare the release notes

For a short, explicit note, pass --notes. Keep shell quoting around the text so punctuation and spaces reach gh as one argument:

gh release create v1.2.3   --repo OWNER/REPO   --verify-tag   --title 'Version 1.2.3'   --notes 'Fixes the import timeout and updates the documented defaults.'

For a longer note, use --notes-file or its short form -F. A file keeps the command readable and gives you a chance to review the exact text before publishing:

gh release create v1.2.3   --repo OWNER/REPO   --verify-tag   -F changelog.md

The file argument is local input. Do not put passwords, access tokens, private incident details or unpublished security information in it unless the repository's release policy explicitly allows that exposure.

GitHub CLI can generate a title and notes with --generate-notes. Add a short opening note with --notes, and use --notes-start-tag to choose the starting tag for generated notes. If you have an annotated Git tag, --notes-from-tag generates the notes from that tag instead:

gh release create v1.2.3   --repo OWNER/REPO   --verify-tag   --generate-notes   --notes-start-tag v1.2.2

Do not assume every commit message is suitable for publication just because it went into generated notes. Review the generated release page after creation.

3. Upload assets with a deliberate label

Append asset paths after the tag and notes options. A shell glob expands before gh runs, so check that it matches the files you intend to upload:

printf '%s\n' ./dist/*.tgz
gh release create v1.2.3   --repo OWNER/REPO   --verify-tag   --notes-file changelog.md   ./dist/*.tgz

To give one asset a display label, append # and the label to the same argument. Quote the whole argument so the shell does not split it:

gh release create v1.2.3   --repo OWNER/REPO   --verify-tag   --notes 'Source archive and checksums.'   './dist/project-1.2.3.tar.gz#Source archive'

Warning: The command changes remote state. Once published, the release and its assets may be consumed by package managers, automation and people. Check filenames, checksums and licensing before you press Enter. A draft is a safer rehearsal when the release is not ready for public consumption.

4. Use a draft or mark a prerelease

Add --draft to save the release as a draft instead of publishing it. This is useful when GitHub's release page should hold the notes and assets while someone performs a final review:

gh release create v1.2.3   --repo OWNER/REPO   --verify-tag   --draft   --generate-notes   ./dist/*.tgz

These labels affect how people discover and consume the release, so decide them separately from the version number.

If the command fails before reporting success, inspect the error and correct the tag, authentication, repository, notes or asset path before retrying. If a draft was created, finish or remove it from the GitHub release page after review. Do not rerun a successful command merely because its terminal output was brief: check the repository's Releases page or use the normal GitHub CLI release inspection command first.

5. Start a discussion when needed

If the repository has a discussion category and the release should open a discussion, pass its exact name with --discussion-category:

gh release create v1.2.3   --repo OWNER/REPO   --verify-tag   --notes-file changelog.md   --discussion-category 'General'

The category must exist and its spelling must match. If discussion creation is not part of the release plan, omit the option. A failed discussion setup can make an otherwise valid release command fail, so check the repository settings first.

6. Fetch an automatically created tag locally

When GitHub CLI created the tag for you, fetch tags after the release so the local checkout can refer to it:

git fetch --tags origin
git show --no-patch --decorate v1.2.3

The fetch updates local remote-tracking tag data. It does not rewrite your branch or working files. If your remote is not named origin, identify the configured remote with git remote -v and use the correct name. If the tag points at an unexpected commit, stop distribution and investigate before building follow-up artefacts.

Done means