Skip to content

Rewritten Tags & Rebases ​

Why rebasing a branch after a release orphans its tags, how it breaks changelogs, and how Relizy detects and recovers from it.

The symptom ​

Between two releases (for example two betas), the changelog suddenly lists the entire history since the last stable release, often with duplicated commits, and every package gets bumped even though only one or two actually changed. The compare link in the changelog header looks correct, but the commit list does not.

The cause: an orphaned tag ​

When Relizy releases a version, it creates a commit (chore(release): bump version to X) and a tag pointing exactly to that commit, then pushes both.

If the branch is rebased afterwards, git rewrites every commit on it, including that release commit, giving it a new SHA. The tag still points to the old commit, which is no longer part of the branch history. The tag is now orphaned.

Before rebase:   ...─ A ─ B(bump 1.1.0) ←── tag v1.1.0
After  rebase:   ...─ A'─ B'(bump 1.1.0)   ← branch HEAD (new SHA)
                       └ B (bump 1.1.0)  ←── tag v1.1.0 still here (orphaned)

To build a changelog, Relizy runs git log "v1.1.0...HEAD". With an orphaned v1.1.0, that range is the symmetric difference between two divergent lines, so it walks all the way back to their common ancestor (near the last stable tag) and returns both the old and the rebased commits, hence the bloat and the duplicates.

Why it keeps happening ​

Every cycle that rebases a branch carrying tagged release commits orphans those tags. The most common trigger is rebasing a long-lived branch (develop, main) - for example rebasing develop onto main after a stable release replays every previously-tagged beta commit with new SHAs.

The fix: never rebase tagged/pushed commits ​

A pushed or tagged commit is published history and must be treated as immutable.

  • ✅ Feature branches: rebase them onto develop freely, before merging. Only the feature's own (unpushed, untagged) commits are rewritten.
  • ✅ Integrate into develop via merge or fast-forward, not by rebasing develop itself.
  • ✅ develop ↔ main: integrate with git merge, never git rebase. Merging keeps every existing SHA, so tags stay valid.
  • ❌ Never rebase develop or main - they carry tagged release commits.

How Relizy recovers ​

Relizy detects when the resolved from tag is no longer reachable from to. Instead of silently producing a wrong changelog, it explains the situation and recovers safely. No commit is ever rewritten - the only possible mutation is moving a tag.

When a git rebase is detected, Relizy locates the equivalent commit (the release commit with the same message that is actually reachable from the current branch) and:

  • Interactively (TTY, no --yes): prompts you to choose between
    • using the equivalent commit for this run only (recommended, non-destructive),
    • re-binding the tag onto the equivalent commit locally,
    • re-binding and force-pushing the corrected tag (asks again before pushing),
    • keeping the orphaned tag, or
    • aborting.
  • Non-interactively (--yes / CI / no TTY): uses the equivalent commit for this run only and logs a warning. Idempotent, and the tag is left untouched.

Configuration ​

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  // Enabled by default. Set to false to opt out entirely.
  detectRewrittenTags: true,

  // Force a strategy instead of the auto behavior (prompt on TTY, ephemeral on CI):
  //   'prompt'    → interactive selection
  //   'ephemeral' → use the reachable equivalent commit for this run only
  //   'rebind'    → move the local tag onto the equivalent commit (no push)
  //   'error'     → stop the release with an explanation
  onRewrittenTag: 'ephemeral',
})
OptionTypeDefaultDescription
detectRewrittenTagsbooleantrueDetect an unreachable from tag and recover instead of failing silently.
onRewrittenTagstringautoprompt | ephemeral | rebind | error. Auto = prompt / ephemeral.

Repairing already-orphaned tags ​

The ephemeral correction fixes the current run without touching anything. To fix a tag permanently, point it at its equivalent commit and force-push it:

bash
# Find the equivalent commit (the bump commit that IS on your branch)
git log --grep="bump version to 1.1.0" --format=%H -n 1

# Re-bind the tag and publish the correction
git tag -f v1.1.0 <equivalent-commit-sha>
git push origin v1.1.0 --force

Force-pushing a tag rewrites already-published history; only do it on repositories where that is acceptable.

Tags orphaned by a rejected push ​

A tag can also be orphaned without any rebase: a release publishes its packages and pushes its tag, but the push of the branch is rejected because new commits landed on it in the meantime. The release commit then only exists behind the tag, and the branch stays on the previous version, so the next release computes the same version again.

Relizy now prevents this situation (see Branch Safety) and stops a release whose tag already exists. If it still happens, there is no equivalent commit to re-bind the tag to: merge the release commit back instead.

bash
git fetch --tags
git merge v1.1.0
git push

The branch gets the bumped versions and changelogs, and the tag becomes part of its history again.

Released under the MIT License.