Skip to content

ci(merge-settings): guard delete_branch_on_merge too (backend#1981) - #256

Merged
LukasWodka merged 1 commit into
developfrom
ci/1981-delete-branch-on-merge-guard
Aug 14, 2026
Merged

ci(merge-settings): guard delete_branch_on_merge too (backend#1981)#256
LukasWodka merged 1 commit into
developfrom
ci/1981-delete-branch-on-merge-guard

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

merge-settings-drift.yml read three booleans and not the fourth. delete_branch_on_merge was 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-app 310, tracebloc-website 206, client 133, client-runtime 82, design-system 81, .github 80.

Correction. The commit message says 1212. The first inventory counted refs/remotes/origin/HEAD as a branch — its git short name is the bare string origin, which slipped past the protected-name filter and produced one bogus row per repo. 1212 − 19 = 1193. The commit message is left as pushed rather than amended, because a force-push needs an explicit instruction. Nothing else in this PR depends on the number.

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 --merged never lists it and git branch -d refuses 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

  • Reads .delete_branch_on_merge in the same gh api repos/$ORG/$repo call — no second request.
  • Its own if, not an elif, so a repo that is wrong twice is reported twice.
  • Folded into the existing fail-closed loop: absent or non-boolean 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 rather than the exotic one.
  • Report-only, like the rest of the file. It never PATCHes, and the remediation names the one command plus what enabling it does not touch (develop/staging/main/master, and non-PR-head branches like gh-pages).
  • Counts toward the run-fail gate, so a green badge keeps meaning "settings are correct" rather than "the check ran".

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 -shellcheck clean; shellcheck clean 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 stubbed gh — the same technique as scripts/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:

mutation result
drop the autodelete check RED
drop autodelete from the fail-closed loop RED
read the wrong TSV field (f4f3) RED
stop requesting the field from the API RED
stop emitting the nodelete output RED
report step ignores nodelete RED
fail gate ignores nodelete RED
drop the remediation command RED

Two 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:

  1. The stub gh ignored --jq and always emitted four fields, so a workflow that stopped requesting delete_branch_on_merge still 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.
  2. The Actions-expression assertion was a bare /outputs\.nodelete != ''/, which matched whichever if: 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

  • Targets develop
  • Linted (actionlint -shellcheck, shellcheck)
  • Guard mutation-proved, and the mutation anchors asserted to have applied
  • Fails closed on unreadable state
  • No ${{ }} interpolation added to any run: block; the workflow is schedule/workflow_dispatch only and reads no event payload
  • Header comment updated to match behaviour

🤖 Generated with Claude Code

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>
@LukasWodka LukasWodka self-assigned this Aug 14, 2026

@saqlainsyed007 saqlainsyed007 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 api call (.delete_branch_on_merge in the @tsv array) — no extra request.
  • cut -f4autodelete; an absent key (non-admin token) renders as an empty TSV field and is caught by the fail-closed for loop before the autodelete check, so it lands in failed, 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 (not elif), 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 literal true/false.
  • nodelete is consistently threaded through the audit output, the step summary (with a none fallback matching drift), the report step's if: + NODELETE env + remediation block, and the run-fail gate if:.
  • Remediation -F delete_branch_on_merge=true sends 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 shujaatTracebloc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@LukasWodka
LukasWodka merged commit a43c0c1 into develop Aug 14, 2026
12 checks passed
@LukasWodka
LukasWodka deleted the ci/1981-delete-branch-on-merge-guard branch August 14, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants