@@ -27,16 +27,32 @@ const { getBaseBranch } = require("./get_base_branch.cjs");
2727const { createAuthenticatedGitHubClient } = require ( "./handler_auth.cjs" ) ;
2828const { buildWorkflowRunUrl } = require ( "./workflow_metadata_helpers.cjs" ) ;
2929const { checkFileProtection } = require ( "./manifest_file_helpers.cjs" ) ;
30- const { renderTemplateFromFile, buildProtectedFileList, encodePathSegments , getPromptPath } = require ( "./messages_core.cjs" ) ;
31- const { COPILOT_REVIEWER_BOT , FAQ_CREATE_PR_PERMISSIONS_URL , MAX_ASSIGNEES } = require ( "./constants.cjs" ) ;
30+ const { renderTemplateFromFile, buildProtectedFileList, getPromptPath } = require ( "./messages_core.cjs" ) ;
31+ const { COPILOT_REVIEWER_BOT , FAQ_CREATE_PR_PERMISSIONS_URL } = require ( "./constants.cjs" ) ;
3232const { isStagedMode } = require ( "./safe_output_helpers.cjs" ) ;
33- const { withRetry, isTransientError, RATE_LIMIT_RETRY_CONFIG } = require ( "./error_recovery.cjs" ) ;
34- const { tryEnforceArrayLimit } = require ( "./limit_enforcement_helpers.cjs" ) ;
33+ const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require ( "./error_recovery.cjs" ) ;
3534const { findAgent, getIssueDetails, assignAgentToIssue } = require ( "./assign_agent_helpers.cjs" ) ;
36- const { globPatternToRegex } = require ( "./glob_pattern_helpers.cjs" ) ;
3735const { ensureFullHistoryForBundle, extractBundlePrerequisiteCommits } = require ( "./git_helpers.cjs" ) ;
3836const { parseDiffGitHeader : parseDiffGitHeaderPaths , extractDiffGitHeaderEntries } = require ( "./patch_path_helpers.cjs" ) ;
3937const { resolveAllowedMentionsFromPayload } = require ( "./resolve_mentions_from_payload.cjs" ) ;
38+ const {
39+ MANAGED_FALLBACK_ISSUE_LABEL ,
40+ LABEL_MAX_RETRIES ,
41+ LABEL_INITIAL_DELAY_MS ,
42+ LABEL_MAX_DELAY_MS ,
43+ summarizeListForLog,
44+ createBundleTempRef,
45+ isLabelTransientError,
46+ parseAllowedBaseBranches,
47+ isBaseBranchAllowed,
48+ parseStringListConfig,
49+ mergeFallbackIssueLabels,
50+ sanitizeFallbackAssignees,
51+ neutralizeClosingKeywordsForIssueBody,
52+ generatePatchPreview,
53+ buildManifestProtectionCreatePrUrl,
54+ renderManifestProtectionFallbackBody,
55+ } = require ( "./create_pull_request_helpers.cjs" ) ;
4056
4157/**
4258 * @typedef {import('./types/handler-factory').HandlerFactoryFunction } HandlerFactoryFunction
@@ -68,35 +84,8 @@ async function createCopilotAssignmentClient(config) {
6884/** @type {string } Safe output type handled by this module */
6985const HANDLER_TYPE = "create_pull_request" ;
7086
71- /** @type {string } Label always added to fallback issues so the triage system can find them */
72- const MANAGED_FALLBACK_ISSUE_LABEL = "agentic-workflows" ;
73-
74- /**
75- * Creates a temporary refs/bundles ref for applying create_pull_request bundles.
76- * Branch names are sanitized for ref compatibility, and a short crypto-random
77- * suffix avoids collisions between branches that sanitize to the same value.
78- *
79- * @param {string } branchName - Target branch name
80- * @returns {string } Temporary bundle ref name
81- */
82- function createBundleTempRef ( branchName ) {
83- const suffix = crypto . randomBytes ( 4 ) . toString ( "hex" ) ;
84- return `refs/bundles/create-pr-${ branchName . replace ( / [ ^ a - z A - Z 0 - 9 - ] / g, "-" ) } -${ suffix } ` ;
85- }
86-
87- /**
88- * Summarize a list for log output to avoid excessively long lines.
89- * @param {string[] } values
90- * @param {number } limit
91- * @returns {string }
92- */
93- function summarizeListForLog ( values , limit = 10 ) {
94- if ( ! Array . isArray ( values ) || values . length === 0 ) {
95- return "(none)" ;
96- }
97- const preview = values . slice ( 0 , limit ) . join ( ", " ) ;
98- return values . length > limit ? `${ preview } ... and ${ values . length - limit } more` : preview ;
99- }
87+ // NOTE: MANAGED_FALLBACK_ISSUE_LABEL, createBundleTempRef, and summarizeListForLog
88+ // are imported from create_pull_request_helpers.cjs above.
10089
10190/**
10291 * Attempt automatic recovery for git am add/add conflicts by preferring the patch version.
@@ -319,131 +308,11 @@ async function rewriteBundleBranchAsSingleCommit(baseBranch, execApi) {
319308 }
320309}
321310
322- /**
323- * Determines if a label API error is transient and worth retrying.
324- * Returns true for:
325- * - The GitHub race condition where a newly-created PR's node ID is not immediately
326- * resolvable via the REST/GraphQL bridge (unprocessable validation error).
327- * - Any standard transient error matched by {@link isTransientError} (network issues,
328- * rate limits, 5xx gateway errors, etc.).
329- * @param {any } error - The error to check
330- * @returns {boolean } True if the error is transient and should be retried
331- */
332- function isLabelTransientError ( error ) {
333- const msg = getErrorMessage ( error ) ;
334- if ( msg . includes ( "Could not resolve to a node with the global id" ) ) {
335- return true ;
336- }
337- return isTransientError ( error ) ;
338- }
339-
340- /** @type {number } Number of retry attempts for label operations */
341- const LABEL_MAX_RETRIES = 5 ;
342- /** @type {number } Base delay in ms used to calculate label retry backoff (3 seconds) */
343- const LABEL_INITIAL_DELAY_MS = 3000 ;
344- /** @type {number } Maximum delay in ms between label retries (30 seconds) */
345- const LABEL_MAX_DELAY_MS = 30000 ;
346-
347- /**
348- * Parse allowed base branch patterns from config value (array or comma-separated string)
349- * @param {string[]|string|undefined } allowedBaseBranchesValue
350- * @returns {Set<string> }
351- */
352- function parseAllowedBaseBranches ( allowedBaseBranchesValue ) {
353- const set = new Set ( ) ;
354- if ( Array . isArray ( allowedBaseBranchesValue ) ) {
355- allowedBaseBranchesValue
356- . map ( branch => String ( branch ) . trim ( ) )
357- . filter ( Boolean )
358- . forEach ( branch => set . add ( branch ) ) ;
359- } else if ( typeof allowedBaseBranchesValue === "string" ) {
360- allowedBaseBranchesValue
361- . split ( "," )
362- . map ( branch => branch . trim ( ) )
363- . filter ( Boolean )
364- . forEach ( branch => set . add ( branch ) ) ;
365- }
366- return set ;
367- }
368-
369- /**
370- * Check if a base branch matches an allowed pattern.
371- * Supports exact matches and "*" glob patterns (e.g. "release/*").
372- * @param {string } baseBranch
373- * @param {Set<string> } allowedBaseBranches
374- * @returns {boolean }
375- */
376- function isBaseBranchAllowed ( baseBranch , allowedBaseBranches ) {
377- if ( allowedBaseBranches . has ( baseBranch ) ) {
378- return true ;
379- }
380- for ( const pattern of allowedBaseBranches ) {
381- if ( pattern === "*" ) {
382- return true ;
383- }
384- if ( pattern . includes ( "*" ) && globPatternToRegex ( pattern , { pathMode : true , caseSensitive : true } ) . test ( baseBranch ) ) {
385- return true ;
386- }
387- }
388- return false ;
389- }
390-
391- /**
392- * Parse config values that may be arrays or comma-separated strings.
393- * @param {string[]|string|undefined } value
394- * @returns {string[] }
395- */
396- function parseStringListConfig ( value ) {
397- if ( ! value ) {
398- return [ ] ;
399- }
400- const raw = Array . isArray ( value ) ? value : String ( value ) . split ( "," ) ;
401- return raw . map ( item => String ( item ) . trim ( ) ) . filter ( Boolean ) ;
402- }
403-
404- /**
405- * Merges the required fallback label with any workflow-configured labels,
406- * deduplicating and filtering empty values.
407- * @param {string[] } [labels]
408- * @returns {string[] }
409- */
410- function mergeFallbackIssueLabels ( labels = [ ] ) {
411- const normalizedLabels = labels
412- . filter ( label => ! ! label )
413- . map ( label => String ( label ) . trim ( ) )
414- . filter ( label => label ) ;
415- return [ ...new Set ( [ MANAGED_FALLBACK_ISSUE_LABEL , ...normalizedLabels ] ) ] ;
416- }
417-
418- /**
419- * Sanitizes configured assignees for fallback issue creation.
420- * Filters invalid values, removes the special "copilot" username (not a valid GitHub user
421- * for issue assignment), and enforces the MAX_ASSIGNEES limit.
422- * Returns null (no assignees field) if the sanitized list is empty.
423- * @param {string[] } assignees - Raw assignees from config
424- * @returns {string[] | null } Sanitized assignees or null if none remain
425- */
426- function sanitizeFallbackAssignees ( assignees ) {
427- if ( ! assignees || assignees . length === 0 ) {
428- return null ;
429- }
430- const sanitized = assignees
431- . filter ( a => typeof a === "string" )
432- . map ( a => a . trim ( ) )
433- . filter ( a => a . length > 0 && a . toLowerCase ( ) !== "copilot" ) ;
434-
435- if ( sanitized . length === 0 ) {
436- return null ;
437- }
438-
439- const limitResult = tryEnforceArrayLimit ( sanitized , MAX_ASSIGNEES , "assignees" ) ;
440- if ( ! limitResult . success ) {
441- core . warning ( `Assignees limit exceeded for fallback issue: ${ limitResult . error } . Using first ${ MAX_ASSIGNEES } .` ) ;
442- return sanitized . slice ( 0 , MAX_ASSIGNEES ) ;
443- }
444-
445- return sanitized ;
446- }
311+ // NOTE: isLabelTransientError, LABEL_MAX_RETRIES, LABEL_INITIAL_DELAY_MS, LABEL_MAX_DELAY_MS,
312+ // parseAllowedBaseBranches, isBaseBranchAllowed, parseStringListConfig, mergeFallbackIssueLabels,
313+ // sanitizeFallbackAssignees, neutralizeClosingKeywordsForIssueBody, generatePatchPreview,
314+ // buildManifestProtectionCreatePrUrl, and renderManifestProtectionFallbackBody
315+ // are imported from create_pull_request_helpers.cjs above.
447316
448317/**
449318 * Creates a fallback GitHub issue, retrying on rate-limit and other transient errors
@@ -494,61 +363,6 @@ async function createFallbackIssue(githubClient, repoParts, title, body, labels,
494363 ) ;
495364}
496365
497- /**
498- * Builds a compare URL used in protected-files fallback issue bodies.
499- * Optionally appends a prefilled PR body that closes the fallback issue.
500- * @param {string } githubServer
501- * @param {{owner: string, repo: string} } repoParts
502- * @param {string } baseBranch
503- * @param {string } branchName
504- * @param {string } title
505- * @param {number } [fallbackIssueNumber]
506- * @returns {string }
507- */
508- function buildManifestProtectionCreatePrUrl ( githubServer , repoParts , baseBranch , branchName , title , fallbackIssueNumber ) {
509- const encodedBase = encodePathSegments ( baseBranch ) ;
510- const encodedHead = encodePathSegments ( branchName ) ;
511- let createPrUrl = `${ githubServer } /${ repoParts . owner } /${ repoParts . repo } /compare/${ encodedBase } ...${ encodedHead } ?expand=1&title=${ encodeURIComponent ( title ) } ` ;
512- if ( typeof fallbackIssueNumber === "number" ) {
513- createPrUrl += `&body=${ encodeURIComponent ( `Closes #${ fallbackIssueNumber } ` ) } ` ;
514- }
515- return createPrUrl ;
516- }
517-
518- /**
519- * Renders protected-files fallback issue body with a prefilled compare URL.
520- * @param {string } mainBodyContent
521- * @param {string } footerContent
522- * @param {string } fileList
523- * @param {string } createPrUrl
524- * @returns {string }
525- */
526- function renderManifestProtectionFallbackBody ( mainBodyContent , footerContent , fileList , createPrUrl ) {
527- const templatePath = getPromptPath ( "manifest_protection_create_pr_fallback.md" ) ;
528- return renderTemplateFromFile ( templatePath , {
529- main_body : mainBodyContent ,
530- footer : footerContent ,
531- files : fileList ,
532- create_pr_url : createPrUrl ,
533- } ) ;
534- }
535-
536- /**
537- * Neutralizes issue-closing keywords in body text to avoid unintended cross-issue closure
538- * when PR content is reused in fallback issue bodies.
539- *
540- * Example: "Closes #123" -> "Closes \\#123"
541- *
542- * @param {string } content
543- * @returns {string }
544- */
545- function neutralizeClosingKeywordsForIssueBody ( content ) {
546- if ( ! content ) {
547- return content ;
548- }
549- return String ( content ) . replace ( / \b ( f i x | f i x e s | f i x e d | c l o s e | c l o s e s | c l o s e d | r e s o l v e | r e s o l v e s | r e s o l v e d ) \s + ( (?: [ a - z 0 - 9 _ . - ] + \/ [ a - z 0 - 9 _ . - ] + ) ? # \d + ) \b / gi, ( _match , keyword , issueRef ) => `${ keyword } ${ String ( issueRef ) . replace ( "#" , "\\#" ) } ` ) ;
550- }
551-
552366/**
553367 * Maximum limits for pull request parameters to prevent resource exhaustion.
554368 * These limits align with GitHub's API constraints and security best practices.
@@ -632,35 +446,7 @@ function enforcePullRequestLimits(patchContent, maxFiles = MAX_FILES) {
632446 }
633447}
634448
635- /**
636- * Generate a patch preview with max 500 lines and 2000 chars for issue body
637- * @param {string } patchContent - The full patch content
638- * @returns {string } Formatted patch preview
639- */
640- function generatePatchPreview ( patchContent ) {
641- if ( ! patchContent || ! patchContent . trim ( ) ) {
642- return "" ;
643- }
644-
645- const lines = patchContent . split ( "\n" ) ;
646- const maxLines = 500 ;
647- const maxChars = 2000 ;
648-
649- // Apply line limit first
650- let preview = lines . length <= maxLines ? patchContent : lines . slice ( 0 , maxLines ) . join ( "\n" ) ;
651- const lineTruncated = lines . length > maxLines ;
652-
653- // Apply character limit
654- const charTruncated = preview . length > maxChars ;
655- if ( charTruncated ) {
656- preview = preview . slice ( 0 , maxChars ) ;
657- }
658-
659- const truncated = lineTruncated || charTruncated ;
660- const summary = truncated ? `Show patch preview (${ Math . min ( maxLines , lines . length ) } of ${ lines . length } lines)` : `Show patch (${ lines . length } lines)` ;
661-
662- return `\n\n<details><summary>${ summary } </summary>\n\n\`\`\`diff\n${ preview } ${ truncated ? "\n... (truncated)" : "" } \n\`\`\`\n\n</details>` ;
663- }
449+ // NOTE: generatePatchPreview is imported from create_pull_request_helpers.cjs above.
664450
665451/**
666452 * Check whether the remote branch already exists and, if so, either reuse it
@@ -2303,10 +2089,11 @@ ${patchPreview}`;
23032089 // Return success with PR details
23042090 return {
23052091 success : true ,
2306- pull_request_number : pullRequest . number ,
2307- pull_request_url : pullRequest . html_url ,
2092+ number : pullRequest . number ,
2093+ url : pullRequest . html_url ,
2094+ managedBody : body ,
23082095 branch_name : branchName ,
2309- temporary_id : temporaryId ,
2096+ temporaryId : temporaryId ,
23102097 repo : itemRepo ,
23112098 } ;
23122099 } catch ( prError ) {
0 commit comments