diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee5aa04..5d97dc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## [1.1.1] - 2026-04-24
+
+* `git-sign-push`: pass `--force-rebase` to the signing rebase so the tip commit actually gets re-signed when HEAD is already a descendant of the base (otherwise `git rebase ` is a no-op and the unsigned commit gets pushed).
+
## [1.1.0] - 2026-04-24
* `git-setup-remotes` now pins `branch.main.pushRemote` to `origin` so direct pushes on the canonical branch never target the personal fork.
diff --git a/pyproject.toml b/pyproject.toml
index 0312b9a..2428f22 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "git-maintainer-tools"
-version = "1.1.0"
+version = "1.1.1"
description = "Small CLIs (git-setup-remotes, git-sign-push) for fork-based OSS maintainer workflows."
readme = "README.md"
requires-python = ">=3.10"
diff --git a/src/git_maintainer_tools/sign_push.py b/src/git_maintainer_tools/sign_push.py
index abadbb1..ed0299a 100644
--- a/src/git_maintainer_tools/sign_push.py
+++ b/src/git_maintainer_tools/sign_push.py
@@ -77,12 +77,18 @@ def resign(base: str) -> None:
`GIT_SEQUENCE_EDITOR=:` turns the interactive rebase TODO-list editor
into a no-op so the rebase just replays the existing commits without
reordering or squashing. `-S` signs each replayed commit.
+
+ `--force-rebase` is required: without it, `git rebase ` is a no-op
+ when HEAD is already a descendant of `base` (the normal shape for a
+ local branch built on top of `origin/main`). The rebase then never
+ replays any commits, `-S` signs nothing, and the still-unsigned tip
+ gets pushed and rejected by `required_signatures` branch rules.
"""
env = os.environ.copy()
env["GIT_SEQUENCE_EDITOR"] = ":"
import subprocess
proc = subprocess.run(
- ["git", "rebase", "--rebase-merges", "-S", base],
+ ["git", "rebase", "--rebase-merges", "--force-rebase", "-S", base],
env=env,
)
if proc.returncode != 0:
diff --git a/tests/test_resign.py b/tests/test_resign.py
new file mode 100644
index 0000000..0c03872
--- /dev/null
+++ b/tests/test_resign.py
@@ -0,0 +1,28 @@
+"""Guard the rebase invocation used to re-sign pending commits."""
+
+from __future__ import annotations
+
+import subprocess
+from unittest.mock import MagicMock
+
+from git_maintainer_tools import sign_push
+
+
+def test_resign_passes_force_rebase(monkeypatch):
+ """`git rebase ` is a no-op when HEAD already descends from base.
+
+ Without `--force-rebase`, `-S` would never replay any commit and the
+ tip would stay unsigned, so push-time `required_signatures` rules
+ reject it. This test pins the flag in place.
+ """
+ fake_run = MagicMock(return_value=MagicMock(returncode=0))
+ monkeypatch.setattr(subprocess, "run", fake_run)
+
+ sign_push.resign("abc123")
+
+ called_cmd = fake_run.call_args.args[0]
+ assert called_cmd[0] == "git"
+ assert "rebase" in called_cmd
+ assert "--force-rebase" in called_cmd
+ assert "-S" in called_cmd
+ assert called_cmd[-1] == "abc123"