From 5bfa96a5b03ee5a0755064602e0937704db61734 Mon Sep 17 00:00:00 2001 From: Riley Mete Date: Wed, 15 Jul 2026 20:27:12 -0700 Subject: [PATCH] fix: allow leading underscore in branch names (valid per git-check-ref-format) (#1486) Branch names starting with an underscore (e.g. _release/v1.2.3) are valid per git check-ref-format but were rejected by validateBranchName's first-character whitelist. Since setupBranch validates a PR's baseRefName after checkout, the action failed on every open PR targeting such a branch. A leading underscore carries no option-injection risk (only a leading dash does, which is still rejected separately). Co-authored-by: Claude Fable 5 --- src/github/operations/branch.ts | 9 ++++++--- test/validate-branch-name.test.ts | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/github/operations/branch.ts b/src/github/operations/branch.ts index 253f7630..e095280b 100644 --- a/src/github/operations/branch.ts +++ b/src/github/operations/branch.ts @@ -27,7 +27,7 @@ function extractFirstLabel(githubData: FetchDataResult): string | undefined { * This prevents command injection by ensuring only safe characters are used. * * Valid branch names: - * - Start with alphanumeric character or @ (not dash, to prevent option injection) + * - Start with alphanumeric character, underscore, or @ (not dash, to prevent option injection) * - Contain only alphanumeric, forward slash, hyphen, underscore, period, hash (#), plus (+), comma (,), or at sign (@) * - Do not start or end with a period * - Do not end with a slash @@ -68,12 +68,15 @@ export function validateBranchName(branchName: string): void { // @ is valid per git-check-ref-format anywhere in a ref name, including the first character // (e.g. ticket conventions like "TICKET-123@add-feature" or prefixes like "@hotfix/..."); // the bare name "@" (HEAD shorthand) and the "@{" sequence (reflog syntax) are rejected below. + // _ is valid per git-check-ref-format anywhere in a ref name, including the first character; + // leading underscores are a common convention for release/internal branches (e.g. + // "_release/v1.2.3"), which previously failed validation as a PR's base branch. // All git calls use execFileSync (not shell interpolation), so none of these characters carry injection risk. - const validPattern = /^[a-zA-Z0-9@][a-zA-Z0-9/_.#+,@-]*$/; + const validPattern = /^[a-zA-Z0-9@_][a-zA-Z0-9/_.#+,@-]*$/; if (!validPattern.test(branchName)) { throw new Error( - `Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character or '@' and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), plus signs (+), commas (,), or at signs (@).`, + `Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character, underscore, or '@' and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), plus signs (+), commas (,), or at signs (@).`, ); } diff --git a/test/validate-branch-name.test.ts b/test/validate-branch-name.test.ts index fe03ce4e..6ee26a00 100644 --- a/test/validate-branch-name.test.ts +++ b/test/validate-branch-name.test.ts @@ -74,6 +74,16 @@ describe("validateBranchName", () => { expect(() => validateBranchName("@hotfix/login-timeout")).not.toThrow(); expect(() => validateBranchName("agent/task@abc123")).not.toThrow(); }); + + it("should accept branch names starting with underscore (git-valid, common for release branches)", () => { + // Leading underscores are valid per git check-ref-format and a common + // convention for release/internal branches. Rejecting them broke the + // action on any open PR whose base branch was e.g. "_release/v1.2.3", + // since setupBranch validates the PR's baseRefName after checkout. + expect(() => validateBranchName("_release/v1.2.3")).not.toThrow(); + expect(() => validateBranchName("_internal")).not.toThrow(); + expect(() => validateBranchName("_wip/feature-x")).not.toThrow(); + }); }); describe("command injection attempts", () => {