Tags, Releases, Release CLI, Changelogs, Evidence, Asset Links, and Release-Orchestration Pipelines: Guided Hands-On Workflow and Core Operations
Build a disposable release workflow from an exact SHA, generate changelog notes, link immutable lab assets, inspect evidence, and clean up only an explicitly identified lab release.
Learning objectives
- Build a synthetic release candidate and record its exact SHA and SHA-256 before publication.
- Create a disposable Semantic-Version-style pre-release tag bound to the exact candidate SHA.
-
Generate release notes from a bounded commit range and publish a
release with
glabor the YAMLreleasekeyword. - Add and verify a safe asset link without depending on mutable “latest” state.
- Inspect the release/evidence/API response and clean up only the exact lab release and, separately, its lab tag.
1. Lab scenario and safety boundary
Create or use a disposable GitLab project named
glci-ch22-release-lab. The lab publishes only synthetic
text, uses a version prefix reserved for this chapter, and never
touches a production tag. If you cannot or do not want to mutate a
GitLab project, the local simulation path still exercises exact-SHA
tagging, changelog generation, digest evidence, release metadata,
superseding, and guarded deletion.
LAB_TAG begins with
v0.0.0-ch22-lab.. Never adapt the cleanup block to
latest, a wildcard, or an unverified user-supplied tag.
2. Preflight: record versions and non-secret identity
git --version
glab --version || true
python --version || python3 --version
git status --short
git remote -v
git rev-parse HEAD
printf 'CI source=%s ref=%s sha=%s pipeline=%s job=%s\n' \
"${CI_PIPELINE_SOURCE:-local}" "${CI_COMMIT_REF_NAME:-local}" \
"${CI_COMMIT_SHA:-$(git rev-parse HEAD)}" "${CI_PIPELINE_ID:-local}" "${CI_JOB_ID:-local}"
Record GitLab offering/tier if known, runner/executor if using CI,
and glab version. Do not print access tokens, job
tokens, or the entire environment.
3. Build a deterministic synthetic candidate once
The candidate is intentionally trivial so identity—not application complexity—remains visible. Commit it before release, then hash it. A release later refers to these bytes; it does not regenerate them.
mkdir -p release-assets evidence
printf 'GitLab CI/CD Chapter 22 disposable asset\n' > release-assets/demo.txt
sha256sum release-assets/demo.txt | tee evidence/SHA256SUMS
git add release-assets/demo.txt
git commit -m "Add Chapter 22 synthetic release asset
Changelog: added" || true
TARGET_SHA="$(git rev-parse HEAD)"
ASSET_SHA="$(sha256sum release-assets/demo.txt | awk '{print $1}')"
printf 'TARGET_SHA=%s\nASSET_SHA=%s\n' "$TARGET_SHA" "$ASSET_SHA" | tee evidence/identity.env
If the commit already exists, the git commit line can
be skipped. What matters is recording TARGET_SHA and
the asset digest before publication.
4. Create an exact disposable pre-release tag
Here “pre-release” means the lab uses a SemVer-style pre-release tag name; GitLab Releases do not need a separate prerelease flag for this lesson. The tag is created from the recorded SHA, not from an implicit moving branch.
LAB_ID="${CI_PIPELINE_ID:-$(date +%Y%m%d%H%M%S)}"
LAB_TAG="v0.0.0-ch22-lab.${LAB_ID}"
TARGET_SHA="${TARGET_SHA:-$(git rev-parse HEAD)}"
case "$LAB_TAG" in
v0.0.0-ch22-lab.*) ;;
*) echo "Refusing unexpected tag: $LAB_TAG" >&2; exit 2 ;;
esac
git tag -a "$LAB_TAG" "$TARGET_SHA" -m "Disposable Chapter 22 release $LAB_TAG"
git rev-parse "$LAB_TAG^{}"
test "$(git rev-parse "$LAB_TAG^{}")" = "$TARGET_SHA"
If using a real disposable GitLab project, push exactly this tag
after reviewing git show --no-patch "$LAB_TAG".
Protected-tag rules can be added only if you are authorized and
understand how they affect cleanup.
5. Generate bounded release notes
The GitLab changelog engine uses commit titles and trailers. In CI, give it an explicit previous tag/SHA and the target SHA so notes cannot drift with future commits.
PREVIOUS_REF="${PREVIOUS_REF:-$(git rev-list --max-parents=0 HEAD)}"
TARGET_SHA="${TARGET_SHA:-$(git rev-parse HEAD)}"
VERSION="${LAB_TAG#v}"
# GitLab-backed path when glab is authenticated:
glab changelog generate \
--from "$PREVIOUS_REF" \
--to "$TARGET_SHA" \
--version "$VERSION" \
> release-notes.md
printf '\nArtifact SHA-256: `%s`\n' "$ASSET_SHA" >> release-notes.md
Local faithful simulation: if glab is
unavailable, generate a short Markdown note from
git log "$PREVIOUS_REF..$TARGET_SHA" --format='- %h %s'
and append the exact artifact digest. Record that this is a
simulation, not GitLab’s changelog API output.
6. CI orchestration: build evidence first, release second
One safe shape is to produce the digest/notes in an earlier job and make the release job consume that evidence. The example assumes the tag already points to the exact candidate; it does not create a release from an unverified branch.
stages: [verify, release]
verify_release_candidate:
stage: verify
image: alpine:3.22
rules:
- if: '$CI_COMMIT_TAG =~ /^v0\.0\.0-ch22-lab\./'
script:
- sha256sum release-assets/demo.txt | tee SHA256SUMS
- printf 'tag=%s\nsha=%s\npipeline=%s\n' "$CI_COMMIT_TAG" "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" > release-evidence.txt
artifacts:
paths: [SHA256SUMS, release-evidence.txt]
expire_in: 7 days
publish_disposable_release:
stage: release
image: registry.gitlab.com/gitlab-org/cli:latest
needs:
- job: verify_release_candidate
artifacts: true
rules:
- if: '$CI_COMMIT_TAG =~ /^v0\.0\.0-ch22-lab\./'
variables:
GLAB_ENABLE_CI_AUTOLOGIN: "true"
script:
- glab --version
- test "$(git rev-parse HEAD)" = "$CI_COMMIT_SHA"
release:
tag_name: "$CI_COMMIT_TAG"
name: "Disposable $CI_COMMIT_TAG"
description: "release-evidence.txt"
Current docs require a script even when the job’s main
purpose is the release stanza. The release action runs
only after that script succeeds.
7. Add a safe asset link
For the lab, link to the committed synthetic file by its full commit SHA. This proves the difference between a versioned pointer and a moving branch URL. For production binaries, Chapter 23 replaces this teaching link with registry/package identities designed for distribution.
RAW_URL="${CI_PROJECT_URL}/-/raw/${CI_COMMIT_SHA}/release-assets/demo.txt"
ASSET_JSON=$(printf '[{"name":"demo.txt @ %s","url":"%s","link_type":"other"}]' \
"$CI_COMMIT_SHORT_SHA" "$RAW_URL")
GLAB_ENABLE_CI_AUTOLOGIN=true \
glab release create "$CI_COMMIT_TAG" \
--ref "$CI_COMMIT_SHA" \
--assets-links "$ASSET_JSON"
Because glab release create updates an existing release
when given new information, use it intentionally. Do not mix this
update step with the YAML release keyword and then
assume both have identical idempotency semantics.
8. Inspect release identity and evidence before any cleanup
# Human-readable CLI view
GLAB_ENABLE_CI_AUTOLOGIN=true glab release view "$LAB_TAG" --repo "$CI_PROJECT_PATH"
# API view with the job token; save response, do not print the token
curl --fail --silent --show-error \
--header "JOB-TOKEN: $CI_JOB_TOKEN" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/releases/$LAB_TAG" \
| tee evidence/release.json
# Verify the tag still names the candidate SHA
test "$(git rev-parse "$LAB_TAG^{}")" = "$TARGET_SHA"
# Re-fetch the exact-SHA asset and verify bytes
curl --fail --location "$RAW_URL" -o evidence/downloaded-demo.txt
printf '%s %s\n' "$ASSET_SHA" evidence/downloaded-demo.txt | sha256sum -c -
The evidence packet should retain the pipeline/job IDs, exact tag, commit SHA, release response/evidence path, link URL, and both producer and consumer digest checks.
9. Authorization exercise: prove one denied action
In an authorized disposable project, configure a protected-tag
pattern for a throwaway namespace only if you have permission.
Attempt release/tag creation as a role that is not allowed. Preserve
the 403 or authorization error and the protected-tag
rule. Do not “fix” the lesson by broadening a PAT or granting global
Maintainer access.
10. Exact cleanup: release and tag are separate resources
The Releases API deletes a release without deleting its associated tag. Conversely, GitLab documents that deleting a tag associated with a release also deletes the release. Make the choice explicit and prove the exact identifier before either operation.
case "$LAB_TAG" in
v0.0.0-ch22-lab.*) ;;
*) echo "Refusing cleanup for non-lab tag: $LAB_TAG" >&2; exit 2 ;;
esac
# First confirm the exact release/tag/SHA you are about to touch.
printf 'cleanup tag=%s sha=%s\n' "$LAB_TAG" "$(git rev-parse "$LAB_TAG^{}")"
test "$(git rev-parse "$LAB_TAG^{}")" = "$TARGET_SHA"
# Delete ONLY the GitLab release record (tag remains):
curl --fail --request DELETE \
--header "JOB-TOKEN: $CI_JOB_TOKEN" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/releases/$LAB_TAG"
# Tag deletion is a separate, deliberate authorized action.
# Example from an authorized workstation after review:
# git push origin ":refs/tags/$LAB_TAG"
Never substitute /permalink/latest for
$LAB_TAG in a destructive call.
11. Challenge: choose the right layer
The release page exists and notes are correct, but a consumer’s SHA-256 differs from the manifest. Should you retry the release job, edit notes, rotate a token, rebuild, or investigate distribution identity? The correct first layer is artifact/asset provenance: preserve the existing release and download evidence, compare the exact asset URL and digest, and identify whether the linked target was mutable. Do not rebuild because that destroys the evidence of what consumers actually received.
12. Verification checklist
- Tag resolves to the recorded
TARGET_SHA. - Artifact SHA-256 was recorded before publication.
- Release notes use a bounded commit range.
- Release creator/tool identity and pipeline/job IDs are recorded.
- Asset URL contains an immutable SHA/version or equivalent identity.
- Consumer re-download matches the expected digest.
- Cleanup is guarded by exact tag prefix and SHA.
Knowledge check
Why build the artifact before the release job?
So the release promotes already verified bytes. Rebuilding in the release step can produce different bytes and breaks provenance.
What does deleting a release through the Releases API do to the Git tag?
It leaves the tag intact. Tag deletion is a separate action.
Why does the lab use a full commit SHA in the asset URL?
To make the URL resolve to immutable repository content instead of a moving branch.
An unauthorized actor gets 403 creating a protected release tag. What should you preserve before changing permissions?
The actor/role, protected-tag rule, exact tag, request/API response, pipeline/job ID, and target SHA.
A downloaded asset hash differs. Is rerunning the release job the right first action?
No. Preserve the failure and diagnose asset identity/storage. A rerun can overwrite useful evidence or create a different object.
Version and compatibility note
GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.
Official references and version notes
Documentation verification date: 2026-09-12.
Releases, changelogs, the Releases API, release links, base release
evidence, and role-based protected tags are documented across
Free/Premium/Ultimate unless a narrower tier is explicitly called
out. The current YAML release flow uses
glab; current docs require glab 1.58.0 or
later. release-cli was deprecated in GitLab 18.0 and is
planned for removal in 20.0. The mandatory lab remains disposable.
It uses no real credentials in examples and treats production-tag
protection or organization-wide release policy as optional,
authorized extensions.
- Releases — official reference.
- CI/CD YAML release keyword — official reference.
- GitLab Release CLI migration — official reference.
- glab release create — official reference.
- Changelogs — official reference.
- Release evidence — official reference.
- Project Releases API — official reference.
- Release links API — official reference.
- Protected tags — official reference.
- CI job token — official reference.
Current assumptions used in this chapter: Version-sensitive YAML, Runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations must be verified against the exact GitLab, GitLab Runner, tool, and external-system versions used in production. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for reproducible work.
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.