fix(release): preserve backtick-delimited content via --notes-file
Summary
The release catalog component drops backtick-delimited content from
every release-page description. The cause is shell eval: the
release: keyword's description: field is shell-eval'd before glab
sees it, and backticks become command substitution. This MR drops the
release: keyword and invokes glab release create --notes-file
directly with the annotated tag body, mirroring the pattern this
catalog already uses for its own create-release job.
The fix also incidentally repairs re-run behavior: glab release create updates an existing release by default, but the release:
keyword wrapper always passes --no-update and fails on retry. By
calling glab directly we inherit the updating default.
The bug
templates/release/template.yml carries the broken description-from-
variable form:
release:
tag_name: $CI_COMMIT_TAG
name: "${CI_PROJECT_TITLE} ${CI_COMMIT_TAG}"
description: "${CI_COMMIT_TAG_MESSAGE}"Trace from kaniko v1.0.1 (job 14510637450, https://gitlab.com/gitlab-com/public-sector/kaniko/-/jobs/14510637450):
/bin/sh: eval: line 198: release-create: not found
/bin/sh: eval: line 198: release: not found
/bin/sh: eval: line 198: vd+.d+.d+: not found
/bin/sh: eval: line 198: refs/tags/v1.0.1: not found
/bin/sh: eval: line 198: :v1.0.1: not foundReproduced locally with a minimal shell harness — every backticked
token in the tag body is parsed as a command lookup and stripped from
the description that reaches the release page; the dot/backslash
mangling (vd+.d+.d+ from `^v\d+\.\d+\.\d+$`) is the giveaway
that double-quoted variable substitution + shell eval is in the loop.
Secondary failure: re-running a release job on the same tag hit
release for tag "v1.0.1" already exists and --no-update flag was specified. The release: keyword always invokes glab with
--no-update; consumers couldn't retry without manually deleting the
release page first.
Candidate fixes considered
A — Keep release: keyword, point description: at a file path.
The release:description: keyword reads file contents when the value
is a single word matching an existing file in $CI_PROJECT_DIR
(release-cli's behavior, inherited by the glab-backed release:
keyword). This would have worked, but it does not unblock the
--no-update retry failure: the keyword wrapper always passes
--no-update. Rejected.
B — Drop the keyword, invoke glab release create --notes-file
directly. Bypasses the eval entirely (file bytes go through
unparsed) AND drops --no-update, so re-runs refresh the existing
release instead of failing. Mirrors this catalog's own
.gitlab-ci.yml create-release job, which has used glab release create --notes-file /tmp/release-notes.md since v2.x without
incident. Chosen.
C — Pre-process CI_COMMIT_TAG_MESSAGE to escape backticks.
Inelegant; preserves the foot-gun for the next escape character that
trips eval ($, ", \). Rejected.
Chosen approach
templates/release/template.yml now:
- Sets
GLAB_ENABLE_CI_AUTOLOGIN=trueso glab authenticates viaCI_JOB_TOKENthrough the gitlab.JobTokenAuthSource path (same as the catalog's own release job). - Fetches the annotated tag (
git fetch origin refs/tags/<tag>:refs/tags/<tag> --force) so%(contents)is available on shallow clones. - Reads the body via
git tag -l --format='%(contents)', strips any PGP signature block (forgit tag -ssigned tags), trims trailing blank lines, writes to/tmp/release-notes.md. - Warns (non-fatal) if the body is empty so lightweight-tag misuse shows up in the job log.
- Calls
glab release create "${CI_COMMIT_TAG}" --name "..." --notes-file /tmp/release-notes.md --ref "${CI_COMMIT_SHA}". Updates existing releases by default.
No input surface change. The upload-release-assets job is
unchanged.
Lab validation
-
Local backtick reproduction — built a minimal
shharness that emulates thedescription: "${VAR}"evaluation path. With the broken pattern, a tag body containing`release-create`,`^v\d+\.\d+\.\d+$`, and`refs/tags/v1.0.1`produced the exact trace lines seen in the kaniko job (thevd+.d+.d+token in particular is diagnostic). With the file-based pattern (printf '%s\n' "${VAR}" > /tmp/notes.md; cat /tmp/notes.md), the body round-trips byte-for-byte. -
python3 scripts/lint-templates.py— all 17 templates pass the v2 canonical shape; the modifiedrelease/template.ymlis green. -
glab ci lint .gitlab-ci.yml— root pipeline yaml is valid. -
Consumer-project lab — pending pre-push approval. Plan: pin a throwaway consumer project to this branch's SHA, push a tag whose annotation body contains backtick-delimited code spans, observe the release page renders the spans intact, retry the job and confirm the release page is refreshed without a
release already existsfailure. Will surface results in the MR before merge.
Breaking change assessment
Not breaking for consumers on v3.x:
- Input surface unchanged:
stage,job_name,package_name,binary_job,attest_job,container_image— same names, same defaults. - Job names unchanged:
create-release(override-able viajob_name) +upload-release-assets. - Output unchanged: release page with the same description, name, tag, and asset links.
- New observable behavior — re-runs on existing tags now succeed
(previously failed). This is strictly additive. Existing consumers
that worked before continue to work; consumers that hit the
--no-updatefailure now pass through.
Lands under [Unreleased] → ### Fixed per Keep a Changelog. Will
ship in the next minor (3.4.0 per the in-flight feat/v3.4.0-prep
branch, or a backport to a 3.3.x patch if the team decides the
backtick fix can't wait for the v3.4.0 cut). Recommend the team treat
this as a patch release given the strictly additive behavior
change; the new behavior is closer to "what consumers always
expected" than to "new contract surface".
Reference cross-link
Per the public-sector reference's
standards/release SKILL,
clause 3: "Surfaces the tag body verbatim as the release
description. The pipeline component reads CI_COMMIT_TAG_MESSAGE
and passes it straight through; no template lives in the pipeline."
The pre-fix behavior violated this clause silently — the body
reached the page minus its backticked content. The fixed component
restores the verbatim contract.
The SKILL itself does not need updating; its inline-fallback example
block still works for projects that haven't migrated to the catalog
component (the failure mode is the same upstream, and the workaround
is the same — switch to a script:-based glab release create --notes-file call). Worth a follow-up to the SKILL to call out the
backtick foot-gun in the inline fallback prose, but out of scope for
this MR.
Known consumers
pipeline/release is included at v3.0.0 by these public-sector
projects (verified by grep across the local mirror of the
public-sector estate; namespace-resolved hits):
assay—gitlab.com/gitlab-com/public-sector/pipeline/release@v3.0.0manifold—$CI_SERVER_FQDN/gitlab-com/public-sector/pipeline/release@v3.0.0postern—$CI_SERVER_FQDN/gitlab-com/public-sector/pipeline/release@v3.0.0tach—$CI_SERVER_FQDN/gitlab-com/public-sector/pipeline/release@v3.0.0
And one project on the older namespace path:
posture—gitlab.com/gitlab-com/public-sector-tools/pipeline/release@v2.5.0
kaniko does not include the catalog component; it carries its
own inline release-create job in .gitlab-ci.yml lines 466-497
with the same release: description: '$CI_COMMIT_TAG_MESSAGE' bug.
The catalog fix here does not propagate to kaniko automatically.
Follow-up MR on kaniko required to either adopt the catalog
component or apply the same --notes-file pattern inline.
Test plan
- Pin a consumer project (suggest: tach on a throwaway branch) to
this MR's HEAD SHA and push a
v0.0.0-bt-testannotated tag with a body containing backtick-delimited code spans (`vX.Y.Z`, fenced blocks, refs). - Confirm the release page renders the backticked content
verbatim — no
evallines in the job log, no missing prose on the page. - Retry the same release job. Confirm it succeeds with "release
updated" semantics instead of failing with
--no-update. - Confirm
upload-release-assetsstill attaches the package registry links (regression check — the asset-link API is unchanged but theneeds: <job_name>dependency now waits on a job that runs more shell, so dependency ordering matters). - Once the v3.4.0 prep branch lands or a patch tag is cut,
bump
kaniko(separate MR) to either consume the catalog component at the new tag OR apply the same--notes-filepattern inline.
Out of scope
- Updating consumer pins. Each consumer bumps to the new tag on its own cadence per the catalog's normal Renovate flow.
- Fixing the kaniko inline
release-createjob. Tracked above as a follow-up MR. - Updating
standards/releaseSKILL prose to call out the backtick foot-gun in the inline fallback example. Tracked as a follow-up.
Commits on this branch:
fix(release): preserve backtick-delimited content via --notes-filedocs(changelog): release backtick fix + re-run idempotency