Concrete findings from running the brownfield migration (README "Template - GitHub Setup") on a real repo (ptr727/ESPHome-NonRoot: ~123 commits/branch, 47 merges, 51 unsigned back to root, 82 tags). The procedure works but several things would have saved time / avoided footguns:
1. For merge-heavy history, prefer filter-branch - and it needs a committer rewrite to verify
The procedure lists git rebase --root --rebase-merges --exec 'git commit --amend --no-edit -S' as primary and filter-branch as a deprecated fallback. For a history with many merge commits, the rebase is fragile (complex todo, easy to derail); git filter-branch --commit-filter rewrites every commit object in place, preserving the exact DAG with zero conflicts, and operating on both refs at once keeps shared ancestry consistent (same new SHA on both branches). It was by far the smoother tool here.
Crucial caveat the docs omit: filter-branch's commit-filter preserves the original committer, so signing alone yields committer != signer and GitHub does not mark the commit Verified - which means Require signed commits still rejects it. You must rewrite the committer to the signing identity:
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --commit-filter '
GIT_COMMITTER_NAME="You" GIT_COMMITTER_EMAIL="you@users.noreply.github.com" git commit-tree -S "$@"
' -- develop main
(rebase --amend -S sets committer to the rewriter automatically, which is why it verifies - worth stating explicitly so the filter-branch path isn't a silent trap.)
2. Ordering: re-sign while rulesets are permissive, and the blocker is per-branch
Step 2 says "temporarily set the ruleset Enforcement to Disabled ... since the admin bypass won't permit the force-push." In practice:
- If you re-sign before importing the strict rulesets (i.e. while the repo still has permissive/no rules), you avoid the disable/re-enable dance entirely. Recommend stating the order: re-sign + force-push first, import strict rulesets after.
- The push rejection is per-branch and depends on which rule each ruleset has. On a partially-configured repo I hit:
develop (deletion-only ruleset) accepted the force-push, but main (had a pull_request rule) rejected it with GH013 ... Changes must be made through a pull request - which doesn't read like a force-push error. Worth noting that either Require a pull request or Block force pushes will reject the direct push, and admin bypass covers neither for git force-push, so the ruleset must be disabled/removed on each affected branch.
3. Tags are orphaned by a root rewrite
A full-history re-sign leaves existing tags (82 release tags here) pointing at the pre-rewrite commits. The Require signed commits rule applies to branches, not tags, so leaving them is fine (the old commits stay reachable via the tags); add --tag-name-filter cat only if you want tags to follow the rewrite. The procedure should call this out so 80+ "dangling" tags don't surprise the maintainer.
4. Verify via the API, not local %G?
Locally git log --pretty=%G? showed G only because allowed_signers happened to be configured; on a fresh machine it can show U (good, untrusted) even though the signature is fine. The authoritative post-push check is:
gh api repos/<owner>/<repo>/commits/<branch> --jq '.commit.verification' # expect verified: true, reason: valid
5. Post-rewrite cleanup
Open bot PRs/branches based on the old history go stale after the force-push; note to let Dependabot recreate them (or rebase) so reviewers aren't confused.
6. Minor
git filter-branch printed envsubst: not found (git-sh-setup i18n) on a WSL runner - harmless, but startling; a one-line "this warning is benign" note would help.
Surfaced while completing the brownfield migration for ptr727/ESPHome-NonRoot (see ptr727/ESPHome-NonRoot#61).
Concrete findings from running the brownfield migration (README "Template - GitHub Setup") on a real repo (
ptr727/ESPHome-NonRoot: ~123 commits/branch, 47 merges, 51 unsigned back to root, 82 tags). The procedure works but several things would have saved time / avoided footguns:1. For merge-heavy history, prefer
filter-branch- and it needs a committer rewrite to verifyThe procedure lists
git rebase --root --rebase-merges --exec 'git commit --amend --no-edit -S'as primary andfilter-branchas a deprecated fallback. For a history with many merge commits, the rebase is fragile (complex todo, easy to derail);git filter-branch --commit-filterrewrites every commit object in place, preserving the exact DAG with zero conflicts, and operating on both refs at once keeps shared ancestry consistent (same new SHA on both branches). It was by far the smoother tool here.Crucial caveat the docs omit:
filter-branch's commit-filter preserves the original committer, so signing alone yields committer != signer and GitHub does not mark the commit Verified - which meansRequire signed commitsstill rejects it. You must rewrite the committer to the signing identity:(
rebase --amend -Ssets committer to the rewriter automatically, which is why it verifies - worth stating explicitly so the filter-branch path isn't a silent trap.)2. Ordering: re-sign while rulesets are permissive, and the blocker is per-branch
Step 2 says "temporarily set the ruleset Enforcement to Disabled ... since the admin bypass won't permit the force-push." In practice:
develop(deletion-only ruleset) accepted the force-push, butmain(had apull_requestrule) rejected it withGH013 ... Changes must be made through a pull request- which doesn't read like a force-push error. Worth noting that eitherRequire a pull requestorBlock force pusheswill reject the direct push, and admin bypass covers neither for git force-push, so the ruleset must be disabled/removed on each affected branch.3. Tags are orphaned by a root rewrite
A full-history re-sign leaves existing tags (82 release tags here) pointing at the pre-rewrite commits. The
Require signed commitsrule applies to branches, not tags, so leaving them is fine (the old commits stay reachable via the tags); add--tag-name-filter catonly if you want tags to follow the rewrite. The procedure should call this out so 80+ "dangling" tags don't surprise the maintainer.4. Verify via the API, not local
%G?Locally
git log --pretty=%G?showedGonly because allowed_signers happened to be configured; on a fresh machine it can showU(good, untrusted) even though the signature is fine. The authoritative post-push check is:5. Post-rewrite cleanup
Open bot PRs/branches based on the old history go stale after the force-push; note to let Dependabot recreate them (or rebase) so reviewers aren't confused.
6. Minor
git filter-branchprintedenvsubst: not found(git-sh-setup i18n) on a WSL runner - harmless, but startling; a one-line "this warning is benign" note would help.Surfaced while completing the brownfield migration for
ptr727/ESPHome-NonRoot(see ptr727/ESPHome-NonRoot#61).