GitLab Releases and Changelog Automation From Your Pipeline
Hand-written release notes rot fast. Here is how I generate GitLab Releases, changelogs, and release evidence from CI, with AI summarizing the commits.
- #gitlab
- #ci-cd
- #releases
- #changelog
For two years our “release notes” were a Slack message someone typed from memory after a deploy. They were always incomplete, often wrong, and impossible to audit later when someone asked “when did we ship that fix?” GitLab has a proper Release object — tied to a tag, with assets, links, and an auto-generated changelog — and once I moved release notes into the pipeline, they became accurate by construction. Here’s the whole automated flow, including the one place AI genuinely shines: turning a wall of commit messages into prose a human wants to read.
A GitLab Release is more than a tag
A Git tag marks a commit. A GitLab Release is a richer object attached to that tag: a title, a description (your notes), downloadable assets, links to artifacts, and milestone associations. It shows up in the Releases page and the project’s deploy history. The release-cli tool creates them from CI:
create-release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- echo "Creating release for $CI_COMMIT_TAG"
release:
tag_name: "$CI_COMMIT_TAG"
name: "Release $CI_COMMIT_TAG"
description: "./release-notes.md"
assets:
links:
- name: "Container image"
url: "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
The release: keyword is what release-cli reads. The job only runs on tag pipelines (if: $CI_COMMIT_TAG), so tagging a commit is what triggers a release. The description can be inline text or a path to a file you generated earlier in the pipeline.
Auto-generating the changelog
GitLab can build a changelog from commit messages following conventional-commit trailers, via the API or the git integration. The simplest path is the changelog API, which you can call from CI:
generate-changelog:
stage: prepare
image: curlimages/curl:8.8.0
rules:
- if: $CI_COMMIT_TAG
script:
- |
curl --request POST \
--header "PRIVATE-TOKEN: $CHANGELOG_TOKEN" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/repository/changelog" \
--data "version=$CI_COMMIT_TAG"
This appends a categorized changelog (features, fixes, etc.) to your CHANGELOG.md based on commit trailers. It relies on disciplined commit messages — feat:, fix:, with Changelog: added trailers — which is the catch. If your history is messy, the auto-changelog is messy too.
Pro Tip: The $CHANGELOG_TOKEN is a project access token with api scope, stored as a masked, protected CI/CD variable. Never inline it, and never paste it into a chat to “help debug the API call.” Share the curl command with the token redacted instead.
Where AI turns commits into readable notes
The auto-changelog gives you a list. What humans actually want is a narrative: “This release fixes the login timeout under load and cuts cold-start time by 40%.” That summarization — many terse commit messages into a short, accurate paragraph — is the single best use of AI in the release process.
The workflow I use: the pipeline collects the commit log into a file, and I (or a reviewer) feed it to an AI assistant to draft the human-facing notes, which then become the release description. AI is a fast junior engineer here — it reads fifty commits faster than I can and produces a clean first draft. But it will occasionally invent a feature that isn’t there or misstate the severity of a fix, so the draft is always reviewed against the actual diff before it ships. Release notes are a public, permanent record; an AI hallucination in them is embarrassing and sometimes misleading about what changed.
collect-commits:
stage: prepare
rules:
- if: $CI_COMMIT_TAG
script:
- git log "$(git describe --tags --abbrev=0 HEAD^)..HEAD" --pretty=format:"- %s" > commits.txt
artifacts:
paths:
- commits.txt
expire_in: 1 week
That commits.txt is the raw material for the AI-drafted notes. The diff between the previous tag and HEAD is exactly the context the model needs.
Attaching evidence and assets
For anything regulated or just well-run, attach evidence to the release: the SBOM, scan results, the signed container digest. release-cli links assets by URL, and you point those at artifacts from earlier jobs:
release:
tag_name: "$CI_COMMIT_TAG"
name: "Release $CI_COMMIT_TAG"
description: "./release-notes.md"
assets:
links:
- name: "SBOM"
url: "$CI_PROJECT_URL/-/jobs/$SBOM_JOB_ID/artifacts/raw/sbom.json"
- name: "Signed image digest"
url: "$CI_PROJECT_URL/-/jobs/$SIGN_JOB_ID/artifacts/raw/digest.txt"
Now the release page is a single, auditable record: what shipped, the notes, the SBOM, and the signed digest. Six months later when someone asks “what was in 2.3.0 and was it scanned,” the answer is one click, not an archaeology project.
Tying it together
The full release pipeline, triggered by a tag:
stages: [prepare, release]
collect-commits:
stage: prepare
rules: [{ if: "$CI_COMMIT_TAG" }]
script:
- git log "$(git describe --tags --abbrev=0 HEAD^)..HEAD" --pretty=format:"- %s" > release-notes.md
create-release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
needs: ["collect-commits"]
rules: [{ if: "$CI_COMMIT_TAG" }]
script:
- echo "Releasing $CI_COMMIT_TAG"
release:
tag_name: "$CI_COMMIT_TAG"
name: "Release $CI_COMMIT_TAG"
description: "release-notes.md"
For a fully polished narrative, I insert the AI-drafting step between these two and review before the release job runs.
Triggering releases the right way
How you trigger the release job matters as much as the job itself. The cleanest pattern is tag-driven: a maintainer pushes a tag, and the tag pipeline runs the release. But who creates the tag, and how? Letting any developer push arbitrary tags means anyone can cut a release, which is rarely what you want for a customer-facing artifact.
I protect release tags the same way I protect branches. In Settings → Repository → Protected tags, restrict who can create tags matching v* to Maintainers. Now the release surface is governed: only authorized people can trigger the release pipeline, and the tag itself is the audit record of who shipped what.
For teams that automate the bump, a separate “prepare release” job opens an MR that updates the version file and, once merged, a maintainer creates the protected tag:
prepare-release:
stage: prepare
rules:
- if: $CI_PIPELINE_SOURCE == "web" # triggered manually from the UI
script:
- ./scripts/bump-version.sh "$RELEASE_LEVEL"
- git push -o merge_request.create origin "release/$RELEASE_LEVEL"
This keeps a human in the loop at the moment that matters — tag creation — while automating the tedious version-file editing around it. The release becomes a deliberate, governed act with a clear trail, not something that fires whenever a branch happens to merge.
The rules that keep this safe
AI is the fast junior on this task: brilliant at summarizing commit history into readable notes, useless at knowing what’s actually true. So every generated note is reviewed against the real diff before it’s published — non-negotiable, because the release is permanent and public. And the token rule is absolute: the changelog and release API tokens are masked CI/CD variables, never inlined, never shared with a chat. For a careful pass over the generated notes and the pipeline diff, our code review dashboard is the right second set of eyes.
My reusable prompt: “Here is the commit log between the last two tags. Draft concise, user-facing release notes grouped into Features, Fixes, and Internal. Do not invent anything not present in the commits; if a commit message is ambiguous, list it verbatim under ‘Other’ rather than guessing.” That guardrail clause cuts hallucinations sharply. Find more in my prompt library and the release-focused prompt packs.
Conclusion
Move release notes into the pipeline and they become accurate by construction: tag a commit, auto-generate the changelog, attach the SBOM and signed digest, and let AI turn the commit log into prose a human will actually read — then review that prose against the diff before it ships. Keep your API tokens in masked variables and out of every chat window. More guides live in the GitLab CI/CD category.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.