From fb6c6b51f47993c0e46812d9cec7d4084fe34159 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Dec 2025 06:34:34 +0000 Subject: [PATCH] fix: Encode branch names in URLs to prevent truncation in markdown links Branch names containing special characters (particularly parentheses) were breaking markdown links in GitHub comments. This caused URLs to be truncated when clicked. Changes: - Add encodeBranchName() helper function that: - Uses encodeURIComponent for basic encoding - Preserves forward slashes (GitHub expects literal / in branch URLs) - Manually encodes parentheses (not encoded by encodeURIComponent per RFC 3986) - Apply encoding to branch URLs in: - update-comment-link.ts (PR compare URLs) - branch-cleanup.ts (branch tree URLs) - comment-logic.ts (branch tree URLs) - comments/common.ts (branch tree URLs) - Improve PR link regex to use greedy match with end anchor - Add test for branch names with special characters --- src/entrypoints/update-comment-link.ts | 16 +++++++++++++++- src/github/operations/branch-cleanup.ts | 22 ++++++++++++++++++---- src/github/operations/comment-logic.ts | 19 +++++++++++++++++-- src/github/operations/comments/common.ts | 16 +++++++++++++++- test/comment-logic.test.ts | 15 +++++++++++++++ 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 849f954..f1be00e 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -15,6 +15,20 @@ import { GITHUB_SERVER_URL } from "../github/api/config"; import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup"; import { updateClaudeComment } from "../github/operations/comments/update-claude-comment"; +/** + * Encodes a branch name for use in a URL, preserving forward slashes. + * GitHub expects literal slashes in branch names (e.g., /tree/feature/branch) + * but other special characters like parentheses need to be encoded. + * Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986, + * but parentheses break markdown links so we encode them manually. + */ +function encodeBranchName(branchName: string): string { + return encodeURIComponent(branchName) + .replace(/%2F/gi, "/") + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); +} + async function run() { try { const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!); @@ -140,7 +154,7 @@ async function run() { const prBody = encodeURIComponent( `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, ); - const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; + const prUrl = `${serverUrl}/${owner}/${repo}/compare/${encodeBranchName(baseBranch)}...${encodeBranchName(claudeBranch)}?quick_pull=1&title=${prTitle}&body=${prBody}`; prLink = `\n[Create a PR](${prUrl})`; } } catch (error) { diff --git a/src/github/operations/branch-cleanup.ts b/src/github/operations/branch-cleanup.ts index 88de6de..d7f120c 100644 --- a/src/github/operations/branch-cleanup.ts +++ b/src/github/operations/branch-cleanup.ts @@ -2,6 +2,20 @@ import type { Octokits } from "../api/client"; import { GITHUB_SERVER_URL } from "../api/config"; import { $ } from "bun"; +/** + * Encodes a branch name for use in a URL, preserving forward slashes. + * GitHub expects literal slashes in branch names (e.g., /tree/feature/branch) + * but other special characters like parentheses need to be encoded. + * Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986, + * but parentheses break markdown links so we encode them manually. + */ +function encodeBranchName(branchName: string): string { + return encodeURIComponent(branchName) + .replace(/%2F/gi, "/") + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); +} + export async function checkAndCommitOrDeleteBranch( octokit: Octokits, owner: string, @@ -80,7 +94,7 @@ export async function checkAndCommitOrDeleteBranch( ); // Set branch link since we now have commits - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`; branchLink = `\n[View branch](${branchUrl})`; } else { console.log( @@ -91,7 +105,7 @@ export async function checkAndCommitOrDeleteBranch( } catch (gitError) { console.error("Error checking/committing changes:", gitError); // If we can't check git status, assume the branch might have changes - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`; branchLink = `\n[View branch](${branchUrl})`; } } else { @@ -102,13 +116,13 @@ export async function checkAndCommitOrDeleteBranch( } } else { // Only add branch link if there are commits - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`; branchLink = `\n[View branch](${branchUrl})`; } } catch (error) { console.error("Error comparing commits on Claude branch:", error); // If we can't compare but the branch exists remotely, include the branch link - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`; + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`; branchLink = `\n[View branch](${branchUrl})`; } } diff --git a/src/github/operations/comment-logic.ts b/src/github/operations/comment-logic.ts index 03b5d86..7032b20 100644 --- a/src/github/operations/comment-logic.ts +++ b/src/github/operations/comment-logic.ts @@ -1,5 +1,19 @@ import { GITHUB_SERVER_URL } from "../api/config"; +/** + * Encodes a branch name for use in a URL, preserving forward slashes. + * GitHub expects literal slashes in branch names (e.g., /tree/feature/branch) + * but other special characters like parentheses need to be encoded. + * Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986, + * but parentheses break markdown links so we encode them manually. + */ +function encodeBranchName(branchName: string): string { + return encodeURIComponent(branchName) + .replace(/%2F/gi, "/") + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); +} + export type ExecutionDetails = { total_cost_usd?: number; duration_ms?: number; @@ -160,7 +174,7 @@ export function updateCommentBody(input: CommentUpdateInput): string { // Extract owner/repo from jobUrl const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//); if (repoMatch) { - branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`; + branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${encodeBranchName(finalBranchName)}`; } } @@ -172,8 +186,9 @@ export function updateCommentBody(input: CommentUpdateInput): string { } // Add PR link (either from content or provided) + // Use greedy match with end anchor to capture full URL even if it contains parentheses const prUrl = - prLinkFromContent || (prLink ? prLink.match(/\(([^)]+)\)/)?.[1] : ""); + prLinkFromContent || (prLink ? prLink.match(/\((.+)\)$/)?.[1] : ""); if (prUrl) { links += ` • [Create PR ➔](${prUrl})`; } diff --git a/src/github/operations/comments/common.ts b/src/github/operations/comments/common.ts index df24c03..56b92d8 100644 --- a/src/github/operations/comments/common.ts +++ b/src/github/operations/comments/common.ts @@ -12,12 +12,26 @@ export function createJobRunLink( return `[View job run](${jobRunUrl})`; } +/** + * Encodes a branch name for use in a URL, preserving forward slashes. + * GitHub expects literal slashes in branch names (e.g., /tree/feature/branch) + * but other special characters like parentheses need to be encoded. + * Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986, + * but parentheses break markdown links so we encode them manually. + */ +function encodeBranchName(branchName: string): string { + return encodeURIComponent(branchName) + .replace(/%2F/gi, "/") + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); +} + export function createBranchLink( owner: string, repo: string, branchName: string, ): string { - const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${branchName}`; + const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(branchName)}`; return `\n[View branch](${branchUrl})`; } diff --git a/test/comment-logic.test.ts b/test/comment-logic.test.ts index d55c82d..658eb8d 100644 --- a/test/comment-logic.test.ts +++ b/test/comment-logic.test.ts @@ -139,6 +139,21 @@ describe("updateCommentBody", () => { ); expect(result).not.toContain("View branch"); }); + + it("encodes special characters in branch names while preserving slashes", () => { + const input = { + ...baseInput, + branchName: "feature/fix(issue)-test", + }; + + const result = updateCommentBody(input); + // Branch name display should show the original name + expect(result).toContain("`feature/fix(issue)-test`"); + // URL should have encoded parentheses but preserved slashes + expect(result).toContain( + "https://github.com/owner/repo/tree/feature/fix%28issue%29-test", + ); + }); }); describe("PR link", () => {