getTagsNotes fails when multiple tags on same commit have different notes refs
Description
When two tags point to the same commit and each has a git note under a different refs/notes/semantic-release-* ref, getTagsNotes() produces unparseable JSON, causing semantic-release to silently ignore the tag's channel information.
Reproduction
Setup
- Branch config:
staging(prerelease:beta) anddevelop(prerelease:alpha) - A commit is released on both branches (e.g., via merge or backmerge), producing two tags on the same commit:
v2.9.0-beta.13→ note inrefs/notes/semantic-release-v2.9.0-beta.13:{"channels":["staging"]}v2.9.0-alpha.1→ note inrefs/notes/semantic-release-v2.9.0-alpha.1:{"channels":["develop"]}
What happens
getTagsNotes() in lib/git.js runs:
git log --tags="*" --decorate-refs="refs/tags/*" --no-walk --format="%d%x09%N" --notes="refs/notes/semantic-release*"When multiple --notes refs match for the same commit, git concatenates the notes with newlines in %N:
(tag: v2.9.0-beta.13, tag: v2.9.0-alpha.1) {"channels":["develop"]}
{"channels":["staging"]}The tab split gives notePart = '{"channels":["develop"]}\n{"channels":["staging"]}', which is not valid JSON. JSON.parse() throws, the catch block silently drops the error, and both tags get no entry in the map → default to channels: [null].
Result
- The prerelease tag (e.g.,
v2.9.0-beta.13) is invisible to its channel becauseisSameChannel('staging', null)returnsfalse - semantic-release falls back to an older tag (e.g.,
beta.12) and tries to recreatebeta.13 - Fails with:
fatal: tag 'v2.9.0-beta.13' already exists - The release is permanently stuck — retrying doesn't help since the tag conflict persists
Root cause
getTagsNotes() assumes %N produces a single-line JSON value per commit, but git concatenates notes from multiple matching refs with newlines.
Suggested fix
In getTagsNotes(), split notePart by newlines and parse each line as a separate JSON object, merging the channels arrays:
// Instead of:
const parsed = JSON.parse(notePart);
// Do:
const noteLines = notePart.split("\n").filter(Boolean);
const mergedChannels = new Set();
for (const line of noteLines) {
const parsed = JSON.parse(line.trim());
if (parsed.channels) {
parsed.channels.forEach((ch) => mergedChannels.add(ch));
}
}
const parsed = { channels: [...mergedChannels] };This ensures that when multiple notes refs exist for the same commit, all channels are preserved.
Environment
- semantic-release version: 25.0.3
- Git version: 2.47.1
- CI: GitLab CI
Workaround
Manually remove one of the conflicting git notes:
git fetch origin "+refs/notes/*:refs/notes/*"
git notes --ref "semantic-release-<conflicting-version>" remove <commit-sha>
git push origin refs/notes/semantic-release-<conflicting-version>Source: semantic-release/semantic-release