getTagsNotes fails when multiple tags on same commit have different notes refs

Author: jean-humannCreated Mar 6, 2026Updated Jul 23, 2026

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) and develop (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 in refs/notes/semantic-release-v2.9.0-beta.13: {"channels":["staging"]}
    • v2.9.0-alpha.1 → note in refs/notes/semantic-release-v2.9.0-alpha.1: {"channels":["develop"]}

What happens

getTagsNotes() in lib/git.js runs:

bash
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 because isSameChannel('staging', null) returns false
  • semantic-release falls back to an older tag (e.g., beta.12) and tries to recreate beta.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:

javascript
// 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:

bash
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