Skip to content

ci: dev is deleted after every release promotion (delete_branch_on_merge plus the cleanup workflow), breaking the target-branch rule #175

Description

@lidge-jun

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/devdelete_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

  1. 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";
    
  2. 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`);
    
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions