ci(merge-settings): guard delete_branch_on_merge too (backend#1981) - #256
Conversation
The weekly check read three booleans and not the fourth. delete_branch_on_merge was off on 10 of 19 active repos, and the org had accumulated 1212 remote branches that were provably merged with no open PR — frontend-app 311, tracebloc-website 207, client 134, design-system 82. The local half is the part that is easy to miss, so the header now says it: we squash-merge, so a merged branch is NOT an ancestor of develop and `git branch --merged` never lists it. The only reliable local "done" signal is [gone] on the upstream, and that exists only if the server deleted the remote branch on merge. With the setting off, no local cleanup command can reap the branch at all — one workspace had 459 local branches, 367 of them redundant. Read in the same API call, checked with its own `if` rather than an `elif` so a repo that is wrong twice is reported twice, and folded into the same fail-closed loop: an absent or non-boolean value is a finding, never a silent pass. These keys are only returned to a principal with admin on the repo, so "unreadable" is the common case, not the exotic one. Report-only, like the rest of the file — the remediation names the one PATCH to run and states what it does not touch. The settings themselves were already flipped by hand (19/19 verified true); this is the enforcement so repo number 20 does not arrive with it off. Mutation-proved against a 13-assertion harness that extracts this step's own `run:` block and runs it against a stubbed gh: 8 mutations, all red. Two of those mutations initially passed and both were the test's fault, not the workflow's — the stub ignored --jq, so dropping the field from the query was invisible, and the Actions-expression assertion matched whichever `if:` still carried the field. Both fixed, then re-proved. The harness is not committed here: .github#253 adds `selftests-cover`, which fails on an unwired selftest, so it lands in a follow-up once that merges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
saqlainsyed007
left a comment
There was a problem hiding this comment.
Correctness review — clean, approving.
Extends merge-settings-drift.yml to also flag delete_branch_on_merge disabled. I traced all five wiring sites and they hang together:
- 4th field folded into the same
gh apicall (.delete_branch_on_mergein the@tsvarray) — no extra request. cut -f4→autodelete; an absent key (non-admin token) renders as an empty TSV field and is caught by the fail-closedforloop before the autodelete check, so it lands infailed, never a silent pass.- The fail-closed loop now covers all four values; a non-boolean
continue 2s past the whole repo, so no double-count. - The autodelete check is a standalone
if(notelif), so a repo that is wrong twice is reported in both buckets — matches the comment. - By the time
if [ "$autodelete" = "false" ]runs, the value is guaranteed literaltrue/false. nodeleteis consistently threaded through the audit output, the step summary (with anonefallback matchingdrift), the report step'sif:+NODELETEenv + remediation block, and the run-fail gateif:.- Remediation
-F delete_branch_on_merge=truesends a typed boolean, matching the existing rebase-merge remediation.
The one item I scrutinized — the untouched "Settings could not be read" block still says "The three merge fields" — is defensible: those genuinely are the three merge-method fields, delete_branch_on_merge is a distinct branch-cleanup setting, and all four are returned together to admins so the "token lacks admin" explanation still holds. Not flagging.
No correctness defects, no convention or performance concerns, no added API cost. LGTM.
shujaatTracebloc
left a comment
There was a problem hiding this comment.
Approve (backend#1981). Extends merge-settings-drift to audit delete_branch_on_merge (off on 10/19 repos; 1212 provably-merged branches accumulated). Correct reasoning: we squash-merge, so a merged branch is not an ancestor of develop and git branch --merged/-d never reap it — the only local "done" signal is [gone], which needs the server to delete on merge. Checked with its own if (a repo wrong twice is reported twice), fail-closed on non-boolean (admin-only field, same false-all-clear guard as the rest), report-only (no PATCH), and wired into the summary, comment, and fail conditions. CI green, Bugbot clean.
Summary
merge-settings-drift.ymlread three booleans and not the fourth.delete_branch_on_mergewas unguarded and off on 10 of 19 active repos:.github,claude-skills,client,design-system,docs,model-zoo,rfcs,start-training,tracebloc-py-package,tracebloc-website.The consequence, measured: 1193 remote branches across the org are provably merged with no open PR (ancestor of the default branch, or a MERGED PR).
frontend-app310,tracebloc-website206,client133,client-runtime82,design-system81,.github80.The local half is the part worth writing down, and the header now does. We squash-merge, so a merged branch is not an ancestor of
develop— the squashed commit is a different commit.git branch --mergednever lists it andgit branch -drefuses it. The only reliable local "this is done" signal is[gone]on the upstream, and that exists only if the server deleted the remote branch on merge. With the setting off, no cleanup command can reap the branch at all. One workspace had 459 local branches, 367 of them redundant.The settings themselves were already flipped by hand — 19/19 verified
true. This PR is the missing enforcement, so repo number 20 does not arrive with it off. Same reasoning the rebase-merge check already documents.Type
Tech-debt / CI.
What it does, and what it deliberately does not
.delete_branch_on_mergein the samegh api repos/$ORG/$repocall — no second request.if, not anelif, so a repo that is wrong twice is reported twice.(unreadable)is the common case rather than the exotic one.develop/staging/main/master, and non-PR-head branches likegh-pages).Out of scope: deleting the 1193 branches that are already stale. Auto-delete only stops new accumulation; the backlog spans seven authors and is a deliberate per-repo sweep.
Test plan
actionlint -shellcheckclean;shellcheckclean on the extracted step; YAML parses.Mutation-proved against a 13-assertion harness that extracts this step's own
run:block and runs it against a stubbedgh— the same technique asscripts/tests/version-bump-gate-selftest.sh. Cases cover compliant/one-off/several-off, the three fail-closed paths (key absent, non-boolean, read failure), independence from the merge-method checks, and that both consumer steps actually consume the finding.Eight mutations, all red:
f4→f3)nodeleteoutputnodeletenodeleteTwo of those initially passed, and both were the test's fault rather than the workflow's — worth recording because both are the failure mode backend#1729 is about:
ghignored--jqand always emitted four fields, so a workflow that stopped requestingdelete_branch_on_mergestill got a four-field response. The test was measuring the stub, not the caller's query. Fixed by having the stub honour the declared field list./outputs\.nodelete != ''/, which matched whicheverif:still carried the field — so removing it from one step was masked by the other. Fixed by anchoring each site to its own leading condition, plus an exact-count assertion.Both re-proved red after fixing.
The harness is not committed here. #253 adds
selftests-cover, which fails on a selftest file that is not wired to a make target, so landing the script now would redden that PR through no fault of its own. It follows once #253 merges — tracked on tracebloc/backend#1982.Fixes tracebloc/backend#1981
Checklist
developactionlint -shellcheck,shellcheck)${{ }}interpolation added to anyrun:block; the workflow isschedule/workflow_dispatchonly and reads no event payload🤖 Generated with Claude Code