@@ -403,6 +403,7 @@ const getPreviewUrlFromGitHubCommitStatus = async (token, owner, repo, commitSha
403403 const pollingIntervalMs = Number . isFinite ( intervalMs ) && intervalMs > 0 ? intervalMs : 10000 ;
404404 const pollingTimeoutMs = Number . isFinite ( timeoutMs ) && timeoutMs > 0 ? timeoutMs : 240000 ;
405405 const deadline = Date . now ( ) + pollingTimeoutMs ;
406+ let lastKnownVercelUrl = "" ;
406407
407408 while ( Date . now ( ) < deadline ) {
408409 const response = await fetch ( `https://api.github.com/repos/${ owner } /${ repo } /commits/${ commitSha } /status` , {
@@ -420,23 +421,123 @@ const getPreviewUrlFromGitHubCommitStatus = async (token, owner, repo, commitSha
420421
421422 const data = await response . json ( ) ;
422423 const statuses = Array . isArray ( data ?. statuses ) ? data . statuses : [ ] ;
423- const urlStatus =
424- statuses . find ( ( status ) => typeof status ?. target_url === "string" && status . target_url . includes ( "vercel.app" ) ) ??
425- statuses . find (
426- ( status ) =>
427- typeof status ?. context === "string" &&
428- status . context . toLowerCase ( ) . includes ( "vercel" ) &&
429- typeof status ?. target_url === "string" ,
430- ) ;
431-
432- if ( urlStatus ?. target_url ) {
433- return urlStatus . target_url ;
424+ const deploymentUrlStatus = statuses . find (
425+ ( status ) => typeof status ?. target_url === "string" && status . target_url . includes ( "vercel.app" ) ,
426+ ) ;
427+ if ( deploymentUrlStatus ?. target_url ) {
428+ return deploymentUrlStatus . target_url ;
429+ }
430+
431+ const vercelStatus = statuses . find (
432+ ( status ) =>
433+ typeof status ?. context === "string" &&
434+ status . context . toLowerCase ( ) . includes ( "vercel" ) &&
435+ typeof status ?. target_url === "string" ,
436+ ) ;
437+ if ( vercelStatus ?. target_url ) {
438+ lastKnownVercelUrl = vercelStatus . target_url ;
434439 }
435440
436441 await sleep ( pollingIntervalMs ) ;
437442 }
438443
439- return "" ;
444+ return lastKnownVercelUrl ;
445+ } ;
446+
447+ const getPreviewUrlFromGitHubPrComments = async ( token , owner , repo , prNumber ) => {
448+ const intervalMs = Number ( process . env . AI_INSPECTOR_VERCEL_PREVIEW_INTERVAL_MS ?? "10000" ) ;
449+ const timeoutMs = Number ( process . env . AI_INSPECTOR_VERCEL_PREVIEW_TIMEOUT_MS ?? "240000" ) ;
450+ const pollingIntervalMs = Number . isFinite ( intervalMs ) && intervalMs > 0 ? intervalMs : 10000 ;
451+ const pollingTimeoutMs = Number . isFinite ( timeoutMs ) && timeoutMs > 0 ? timeoutMs : 240000 ;
452+ const deadline = Date . now ( ) + pollingTimeoutMs ;
453+ const vercelUrlPattern = / h t t p s : \/ \/ [ ^ \s ) ] + \. v e r c e l \. a p p [ ^ \s ) ] * / gi;
454+ let lastKnownVercelUrl = "" ;
455+
456+ while ( Date . now ( ) < deadline ) {
457+ const response = await fetch (
458+ `https://api.github.com/repos/${ owner } /${ repo } /issues/${ prNumber } /comments?per_page=50` ,
459+ {
460+ headers : {
461+ Accept : "application/vnd.github+json" ,
462+ Authorization : `Bearer ${ token } ` ,
463+ "X-GitHub-Api-Version" : "2022-11-28" ,
464+ } ,
465+ } ,
466+ ) ;
467+
468+ if ( ! response . ok ) {
469+ const body = await response . text ( ) ;
470+ throw new Error ( `Failed to fetch PR comments from GitHub (${ response . status } ): ${ body } ` ) ;
471+ }
472+
473+ const comments = await response . json ( ) ;
474+ if ( Array . isArray ( comments ) ) {
475+ for ( const comment of comments ) {
476+ const body = typeof comment ?. body === "string" ? comment . body : "" ;
477+ const matches = body . match ( vercelUrlPattern ) ;
478+ if ( matches && matches . length > 0 ) {
479+ lastKnownVercelUrl = matches [ 0 ] ;
480+ if ( lastKnownVercelUrl . includes ( ".vercel.app" ) ) {
481+ return lastKnownVercelUrl ;
482+ }
483+ }
484+ }
485+ }
486+
487+ await sleep ( pollingIntervalMs ) ;
488+ }
489+
490+ return lastKnownVercelUrl ;
491+ } ;
492+
493+ const normalizePreviewUrl = ( url ) => {
494+ if ( ! url ) {
495+ return "" ;
496+ }
497+
498+ const trimmed = url . trim ( ) ;
499+ if ( ! trimmed ) {
500+ return "" ;
501+ }
502+
503+ if ( trimmed . startsWith ( "http://" ) || trimmed . startsWith ( "https://" ) ) {
504+ return trimmed ;
505+ }
506+
507+ if ( trimmed . includes ( "." ) ) {
508+ return `https://${ trimmed } ` ;
509+ }
510+
511+ return trimmed ;
512+ } ;
513+
514+ const resolvePreviewUrl = async ( { token, owner, repo, branchName, commitSha, prNumber } ) => {
515+ const templateUrl = normalizePreviewUrl ( getPreviewUrl ( branchName ) ) ;
516+
517+ try {
518+ const vercelApiUrl = normalizePreviewUrl ( await getPreviewUrlFromVercel ( branchName ) ) ;
519+ if ( vercelApiUrl ) {
520+ return vercelApiUrl ;
521+ }
522+
523+ const commitStatusUrl = normalizePreviewUrl ( await getPreviewUrlFromGitHubCommitStatus ( token , owner , repo , commitSha ) ) ;
524+ if ( commitStatusUrl . includes ( ".vercel.app" ) ) {
525+ return commitStatusUrl ;
526+ }
527+
528+ const commentUrl = normalizePreviewUrl ( await getPreviewUrlFromGitHubPrComments ( token , owner , repo , prNumber ) ) ;
529+ if ( commentUrl ) {
530+ return commentUrl ;
531+ }
532+
533+ if ( commitStatusUrl ) {
534+ return commitStatusUrl ;
535+ }
536+ } catch ( previewError ) {
537+ console . error ( "Preview URL resolution failed" , previewError ) ;
538+ }
539+
540+ return templateUrl ;
440541} ;
441542
442543const applyPatch = ( patch ) => {
@@ -606,15 +707,14 @@ const main = async () => {
606707
607708 const prUrl = pr . html_url ;
608709 const commitSha = runGitOutput ( [ "rev-parse" , "HEAD" ] ) . trim ( ) ;
609- let previewUrl = getPreviewUrl ( branchName ) ;
610- try {
611- previewUrl =
612- ( await getPreviewUrlFromVercel ( branchName ) ) ||
613- ( await getPreviewUrlFromGitHubCommitStatus ( githubToken , owner , repoName , commitSha ) ) ||
614- previewUrl ;
615- } catch ( previewError ) {
616- console . error ( `Task ${ taskId } preview URL resolution failed` , previewError ) ;
617- }
710+ const previewUrl = await resolvePreviewUrl ( {
711+ token : githubToken ,
712+ owner,
713+ repo : repoName ,
714+ branchName,
715+ commitSha,
716+ prNumber : pr . number ,
717+ } ) ;
618718
619719 await taskRef . update ( {
620720 status : "completed" ,
0 commit comments