Chapter 24Lesson 02~340 minutes

Releases, Tags, Release CLI, Evidence, Changelogs, and Deployment Traceability: Guided Hands-On Workflow and Core Operations

Create a disposable release from an exact commit, inspect it through Git/glab/API, attach only synthetic or durable package-style links, generate a controlled changelog, verify identity, and clean up safely.

Hands-onglabReleases APIAssetsChangelogVerification

Learning objectives

  • Inspect exact commit/tag/release state before creating anything.
  • Create a disposable tag and GitLab Release using UI/glab/API-safe patterns.
  • Attach synthetic or durable package-style links instead of fragile expiring job-artifact URLs.
  • Generate a controlled changelog from commits with Git trailers.
  • Verify commit, tag, release, asset identity, and evidence before cleanup.
Availability baseline (verified 2026-08-22 against current GitLab 19.3 documentation). Project Releases, Releases API, release asset links, automatic release-evidence snapshots, changelog generation, protected tags, and the GitLab CLI release/changelog commands have Free-compatible paths on GitLab.com, Self-Managed, and Dedicated. Creating or updating a Release requires at least Developer access, but a protected tag can impose a stricter tag-creation/deletion boundary. Current glab release delete documentation requires Maintainer-or-higher, so the fully scripted glab cleanup path is Maintainer-scoped; Developer-level learners can use the documented UI/Releases API path where permitted or simulate cleanup. On-demand evidence recollection is Premium/Ultimate and limited to Self-Managed/Dedicated; retaining report artifacts as release evidence is Ultimate. release-cli was deprecated in GitLab 18.0 and is scheduled for removal in 20.0; use glab or the Releases API for new automation. The mandatory labs use only a disposable project/tag, synthetic asset links or package/image coordinates, and no paid feature.

1. Disposable scenario and safety boundary

Create a private disposable GitLab project or a clearly isolated branch/tag in an existing throwaway project. The lab never publishes a real customer release, never points at production artifacts, and never uses a persistent access token unless you deliberately choose an optional API exercise.

Mandatory path: GitLab Free, Developer-or-higher access for create/update operations; use Maintainer-or-higher for the fully scripted glab release delete cleanup shown later, or use the documented UI/Releases API path/simulation where your role permits. Git + glab are used, and a runner is needed only for the optional CI extension. Use tag v0.24.0-lab, synthetic notes, and an example.invalid asset URL or a durable disposable package/image coordinate created in earlier chapter-style fixtures.

2. Preflight: prove project, branch, HEAD, tags, and release absence

Do not create a release until the intended source is recorded:

git fetch --tags --prune
git status --short
PROJECT_PATH="$(glab repo view -F json | python -c 'import json,sys; print(json.load(sys.stdin).get("path_with_namespace", ""))')"
HEAD_SHA="$(git rev-parse HEAD)"
printf "project=%s\nhead=%s\n" "$PROJECT_PATH" "$HEAD_SHA" | tee ch24-preflight.txt

TAG=v0.24.0-lab
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then echo "tag already exists" >&2; exit 1; fi
if glab release view "$TAG" -F json >/dev/null 2>&1; then echo "release already exists" >&2; exit 1; fi

If glab repo view output differs on your installed version, use the project URL/UI and record it manually. The required invariant is not the convenience command; it is unambiguous project/ref/SHA evidence.

3. Create two controlled changelog commits

Use harmless text files so the changelog range is deterministic:

git switch -c ch24/release-lab
printf "release lab base\n" > ch24-release.txt
git add ch24-release.txt
git commit -m "Add release lab fixture

Changelog: added"
BASE_SHA="$(git rev-parse HEAD)"

printf "release lab verification\n" >> ch24-release.txt
git add ch24-release.txt
git commit -m "Add release verification note

Changelog: fixed"
RELEASE_SHA="$(git rev-parse HEAD)"
printf "base=%s\nrelease=%s\n" "$BASE_SHA" "$RELEASE_SHA"

Push only the disposable branch if a hosted Release will be created from it.

4. Prediction ledger before mutation

Prediction Expected state
Tag v0.24.0-lab points to exactly RELEASE_SHA.
Release One Release object exists for that tag, with synthetic notes.
Assets One synthetic/durable link; no secret-bearing URL and no expiring job-artifact dependency in the mandatory path.
Evidence Automatic evidence file/evidence SHA becomes visible with the Release.
Deployment None is created merely by creating a Release.
Cleanup Release is deleted first, then disposable tag/branch; deleting the Release alone must leave the tag.

5. Create and independently inspect the Git tag

Create an annotated test tag so Git-native metadata is visible:

TAG=v0.24.0-lab
git tag -a "$TAG" "$RELEASE_SHA" -m "Disposable Chapter 24 release"
git show-ref --tags "$TAG"
git rev-list -n 1 "$TAG"
git cat-file -t "$TAG"
git show "$TAG" --no-patch --decorate

git rev-list -n 1 gives the commit reached by the tag. For an annotated tag, git show-ref can identify the tag object itself, so do not compare the tag-object SHA to the commit SHA by accident.

6. Push only the disposable tag

Push the branch first, then the tag. If a protected-tag rule rejects the push, preserve the rejection and do not weaken governance merely to finish the lab. Either choose a permitted disposable tag pattern or use the local/API simulation path.

git push -u origin ch24/release-lab
git push origin "refs/tags/$TAG"
# Re-read from the remote-tracking context afterward:
git ls-remote --tags origin "refs/tags/$TAG" "refs/tags/$TAG^{}"

7. Create the Release with glab

Use glab release create against the explicit tag. A release can also be created through the UI or POST /projects/:id/releases. Keep the notes synthetic:

cat > ch24-notes.md <<'EOF'
## Chapter 24 disposable release

- Source: controlled lab commits
- Artifact identity: synthetic only
- No production deployment
EOF

glab release create "$TAG" --name "Chapter 24 Lab $TAG" -F ch24-notes.md

If the Release already exists unexpectedly, stop and inspect instead of deleting it blindly. Existing state may belong to a previous lab attempt.

8. Verify Release metadata through glab and API

Read after write from two surfaces:

glab release view "$TAG" -F json > ch24-release.json
glab release list -F json --per-page 20 > ch24-releases.json
# API equivalent (do not print token values):
# GET /api/v4/projects/:id/releases/:tag_name

Compare release.commit.id with git rev-list -n 1 "$TAG". Record evidence_sha and the evidence file path if returned.

10. Generate a controlled changelog

Generate Markdown from the controlled commit range. The version must follow semantic versioning; because the lab tag contains a suffix, pass an explicit semantic version to the changelog command:

glab changelog generate --version 0.24.0 --from "$BASE_SHA" --to "$RELEASE_SHA" > ch24-generated-changelog.md
cat ch24-generated-changelog.md

Expected entries come from commits carrying the Changelog trailer. Review the generated Markdown; do not automatically treat it as customer-ready notes.

11. Optional CI creation path with job-scoped authentication

For a disposable tag pipeline, current GitLab supports glab authentication with the job token through CI auto-login:

release_with_glab:
  stage: release
  image: registry.gitlab.com/gitlab-org/cli:latest
  rules:
    - if: $CI_COMMIT_TAG == "v0.24.0-lab"
  variables:
    GLAB_ENABLE_CI_AUTOLOGIN: "true"
  script:
    - |
      if glab release view "$CI_COMMIT_TAG" -F json > release-existing.json; then
        echo "Release exists; compare identity before any update."
      else
        echo "Release lookup failed. Verify that the cause is a genuine not-found result, not authentication/authorization, before creating." >&2
        exit 1
      fi

This fixture demonstrates an inspect-then-create idempotence pattern. Do not set GITLAB_TOKEN equal to CI_JOB_TOKEN.

12. Verify the release chain independently

Produce one evidence file:

TAG_COMMIT="$(git rev-list -n 1 "$TAG")"
RELEASE_COMMIT="$(python - <<'PY2'
import json
print(json.load(open("ch24-release.json"))["commit"]["id"] )
PY2
)"
printf "tag_commit=%s\nrelease_commit=%s\n" "$TAG_COMMIT" "$RELEASE_COMMIT" | tee ch24-identity.txt
test "$TAG_COMMIT" = "$RELEASE_COMMIT"

If using a real disposable package/image link, also verify its package checksum or OCI digest from its registry API and compare that value with the asset ledger.

13. Challenge: choose the correct surface

Your team needs an immutable binary for a release, a human-readable summary, and a machine-readable audit snapshot. Which GitLab surfaces should own each?

Expected reasoning: store bytes in a durable package/OCI registry and verify checksum/digest; use the GitLab Release for human-facing version metadata and links; use release evidence plus pipeline/deployment/registry evidence for audit. Do not upload a binary only as an expiring job artifact and call the problem solved.

14. Cleanup in an evidence-preserving order

First save the final Release JSON, tag→commit mapping, asset ledger, changelog, and any evidence JSON URL/metadata. Then delete the Release and prove the tag still exists. Finally delete the disposable tag and branch.

glab release view "$TAG" -F json > ch24-release-final.json
glab release delete "$TAG" --yes  # current glab docs: Maintainer-or-higher
# Release deletion must not delete the Git tag:
git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null
# Now remove the disposable tag and branch:
git push origin ":refs/tags/$TAG"
git push origin --delete ch24/release-lab
# Verify absence:
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then echo "tag cleanup failed" >&2; exit 1; fi

If the tag is protected, follow the protected-tag UI/API permission model instead of bypassing protection. Never force-delete or weaken a production release tag rule for a lab.

Knowledge check

Why inspect the tag and Release independently after creation?

Why is an expiring job-artifact URL a weak long-lived release asset?

What is the safe CI job-token configuration for glab release operations?

After deleting a Release, should its tag still exist?

What should happen if a protected-tag rule blocks the lab tag?

Summary

The workflow created a controlled commit range, annotated tag, Release metadata, durable/synthetic asset identity, generated changelog, and independent identity checks. Cleanup proved the Release/tag deletion semantics instead of assuming them.

Official references

Primary sources used for the current GitLab 19.3 behavior taught in this lesson:

Next lesson

Configuration, Design Choices, and Tradeoffs

Turn the mechanics into a reusable release architecture: tag policy, idempotent automation, release-note ownership, durable assets, and evidence/deployment design.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.