Tags, Releases, Release Notes, Assets, Changelogs, and Release Governance: Guided Hands-On Workflow and Core Operations
Build a disposable release from an annotated tag, attach and verify a harmless asset, inspect the hosted release object, and clean up without confusing release deletion with tag deletion.
Learning objectives
- Create a disposable repository and inspect its current release/tag state before mutation.
- Create and push an annotated tag that names an exact commit.
- Create a draft Release from the existing tag, attach/edit notes and a harmless asset, then publish and inspect metadata.
- Download and hash an uploaded asset and separately download a generated source archive.
- Use gh/API/Git evidence to prove which resource each operation changed.
- Delete only disposable releases/tags and verify that deleting one does not implicitly delete the other.
1. Create the disposable release laboratory
gh auth status --active --hostname github.com
# Create a public disposable repository and clone it.
gh repo create github-release-lab --public --clone --add-readme
cd github-release-lab
git status --short --branch
git remote -v
gh repo view --json nameWithOwner,visibility,defaultBranchRef,viewerPermission
The repository is the hosted container. At this point no release has been created. Confirm that before adding any tag:
gh release list --json tagName,name,isDraft,isPrerelease,isLatest,isImmutable
git tag --list
2. Create an annotated tag and prove its target before pushing
An annotated tag creates a tag object in addition to the ref. That gives the version a message and tagger metadata. For this lab, create a small commit, capture its OID, then tag exactly that commit.
printf "release lab v0.1
" > RELEASE-DEMO.txt
git add RELEASE-DEMO.txt
git commit -m "Add v0.1 release demo"
COMMIT_V01=$(git rev-parse HEAD)
git tag -a v0.1.0 "$COMMIT_V01" -m "Disposable v0.1.0 release"
# Read-only proof before publishing the tag.
git show --no-patch --decorate v0.1.0
git rev-list -n 1 v0.1.0
# Only after the target is verified:
git push origin main
git push origin refs/tags/v0.1.0
$COMMIT_V01 = git rev-parse HEAD and
Set-Content RELEASE-DEMO.txt "release lab v0.1". The
Git/tag semantics are identical.
3. Create the GitHub Release as a draft and refuse implicit tag creation
Use the already-pushed tag. --verify-tag is the
guardrail: if the remote tag is absent, the command aborts rather
than creating a tag at an implicit target.
cat > release-notes.md <<'EOF'
## v0.1.0
Disposable training release.
- Exact tag target verified before publication.
- Asset hash will be recorded before publish.
EOF
printf "artifact for v0.1.0
" > release-evidence.txt
gh release create v0.1.0 release-evidence.txt \
--draft \
--verify-tag \
--title "v0.1.0 — disposable lab" \
--notes-file release-notes.md
gh release view v0.1.0 --json tagName,name,isDraft,isPrerelease,isImmutable,targetCommitish,assets,url
The Release object is now a draft. The tag existed beforehand; the asset is a hosted Release asset; neither operation changes the commit graph.
4. Hash the asset, edit the notes, then publish
Compute the local SHA-256 before publication and place it in the release notes. On production releases this digest should be generated by the build/release system, not copied from memory.
# Git Bash/Bash/zsh
sha256sum release-evidence.txt
# Append the observed digest to release-notes.md, then update the draft.
gh release edit v0.1.0 --notes-file release-notes.md
# Publish the already-assembled draft.
gh release edit v0.1.0 --draft=false
gh release view v0.1.0 --json tagName,name,isDraft,isPrerelease,isImmutable,publishedAt,assets,url
Get-FileHash .\release-evidence.txt -Algorithm SHA256.
Use the observed value; do not paste a fake digest and call it
verification.
5. Download the uploaded asset and compare it with a source archive
mkdir -p downloads/assets downloads/source
# Uploaded Release asset
gh release download v0.1.0 -p 'release-evidence.txt' -D downloads/assets
sha256sum downloads/assets/release-evidence.txt
# GitHub-generated source archive for the same tag
gh release download v0.1.0 --archive=zip -D downloads/source
# Inspect file names/sizes rather than treating them as the same artifact.
ls -lh downloads/assets downloads/source
The uploaded text file is a Release asset you supplied. The source ZIP is generated from repository contents at the tag. GitHub’s immutable-release verification command cannot verify the generated source ZIP/tarball because those archives are created on request; use tag/commit identity for source and attestation/digest evidence for uploaded immutable assets.
6. Generate notes as a preview, then decide what belongs in the human-facing release
Generated notes are useful input, not an approval. You can create a
separate disposable tag/Release later with
--generate-notes, or use the web “Generate release
notes” control. A .github/release.yml file can control
categories and exclusions. For this first release, manually authored
notes make the mapping between evidence and prose easier to inspect.
# Read-only list gives structured state for audit evidence.
gh release list --json tagName,name,isDraft,isPrerelease,isLatest,isImmutable,publishedAt
# REST inspection of this specific hosted Release
gh api -H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
repos/{owner}/{repo}/releases/tags/v0.1.0 \
--jq '{id,tag_name,target_commitish,draft,prerelease,immutable,assets:[.assets[]|{name,size,digest}]}'
{owner} and {repo} are resolved by
gh api from the current repository when written in
braces.
7. Cleanup proves Release deletion and tag deletion are separate operations
github-release-lab. Preserve the evidence you want
first. Do not use this sequence on a consumed production version.
# Delete the GitHub Release object only.
gh release delete v0.1.0 --yes
# Prove the Git tag still exists remotely/local.
git ls-remote --tags origin 'refs/tags/v0.1.0*'
git tag --list v0.1.0
# Now delete the remote tag explicitly, then the local tag.
git push origin :refs/tags/v0.1.0
git tag -d v0.1.0
# Final proof
gh release list
git ls-remote --tags origin
git tag --list
gh release delete --cleanup-tag exists if you
intentionally want coupled cleanup, but keeping the two operations
separate in this lesson makes the resource boundary observable.
8. Challenge: choose the right control
Your team has already pushed v0.2.0 at a verified
commit and wants a Release only if that exact remote tag exists.
Which surface/control should you choose?
Use gh release create v0.2.0 --verify-tag .... Do not
use --target as a substitute for a pre-existing
verified tag, because the goal is to fail closed if release
preparation is incomplete.
9. Lesson summary
The workflow was deliberately two-phase: establish an exact Git tag first, then create a GitHub Release object around it. Drafts let you assemble notes/assets before publication. Uploaded assets and generated source archives are different artifact classes. Cleanup demonstrated that Release deletion and tag deletion are independent unless you explicitly couple them.
Knowledge check
Why did the lab create the tag before running
gh release create?
So the exact commit identity could be inspected first and
--verify-tag could fail closed instead of allowing
implicit tag creation.
After gh release delete v0.1.0 --yes, what should
you expect from git ls-remote --tags?
The tag should still exist unless you separately delete it or
used the explicit --cleanup-tag option.
What is the strongest evidence that the downloaded uploaded asset matches the local file in this non-immutable lab?
Compare an independently recorded SHA-256 digest. Immutable release attestations provide stronger platform-verifiable evidence when that control is enabled.
Why does publishing a Release not change your Git commit graph?
A Release is hosted metadata associated with a tag. The commit graph changes only through Git commit/ref operations, not by editing Release title/body/assets.
What permission class manages releases?
Repository collaborators/people with write access can create, edit, and delete releases; API tokens also need the documented contents/write permissions for mutation.
Further reading — current official GitHub sources
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.