Chapter 22Lesson 02~190 minutes

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.

Hands-onglabChangelogAsset linksCleanup

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 glab or the YAML release keyword.
  • 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.

Mutation guard: every destructive command must first prove that 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.

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.

Free path: role-based protected tags are documented on Free. More granular user/group protected-tag entries have tier-specific differences. If your account cannot safely create a second actor, simulate the authorization matrix locally and label the result as a simulation.

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?

What does deleting a release through the Releases API do to the Git tag?

Why does the lab use a full commit SHA in the asset URL?

An unauthorized actor gets 403 creating a protected release tag. What should you preserve before changing permissions?

A downloaded asset hash differs. Is rerunning the release job the right first action?

Next lesson

Configuration, design choices, and tradeoffs

Choose when to use the YAML release keyword, glab, or the API; how to protect tags; and how to balance generated notes, human curation, immutable distribution, authorization, and rollback.

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.

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.

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