Correction to the root cause below. The primary deleter is the repository setting delete_branch_on_merge, not the scheduled cleanup workflow. Verified 2026-09-14 via gh api repos/lidge-jun/codexclaw:
{"allow_merge_commit":true,"delete_branch_on_merge":true,"default_branch":"main"}
GitHub deletes a merged PR's head branch immediately under that setting, skipping only branches with protection. Ruleset protect-main (id 20884837) covers refs/heads/main alone, so dev is unprotected and gets deleted the moment a dev -> main promotion PR merges. PR #169 merged at 2026-09-13T09:48:01Z; the scheduled cleanup run 34756673206 did not start until 12:17:11Z, so dev was already gone before the workflow ran. The cleanup workflow has the same gap and would delete dev on its own schedule, but it is the second path, not the first.
That makes the fix two-part: exempt dev in the workflow and stop the repository setting from reaching it. The most direct remedy is a ruleset or branch protection on refs/heads/dev — delete_branch_on_merge honours protection, and the workflow already honours protectedByGitHub, so one protection rule closes both paths at once. An explicit never-delete set in the workflow is still worth having as defence in depth.
cleanup-closed-pr-branches.yml deletes the dev branch every time a release promotion merges, which breaks the contribution path that enforce-pr-target.yml requires.
The loop
-
A release promotion opens a PR whose head branch is dev and whose base is main. enforce-pr-target.yml explicitly exempts that shape:
// dev -> main is the release promotion path and is exempt.
const PROMOTION_BASE = "main";
const PROMOTION_HEAD = "dev";
-
That PR merges, and dev is deleted — immediately by delete_branch_on_merge, and independently by the scheduled cleanup, which sees a closed PR whose head is dev and finds no reason to skip it. Its only skip conditions are GitHub branch protection and "already gone":
if (protectedByGitHub.has(entry.branch)) {
core.info(`skip ${entry.branch}: branch protection`);
-
enforce-pr-target.yml still requires every contribution to target dev, so the next PR cannot be opened correctly. It gets [WRONG BRANCH]-prefixed and drafted no matter what base it picks, because the required base does not exist.
Observed
By 2026-09-14, git ls-remote --heads origin listed only main, agent/report-publication-20260913, and an unrelated topic branch — no dev. git fetch origin dev failed with couldn't find remote ref dev while the stale local origin/dev tracking ref still pointed at e6dc8ea7, which is how the deletion stays invisible in an existing checkout.
PR #174 hit this: opened against main because dev was gone, immediately prefixed [WRONG BRANCH], and only recoverable by recreating dev from main by hand and retargeting.
Why the workflow's own comment does not cover it
The file header reasons about when cleanup starts (default-branch schedule only) and about the security of pull_request_target. Neither addresses which branches are permanent. The head-branch-of-a-closed-PR rule is correct for topic branches and wrong for the one long-lived integration branch that the promotion path necessarily uses as a head.
Suggested fix
Protect refs/heads/dev, which closes both deletion paths at once, and add an explicit never-delete set alongside the branch-protection check as defence in depth:
const PERMANENT = new Set(["dev", defaultBranch]);
if (PERMANENT.has(entry.branch)) {
core.info(`skip ${entry.branch}: permanent branch`);
continue;
}
The workflow should not depend on a setting it does not own — the failure is silent and only surfaces one release cycle later, when the next contributor cannot open a PR.
dev has been recreated at 9e279a45 and now carries the #174 merge; it will be deleted again by the next promotion until this is fixed.
Correction to the root cause below. The primary deleter is the repository setting
delete_branch_on_merge, not the scheduled cleanup workflow. Verified 2026-09-14 viagh api repos/lidge-jun/codexclaw:GitHub deletes a merged PR's head branch immediately under that setting, skipping only branches with protection. Ruleset
protect-main(id 20884837) coversrefs/heads/mainalone, sodevis unprotected and gets deleted the moment adev->mainpromotion PR merges. PR #169 merged at 2026-09-13T09:48:01Z; the scheduled cleanup run 34756673206 did not start until 12:17:11Z, sodevwas already gone before the workflow ran. The cleanup workflow has the same gap and would deletedevon its own schedule, but it is the second path, not the first.That makes the fix two-part: exempt
devin the workflow and stop the repository setting from reaching it. The most direct remedy is a ruleset or branch protection onrefs/heads/dev—delete_branch_on_mergehonours protection, and the workflow already honoursprotectedByGitHub, so one protection rule closes both paths at once. An explicit never-delete set in the workflow is still worth having as defence in depth.cleanup-closed-pr-branches.ymldeletes thedevbranch every time a release promotion merges, which breaks the contribution path thatenforce-pr-target.ymlrequires.The loop
A release promotion opens a PR whose head branch is
devand whose base ismain.enforce-pr-target.ymlexplicitly exempts that shape:That PR merges, and
devis deleted — immediately bydelete_branch_on_merge, and independently by the scheduled cleanup, which sees a closed PR whose head isdevand finds no reason to skip it. Its only skip conditions are GitHub branch protection and "already gone":enforce-pr-target.ymlstill requires every contribution to targetdev, so the next PR cannot be opened correctly. It gets[WRONG BRANCH]-prefixed and drafted no matter what base it picks, because the required base does not exist.Observed
By 2026-09-14,
git ls-remote --heads originlisted onlymain,agent/report-publication-20260913, and an unrelated topic branch — nodev.git fetch origin devfailed withcouldn't find remote ref devwhile the stale localorigin/devtracking ref still pointed ate6dc8ea7, which is how the deletion stays invisible in an existing checkout.PR #174 hit this: opened against
mainbecausedevwas gone, immediately prefixed[WRONG BRANCH], and only recoverable by recreatingdevfrommainby hand and retargeting.Why the workflow's own comment does not cover it
The file header reasons about when cleanup starts (default-branch schedule only) and about the security of
pull_request_target. Neither addresses which branches are permanent. The head-branch-of-a-closed-PR rule is correct for topic branches and wrong for the one long-lived integration branch that the promotion path necessarily uses as a head.Suggested fix
Protect
refs/heads/dev, which closes both deletion paths at once, and add an explicit never-delete set alongside the branch-protection check as defence in depth:The workflow should not depend on a setting it does not own — the failure is silent and only surfaces one release cycle later, when the next contributor cannot open a PR.
devhas been recreated at9e279a45and now carries the #174 merge; it will be deleted again by the next promotion until this is fixed.