Chapter 11Lesson 02~165 minutes

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.

Annotated tagsgh releaseAssetsVerification

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.
Mandatory path: GitHub.com, GitHub Free, one disposable public personal repository, GitHub CLI authenticated to your own account, and local Git. Release management requires write access. Immutable releases stay disabled for this lab so cleanup remains reversible.

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
PowerShell: use $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
PowerShell hash: 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

Destructive but disposable: run these commands only in 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?

After gh release delete v0.1.0 --yes, what should you expect from git ls-remote --tags?

What is the strongest evidence that the downloaded uploaded asset matches the local file in this non-immutable lab?

Why does publishing a Release not change your Git commit graph?

What permission class manages releases?

Next lesson

Next: Tags, Releases, Release Notes, Assets, Changelogs, and Release Governance: Configuration, Design Choices, and Tradeoffs

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.