@meirblachman/pr-review-needed 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.min.js +82 -39
  2. package/package.json +1 -1
package/dist/index.min.js CHANGED
@@ -1,30 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import{Command as go}from"commander";import{existsSync as oo,readFileSync as ro,writeFileSync as Oe}from"node:fs";import{resolve as z,dirname as so}from"node:path";import{fileURLToPath as io}from"node:url";import{DefaultAzureCredential as Bt}from"@azure/identity";import*as je from"azure-devops-node-api";import{BearerCredentialHandler as Ut}from"azure-devops-node-api/handlers/bearertoken.js";var B="\x1B[0m",ye="\x1B[2m",Be="\x1B[36m",Ot="\x1B[32m",It="\x1B[33m",Ft="\x1B[31m",Ue="\x1B[1m",_e=!1;function ze(e){_e=e}function H(){return ye+new Date().toISOString().slice(11,19)+B}function A(e){console.log(`${H()} ${Be}\u2139${B} ${e}`)}function x(e){console.log(`${H()} ${Ot}\u2714${B} ${e}`)}function C(e){console.log(`${H()} ${It}\u26A0${B} ${e}`)}function K(e){console.error(`${H()} ${Ft}\u2716${B} ${e}`)}function $(e){_e&&console.log(`${H()} ${ye}\xB7 ${e}${B}`)}function Re(e){console.log(`
3
- ${Ue}${Be}\u25B8 ${e}${B}`)}function _(e,t){console.log(` ${ye}${e}:${B} ${Ue}${t}${B}`)}var _t="499b84ac-1321-427f-aa17-267ca6975798";async function X(){let e=process.env.SYSTEM_ACCESSTOKEN;if(e)return $("Using SYSTEM_ACCESSTOKEN from Azure Pipelines environment"),e;try{$("Requesting token for Azure DevOps resource via DefaultAzureCredential\u2026");let n=await new Bt().getToken(`${_t}/.default`);return $("Token acquired successfully"),n.token}catch(t){let n=t instanceof Error?t.message:String(t);throw new Error(`Failed to obtain Azure DevOps token. Set SYSTEM_ACCESSTOKEN in Azure Pipelines, or configure environment credentials (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET), or log in with \`az login\`.
4
- ${n}`,{cause:t})}}async function Q(e,t){$(`Connecting to ${e}\u2026`);let n=new Ut(t),o=new je.WebApi(e,n),r=await o.getGitApi(),l=await o.getBuildApi(),i=await o.getCoreApi(),a=await o.getPolicyApi();return{gitApi:r,buildApi:l,coreApi:i,policyApi:a}}var I=new Map;async function ve(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).gitApi}async function qe(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).buildApi}async function Ge(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).policyApi}async function We(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).coreApi}import{readFileSync as Vt}from"node:fs";import{resolve as Qe,dirname as Jt}from"node:path";import{fileURLToPath as Yt}from"node:url";import{DefaultAzureCredential as qt}from"@azure/identity";var U=class extends Error{constructor(t){super(t),this.name="NonRetryableError"}},zt={maxAttempts:4,baseDelayMs:2e3,maxDelayMs:3e4};function jt(e){return new Promise(t=>setTimeout(t,e))}async function E(e,t,n){let{maxAttempts:o,baseDelayMs:r,maxDelayMs:l}={...zt,...n};for(let i=1;i<=o;i++)try{return await t()}catch(a){if(a instanceof U)throw a;let m=a instanceof Error?a.message:String(a);if(i===o)throw K(`${e} failed after ${o} attempts: ${m}`),a;let s=Math.min(r*2**(i-1),l);C(`${e} failed (attempt ${i}/${o}), retrying in ${(s/1e3).toFixed(1)}s\u2026 \u2014 ${m}`),await jt(s)}throw new Error("unreachable")}var Gt="https://graph.microsoft.com/.default",Wt="https://graph.microsoft.com/v1.0";async function He(){try{return(await new qt().getToken(Gt)).token}catch(e){let t=e instanceof Error?e.message:String(e);throw new Error(`Failed to get Microsoft Graph token. Configure environment credentials (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET), or log in with \`az login\`.
5
- ${t}`,{cause:e})}}async function Ht(e,t){let n=await fetch(t,{headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let o=await n.text();throw n.status>=400&&n.status<500?new U(`Graph API ${n.status}: ${o}`):new Error(`Graph API ${n.status}: ${o}`)}return await n.json()}async function Ve(e,t){let n=[],o=`${Wt}/users/${encodeURIComponent(t)}/directReports?$select=mail,userPrincipalName,displayName&$top=999`;for(;o;){let r=await E(`Fetch direct reports for ${t}`,()=>Ht(e,o));for(let l of r.value){let i=l.mail?.toLowerCase(),a=l.userPrincipalName?.toLowerCase();i&&n.push(i),a&&a!==i&&n.push(a)}o=r["@odata.nextLink"]}return n}async function Je(e){A(`Fetching direct reports for ${e} from Microsoft Graph\u2026`);let t=await He(),n=await Ve(t,e);x(`Found ${n.length} direct reports for ${e}`);for(let o of n)$(` ${o}`);return n}async function Ye(e){A(`Fetching full org tree under ${e} from Microsoft Graph\u2026`);let t=await He(),n=10,o=[],r=[e.toLowerCase()],l=[e],i=new Set;for(;l.length>0;){let a=l.filter(c=>!i.has(c));for(let c of a)i.add(c);if(a.length===0)break;$(` Processing ${a.length} users (concurrency: ${n})\u2026`);let m=[];for(let c=0;c<a.length;c+=n){let s=a.slice(c,c+n),f=await Promise.all(s.map(async u=>{try{let d=await Ve(t,u);return $(` ${u} \u2192 ${d.length} direct reports`),{upn:u,reports:d}}catch(d){if(d instanceof U&&d.message.includes("404"))return C(` ${u} \u2014 not found in directory, keeping as member`),{upn:u,reports:[u]};throw d}}));for(let{upn:u,reports:d}of f){d.length>0&&!(d.length===1&&d[0]===u)&&r.push(u.toLowerCase());for(let g of d)o.push(g),m.push(g)}}l=m}return x(`Found ${o.length} total org members under ${e} (${r.length} managers)`),{members:o,managers:r}}function Ze(e){let t=e.match(/https?:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/);if(t)return{orgUrl:`https://dev.azure.com/${t[1]}`,project:t[2],repository:t[3]};let n=e.match(/https?:\/\/(?:[^@]+@)?([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/\s]+)/);if(n)return{orgUrl:`https://dev.azure.com/${n[1]}`,project:n[2],repository:n[3]};let o=e.match(/ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/\s]+)/);if(o)return{orgUrl:`https://dev.azure.com/${o[1]}`,project:o[2],repository:o[3]};let r=e.match(/vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/\s]+)/);return r?{orgUrl:`https://dev.azure.com/${r[1]}`,project:r[2],repository:r[3]}:null}var ee=[{label:"XS",maxChanges:10},{label:"S",maxChanges:40},{label:"M",maxChanges:100},{label:"L",maxChanges:400},{label:"XL",maxChanges:1e3}];function te(e){return e?.uniqueName?.toLowerCase()??""}function Ke(e,t,n,o){return{totalConflicts:[...e.approved,...e.needingReview,...e.waitingOnAuthor].filter(i=>i.hasMergeConflict).length,mergeRestarted:t,mergeRestartFailed:n,repoStats:o}}function be(e,t,n,o){let r=[...t.approved,...t.needingReview,...t.waitingOnAuthor];return{repoLabel:e,approved:t.approved.length,needingReview:t.needingReview.length,waitingOnAuthor:t.waitingOnAuthor.length,conflicts:r.filter(l=>l.hasMergeConflict).length,mergeRestarted:n,mergeRestartFailed:o}}var Xe=[{label:"\u26A0\uFE0F Aging",minDays:7},{label:"\u{1F534} Stale",minDays:14},{label:"\u{1F480} Abandoned",minDays:30}];function Zt(e){let t=e?Qe(e):Qe(Jt(Yt(import.meta.url)),"..","pr-review-config.json"),n=Vt(t,"utf-8");return JSON.parse(n)}function Kt(e){if(!e.repositories||e.repositories.length===0)throw new Error("Config must specify 'repositories' (array of repository objects with a 'url' field).");return e.repositories.map(t=>{let n=Ze(t.url);if(!n)throw new Error(`Invalid ADO repository URL: ${t.url}`);return{...n,skipRestartMerge:t.skipRestartMerge??!1,patterns:{ignore:t.patterns?.ignore??[],labels:t.patterns?.labels??{}}}})}async function Xt(e){let t=new Set((e.teamMembers??[]).map(o=>o.toLowerCase())),n=new Set;if(e.orgManager){let o=await Ye(e.orgManager);for(let r of o.members)t.add(r);if(t.add(e.orgManager.toLowerCase()),e.ignoreManagers)for(let r of o.managers)n.add(r.toLowerCase())}if(e.manager){let o=await Je(e.manager);for(let r of o)t.add(r);t.add(e.manager.toLowerCase()),e.ignoreManagers&&n.add(e.manager.toLowerCase())}return{teamMembers:t,ignoredUsers:n}}function Qt(e){if(e.quantifier?.enabled===!1)return;let t=e.quantifier?.excludedPatterns??[],n=e.quantifier?.thresholds?e.quantifier.thresholds.map(o=>({label:o.label,maxChanges:o.maxChanges})):ee;return{enabled:!0,excludedPatterns:t,thresholds:n}}function en(e){return e.staleness?.enabled===!1?{enabled:!1,thresholds:[]}:{enabled:!0,thresholds:e.staleness?.thresholds?e.staleness.thresholds.map(n=>({label:n.label,minDays:n.minDays})).sort((n,o)=>o.minDays-n.minDays):Xe.slice().sort((n,o)=>o.minDays-n.minDays)}}function tn(e){if(!(!e.autoNudge||e.autoNudge.enabled===!1))return{enabled:!0,minStalenessLevel:e.autoNudge.minStalenessLevel,cooldownDays:e.autoNudge.cooldownDays??7,commentTemplate:e.autoNudge.commentTemplate??"\u23F0 This PR has been waiting for review for {{days}} days. Reviewers: {{reviewers}}. Please take a look!",dryRun:e.autoNudge.dryRun??!1,historyFile:e.autoNudge.historyFile??".pr-nudge-history.json"}}async function et(e){let t=Zt(e),n=Kt(t),{teamMembers:o,ignoredUsers:r}=await Xt(t),l=new Set((t.botUsers??[]).map(g=>g.toLowerCase())),i=new Set((t.aiBotUsers??[]).map(g=>g.toLowerCase())),a=new Set((t.starredUsers??[]).map(g=>g.toLowerCase())),m=Qt(t),c=t.restartMergeAfterDays??30,s=en(t),f=t.notifications,u=t.webhook,d=tn(t);return{repos:n,teamMembers:o,ignoredUsers:r,botUsers:l,aiBotUsers:i,starredUsers:a,quantifier:m,restartMergeAfterDays:c,staleness:s,notifications:f,webhook:u,autoNudge:d}}import{PullRequestStatus as rn}from"azure-devops-node-api/interfaces/GitInterfaces.js";import{BuildResult as se,BuildStatus as ie}from"azure-devops-node-api/interfaces/BuildInterfaces.js";import{PolicyEvaluationStatus as j}from"azure-devops-node-api/interfaces/PolicyInterfaces.js";import{LineDiffBlockChangeType as ne,VersionControlChangeType as tt}from"azure-devops-node-api/interfaces/GitInterfaces.js";import nn from"picomatch";function oe(e,t=ee){let n=[...t].sort((o,r)=>o.maxChanges-r.maxChanges);for(let o of n)if(e<=o.maxChanges)return o.label;return n[n.length-1].label}function on(e,t){if(t.length===0)return!1;let n=e.replace(/^\//,"");return t.some(o=>o(n))}function nt(e){let t=0,n=0;for(let o of e){let r=o.changeType??ne.None;r===ne.Add?t+=o.modifiedLinesCount??0:r===ne.Delete?n+=o.originalLinesCount??0:r===ne.Edit&&(t+=o.modifiedLinesCount??0,n+=o.originalLinesCount??0)}return{added:t,deleted:n}}async function ot(e,t,n,o,r){let l=r.excludedPatterns.map(w=>nn(w,{dot:!0})),i=await E(`Fetch iterations for PR #${o}`,()=>e.getPullRequestIterations(t,o,n));if(!i||i.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:oe(0,r.thresholds)};let a=i[i.length-1],m=a.id,c=[],s=0,f=100;for(;;){let w=await E(`Fetch iteration changes for PR #${o} iter ${m}`,()=>e.getPullRequestIterationChanges(t,o,m,n,f,s)),R=w.changeEntries??[];for(let P of R){let D=P.item?.path??"",O=P.originalPath;D&&!on(D,l)&&c.push({path:D,originalPath:O??void 0,changeType:P.changeType??0})}if((w.nextSkip??0)===0&&(w.nextTop??0)===0||(s=w.nextSkip??s+f,R.length===0))break}if(c.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:oe(0,r.thresholds)};let u=a.sourceRefCommit?.commitId,d=a.targetRefCommit?.commitId;if(!u||!d){$(` PR #${o} \u2014 no commit refs, using file count as proxy`);let w=c.length;return{linesAdded:w,linesDeleted:0,totalChanges:w,label:oe(w,r.thresholds)}}let g=c.map(w=>{let R=(w.changeType&tt.Add)!==0,P=(w.changeType&tt.Delete)!==0;return{originalPath:R?void 0:w.originalPath??w.path,path:P?void 0:w.path}}),h=0,p=0,b=10,y=0;for(let w=0;w<g.length;w+=b){let R=g.slice(w,w+b),P={baseVersionCommit:d,targetVersionCommit:u,fileDiffParams:R};try{let D=await e.getFileDiffs(P,n,t);for(let O of D){let{added:G,deleted:W}=nt(O.lineDiffBlocks??[]);h+=G,p+=W}}catch{for(let D of R)try{let O={baseVersionCommit:d,targetVersionCommit:u,fileDiffParams:[D]},G=await e.getFileDiffs(O,n,t);for(let W of G){let{added:$e,deleted:we}=nt(W.lineDiffBlocks??[]);h+=$e,p+=we}}catch{y++}}}y>0&&$(` PR #${o} \u2014 skipped ${y} files (not found at specified version)`);let S=h+p,T=oe(S,r.thresholds);return $(` PR #${o} \u2014 +${h} -${p} = ${S} (${T})`),{linesAdded:h,linesDeleted:p,totalChanges:S,label:T}}async function re(e,t,n){let o=new Array(e.length),r=0;async function l(){for(;r<e.length;){let a=r++;o[a]=await n(e[a])}}let i=Array.from({length:Math.min(t,e.length)},()=>l());return await Promise.all(i),o}import rt from"picomatch";function st(e,t,n){if(Object.keys(n).length===0)return[];let o=t.map(i=>rt(i,{dot:!0})),r=e.map(i=>i.replace(/^\//,"")).filter(i=>!o.some(a=>a(i)));if(r.length===0)return[];let l=[];for(let[i,a]of Object.entries(n)){let m=a.map(c=>rt(c,{dot:!0}));r.some(c=>m.some(s=>s(c)))&&l.push(i)}return l}function sn(e){let t=[],n=0,o=0;for(let r of e){if(r.isDraft){n++,$(` #${r.pullRequestId} \u2014 draft, skipping`);continue}if((r.labels??[]).map(i=>i.name??"").some(i=>i.toUpperCase()==="NO-MERGE")){o++,$(` #${r.pullRequestId} \u2014 NO-MERGE label, skipping`);continue}t.push(r)}return n>0&&$(`Skipped ${n} draft PRs`),o>0&&$(`Skipped ${o} NO-MERGE PRs`),t}async function an(e,t,n,o){let r=await E(`Fetch iterations for PR #${o} (file patterns)`,()=>e.getPullRequestIterations(t,o,n));if(!r||r.length===0)return[];let i=r[r.length-1].id;if(i==null)return[];let a=[],m=0,c=100;for(;;){let s=await E(`Fetch iteration changes for PR #${o} iter ${i} (file patterns)`,()=>e.getPullRequestIterationChanges(t,o,i,n,c,m));for(let f of s.changeEntries??[]){let u=f.item?.path??"";u&&a.push(u)}if((s.nextSkip??0)===0&&(s.nextTop??0)===0||(m=s.nextSkip??m+c,(s.changeEntries??[]).length===0))break}return a}function cn(e,t){if(e===ie.InProgress)return"inProgress";if(e===ie.NotStarted)return"notStarted";switch(t){case se.Succeeded:return"succeeded";case se.Failed:return"failed";case se.PartiallySucceeded:return"partiallySucceeded";case se.Canceled:return"canceled";default:return"none"}}async function ln(e,t,n,o){try{let r=`refs/pull/${o}/merge`,l=await E(`Fetch builds for PR #${o}`,()=>e.getBuilds(n,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,10,void 0,void 0,void 0,void 0,r,void 0,t,"TfsGit"));if(!l||l.length===0)return;let i=new Map;for(let u of l){let d=u.definition?.id??0;i.has(d)||i.set(d,u)}let a=[],m=0,c=0,s=0,f=0;for(let u of i.values()){let d=cn(u.status,u.result);switch(a.push({id:u.id??0,name:u.definition?.name??"Unknown",status:ie[u.status??ie.None]??"None",result:d}),d){case"succeeded":m++;break;case"failed":c++;break;case"inProgress":case"notStarted":s++;break;default:f++;break}}return{total:a.length,succeeded:m,failed:c,inProgress:s,other:f,runs:a}}catch(r){let l=r instanceof Error?r.message:String(r);$(` #${o} \u2014 failed to fetch pipeline status: ${l}`);return}}function un(e){switch(e){case j.Queued:return"queued";case j.Running:return"running";case j.Approved:return"approved";case j.Rejected:return"rejected";case j.NotApplicable:return"notApplicable";case j.Broken:return"broken";default:return"queued"}}async function dn(e,t,n,o){try{let r=`vstfs:///CodeReview/CodeReviewId/${n}/${o}`,l=await E(`Fetch policy evaluations for PR #${o}`,()=>e.getPolicyEvaluations(t,r));if(!l||l.length===0)return;let i=[],a=0,m=0,c=0,s=0;for(let f of l){let u=un(f.status);if(u!=="notApplicable")switch(i.push({evaluationId:f.evaluationId??"",displayName:f.configuration?.type?.displayName??"Unknown Policy",status:u,isBlocking:f.configuration?.isBlocking??!1,completedDate:f.completedDate?f.completedDate.toISOString():void 0}),u){case"approved":a++;break;case"rejected":case"broken":m++;break;case"running":case"queued":c++;break;default:s++;break}}return i.length===0?void 0:{total:i.length,approved:a,rejected:m,running:c,other:s,evaluations:i}}catch(r){let l=r instanceof Error?r.message:String(r);$(` #${o} \u2014 failed to fetch policy evaluations: ${l}`);return}}async function it(e,t,n,o,r,l={ignore:[],labels:{}},i,a,m){let c;try{let g=new URL(o);g.hostname.endsWith(".visualstudio.com")?c=`https://dev.azure.com/${g.hostname.replace(".visualstudio.com","")}`:c=o.replace(/\/$/,"")}catch{c=o.replace(/\/$/,"")}let s=await E("Fetch pull requests",()=>e.getPullRequests(t,{status:rn.Active},n));$(`API returned ${s.length} active pull requests`);let f;if(i)try{f=(await E("Resolve repository GUID",()=>e.getRepository(t,n))).id}catch(g){let h=g instanceof Error?g.message:String(g);$(`Failed to resolve repository GUID for ${t}: ${h}`)}let u=sn(s);A(`Fetching threads for ${u.length} PRs in ${n}/${t} (concurrency: ${10})\u2026`);let d=await re(u,10,async g=>{let h=g.pullRequestId;$(` #${h} \u2014 fetching threads\u2026`);let b=(await E(`Fetch threads for PR #${h}`,()=>e.getThreads(t,h,n))).map(D=>({id:D.id??0,publishedDate:new Date(D.publishedDate??0),comments:(D.comments??[]).filter(O=>!O.isDeleted).map(O=>({authorUniqueName:te(O.author),publishedDate:new Date(O.publishedDate??0)}))})),y=(g.reviewers??[]).map(D=>({displayName:D.displayName??"",uniqueName:te(D),vote:D.vote??0})),S=`${c}/${n}/_git/${t}/pullrequest/${h}`,T=(g.labels??[]).map(D=>D.name??""),w=[];if(Object.keys(l.labels).length>0){let D=await an(e,t,n,h);w=st(D,l.ignore,l.labels),w.length>0&&$(` #${h} \u2014 detected labels: ${w.join(", ")}`)}let R=i&&f?await ln(i,f,n,h):void 0,P=a&&m?await dn(a,n,m,h):void 0;return{id:h,title:g.title??"(no title)",author:g.createdBy?.displayName??"Unknown",authorUniqueName:te(g.createdBy),url:S,createdDate:new Date(g.creationDate??0),reviewers:y,threads:b,labels:T,detectedLabels:w,mergeStatus:g.mergeStatus??0,lastSourcePushDate:g.lastMergeSourceCommit?.committer?.date?new Date(g.lastMergeSourceCommit.committer.date):void 0,size:r?await ot(e,t,n,h,r):void 0,description:g.description??void 0,sourceBranch:g.sourceRefName??void 0,targetBranch:g.targetRefName??void 0,pipelineStatus:R,policyStatus:P}});return $(`${d.length} PRs remain after filtering`),d}async function at(e,t,n,o){for(let r of o)if(r.detectedLabels.length!==0){for(let l of r.detectedLabels)if(!r.labels.some(i=>i.toLowerCase()===l.toLowerCase()))try{await E(`Add label '${l}' to PR #${r.id}`,()=>e.createPullRequestLabel({name:l},t,r.id,n)),r.labels.push(l),$(` #${r.id} \u2014 added label '${l}'`)}catch(i){let a=i instanceof Error?i.message:String(i);$(` #${r.id} \u2014 failed to add label '${l}': ${a}`)}}}var gn=1440*60*1e3,fn=["TF401398","TF401027"],mn=["TF401027"];async function ct(e,t,n,o,r,l=new Date){if(r<0)return $("Restart merge is disabled (restartMergeAfterDays < 0)"),{restarted:0,failed:0,restartedPrIds:[]};let i=new Date(l.getTime()-r*gn),a=o.filter(f=>f.createdDate<i);if(a.length===0)return $("No PRs older than the restart-merge threshold"),{restarted:0,failed:0,restartedPrIds:[]};A(`Restarting merge for ${a.length} PR(s) older than ${r} days\u2026`);let m=0,c=0,s=[];for(let f of a)try{await E(`Restart merge for PR #${f.id}`,async()=>{try{return await e.updatePullRequest({mergeStatus:1},t,f.id,n)}catch(u){let d=u instanceof Error?u.message:String(u);throw fn.some(g=>d.includes(g))?new U(d):u}}),$(` #${f.id} "${f.title}" \u2014 merge restarted`),m++,s.push(f.id)}catch(u){let d=u instanceof Error?u.message:String(u);if(C(` #${f.id} "${f.title}" \u2014 failed to restart merge: ${d}`),c++,mn.some(g=>d.includes(g))){C("Stopping restart-merge for this repository due to permission error"),c+=a.length-a.indexOf(f)-1;break}}return x(`Restarted merge for ${m}/${a.length} PR(s)`),{restarted:m,failed:c,restartedPrIds:s}}import{PullRequestAsyncStatus as Se}from"azure-devops-node-api/interfaces/GitInterfaces.js";var pn=["build","[bot]","team foundation","microsoft.visualstudio.com"],hn=["dependabot[bot]","renovate[bot]","github-actions[bot]","snyk-bot","greenkeeper[bot]","depfu[bot]","imgbot[bot]","allcontributors[bot]"],lt=["github copilot","copilot[bot]","claude","codex"];function Te(e,t=new Set,n){let o=e.toLowerCase();return!!(t.has(o)||pn.some(r=>o.includes(r))||n&&t.has(n.toLowerCase()))}function ae(e,t=new Set,n){let o=e.toLowerCase();if(t.has(o)||lt.some(r=>o.includes(r)))return!0;if(n){let r=n.toLowerCase();if(t.has(r)||lt.some(l=>r.includes(l)))return!0}return!1}function ce(e,t=new Set,n,o=new Set){let r=e.toLowerCase();return hn.some(l=>r.includes(l))||Te(r,t,n)||ae(r,o,n)}function $n(e,t=new Set,n){return ae(e,t,n)}function Ae(e,t,n=new Set,o,r=new Set){if($n(t,r,o))switch(e){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}if(ce(t,n,o,r))return"APPROVE";switch(e){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}}function Pe(e,t=new Set,n=new Set){let o=e.authorUniqueName,r=[];for(let l of e.threads)for(let i of l.comments)Te(i.authorUniqueName,t)||ae(i.authorUniqueName,n)||r.push({date:i.publishedDate,isAuthor:i.authorUniqueName===o});return e.lastSourcePushDate&&r.push({date:e.lastSourcePushDate,isAuthor:!0}),r}function ut(e,t=new Set,n,o=new Set,r=new Set,l=new Set,i=new Set){let a=[],m=[],c=[];for(let s of e){if(o.has(s.authorUniqueName)){$(` #${s.id} "${s.title}" \u2014 author ${s.authorUniqueName} is ignored, skipping`);continue}let f=t.size===0||t.has(s.authorUniqueName),u=i.has(s.authorUniqueName);if(s.reviewers.some(R=>R.vote>=5&&!Te(R.uniqueName,r,R.displayName)&&!ae(R.uniqueName,l,R.displayName))){let R=s.mergeStatus===Se.Conflicts;$(` #${s.id} "${s.title}" \u2014 approved`),a.push({id:s.id,title:s.title,author:s.author,url:s.url,createdDate:s.createdDate,hasMergeConflict:R,isTeamMember:f,isStarred:u,action:Ae("approved",s.authorUniqueName,r,s.author,l),repository:n,size:s.size,detectedLabels:s.detectedLabels.length>0?s.detectedLabels:void 0,pipelineStatus:s.pipelineStatus,policyStatus:s.policyStatus});continue}let g=Pe(s,r,l),h=g.filter(R=>R.isAuthor).sort((R,P)=>R.date.getTime()-P.date.getTime()),p=g.filter(R=>!R.isAuthor).sort((R,P)=>R.date.getTime()-P.date.getTime()),b=h.length>0?h[h.length-1]:null,y=p.length>0?p[p.length-1]:null,S=!1;if(b&&y?S=b.date.getTime()>y.date.getTime():(b&&!y||!b&&!y)&&(S=!0),!S){let R=s.mergeStatus===Se.Conflicts;$(` #${s.id} "${s.title}" \u2014 reviewer acted last`),c.push({id:s.id,title:s.title,author:s.author,url:s.url,lastReviewerActivityDate:y.date,hasMergeConflict:R,isTeamMember:f,isStarred:u,action:Ae("waitingOnAuthor",s.authorUniqueName,r,s.author,l),repository:n,size:s.size,detectedLabels:s.detectedLabels.length>0?s.detectedLabels:void 0,pipelineStatus:s.pipelineStatus,policyStatus:s.policyStatus});continue}let T;y?T=h.find(P=>P.date.getTime()>y.date.getTime())?.date??s.createdDate:T=s.createdDate;let w=s.mergeStatus===Se.Conflicts;$(` #${s.id} "${s.title}" \u2014 needs review (waiting since ${T.toISOString()}${w?", has conflicts":""})`),m.push({id:s.id,title:s.title,author:s.author,url:s.url,waitingSince:T,hasMergeConflict:w,isTeamMember:f,isStarred:u,action:Ae("needingReview",s.authorUniqueName,r,s.author,l),repository:n,size:s.size,detectedLabels:s.detectedLabels.length>0?s.detectedLabels:void 0,reviewerNames:s.reviewers.map(R=>R.displayName),pipelineStatus:s.pipelineStatus,policyStatus:s.policyStatus})}return $(`${a.length} approved PRs`),$(`${c.length} PRs waiting on author`),a.sort((s,f)=>s.createdDate.getTime()-f.createdDate.getTime()),m.sort((s,f)=>s.waitingSince.getTime()-f.waitingSince.getTime()),c.sort((s,f)=>s.lastReviewerActivityDate.getTime()-f.lastReviewerActivityDate.getTime()),{approved:a,needingReview:m,waitingOnAuthor:c}}function dt(e){let t=e.flatMap(r=>r.approved),n=e.flatMap(r=>r.needingReview),o=e.flatMap(r=>r.waitingOnAuthor);return t.sort((r,l)=>r.createdDate.getTime()-l.createdDate.getTime()),n.sort((r,l)=>r.waitingSince.getTime()-l.waitingSince.getTime()),o.sort((r,l)=>r.lastReviewerActivityDate.getTime()-l.lastReviewerActivityDate.getTime()),{approved:t,needingReview:n,waitingOnAuthor:o}}function k(e,t,n=new Date){if(t.length===0)return null;let o=n.getTime()-e.getTime(),r=Math.floor(o/(1e3*60*60*24));for(let l of t)if(r>=l.minDays)return l.label;return null}function le(e,t=new Date){let n=t.getTime()-e.getTime(),o=Math.floor(n/(1e3*60*60*24)),r=Math.floor(n/(1e3*60*60)),l=Math.floor(n/(1e3*60)),i;return o>3?i="high":o>1?i="medium":i="low",{days:o,hours:r,minutes:l,urgency:i}}function ue(e){return e==="XS"||e==="S"?"low":e==="M"?"medium":"high"}function de(e,t){let{approved:n,needingReview:o,waitingOnAuthor:r}=e,l=n.length+o.length+r.length,i=`Total: ${l} open PR${l===1?"":"s"} \u2014 ${n.length} approved, ${o.length} needing review, ${r.length} waiting on author`;return t&&(i+=`, ${t.totalConflicts} with conflicts`,(t.mergeRestarted>0||t.mergeRestartFailed>0)&&(i+=`, ${t.mergeRestarted} merge restarted`,t.mergeRestartFailed>0&&(i+=` (${t.mergeRestartFailed} failed)`))),i}function J(e){return e?e.failed>0?`\u{1F534} ${e.failed}/${e.total} failed`:e.inProgress>0?`\u{1F7E1} ${e.inProgress}/${e.total} running`:e.succeeded===e.total?`\u{1F7E2} ${e.total}/${e.total} passed`:`\u26AA ${e.total} pipeline(s)`:""}function Y(e){return e?e.rejected>0?`\u{1F534} ${e.rejected}/${e.total} rejected`:e.running>0?`\u{1F7E1} ${e.running}/${e.total} running`:e.approved===e.total?`\u{1F7E2} ${e.total}/${e.total} approved`:`\u26AA ${e.total} policy(ies)`:""}function gt(e,t=new Date){let n=le(e,t),o=n.urgency==="high"?"\u{1F534}":n.urgency==="medium"?"\u{1F7E1}":"\u{1F7E2}",r;return n.days>0?r=`${n.days} day${n.days===1?"":"s"} ago`:n.hours>0?r=`${n.hours} hour${n.hours===1?"":"s"} ago`:r=`${n.minutes} minute${n.minutes===1?"":"s"} ago`,`${o} ${r}`}function ft(e){let t=ue(e.label);return`${t==="low"?"\u{1F7E2}":t==="medium"?"\u{1F7E1}":"\u{1F534}"} ${e.label}`}function mt(e){switch(e){case"APPROVE":return"\u{1F7E2} APPROVE";case"REVIEW":return"\u{1F50D} REVIEW";case"PENDING":return"\u23F3 PENDING"}}function pt(e){return!e||e.length===0?"":" "+e.map(t=>`\`${F(t)}\``).join(" ")}function F(e){return e.replace(/[\r\n]+/g," ").trim().replace(/\[/g,"\\[").replace(/\]/g,"\\]").replace(/\|/g,"\\|")}function ht(e,t){return t?`\u2B50 ${e}`:e}function De(e,t,n,o,r=!1){if(e.length===0)return`_${n}_
6
-
7
- `;let l=e.some(s=>s.size!=null),i=e.some(s=>s.stalenessBadge),a=e.some(s=>s.policyStatus!=null||s.pipelineStatus!=null);if(r){let s=["PR","Repository","Author","Action"];l&&s.push("Size"),a&&s.push("Policies"),i&&s.push("Staleness"),s.push(t);let f=`| ${s.join(" | ")} |
8
- |${s.map(()=>"---").join("|")}|
9
- `;for(let u of e){let d=u.hasMergeConflict?" \u274C":"",g=F(u.title),h=F(ht(u.author,u.isStarred)),p=F(u.repository??"Unknown"),b=pt(u.detectedLabels),y=`[#${u.id} - ${g}](${u.url})${d}${b}`,S=gt(u.dateColumn,o),T=mt(u.action),w=l?` ${u.size?ft(u.size):""} |`:"",R=a?` ${u.policyStatus?Y(u.policyStatus):J(u.pipelineStatus)} |`:"",P=i?` ${u.stalenessBadge??""} |`:"";f+=`| ${y} | ${p} | ${h} | ${T} |${w}${R}${P} ${S} |
10
- `}return f+`
11
- `}let m=["PR","Author","Action"];l&&m.push("Size"),a&&m.push("Policies"),i&&m.push("Staleness"),m.push(t);let c=`| ${m.join(" | ")} |
12
- |${m.map(()=>"---").join("|")}|
13
- `;for(let s of e){let f=s.hasMergeConflict?" \u274C":"",u=F(s.title),d=F(ht(s.author,s.isStarred)),g=pt(s.detectedLabels),h=`[#${s.id} - ${u}](${s.url})${f}${g}`,p=gt(s.dateColumn,o),b=mt(s.action),y=l?` ${s.size?ft(s.size):""} |`:"",S=a?` ${s.policyStatus?Y(s.policyStatus):J(s.pipelineStatus)} |`:"",T=i?` ${s.stalenessBadge??""} |`:"";c+=`| ${h} | ${d} | ${b} |${y}${S}${T} ${p} |
2
+ import{Command as wo}from"commander";import{existsSync as lo,readFileSync as uo,writeFileSync as Oe}from"node:fs";import{resolve as z,dirname as go}from"node:path";import{fileURLToPath as fo}from"node:url";import{DefaultAzureCredential as Ut}from"@azure/identity";import*as je from"azure-devops-node-api";import{BearerCredentialHandler as zt}from"azure-devops-node-api/handlers/bearertoken.js";var _="\x1B[0m",ye="\x1B[2m",_e="\x1B[36m",Ft="\x1B[32m",_t="\x1B[33m",Bt="\x1B[31m",Be="\x1B[1m",Ue=!1;function ze(e){Ue=e}function H(){return ye+new Date().toISOString().slice(11,19)+_}function A(e){console.log(`${H()} ${_e}\u2139${_} ${e}`)}function x(e){console.log(`${H()} ${Ft}\u2714${_} ${e}`)}function C(e){console.log(`${H()} ${_t}\u26A0${_} ${e}`)}function K(e){console.error(`${H()} ${Bt}\u2716${_} ${e}`)}function $(e){Ue&&console.log(`${H()} ${ye}\xB7 ${e}${_}`)}function Re(e){console.log(`
3
+ ${Be}${_e}\u25B8 ${e}${_}`)}function U(e,t){console.log(` ${ye}${e}:${_} ${Be}${t}${_}`)}var jt="499b84ac-1321-427f-aa17-267ca6975798";async function X(){let e=process.env.SYSTEM_ACCESSTOKEN;if(e)return $("Using SYSTEM_ACCESSTOKEN from Azure Pipelines environment"),e;try{$("Requesting token for Azure DevOps resource via DefaultAzureCredential\u2026");let n=await new Ut().getToken(`${jt}/.default`);return $("Token acquired successfully"),n.token}catch(t){let n=t instanceof Error?t.message:String(t);throw new Error(`Failed to obtain Azure DevOps token. Set SYSTEM_ACCESSTOKEN in Azure Pipelines, or configure environment credentials (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET), or log in with \`az login\`.
4
+ ${n}`,{cause:t})}}async function Q(e,t){$(`Connecting to ${e}\u2026`);let n=new zt(t),o=new je.WebApi(e,n),r=await o.getGitApi(),l=await o.getBuildApi(),s=await o.getCoreApi(),a=await o.getPolicyApi();return{gitApi:r,buildApi:l,coreApi:s,policyApi:a}}var I=new Map;async function ve(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).gitApi}async function qe(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).buildApi}async function Ge(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).policyApi}async function We(e){if(!I.has(e)){let t=await X(),n=await Q(e,t);I.set(e,n)}return I.get(e).coreApi}import{readFileSync as Jt}from"node:fs";import{resolve as Qe,dirname as Zt}from"node:path";import{fileURLToPath as Kt}from"node:url";import{DefaultAzureCredential as Wt}from"@azure/identity";var B=class extends Error{constructor(t){super(t),this.name="NonRetryableError"}},qt={maxAttempts:4,baseDelayMs:2e3,maxDelayMs:3e4};function Gt(e){return new Promise(t=>setTimeout(t,e))}async function E(e,t,n){let{maxAttempts:o,baseDelayMs:r,maxDelayMs:l}={...qt,...n};for(let s=1;s<=o;s++)try{return await t()}catch(a){if(a instanceof B)throw a;let f=a instanceof Error?a.message:String(a);if(s===o)throw K(`${e} failed after ${o} attempts: ${f}`),a;let i=Math.min(r*2**(s-1),l);C(`${e} failed (attempt ${s}/${o}), retrying in ${(i/1e3).toFixed(1)}s\u2026 \u2014 ${f}`),await Gt(i)}throw new Error("unreachable")}var Ht="https://graph.microsoft.com/.default",Vt="https://graph.microsoft.com/v1.0";async function He(){try{return(await new Wt().getToken(Ht)).token}catch(e){let t=e instanceof Error?e.message:String(e);throw new Error(`Failed to get Microsoft Graph token. Configure environment credentials (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET), or log in with \`az login\`.
5
+ ${t}`,{cause:e})}}async function Yt(e,t){let n=await fetch(t,{headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let o=await n.text();throw n.status>=400&&n.status<500?new B(`Graph API ${n.status}: ${o}`):new Error(`Graph API ${n.status}: ${o}`)}return await n.json()}async function Ve(e,t){let n=[],o=`${Vt}/users/${encodeURIComponent(t)}/directReports?$select=mail,userPrincipalName,displayName&$top=999`;for(;o;){let r=await E(`Fetch direct reports for ${t}`,()=>Yt(e,o));for(let l of r.value){let s=l.mail?.toLowerCase(),a=l.userPrincipalName?.toLowerCase();s&&n.push(s),a&&a!==s&&n.push(a)}o=r["@odata.nextLink"]}return n}async function Ye(e){A(`Fetching direct reports for ${e} from Microsoft Graph\u2026`);let t=await He(),n=await Ve(t,e);x(`Found ${n.length} direct reports for ${e}`);for(let o of n)$(` ${o}`);return n}async function Je(e){A(`Fetching full org tree under ${e} from Microsoft Graph\u2026`);let t=await He(),n=10,o=[],r=[e.toLowerCase()],l=[e],s=new Set;for(;l.length>0;){let a=l.filter(c=>!s.has(c));for(let c of a)s.add(c);if(a.length===0)break;$(` Processing ${a.length} users (concurrency: ${n})\u2026`);let f=[];for(let c=0;c<a.length;c+=n){let i=a.slice(c,c+n),p=await Promise.all(i.map(async u=>{try{let d=await Ve(t,u);return $(` ${u} \u2192 ${d.length} direct reports`),{upn:u,reports:d}}catch(d){if(d instanceof B&&d.message.includes("404"))return C(` ${u} \u2014 not found in directory, keeping as member`),{upn:u,reports:[u]};throw d}}));for(let{upn:u,reports:d}of p){d.length>0&&!(d.length===1&&d[0]===u)&&r.push(u.toLowerCase());for(let g of d)o.push(g),f.push(g)}}l=f}return x(`Found ${o.length} total org members under ${e} (${r.length} managers)`),{members:o,managers:r}}function Ze(e){let t=e.match(/https?:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/);if(t)return{orgUrl:`https://dev.azure.com/${t[1]}`,project:t[2],repository:t[3]};let n=e.match(/https?:\/\/(?:[^@]+@)?([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/\s]+)/);if(n)return{orgUrl:`https://dev.azure.com/${n[1]}`,project:n[2],repository:n[3]};let o=e.match(/ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/\s]+)/);if(o)return{orgUrl:`https://dev.azure.com/${o[1]}`,project:o[2],repository:o[3]};let r=e.match(/vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/\s]+)/);return r?{orgUrl:`https://dev.azure.com/${r[1]}`,project:r[2],repository:r[3]}:null}var ee=[{label:"XS",maxChanges:10},{label:"S",maxChanges:40},{label:"M",maxChanges:100},{label:"L",maxChanges:400},{label:"XL",maxChanges:1e3}];function te(e){return e?.uniqueName?.toLowerCase()??""}function Ke(e,t,n,o){return{totalConflicts:[...e.approved,...e.needingReview,...e.waitingOnAuthor].filter(s=>s.hasMergeConflict).length,mergeRestarted:t,mergeRestartFailed:n,repoStats:o}}function be(e,t,n,o){let r=[...t.approved,...t.needingReview,...t.waitingOnAuthor];return{repoLabel:e,approved:t.approved.length,needingReview:t.needingReview.length,waitingOnAuthor:t.waitingOnAuthor.length,conflicts:r.filter(l=>l.hasMergeConflict).length,mergeRestarted:n,mergeRestartFailed:o}}var Xe=[{label:"\u26A0\uFE0F Aging",minDays:7},{label:"\u{1F534} Stale",minDays:14},{label:"\u{1F480} Abandoned",minDays:30}];function Xt(e){let t=e?Qe(e):Qe(Zt(Kt(import.meta.url)),"..","pr-review-config.json"),n=Jt(t,"utf-8");return JSON.parse(n)}function Qt(e){if(!e.repositories||e.repositories.length===0)throw new Error("Config must specify 'repositories' (array of repository objects with a 'url' field).");return e.repositories.map(t=>{let n=Ze(t.url);if(!n)throw new Error(`Invalid ADO repository URL: ${t.url}`);return{...n,skipRestartMerge:t.skipRestartMerge??!1,patterns:{ignore:t.patterns?.ignore??[],labels:t.patterns?.labels??{}}}})}async function en(e){let t=new Set((e.teamMembers??[]).map(o=>o.toLowerCase())),n=new Set;if(e.orgManager){let o=await Je(e.orgManager);for(let r of o.members)t.add(r);if(t.add(e.orgManager.toLowerCase()),e.ignoreManagers)for(let r of o.managers)n.add(r.toLowerCase())}if(e.manager){let o=await Ye(e.manager);for(let r of o)t.add(r);t.add(e.manager.toLowerCase()),e.ignoreManagers&&n.add(e.manager.toLowerCase())}return{teamMembers:t,ignoredUsers:n}}function tn(e){if(e.quantifier?.enabled===!1)return;let t=e.quantifier?.excludedPatterns??[],n=e.quantifier?.thresholds?e.quantifier.thresholds.map(o=>({label:o.label,maxChanges:o.maxChanges})):ee;return{enabled:!0,excludedPatterns:t,thresholds:n}}function nn(e){return e.staleness?.enabled===!1?{enabled:!1,thresholds:[]}:{enabled:!0,thresholds:e.staleness?.thresholds?e.staleness.thresholds.map(n=>({label:n.label,minDays:n.minDays})).sort((n,o)=>o.minDays-n.minDays):Xe.slice().sort((n,o)=>o.minDays-n.minDays)}}function on(e){if(!(!e.autoNudge||e.autoNudge.enabled===!1))return{enabled:!0,minStalenessLevel:e.autoNudge.minStalenessLevel,cooldownDays:e.autoNudge.cooldownDays??7,commentTemplate:e.autoNudge.commentTemplate??"\u23F0 This PR has been waiting for review for {{days}} days. Reviewers: {{reviewers}}. Please take a look!",dryRun:e.autoNudge.dryRun??!1,historyFile:e.autoNudge.historyFile??".pr-nudge-history.json"}}async function et(e){let t=Xt(e),n=Qt(t),{teamMembers:o,ignoredUsers:r}=await en(t),l=new Set((t.botUsers??[]).map(g=>g.toLowerCase())),s=new Set((t.aiBotUsers??[]).map(g=>g.toLowerCase())),a=new Set((t.starredUsers??[]).map(g=>g.toLowerCase())),f=tn(t),c=t.restartMergeAfterDays??30,i=nn(t),p=t.notifications,u=t.webhook,d=on(t);return{repos:n,teamMembers:o,ignoredUsers:r,botUsers:l,aiBotUsers:s,starredUsers:a,quantifier:f,restartMergeAfterDays:c,staleness:i,notifications:p,webhook:u,autoNudge:d}}import{PullRequestStatus as an}from"azure-devops-node-api/interfaces/GitInterfaces.js";import{BuildResult as ie,BuildStatus as se}from"azure-devops-node-api/interfaces/BuildInterfaces.js";import{PolicyEvaluationStatus as j}from"azure-devops-node-api/interfaces/PolicyInterfaces.js";import{LineDiffBlockChangeType as ne,VersionControlChangeType as tt}from"azure-devops-node-api/interfaces/GitInterfaces.js";import rn from"picomatch";function oe(e,t=ee){let n=[...t].sort((o,r)=>o.maxChanges-r.maxChanges);for(let o of n)if(e<=o.maxChanges)return o.label;return n[n.length-1].label}function sn(e,t){if(t.length===0)return!1;let n=e.replace(/^\//,"");return t.some(o=>o(n))}function nt(e){let t=0,n=0;for(let o of e){let r=o.changeType??ne.None;r===ne.Add?t+=o.modifiedLinesCount??0:r===ne.Delete?n+=o.originalLinesCount??0:r===ne.Edit&&(t+=o.modifiedLinesCount??0,n+=o.originalLinesCount??0)}return{added:t,deleted:n}}async function ot(e,t,n,o,r){let l=r.excludedPatterns.map(y=>rn(y,{dot:!0})),s=await E(`Fetch iterations for PR #${o}`,()=>e.getPullRequestIterations(t,o,n));if(!s||s.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:oe(0,r.thresholds)};let a=s[s.length-1],f=a.id,c=[],i=0,p=100;for(;;){let y=await E(`Fetch iteration changes for PR #${o} iter ${f}`,()=>e.getPullRequestIterationChanges(t,o,f,n,p,i)),R=y.changeEntries??[];for(let T of R){let D=T.item?.path??"",O=T.originalPath;D&&!sn(D,l)&&c.push({path:D,originalPath:O??void 0,changeType:T.changeType??0})}if((y.nextSkip??0)===0&&(y.nextTop??0)===0||(i=y.nextSkip??i+p,R.length===0))break}if(c.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:oe(0,r.thresholds)};let u=a.sourceRefCommit?.commitId,d=a.targetRefCommit?.commitId;if(!u||!d){$(` PR #${o} \u2014 no commit refs, using file count as proxy`);let y=c.length;return{linesAdded:y,linesDeleted:0,totalChanges:y,label:oe(y,r.thresholds)}}let g=c.map(y=>{let R=(y.changeType&tt.Add)!==0,T=(y.changeType&tt.Delete)!==0;return{originalPath:R?void 0:y.originalPath??y.path,path:T?void 0:y.path}}),h=0,m=0,b=10,w=0;for(let y=0;y<g.length;y+=b){let R=g.slice(y,y+b),T={baseVersionCommit:d,targetVersionCommit:u,fileDiffParams:R};try{let D=await e.getFileDiffs(T,n,t);for(let O of D){let{added:G,deleted:W}=nt(O.lineDiffBlocks??[]);h+=G,m+=W}}catch{for(let D of R)try{let O={baseVersionCommit:d,targetVersionCommit:u,fileDiffParams:[D]},G=await e.getFileDiffs(O,n,t);for(let W of G){let{added:$e,deleted:we}=nt(W.lineDiffBlocks??[]);h+=$e,m+=we}}catch{w++}}}w>0&&$(` PR #${o} \u2014 skipped ${w} files (not found at specified version)`);let S=h+m,P=oe(S,r.thresholds);return $(` PR #${o} \u2014 +${h} -${m} = ${S} (${P})`),{linesAdded:h,linesDeleted:m,totalChanges:S,label:P}}async function re(e,t,n){let o=new Array(e.length),r=0;async function l(){for(;r<e.length;){let a=r++;o[a]=await n(e[a])}}let s=Array.from({length:Math.min(t,e.length)},()=>l());return await Promise.all(s),o}import rt from"picomatch";function it(e,t,n){if(Object.keys(n).length===0)return[];let o=t.map(s=>rt(s,{dot:!0})),r=e.map(s=>s.replace(/^\//,"")).filter(s=>!o.some(a=>a(s)));if(r.length===0)return[];let l=[];for(let[s,a]of Object.entries(n)){let f=a.map(c=>rt(c,{dot:!0}));r.some(c=>f.some(i=>i(c)))&&l.push(s)}return l}function cn(e){let t=[],n=0,o=0;for(let r of e){if(r.isDraft){n++,$(` #${r.pullRequestId} \u2014 draft, skipping`);continue}if((r.labels??[]).map(s=>s.name??"").some(s=>s.toUpperCase()==="NO-MERGE")){o++,$(` #${r.pullRequestId} \u2014 NO-MERGE label, skipping`);continue}t.push(r)}return n>0&&$(`Skipped ${n} draft PRs`),o>0&&$(`Skipped ${o} NO-MERGE PRs`),t}async function ln(e,t,n,o){let r=await E(`Fetch iterations for PR #${o} (file patterns)`,()=>e.getPullRequestIterations(t,o,n));if(!r||r.length===0)return[];let s=r[r.length-1].id;if(s==null)return[];let a=[],f=0,c=100;for(;;){let i=await E(`Fetch iteration changes for PR #${o} iter ${s} (file patterns)`,()=>e.getPullRequestIterationChanges(t,o,s,n,c,f));for(let p of i.changeEntries??[]){let u=p.item?.path??"";u&&a.push(u)}if((i.nextSkip??0)===0&&(i.nextTop??0)===0||(f=i.nextSkip??f+c,(i.changeEntries??[]).length===0))break}return a}function un(e,t){if(e===se.InProgress)return"inProgress";if(e===se.NotStarted)return"notStarted";switch(t){case ie.Succeeded:return"succeeded";case ie.Failed:return"failed";case ie.PartiallySucceeded:return"partiallySucceeded";case ie.Canceled:return"canceled";default:return"none"}}async function dn(e,t,n,o){try{let r=`refs/pull/${o}/merge`,l=await E(`Fetch builds for PR #${o}`,()=>e.getBuilds(n,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,void 0,10,void 0,void 0,void 0,void 0,r,void 0,t,"TfsGit"));if(!l||l.length===0)return;let s=new Map;for(let u of l){let d=u.definition?.id??0;s.has(d)||s.set(d,u)}let a=[],f=0,c=0,i=0,p=0;for(let u of s.values()){let d=un(u.status,u.result);switch(a.push({id:u.id??0,name:u.definition?.name??"Unknown",status:se[u.status??se.None]??"None",result:d}),d){case"succeeded":f++;break;case"failed":c++;break;case"inProgress":case"notStarted":i++;break;default:p++;break}}return{total:a.length,succeeded:f,failed:c,inProgress:i,other:p,runs:a}}catch(r){let l=r instanceof Error?r.message:String(r);$(` #${o} \u2014 failed to fetch pipeline status: ${l}`);return}}function gn(e){switch(e){case j.Queued:return"queued";case j.Running:return"running";case j.Approved:return"approved";case j.Rejected:return"rejected";case j.NotApplicable:return"notApplicable";case j.Broken:return"broken";default:return"queued"}}var st="0609b952-1397-4640-95ec-e00a01b2c241",fn="cbdc66da-9728-4af8-aada-9a5a32e4a226",at="fa4e907d-c16b-4a4c-9dfa-4906e5d171dd",mn="67ed70bd-2a6b-4006-af44-be590463f46d",pn=new Set([at,mn]);function hn(e,t,n){if(!e||!n)return t;switch(e){case st:{let o=n.displayName;if(o)return`Build: ${o}`;let r=n.buildDefinitionId;return r!=null?`Build #${r}`:t}case fn:{let o=n.statusName,r=n.defaultDisplayName;if(r)return r;if(o){let l=n.statusGenre;return l?`${o} (${l})`:o}return t}case at:{let o=n.minimumApproverCount;return o!=null?`${t} (${o})`:t}default:return t}}async function $n(e,t,n,o,r){try{let l=`vstfs:///CodeReview/CodeReviewId/${n}/${o}`,s=await E(`Fetch policy evaluations for PR #${o}`,()=>e.getPolicyEvaluations(t,l));if(!s||s.length===0)return;let a=[],f=0,c=0,i=0,p=0,u=new Set;for(let d of s){let g=gn(d.status);if(g==="notApplicable")continue;let h=d.configuration?.type?.displayName??"Unknown Policy",m=d.configuration?.type?.id;if(m&&pn.has(m)){if(u.has(m))continue;u.add(m)}let b=hn(m,h,d.configuration?.settings),w;if(m===st&&r){let S=d.context?.buildId;S!=null&&(w=`${r}/${t}/_build/results?buildId=${S}`)}switch(a.push({evaluationId:d.evaluationId??"",displayName:b,status:g,isBlocking:d.configuration?.isBlocking??!1,completedDate:d.completedDate?d.completedDate.toISOString():void 0,buildUrl:w}),g){case"approved":f++;break;case"rejected":case"broken":c++;break;case"running":case"queued":i++;break;default:p++;break}}return a.length===0?void 0:{total:a.length,approved:f,rejected:c,running:i,other:p,evaluations:a}}catch(l){let s=l instanceof Error?l.message:String(l);$(` #${o} \u2014 failed to fetch policy evaluations: ${s}`);return}}async function ct(e,t,n,o,r,l={ignore:[],labels:{}},s,a,f){let c;try{let g=new URL(o);g.hostname.endsWith(".visualstudio.com")?c=`https://dev.azure.com/${g.hostname.replace(".visualstudio.com","")}`:c=o.replace(/\/$/,"")}catch{c=o.replace(/\/$/,"")}let i=await E("Fetch pull requests",()=>e.getPullRequests(t,{status:an.Active},n));$(`API returned ${i.length} active pull requests`);let p;if(s)try{p=(await E("Resolve repository GUID",()=>e.getRepository(t,n))).id}catch(g){let h=g instanceof Error?g.message:String(g);$(`Failed to resolve repository GUID for ${t}: ${h}`)}let u=cn(i);A(`Fetching threads for ${u.length} PRs in ${n}/${t} (concurrency: ${10})\u2026`);let d=await re(u,10,async g=>{let h=g.pullRequestId;$(` #${h} \u2014 fetching threads\u2026`);let b=(await E(`Fetch threads for PR #${h}`,()=>e.getThreads(t,h,n))).map(D=>({id:D.id??0,publishedDate:new Date(D.publishedDate??0),comments:(D.comments??[]).filter(O=>!O.isDeleted).map(O=>({authorUniqueName:te(O.author),publishedDate:new Date(O.publishedDate??0)}))})),w=(g.reviewers??[]).map(D=>({displayName:D.displayName??"",uniqueName:te(D),vote:D.vote??0})),S=`${c}/${n}/_git/${t}/pullrequest/${h}`,P=(g.labels??[]).map(D=>D.name??""),y=[];if(Object.keys(l.labels).length>0){let D=await ln(e,t,n,h);y=it(D,l.ignore,l.labels),y.length>0&&$(` #${h} \u2014 detected labels: ${y.join(", ")}`)}let R=s&&p?await dn(s,p,n,h):void 0,T=a&&f?await $n(a,n,f,h,c):void 0;return{id:h,title:g.title??"(no title)",author:g.createdBy?.displayName??"Unknown",authorUniqueName:te(g.createdBy),url:S,createdDate:new Date(g.creationDate??0),reviewers:w,threads:b,labels:P,detectedLabels:y,mergeStatus:g.mergeStatus??0,lastSourcePushDate:g.lastMergeSourceCommit?.committer?.date?new Date(g.lastMergeSourceCommit.committer.date):void 0,size:r?await ot(e,t,n,h,r):void 0,description:g.description??void 0,sourceBranch:g.sourceRefName??void 0,targetBranch:g.targetRefName??void 0,pipelineStatus:R,policyStatus:T}});return $(`${d.length} PRs remain after filtering`),d}async function lt(e,t,n,o){for(let r of o)if(r.detectedLabels.length!==0){for(let l of r.detectedLabels)if(!r.labels.some(s=>s.toLowerCase()===l.toLowerCase()))try{await E(`Add label '${l}' to PR #${r.id}`,()=>e.createPullRequestLabel({name:l},t,r.id,n)),r.labels.push(l),$(` #${r.id} \u2014 added label '${l}'`)}catch(s){let a=s instanceof Error?s.message:String(s);$(` #${r.id} \u2014 failed to add label '${l}': ${a}`)}}}var wn=1440*60*1e3,yn=["TF401398","TF401027"],Rn=["TF401027"];async function ut(e,t,n,o,r,l=new Date){if(r<0)return $("Restart merge is disabled (restartMergeAfterDays < 0)"),{restarted:0,failed:0,restartedPrIds:[]};let s=new Date(l.getTime()-r*wn),a=o.filter(p=>p.createdDate<s);if(a.length===0)return $("No PRs older than the restart-merge threshold"),{restarted:0,failed:0,restartedPrIds:[]};A(`Restarting merge for ${a.length} PR(s) older than ${r} days\u2026`);let f=0,c=0,i=[];for(let p of a)try{await E(`Restart merge for PR #${p.id}`,async()=>{try{return await e.updatePullRequest({mergeStatus:1},t,p.id,n)}catch(u){let d=u instanceof Error?u.message:String(u);throw yn.some(g=>d.includes(g))?new B(d):u}}),$(` #${p.id} "${p.title}" \u2014 merge restarted`),f++,i.push(p.id)}catch(u){let d=u instanceof Error?u.message:String(u);if(C(` #${p.id} "${p.title}" \u2014 failed to restart merge: ${d}`),c++,Rn.some(g=>d.includes(g))){C("Stopping restart-merge for this repository due to permission error"),c+=a.length-a.indexOf(p)-1;break}}return x(`Restarted merge for ${f}/${a.length} PR(s)`),{restarted:f,failed:c,restartedPrIds:i}}import{PullRequestAsyncStatus as Se}from"azure-devops-node-api/interfaces/GitInterfaces.js";var vn=["build","[bot]","team foundation","microsoft.visualstudio.com"],bn=["dependabot[bot]","renovate[bot]","github-actions[bot]","snyk-bot","greenkeeper[bot]","depfu[bot]","imgbot[bot]","allcontributors[bot]"],dt=["github copilot","copilot[bot]","claude","codex"];function Pe(e,t=new Set,n){let o=e.toLowerCase();return!!(t.has(o)||vn.some(r=>o.includes(r))||n&&t.has(n.toLowerCase()))}function ae(e,t=new Set,n){let o=e.toLowerCase();if(t.has(o)||dt.some(r=>o.includes(r)))return!0;if(n){let r=n.toLowerCase();if(t.has(r)||dt.some(l=>r.includes(l)))return!0}return!1}function ce(e,t=new Set,n,o=new Set){let r=e.toLowerCase();return bn.some(l=>r.includes(l))||Pe(r,t,n)||ae(r,o,n)}function Sn(e,t=new Set,n){return ae(e,t,n)}function Ae(e,t,n=new Set,o,r=new Set){if(Sn(t,r,o))switch(e){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}if(ce(t,n,o,r))return"APPROVE";switch(e){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}}function Te(e,t=new Set,n=new Set){let o=e.authorUniqueName,r=[];for(let l of e.threads)for(let s of l.comments)Pe(s.authorUniqueName,t)||ae(s.authorUniqueName,n)||r.push({date:s.publishedDate,isAuthor:s.authorUniqueName===o});return e.lastSourcePushDate&&r.push({date:e.lastSourcePushDate,isAuthor:!0}),r}function gt(e,t=new Set,n,o=new Set,r=new Set,l=new Set,s=new Set){let a=[],f=[],c=[];for(let i of e){if(o.has(i.authorUniqueName)){$(` #${i.id} "${i.title}" \u2014 author ${i.authorUniqueName} is ignored, skipping`);continue}let p=t.size===0||t.has(i.authorUniqueName),u=s.has(i.authorUniqueName);if(i.reviewers.some(R=>R.vote>=5&&!Pe(R.uniqueName,r,R.displayName)&&!ae(R.uniqueName,l,R.displayName))){let R=i.mergeStatus===Se.Conflicts;$(` #${i.id} "${i.title}" \u2014 approved`),a.push({id:i.id,title:i.title,author:i.author,url:i.url,createdDate:i.createdDate,hasMergeConflict:R,isTeamMember:p,isStarred:u,action:Ae("approved",i.authorUniqueName,r,i.author,l),repository:n,size:i.size,detectedLabels:i.detectedLabels.length>0?i.detectedLabels:void 0,pipelineStatus:i.pipelineStatus,policyStatus:i.policyStatus});continue}let g=Te(i,r,l),h=g.filter(R=>R.isAuthor).sort((R,T)=>R.date.getTime()-T.date.getTime()),m=g.filter(R=>!R.isAuthor).sort((R,T)=>R.date.getTime()-T.date.getTime()),b=h.length>0?h[h.length-1]:null,w=m.length>0?m[m.length-1]:null,S=!1;if(b&&w?S=b.date.getTime()>w.date.getTime():(b&&!w||!b&&!w)&&(S=!0),!S){let R=i.mergeStatus===Se.Conflicts;$(` #${i.id} "${i.title}" \u2014 reviewer acted last`),c.push({id:i.id,title:i.title,author:i.author,url:i.url,lastReviewerActivityDate:w.date,hasMergeConflict:R,isTeamMember:p,isStarred:u,action:Ae("waitingOnAuthor",i.authorUniqueName,r,i.author,l),repository:n,size:i.size,detectedLabels:i.detectedLabels.length>0?i.detectedLabels:void 0,pipelineStatus:i.pipelineStatus,policyStatus:i.policyStatus});continue}let P;w?P=h.find(T=>T.date.getTime()>w.date.getTime())?.date??i.createdDate:P=i.createdDate;let y=i.mergeStatus===Se.Conflicts;$(` #${i.id} "${i.title}" \u2014 needs review (waiting since ${P.toISOString()}${y?", has conflicts":""})`),f.push({id:i.id,title:i.title,author:i.author,url:i.url,waitingSince:P,hasMergeConflict:y,isTeamMember:p,isStarred:u,action:Ae("needingReview",i.authorUniqueName,r,i.author,l),repository:n,size:i.size,detectedLabels:i.detectedLabels.length>0?i.detectedLabels:void 0,reviewerNames:i.reviewers.map(R=>R.displayName),pipelineStatus:i.pipelineStatus,policyStatus:i.policyStatus})}return $(`${a.length} approved PRs`),$(`${c.length} PRs waiting on author`),a.sort((i,p)=>i.createdDate.getTime()-p.createdDate.getTime()),f.sort((i,p)=>i.waitingSince.getTime()-p.waitingSince.getTime()),c.sort((i,p)=>i.lastReviewerActivityDate.getTime()-p.lastReviewerActivityDate.getTime()),{approved:a,needingReview:f,waitingOnAuthor:c}}function ft(e){let t=e.flatMap(r=>r.approved),n=e.flatMap(r=>r.needingReview),o=e.flatMap(r=>r.waitingOnAuthor);return t.sort((r,l)=>r.createdDate.getTime()-l.createdDate.getTime()),n.sort((r,l)=>r.waitingSince.getTime()-l.waitingSince.getTime()),o.sort((r,l)=>r.lastReviewerActivityDate.getTime()-l.lastReviewerActivityDate.getTime()),{approved:t,needingReview:n,waitingOnAuthor:o}}function k(e,t,n=new Date){if(t.length===0)return null;let o=n.getTime()-e.getTime(),r=Math.floor(o/(1e3*60*60*24));for(let l of t)if(r>=l.minDays)return l.label;return null}function le(e,t=new Date){let n=t.getTime()-e.getTime(),o=Math.floor(n/(1e3*60*60*24)),r=Math.floor(n/(1e3*60*60)),l=Math.floor(n/(1e3*60)),s;return o>3?s="high":o>1?s="medium":s="low",{days:o,hours:r,minutes:l,urgency:s}}function ue(e){return e==="XS"||e==="S"?"low":e==="M"?"medium":"high"}function de(e,t){let{approved:n,needingReview:o,waitingOnAuthor:r}=e,l=n.length+o.length+r.length,s=`Total: ${l} open PR${l===1?"":"s"} \u2014 ${n.length} approved, ${o.length} needing review, ${r.length} waiting on author`;return t&&(s+=`, ${t.totalConflicts} with conflicts`,(t.mergeRestarted>0||t.mergeRestartFailed>0)&&(s+=`, ${t.mergeRestarted} merge restarted`,t.mergeRestartFailed>0&&(s+=` (${t.mergeRestartFailed} failed)`))),s}function Y(e){return e?e.failed>0?`\u{1F534} ${e.failed}/${e.total} failed`:e.inProgress>0?`\u{1F7E1} ${e.inProgress}/${e.total} running`:e.succeeded===e.total?`\u{1F7E2} ${e.total}/${e.total} passed`:`\u26AA ${e.total} pipeline(s)`:""}function J(e){return e?e.rejected>0?`\u{1F534} ${e.rejected}/${e.total} rejected`:e.running>0?`\u{1F7E1} ${e.running}/${e.total} running`:e.approved===e.total?`\u{1F7E2} ${e.total}/${e.total} approved`:`\u26AA ${e.total} policy(ies)`:""}function mt(e,t=new Date){let n=le(e,t),o=n.urgency==="high"?"\u{1F534}":n.urgency==="medium"?"\u{1F7E1}":"\u{1F7E2}",r;return n.days>0?r=`${n.days} day${n.days===1?"":"s"} ago`:n.hours>0?r=`${n.hours} hour${n.hours===1?"":"s"} ago`:r=`${n.minutes} minute${n.minutes===1?"":"s"} ago`,`${o} ${r}`}function pt(e){let t=ue(e.label);return`${t==="low"?"\u{1F7E2}":t==="medium"?"\u{1F7E1}":"\u{1F534}"} ${e.label}`}function ht(e){switch(e){case"APPROVE":return"\u{1F7E2} APPROVE";case"REVIEW":return"\u{1F50D} REVIEW";case"PENDING":return"\u23F3 PENDING"}}function $t(e){return!e||e.length===0?"":" "+e.map(t=>`\`${F(t)}\``).join(" ")}function F(e){return e.replace(/[\r\n]+/g," ").trim().replace(/\[/g,"\\[").replace(/\]/g,"\\]").replace(/\|/g,"\\|")}function wt(e,t){return t?`\u2B50 ${e}`:e}function De(e,t,n,o,r=!1){if(e.length===0)return`_${n}_
6
+
7
+ `;let l=e.some(i=>i.size!=null),s=e.some(i=>i.stalenessBadge),a=e.some(i=>i.policyStatus!=null||i.pipelineStatus!=null);if(r){let i=["PR","Repository","Author","Action"];l&&i.push("Size"),a&&i.push("Policies"),s&&i.push("Staleness"),i.push(t);let p=`| ${i.join(" | ")} |
8
+ |${i.map(()=>"---").join("|")}|
9
+ `;for(let u of e){let d=u.hasMergeConflict?" \u274C":"",g=F(u.title),h=F(wt(u.author,u.isStarred)),m=F(u.repository??"Unknown"),b=$t(u.detectedLabels),w=`[#${u.id} - ${g}](${u.url})${d}${b}`,S=mt(u.dateColumn,o),P=ht(u.action),y=l?` ${u.size?pt(u.size):""} |`:"",R=a?` ${u.policyStatus?J(u.policyStatus):Y(u.pipelineStatus)} |`:"",T=s?` ${u.stalenessBadge??""} |`:"";p+=`| ${w} | ${m} | ${h} | ${P} |${y}${R}${T} ${S} |
10
+ `}return p+`
11
+ `}let f=["PR","Author","Action"];l&&f.push("Size"),a&&f.push("Policies"),s&&f.push("Staleness"),f.push(t);let c=`| ${f.join(" | ")} |
12
+ |${f.map(()=>"---").join("|")}|
13
+ `;for(let i of e){let p=i.hasMergeConflict?" \u274C":"",u=F(i.title),d=F(wt(i.author,i.isStarred)),g=$t(i.detectedLabels),h=`[#${i.id} - ${u}](${i.url})${p}${g}`,m=mt(i.dateColumn,o),b=ht(i.action),w=l?` ${i.size?pt(i.size):""} |`:"",S=a?` ${i.policyStatus?J(i.policyStatus):Y(i.pipelineStatus)} |`:"",P=s?` ${i.stalenessBadge??""} |`:"";c+=`| ${h} | ${d} | ${b} |${w}${S}${P} ${m} |
14
14
  `}return c+`
15
- `}function wn(e){return e.every(n=>n.isTeamMember)?{team:e,community:[]}:{team:e.filter(n=>n.isTeamMember),community:e.filter(n=>!n.isTeamMember)}}function Ce(e,t,n,o,r,l,i=!1){let a=`## ${e}
15
+ `}function An(e){return e.every(n=>n.isTeamMember)?{team:e,community:[]}:{team:e.filter(n=>n.isTeamMember),community:e.filter(n=>!n.isTeamMember)}}function Ce(e,t,n,o,r,l,s=!1){let a=`## ${e}
16
16
 
17
- `,{team:m,community:c}=wn(t);return c.length>0?(a+=`### Team PRs
17
+ `,{team:f,community:c}=An(t);return c.length>0?(a+=`### Team PRs
18
18
 
19
- `,a+=De(m.map(n),o,"No team PRs.",l,i),a+=`### Community Contributions
19
+ `,a+=De(f.map(n),o,"No team PRs.",l,s),a+=`### Community Contributions
20
20
 
21
- `,a+=De(c.map(n),o,"No community PRs.",l,i)):a+=De(t.map(n),o,r,l,i),a}function yn(e){let t=`## \u{1F4CA} Statistics per Repository
21
+ `,a+=De(c.map(n),o,"No community PRs.",l,s)):a+=De(t.map(n),o,r,l,s),a}function Pn(e){let t=`## \u{1F4CA} Statistics per Repository
22
22
 
23
23
  `;t+=`| Repository | Open PRs | \u2705 Approved | \u{1F440} Needs Review | \u270D\uFE0F Waiting on Author | \u274C Conflicts | \u{1F504} Merge Restarted |
24
24
  `,t+=`|---|---|---|---|---|---|---|
25
25
  `;for(let n of e){let o=n.approved+n.needingReview+n.waitingOnAuthor,r=n.mergeRestarted>0?n.mergeRestartFailed>0?`${n.mergeRestarted} (${n.mergeRestartFailed} failed)`:`${n.mergeRestarted}`:"0";t+=`| ${F(n.repoLabel)} | ${o} | ${n.approved} | ${n.needingReview} | ${n.waitingOnAuthor} | ${n.conflicts} | ${r} |
26
26
  `}return t+`
27
- `}function Z(e){return e<1?"< 1 day":`${e} day${e===1?"":"s"}`}function Rn(e){let t=`## \u{1F4C8} Review Metrics
27
+ `}function Z(e){return e<1?"< 1 day":`${e} day${e===1?"":"s"}`}function Tn(e){let t=`## \u{1F4C8} Review Metrics
28
28
 
29
29
  `;if(t+=`### Summary
30
30
 
@@ -42,13 +42,13 @@ ${t}`,{cause:e})}}async function Ht(e,t){let n=await fetch(t,{headers:{Authoriza
42
42
  |---|---|---|---|---|
43
43
  `;for(let n of e.perAuthor){let o=n.fastestReviewInDays!==null?Z(n.fastestReviewInDays):"N/A",r=n.isStarred?`\u2B50 ${F(n.author)}`:F(n.author);t+=`| ${r} | ${n.openPrCount} | ${Z(n.avgAgeInDays)} | ${n.avgReviewRounds} | ${o} |
44
44
  `}t+=`
45
- `}return t}function vn(e){if(e.length===0)return"";let t=`## \u{1F465} Reviewer Workload
45
+ `}return t}function Dn(e){if(e.length===0)return"";let t=`## \u{1F465} Reviewer Workload
46
46
 
47
47
  `;t+=`| Reviewer | Assigned | Pending | Completed | Avg Response | Load |
48
48
  |---|---|---|---|---|---|
49
49
  `;for(let n of e){let o=n.avgResponseTimeInDays!==null?Z(n.avgResponseTimeInDays):"N/A",r=n.isStarred?`\u2B50 ${F(n.displayName)}`:F(n.displayName);t+=`| ${r} | ${n.assignedPrCount} | ${n.pendingReviewCount} | ${n.completedReviewCount} | ${o} | ${n.loadIndicator} |
50
50
  `}return t+`
51
- `}function bn(e){let t=`## \u{1F517} PR Dependencies
51
+ `}function Cn(e){let t=`## \u{1F517} PR Dependencies
52
52
 
53
53
  `;if(e.chains.length>0){t+=`### Dependency Chains
54
54
 
@@ -62,7 +62,7 @@ ${t}`,{cause:e})}}async function Ht(e,t){let n=await fetch(t,{headers:{Authoriza
62
62
  `,t+=`|----|-----------|--------|
63
63
  `;for(let n of e.dependencies)e.blockedPrIds.includes(n.fromPrId)&&(t+=`| #${n.fromPrId} | #${n.toPrId} | ${n.details} |
64
64
  `);t+=`
65
- `}return t}function ge(e){switch(e){case"elite":return"\u{1F7E2} Elite";case"high":return"\u{1F7E2} High";case"medium":return"\u{1F7E1} Medium";case"low":return"\u{1F534} Low"}}function fe(e){return e===null?"\u2014":e===0?"\u2192 stable":e>0?`\u2197\uFE0F +${e}`:`\u2198\uFE0F ${e}`}function Sn(e){let t=e.current,n=`## \u{1F4C8} DORA Metrics
65
+ `}return t}function ge(e){switch(e){case"elite":return"\u{1F7E2} Elite";case"high":return"\u{1F7E2} High";case"medium":return"\u{1F7E1} Medium";case"low":return"\u{1F534} Low"}}function fe(e){return e===null?"\u2014":e===0?"\u2192 stable":e>0?`\u2197\uFE0F +${e}`:`\u2198\uFE0F ${e}`}function xn(e){let t=e.current,n=`## \u{1F4C8} DORA Metrics
66
66
 
67
67
  `;return n+=`| Metric | Value | Rating | Trend |
68
68
  `,n+=`|--------|-------|--------|-------|
@@ -71,24 +71,24 @@ ${t}`,{cause:e})}}async function Ht(e,t){let n=await fetch(t,{headers:{Authoriza
71
71
  `,n+=`| Change Failure Rate | ${t.changeFailureRate.percentage}% | ${ge(t.changeFailureRate.rating)} | ${fe(e.deltas.changeFailureRate)} |
72
72
  `,n+=`| Mean Time to Restore | ${t.meanTimeToRestore.medianHours}h | ${ge(t.meanTimeToRestore.rating)} | ${fe(e.deltas.meanTimeToRestore)} |
73
73
  `,n+=`
74
- `,n}function $t(e){let{analysis:t,multiRepo:n,stats:o,staleness:r,metrics:l,workload:i,dependencyGraph:a,doraTrend:m}=e,c=new Date,{approved:s,needingReview:f,waitingOnAuthor:u}=t,d=r?.enabled!==!1?r?.thresholds??[]:[],g=`_Last updated: ${c.toISOString()}_
74
+ `,n}function yt(e){let{analysis:t,multiRepo:n,stats:o,staleness:r,metrics:l,workload:s,dependencyGraph:a,doraTrend:f}=e,c=new Date,{approved:i,needingReview:p,waitingOnAuthor:u}=t,d=r?.enabled!==!1?r?.thresholds??[]:[],g=`_Last updated: ${c.toISOString()}_
75
75
 
76
- `;return g+=Ce("\u2705 Approved",s,h=>({...h,dateColumn:h.createdDate,stalenessBadge:k(h.createdDate,d,c)}),"Created","No approved PRs.",c,n),g+=Ce("\u{1F440} PRs Needing Review",f,h=>({...h,dateColumn:h.waitingSince,stalenessBadge:k(h.waitingSince,d,c)}),"Waiting for feedback","No PRs currently need review.",c,n),g+=Ce("\u270D\uFE0F Waiting on Author",u,h=>({...h,dateColumn:h.lastReviewerActivityDate,stalenessBadge:k(h.lastReviewerActivityDate,d,c)}),"Last reviewer activity","No PRs waiting on author.",c,n),o?.repoStats&&o.repoStats.length>1&&(g+=yn(o.repoStats)),l&&(g+=Rn(l)),i&&i.length>0&&(g+=vn(i)),a&&a.dependencies.length>0&&(g+=bn(a)),m&&(g+=Sn(m)),g+=`_${de(t,o)}._
77
- `,g}var v="\x1B[0m",M="\x1B[2m",N="\x1B[1m",me="\x1B[31m",pe="\x1B[32m",he="\x1B[33m",q="\x1B[36m",An="\x1B[37m",yt="\x1B[41m",Tn="\x1B[42m",Rt="\x1B[43m";function Pn(e,t){return`\x1B]8;;${t}\x1B\\${e}\x1B]8;;\x1B\\`}function Dn(e,t){let n=le(e,t),o;return n.days>0?o=`${n.days}d`:n.hours>0?o=`${n.hours}h`:o=`${n.minutes}m`,n.urgency==="high"?{color:me,label:o}:n.urgency==="medium"?{color:he,label:o}:{color:pe,label:o}}function Ne(e,t){return`${e}${N} ${t} ${v}`}function Cn(e){return e?` ${me}\u26A0 conflict${v}`:""}function xn(e){if(!e)return"";let t=ue(e.label);return` ${t==="low"?pe:t==="medium"?he:me}${N}${e.label}${v}`}function En(e,t){let n=e.replace(/\x1b\][^\x1b]*\x1b\\|\x1b\[[0-9;]*m/g,""),o=t-n.length;return o>0?e+" ".repeat(o):e}function Ee(e,t){return e.length>t?e.slice(0,t-1)+"\u2026":e}function Nn(e){switch(e){case"APPROVE":return`${pe}${N}APPROVE${v}`;case"REVIEW":return`${he}${N}REVIEW${v}`;case"PENDING":return`${M}PENDING${v}`}}function wt(e,t,n,o,r,l,i,a,m,c,s,f,u,d){let{color:g,label:h}=Dn(r,i),p=`${g}${N}${h}${v}`,b=`${M}#${e}${v}`,y=Ee(t.replace(/[\r\n]+/g," ").trim(),60),S=Pn(`${b} ${An}${y}${v}`,o),w=`${M}${u?"\u2B50 ":""}${Ee(n,20)}${v}`,R=Cn(l),P=xn(m),D=Nn(a),O=c&&c.length>0?" "+c.map(we=>`${M}[${we}]${v}`).join(" "):"",G=s?` ${he}${s}${v}`:"",W=d?` ${Y(d)}`:"",$e=!d&&f?` ${J(f)}`:"";return` ${p} ${En(S,80)} ${w}${P} ${D}${R}${W}${$e}${G}${O}`}function xe(e,t,n,o,r,l){let i=[];if(i.push(`
76
+ `;return g+=Ce("\u2705 Approved",i,h=>({...h,dateColumn:h.createdDate,stalenessBadge:k(h.createdDate,d,c)}),"Created","No approved PRs.",c,n),g+=Ce("\u{1F440} PRs Needing Review",p,h=>({...h,dateColumn:h.waitingSince,stalenessBadge:k(h.waitingSince,d,c)}),"Waiting for feedback","No PRs currently need review.",c,n),g+=Ce("\u270D\uFE0F Waiting on Author",u,h=>({...h,dateColumn:h.lastReviewerActivityDate,stalenessBadge:k(h.lastReviewerActivityDate,d,c)}),"Last reviewer activity","No PRs waiting on author.",c,n),o?.repoStats&&o.repoStats.length>1&&(g+=Pn(o.repoStats)),l&&(g+=Tn(l)),s&&s.length>0&&(g+=Dn(s)),a&&a.dependencies.length>0&&(g+=Cn(a)),f&&(g+=xn(f)),g+=`_${de(t,o)}._
77
+ `,g}var v="\x1B[0m",L="\x1B[2m",N="\x1B[1m",me="\x1B[31m",pe="\x1B[32m",he="\x1B[33m",q="\x1B[36m",En="\x1B[37m",vt="\x1B[41m",Nn="\x1B[42m",bt="\x1B[43m";function kn(e,t){return`\x1B]8;;${t}\x1B\\${e}\x1B]8;;\x1B\\`}function Ln(e,t){let n=le(e,t),o;return n.days>0?o=`${n.days}d`:n.hours>0?o=`${n.hours}h`:o=`${n.minutes}m`,n.urgency==="high"?{color:me,label:o}:n.urgency==="medium"?{color:he,label:o}:{color:pe,label:o}}function Ne(e,t){return`${e}${N} ${t} ${v}`}function Mn(e){return e?` ${me}\u26A0 conflict${v}`:""}function On(e){if(!e)return"";let t=ue(e.label);return` ${t==="low"?pe:t==="medium"?he:me}${N}${e.label}${v}`}function In(e,t){let n=e.replace(/\x1b\][^\x1b]*\x1b\\|\x1b\[[0-9;]*m/g,""),o=t-n.length;return o>0?e+" ".repeat(o):e}function Ee(e,t){return e.length>t?e.slice(0,t-1)+"\u2026":e}function Fn(e){switch(e){case"APPROVE":return`${pe}${N}APPROVE${v}`;case"REVIEW":return`${he}${N}REVIEW${v}`;case"PENDING":return`${L}PENDING${v}`}}function Rt(e,t,n,o,r,l,s,a,f,c,i,p,u,d){let{color:g,label:h}=Ln(r,s),m=`${g}${N}${h}${v}`,b=`${L}#${e}${v}`,w=Ee(t.replace(/[\r\n]+/g," ").trim(),60),S=kn(`${b} ${En}${w}${v}`,o),y=`${L}${u?"\u2B50 ":""}${Ee(n,20)}${v}`,R=Mn(l),T=On(f),D=Fn(a),O=c&&c.length>0?" "+c.map(we=>`${L}[${we}]${v}`).join(" "):"",G=i?` ${he}${i}${v}`:"",W=d?` ${J(d)}`:"",$e=!d&&p?` ${Y(p)}`:"";return` ${m} ${In(S,80)} ${y}${T} ${D}${R}${W}${$e}${G}${O}`}function xe(e,t,n,o,r,l){let s=[];if(s.push(`
78
78
  ${Ne(t,`${e} (${n.length})`)}
79
- `),n.length===0)return i.push(` ${M}No PRs${v}
80
- `),i.join(`
81
- `);if(l){let a=new Map;for(let m of n){let c=l(m)??"Unknown";a.has(c)||a.set(c,[]),a.get(c).push(m)}for(let[m,c]of a){i.push(` ${N}${q}\u{1F4C2} ${m}${v}`);for(let s of c){let{id:f,title:u,author:d,url:g,date:h,hasMergeConflict:p,action:b,size:y,detectedLabels:S,stalenessBadge:T,pipelineStatus:w,isStarred:R,policyStatus:P}=o(s);i.push(wt(f,u,d,g,h,p,r,b,y,S,T,w,R,P))}i.push("")}}else{for(let a of n){let{id:m,title:c,author:s,url:f,date:u,hasMergeConflict:d,action:g,size:h,detectedLabels:p,stalenessBadge:b,pipelineStatus:y,isStarred:S,policyStatus:T}=o(a);i.push(wt(m,c,s,f,u,d,r,g,h,p,b,y,S,T))}i.push("")}return i.join(`
82
- `)}function kn(e){let t=[];return t.push(`
83
- ${Ne(Rt,"\u{1F4C8} Review Metrics")}
84
- `),t.push(` ${M}Median PR age:${v} ${N}${e.aggregate.medianAgeInDays}d${v} ${M}Avg first review:${v} ${N}${e.aggregate.avgTimeToFirstReviewInDays??"N/A"}d${v} ${M}Avg rounds:${v} ${N}${e.aggregate.avgReviewRounds}${v} ${M}No review:${v} ${N}${e.aggregate.prsWithNoReviewActivity}${v}`),t.push(""),t.join(`
85
- `)}function Mn(e){if(e.length===0)return"";let t=[];t.push(`
86
- ${Ne(yt,"\u{1F465} Top Reviewer Bottlenecks")}
79
+ `),n.length===0)return s.push(` ${L}No PRs${v}
80
+ `),s.join(`
81
+ `);if(l){let a=new Map;for(let f of n){let c=l(f)??"Unknown";a.has(c)||a.set(c,[]),a.get(c).push(f)}for(let[f,c]of a){s.push(` ${N}${q}\u{1F4C2} ${f}${v}`);for(let i of c){let{id:p,title:u,author:d,url:g,date:h,hasMergeConflict:m,action:b,size:w,detectedLabels:S,stalenessBadge:P,pipelineStatus:y,isStarred:R,policyStatus:T}=o(i);s.push(Rt(p,u,d,g,h,m,r,b,w,S,P,y,R,T))}s.push("")}}else{for(let a of n){let{id:f,title:c,author:i,url:p,date:u,hasMergeConflict:d,action:g,size:h,detectedLabels:m,stalenessBadge:b,pipelineStatus:w,isStarred:S,policyStatus:P}=o(a);s.push(Rt(f,c,i,p,u,d,r,g,h,m,b,w,S,P))}s.push("")}return s.join(`
82
+ `)}function _n(e){let t=[];return t.push(`
83
+ ${Ne(bt,"\u{1F4C8} Review Metrics")}
84
+ `),t.push(` ${L}Median PR age:${v} ${N}${e.aggregate.medianAgeInDays}d${v} ${L}Avg first review:${v} ${N}${e.aggregate.avgTimeToFirstReviewInDays??"N/A"}d${v} ${L}Avg rounds:${v} ${N}${e.aggregate.avgReviewRounds}${v} ${L}No review:${v} ${N}${e.aggregate.prsWithNoReviewActivity}${v}`),t.push(""),t.join(`
85
+ `)}function Bn(e){if(e.length===0)return"";let t=[];t.push(`
86
+ ${Ne(vt,"\u{1F465} Top Reviewer Bottlenecks")}
87
87
  `);let n=e.slice(0,5);for(let o of n){let r=o.avgResponseTimeInDays!==null?`${o.avgResponseTimeInDays}d avg`:"no response",l=o.isStarred?"\u2B50 ":"";t.push(` ${o.loadIndicator} ${N}${l}${o.displayName}${v} \u2014 ${o.pendingReviewCount} pending / ${o.assignedPrCount} assigned (${r})`)}return t.push(""),t.join(`
88
- `)}function vt(e){let{analysis:t,repoLabel:n,multiRepo:o=!1,stats:r,staleness:l,metrics:i,workload:a,dependencyGraph:m}=e,c=new Date,{approved:s,needingReview:f,waitingOnAuthor:u}=t,d=l?.enabled!==!1?l?.thresholds??[]:[],g=[];g.push(""),g.push(`${N}${q} \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510${v}`),g.push(`${N}${q} \u2502 \u{1F4CB} PR Review Dashboard \u2502${v}`),g.push(`${N}${q} \u2502 ${v}${M}${Ee(n,42)}${v}${N}${q}${" ".repeat(Math.max(0,42-n.length))}\u2502${v}`),g.push(`${N}${q} \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518${v}`),g.push("");let h=o?p=>p.repository:void 0;return g.push(xe("\u2705 Approved",Tn,s,p=>({id:p.id,title:p.title,author:p.author,url:p.url,date:p.createdDate,hasMergeConflict:p.hasMergeConflict,action:p.action,size:p.size,detectedLabels:p.detectedLabels,stalenessBadge:k(p.createdDate,d,c),pipelineStatus:p.pipelineStatus,isStarred:p.isStarred,policyStatus:p.policyStatus}),c,h)),g.push(xe("\u{1F440} Needing Review",Rt,f,p=>({id:p.id,title:p.title,author:p.author,url:p.url,date:p.waitingSince,hasMergeConflict:p.hasMergeConflict,action:p.action,size:p.size,detectedLabels:p.detectedLabels,stalenessBadge:k(p.waitingSince,d,c),pipelineStatus:p.pipelineStatus,isStarred:p.isStarred,policyStatus:p.policyStatus}),c,h)),g.push(xe("\u270D\uFE0F Waiting on Author",yt,u,p=>({id:p.id,title:p.title,author:p.author,url:p.url,date:p.lastReviewerActivityDate,hasMergeConflict:p.hasMergeConflict,action:p.action,size:p.size,detectedLabels:p.detectedLabels,stalenessBadge:k(p.lastReviewerActivityDate,d,c),pipelineStatus:p.pipelineStatus,isStarred:p.isStarred,policyStatus:p.policyStatus}),c,h)),i&&g.push(kn(i)),a&&a.length>0&&g.push(Mn(a)),m&&m.dependencies.length>0&&g.push(Ln(m)),g.push(` ${M}${de(t,r)}${v}`),g.push(` ${M}Updated: ${c.toLocaleString()}${v}`),g.push(""),g.join(`
89
- `)}function Ln(e){let t=[];t.push(` ${N}\u{1F517} PR Dependencies${v}`),t.push(` ${M}${e.chains.length} chain(s), ${e.blockedPrIds.length} blocked PR(s), ${e.dependencies.length} dependency link(s)${v}`);for(let n of e.chains.slice(0,5)){let o=n.prIds.map(l=>`#${l}`).join(" \u2192 "),r=n.status==="blocked"?`${me}\u26A0\uFE0F blocked${v}`:`${pe}\u2705 ready${v}`;t.push(` Chain ${n.chainId}: ${o} ${r}`)}return e.chains.length>5&&t.push(` ${M}... and ${e.chains.length-5} more chain(s)${v}`),t.push(""),t.join(`
90
- `)}function bt(e,t){return Math.abs(t.getTime()-e.getTime())/(1e3*60*60*24)}function On(e){if(e.length===0)return 0;let t=[...e].sort((o,r)=>o-r),n=Math.floor(t.length/2);return t.length%2!==0?t[n]:(t[n-1]+t[n])/2}function In(e,t,n){let o=Pe(e,t),r=o.filter(f=>!f.isAuthor).sort((f,u)=>f.date.getTime()-u.date.getTime()),l=bt(e.createdDate,n),i=r.length>0?bt(e.createdDate,r[0].date):null,a=o.sort((f,u)=>f.date.getTime()-u.date.getTime()),m=0,c=!1;for(let f of a)f.isAuthor?c=!0:c&&(m++,c=!1);let s=a.length>0?a[a.length-1].date:e.createdDate;return{prId:e.id,title:e.title,author:e.author,url:e.url,ageInDays:Math.round(l*10)/10,timeToFirstReviewInDays:i!==null?Math.round(i*10)/10:null,reviewRounds:m,lastActivityDate:s}}function ke(e,t=new Set,n=new Date,o=new Set){let r=e.map(d=>In(d,t,n)),l=r.map(d=>d.ageInDays),i=r.map(d=>d.timeToFirstReviewInDays).filter(d=>d!==null),a=r.map(d=>d.reviewRounds),m=r.filter(d=>d.timeToFirstReviewInDays===null).length,c={medianAgeInDays:Math.round(On(l)*10)/10,avgTimeToFirstReviewInDays:i.length>0?Math.round(i.reduce((d,g)=>d+g,0)/i.length*10)/10:null,avgReviewRounds:a.length>0?Math.round(a.reduce((d,g)=>d+g,0)/a.length*10)/10:0,prsWithNoReviewActivity:m,totalPrs:e.length},s=new Map,f=new Map;for(let d of e)f.set(d.author,d.authorUniqueName);for(let d of r)s.has(d.author)||s.set(d.author,[]),s.get(d.author).push(d);let u=[];for(let[d,g]of s){let h=g.map(y=>y.ageInDays),p=g.map(y=>y.reviewRounds),b=g.map(y=>y.timeToFirstReviewInDays).filter(y=>y!==null);u.push({author:d,isStarred:o.has(f.get(d)??""),openPrCount:g.length,avgAgeInDays:Math.round(h.reduce((y,S)=>y+S,0)/h.length*10)/10,avgReviewRounds:Math.round(p.reduce((y,S)=>y+S,0)/p.length*10)/10,fastestReviewInDays:b.length>0?Math.round(Math.min(...b)*10)/10:null})}return u.sort((d,g)=>g.openPrCount-d.openPrCount),{perPr:r,aggregate:c,perAuthor:u}}var Fn={light:{maxPending:10,maxAvgResponseDays:2},medium:{maxPending:20,maxAvgResponseDays:4}};function Bn(e,t,n){let o=r=>t!==null&&t>r;return e>n.medium.maxPending||o(n.medium.maxAvgResponseDays)?"\u{1F534}":e>n.light.maxPending||o(n.light.maxAvgResponseDays)?"\u{1F7E1}":"\u{1F7E2}"}function Me(e,t,n=new Set,o=Fn,r=new Set,l=new Set){let i=new Set(t.needingReview.map(c=>c.id)),a=new Map;for(let c of e)if(!ce(c.authorUniqueName,n,c.author,r))for(let s of c.reviewers){if(ce(s.uniqueName,n,s.displayName,r))continue;let f=s.uniqueName.toLowerCase();a.has(f)||a.set(f,{displayName:s.displayName,assignedPrCount:0,pendingReviewCount:0,completedReviewCount:0,responseTimes:[]});let u=a.get(f);u.assignedPrCount++,s.vote>=5?u.completedReviewCount++:i.has(c.id)&&u.pendingReviewCount++;let d=c.threads.flatMap(g=>g.comments).filter(g=>g.authorUniqueName.toLowerCase()===f).sort((g,h)=>g.publishedDate.getTime()-h.publishedDate.getTime());if(d.length>0){let g=(d[0].publishedDate.getTime()-c.createdDate.getTime())/864e5;g>=0&&u.responseTimes.push(g)}}let m=[];for(let[c,s]of a){let f=s.responseTimes.length>0?Math.round(s.responseTimes.reduce((u,d)=>u+d,0)/s.responseTimes.length*10)/10:null;m.push({reviewer:c,displayName:s.displayName,isStarred:l.has(c),assignedPrCount:s.assignedPrCount,pendingReviewCount:s.pendingReviewCount,completedReviewCount:s.completedReviewCount,avgResponseTimeInDays:f,loadIndicator:Bn(s.pendingReviewCount,f,o)})}return m.sort((c,s)=>s.pendingReviewCount-c.pendingReviewCount),m}function St(e,t){let n=t?` ${t}`:"",o=e.isStarred?"\u2B50 ":"";return`[#${e.id} - ${e.title}](${e.url}) \u2014 ${o}${e.author}${n}`}function Un(e,t,n,o){let r=n?.enabled!==!1?n?.thresholds??[]:[],i=[{type:"TextBlock",text:`\u{1F4CB} PR Review Summary \u2014 ${e.approved.length+e.needingReview.length+e.waitingOnAuthor.length} open PRs`,size:"Large",weight:"Bolder"},{type:"TextBlock",text:`\u2705 ${e.approved.length} approved | \u{1F440} ${e.needingReview.length} needing review | \u270D\uFE0F ${e.waitingOnAuthor.length} waiting on author | \u274C ${t.totalConflicts} conflicts`,wrap:!0,spacing:"Small"}],a=m=>!o||o.includes(m);if(a("needingReview")&&e.needingReview.length>0){i.push({type:"TextBlock",text:`**\u{1F440} PRs Needing Review (${e.needingReview.length})**`,separator:!0,spacing:"Medium"});let m=e.needingReview.slice(0,15);for(let c of m){let s=k(c.waitingSince,r);i.push({type:"TextBlock",text:St(c,s),wrap:!0,spacing:"Small"})}e.needingReview.length>15&&i.push({type:"TextBlock",text:`_\u2026and ${e.needingReview.length-15} more_`,spacing:"Small"})}if(a("waitingOnAuthor")&&e.waitingOnAuthor.length>0){i.push({type:"TextBlock",text:`**\u270D\uFE0F Waiting on Author (${e.waitingOnAuthor.length})**`,separator:!0,spacing:"Medium"});let m=e.waitingOnAuthor.slice(0,10);for(let c of m){let s=k(c.lastReviewerActivityDate,r);i.push({type:"TextBlock",text:St(c,s),wrap:!0,spacing:"Small"})}e.waitingOnAuthor.length>10&&i.push({type:"TextBlock",text:`_\u2026and ${e.waitingOnAuthor.length-10} more_`,spacing:"Small"})}return a("approved")&&e.approved.length>0&&(i.push({type:"TextBlock",text:`**\u2705 Approved (${e.approved.length})**`,separator:!0,spacing:"Medium"}),i.push({type:"TextBlock",text:`${e.approved.length} PRs approved and ready to merge.`,spacing:"Small"})),{type:"message",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:{type:"AdaptiveCard",$schema:"http://adaptivecards.io/schemas/adaptive-card.json",version:"1.4",body:i}}]}}async function At(e,t,n,o){let r=Un(e,t,o,n.filters?.sections);try{let l=await fetch(n.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});l.ok?x("Teams notification sent successfully"):C(`Teams notification failed: ${l.status} ${l.statusText}`)}catch(l){let i=l instanceof Error?l.message:String(l);C(`Teams notification failed: ${i}`)}}async function Le(e,t,n,o){if(!n.teams?.webhookUrl){$("No notification webhooks configured, skipping");return}A("Sending Teams notification\u2026"),await At(e,t,n.teams,o)}import{writeFileSync as _n}from"node:fs";function Tt(e,t,n){let o=e.reduce((c,s)=>c+s.analysis.approved.length+s.analysis.needingReview.length+s.analysis.waitingOnAuthor.length,0),r,l=e.filter(c=>c.metrics).map(c=>c.metrics);if(l.length>0){let c=l.map(s=>s.aggregate);r={medianAgeInDays:c.reduce((s,f)=>s+f.medianAgeInDays,0)/c.length,avgTimeToFirstReviewInDays:zn(c.map(s=>s.avgTimeToFirstReviewInDays)),avgReviewRounds:c.reduce((s,f)=>s+f.avgReviewRounds,0)/c.length,prsWithNoReviewActivity:c.reduce((s,f)=>s+f.prsWithNoReviewActivity,0),totalPrs:c.reduce((s,f)=>s+f.totalPrs,0)}}let i,a=e.filter(c=>c.staleness).map(c=>c.staleness);if(a.length>0){i={};for(let c of a)for(let[s,f]of Object.entries(c))i[s]=(i[s]??0)+f}let m={totalPrs:o,metrics:r,staleness:i};return{generatedAt:(n??new Date).toISOString(),version:t,repositories:e,aggregate:m}}async function Pt(e,t){let n=JSON.stringify(e,null,2);t==="-"?process.stdout.write(n+`
91
- `):(_n(t,n,"utf-8"),x(`JSON report written to ${t}`))}async function Dt(e,t){let n=t.method??"POST";try{let o=await fetch(t.url,{method:n,headers:{"Content-Type":"application/json",...t.headers},body:JSON.stringify(e)});o.ok?x(`Webhook ${n} to ${t.url} succeeded (${o.status})`):C(`Webhook ${n} to ${t.url} returned ${o.status}: ${o.statusText}`)}catch(o){let r=o instanceof Error?o.message:String(o);C(`Webhook ${n} to ${t.url} failed: ${r}`)}}function zn(e){let t=e.filter(n=>n!==null);return t.length===0?null:t.reduce((n,o)=>n+o,0)/t.length}import{readFileSync as jn,writeFileSync as qn,existsSync as Gn}from"node:fs";import{resolve as Ct}from"node:path";function Wn(e){let t=Ct(e);if(!Gn(t))return{entries:[]};try{let n=jn(t,"utf-8"),o=JSON.parse(n);return o&&Array.isArray(o.entries)?o:{entries:[]}}catch{return C(`Failed to parse nudge history at ${t}, starting fresh`),{entries:[]}}}function Hn(e,t){let n=Ct(e);qn(n,JSON.stringify(t,null,2),"utf-8")}function Vn(e,t,n,o,r=new Date){let l=new Map;for(let m of o.entries)l.set(`${m.repoUrl}:${m.prId}`,m);let i=t.thresholds,a=-1;return n.minStalenessLevel&&(a=i.findIndex(m=>m.label===n.minStalenessLevel)),e.filter(m=>{let c=k(m.waitingSince,i,r);if(!c)return!1;if(a>=0){let u=i.findIndex(d=>d.label===c);if(u<0||u>a)return!1}let s=`${m.url}:${m.id}`,f=l.get(s);if(f){let u=new Date(f.lastNudgedAt);if((r.getTime()-u.getTime())/(1e3*60*60*24)<n.cooldownDays)return!1}return!0})}function Jn(e,t,n){return t.commentTemplate.replace(/\{\{days\}\}/g,String(n)).replace(/\{\{reviewers\}\}/g,e.reviewerNames?.join(", ")??"Reviewers").replace(/\{\{title\}\}/g,e.title).replace(/\{\{author\}\}/g,e.author)}function Yn(e){let t=e.match(/^(https:\/\/dev\.azure\.com\/[^/]+)\/([^/]+)\/_git\/([^/]+)/);return t?{orgUrl:t[1],project:t[2],repoName:t[3]}:null}async function Zn(e,t,n,o,r){let l=`${e}/${t}/_apis/git/repositories/${n}/pullRequests/${o}/threads?api-version=7.1`,a=await fetch(l,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({comments:[{content:r,commentType:1}],status:"active"})});if(!a.ok)throw new Error(`ADO API returned ${a.status}: ${a.statusText}`)}async function xt(e,t,n,o=new Date){let r=Wn(n.historyFile),l=Vn(e.needingReview,t,n,r,o),i={nudged:0,skipped:0,errors:0};if(l.length===0)return A("Auto-nudge: no PRs eligible for nudging"),i;A(`Auto-nudge: ${l.length} PR(s) eligible for nudging`);for(let a of l){let m=o.getTime()-a.waitingSince.getTime(),c=Math.floor(m/(1e3*60*60*24)),s=Jn(a,n,c);if(n.dryRun){A(` [DRY RUN] Would nudge #${a.id} "${a.title}" (${c} days)`),i.nudged++;continue}let f=Yn(a.url);if(!f){C(` #${a.id} \u2014 could not parse ADO URL: ${a.url}`),i.errors++;continue}try{await Zn(f.orgUrl,f.project,f.repoName,a.id,s),x(` #${a.id} "${a.title}" \u2014 nudged (${c} days)`),i.nudged++;let u=r.entries.findIndex(g=>g.repoUrl===a.url&&g.prId===a.id),d={prId:a.id,repoUrl:a.url,lastNudgedAt:o.toISOString(),nudgeCount:u>=0?r.entries[u].nudgeCount+1:1};u>=0?r.entries[u]=d:r.entries.push(d)}catch(u){let d=u instanceof Error?u.message:String(u);C(` #${a.id} "${a.title}" \u2014 nudge failed: ${d}`),i.errors++}}return n.dryRun||Hn(n.historyFile,r),A(`Auto-nudge: ${i.nudged} nudged, ${i.skipped} skipped, ${i.errors} errors`),i}import{readFileSync as Kn}from"node:fs";import{resolve as Et,dirname as Xn}from"node:path";import{fileURLToPath as Qn}from"node:url";var eo=Xn(Qn(import.meta.url));function to(){return`<!DOCTYPE html>
88
+ `)}function St(e){let{analysis:t,repoLabel:n,multiRepo:o=!1,stats:r,staleness:l,metrics:s,workload:a,dependencyGraph:f}=e,c=new Date,{approved:i,needingReview:p,waitingOnAuthor:u}=t,d=l?.enabled!==!1?l?.thresholds??[]:[],g=[];g.push(""),g.push(`${N}${q} \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510${v}`),g.push(`${N}${q} \u2502 \u{1F4CB} PR Review Dashboard \u2502${v}`),g.push(`${N}${q} \u2502 ${v}${L}${Ee(n,42)}${v}${N}${q}${" ".repeat(Math.max(0,42-n.length))}\u2502${v}`),g.push(`${N}${q} \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518${v}`),g.push("");let h=o?m=>m.repository:void 0;return g.push(xe("\u2705 Approved",Nn,i,m=>({id:m.id,title:m.title,author:m.author,url:m.url,date:m.createdDate,hasMergeConflict:m.hasMergeConflict,action:m.action,size:m.size,detectedLabels:m.detectedLabels,stalenessBadge:k(m.createdDate,d,c),pipelineStatus:m.pipelineStatus,isStarred:m.isStarred,policyStatus:m.policyStatus}),c,h)),g.push(xe("\u{1F440} Needing Review",bt,p,m=>({id:m.id,title:m.title,author:m.author,url:m.url,date:m.waitingSince,hasMergeConflict:m.hasMergeConflict,action:m.action,size:m.size,detectedLabels:m.detectedLabels,stalenessBadge:k(m.waitingSince,d,c),pipelineStatus:m.pipelineStatus,isStarred:m.isStarred,policyStatus:m.policyStatus}),c,h)),g.push(xe("\u270D\uFE0F Waiting on Author",vt,u,m=>({id:m.id,title:m.title,author:m.author,url:m.url,date:m.lastReviewerActivityDate,hasMergeConflict:m.hasMergeConflict,action:m.action,size:m.size,detectedLabels:m.detectedLabels,stalenessBadge:k(m.lastReviewerActivityDate,d,c),pipelineStatus:m.pipelineStatus,isStarred:m.isStarred,policyStatus:m.policyStatus}),c,h)),s&&g.push(_n(s)),a&&a.length>0&&g.push(Bn(a)),f&&f.dependencies.length>0&&g.push(Un(f)),g.push(` ${L}${de(t,r)}${v}`),g.push(` ${L}Updated: ${c.toLocaleString()}${v}`),g.push(""),g.join(`
89
+ `)}function Un(e){let t=[];t.push(` ${N}\u{1F517} PR Dependencies${v}`),t.push(` ${L}${e.chains.length} chain(s), ${e.blockedPrIds.length} blocked PR(s), ${e.dependencies.length} dependency link(s)${v}`);for(let n of e.chains.slice(0,5)){let o=n.prIds.map(l=>`#${l}`).join(" \u2192 "),r=n.status==="blocked"?`${me}\u26A0\uFE0F blocked${v}`:`${pe}\u2705 ready${v}`;t.push(` Chain ${n.chainId}: ${o} ${r}`)}return e.chains.length>5&&t.push(` ${L}... and ${e.chains.length-5} more chain(s)${v}`),t.push(""),t.join(`
90
+ `)}function At(e,t){return Math.abs(t.getTime()-e.getTime())/(1e3*60*60*24)}function zn(e){if(e.length===0)return 0;let t=[...e].sort((o,r)=>o-r),n=Math.floor(t.length/2);return t.length%2!==0?t[n]:(t[n-1]+t[n])/2}function jn(e,t,n){let o=Te(e,t),r=o.filter(p=>!p.isAuthor).sort((p,u)=>p.date.getTime()-u.date.getTime()),l=At(e.createdDate,n),s=r.length>0?At(e.createdDate,r[0].date):null,a=o.sort((p,u)=>p.date.getTime()-u.date.getTime()),f=0,c=!1;for(let p of a)p.isAuthor?c=!0:c&&(f++,c=!1);let i=a.length>0?a[a.length-1].date:e.createdDate;return{prId:e.id,title:e.title,author:e.author,url:e.url,ageInDays:Math.round(l*10)/10,timeToFirstReviewInDays:s!==null?Math.round(s*10)/10:null,reviewRounds:f,lastActivityDate:i}}function ke(e,t=new Set,n=new Date,o=new Set){let r=e.map(d=>jn(d,t,n)),l=r.map(d=>d.ageInDays),s=r.map(d=>d.timeToFirstReviewInDays).filter(d=>d!==null),a=r.map(d=>d.reviewRounds),f=r.filter(d=>d.timeToFirstReviewInDays===null).length,c={medianAgeInDays:Math.round(zn(l)*10)/10,avgTimeToFirstReviewInDays:s.length>0?Math.round(s.reduce((d,g)=>d+g,0)/s.length*10)/10:null,avgReviewRounds:a.length>0?Math.round(a.reduce((d,g)=>d+g,0)/a.length*10)/10:0,prsWithNoReviewActivity:f,totalPrs:e.length},i=new Map,p=new Map;for(let d of e)p.set(d.author,d.authorUniqueName);for(let d of r)i.has(d.author)||i.set(d.author,[]),i.get(d.author).push(d);let u=[];for(let[d,g]of i){let h=g.map(w=>w.ageInDays),m=g.map(w=>w.reviewRounds),b=g.map(w=>w.timeToFirstReviewInDays).filter(w=>w!==null);u.push({author:d,isStarred:o.has(p.get(d)??""),openPrCount:g.length,avgAgeInDays:Math.round(h.reduce((w,S)=>w+S,0)/h.length*10)/10,avgReviewRounds:Math.round(m.reduce((w,S)=>w+S,0)/m.length*10)/10,fastestReviewInDays:b.length>0?Math.round(Math.min(...b)*10)/10:null})}return u.sort((d,g)=>g.openPrCount-d.openPrCount),{perPr:r,aggregate:c,perAuthor:u}}var qn={light:{maxPending:10,maxAvgResponseDays:2},medium:{maxPending:20,maxAvgResponseDays:4}};function Gn(e,t,n){let o=r=>t!==null&&t>r;return e>n.medium.maxPending||o(n.medium.maxAvgResponseDays)?"\u{1F534}":e>n.light.maxPending||o(n.light.maxAvgResponseDays)?"\u{1F7E1}":"\u{1F7E2}"}function Le(e,t,n=new Set,o=qn,r=new Set,l=new Set){let s=new Set(t.needingReview.map(c=>c.id)),a=new Map;for(let c of e)if(!ce(c.authorUniqueName,n,c.author,r))for(let i of c.reviewers){if(ce(i.uniqueName,n,i.displayName,r))continue;let p=i.uniqueName.toLowerCase();a.has(p)||a.set(p,{displayName:i.displayName,assignedPrCount:0,pendingReviewCount:0,completedReviewCount:0,responseTimes:[]});let u=a.get(p);u.assignedPrCount++,i.vote>=5?u.completedReviewCount++:s.has(c.id)&&u.pendingReviewCount++;let d=c.threads.flatMap(g=>g.comments).filter(g=>g.authorUniqueName.toLowerCase()===p).sort((g,h)=>g.publishedDate.getTime()-h.publishedDate.getTime());if(d.length>0){let g=(d[0].publishedDate.getTime()-c.createdDate.getTime())/864e5;g>=0&&u.responseTimes.push(g)}}let f=[];for(let[c,i]of a){let p=i.responseTimes.length>0?Math.round(i.responseTimes.reduce((u,d)=>u+d,0)/i.responseTimes.length*10)/10:null;f.push({reviewer:c,displayName:i.displayName,isStarred:l.has(c),assignedPrCount:i.assignedPrCount,pendingReviewCount:i.pendingReviewCount,completedReviewCount:i.completedReviewCount,avgResponseTimeInDays:p,loadIndicator:Gn(i.pendingReviewCount,p,o)})}return f.sort((c,i)=>i.pendingReviewCount-c.pendingReviewCount),f}function Pt(e,t){let n=t?` ${t}`:"",o=e.isStarred?"\u2B50 ":"";return`[#${e.id} - ${e.title}](${e.url}) \u2014 ${o}${e.author}${n}`}function Wn(e,t,n,o){let r=n?.enabled!==!1?n?.thresholds??[]:[],s=[{type:"TextBlock",text:`\u{1F4CB} PR Review Summary \u2014 ${e.approved.length+e.needingReview.length+e.waitingOnAuthor.length} open PRs`,size:"Large",weight:"Bolder"},{type:"TextBlock",text:`\u2705 ${e.approved.length} approved | \u{1F440} ${e.needingReview.length} needing review | \u270D\uFE0F ${e.waitingOnAuthor.length} waiting on author | \u274C ${t.totalConflicts} conflicts`,wrap:!0,spacing:"Small"}],a=f=>!o||o.includes(f);if(a("needingReview")&&e.needingReview.length>0){s.push({type:"TextBlock",text:`**\u{1F440} PRs Needing Review (${e.needingReview.length})**`,separator:!0,spacing:"Medium"});let f=e.needingReview.slice(0,15);for(let c of f){let i=k(c.waitingSince,r);s.push({type:"TextBlock",text:Pt(c,i),wrap:!0,spacing:"Small"})}e.needingReview.length>15&&s.push({type:"TextBlock",text:`_\u2026and ${e.needingReview.length-15} more_`,spacing:"Small"})}if(a("waitingOnAuthor")&&e.waitingOnAuthor.length>0){s.push({type:"TextBlock",text:`**\u270D\uFE0F Waiting on Author (${e.waitingOnAuthor.length})**`,separator:!0,spacing:"Medium"});let f=e.waitingOnAuthor.slice(0,10);for(let c of f){let i=k(c.lastReviewerActivityDate,r);s.push({type:"TextBlock",text:Pt(c,i),wrap:!0,spacing:"Small"})}e.waitingOnAuthor.length>10&&s.push({type:"TextBlock",text:`_\u2026and ${e.waitingOnAuthor.length-10} more_`,spacing:"Small"})}return a("approved")&&e.approved.length>0&&(s.push({type:"TextBlock",text:`**\u2705 Approved (${e.approved.length})**`,separator:!0,spacing:"Medium"}),s.push({type:"TextBlock",text:`${e.approved.length} PRs approved and ready to merge.`,spacing:"Small"})),{type:"message",attachments:[{contentType:"application/vnd.microsoft.card.adaptive",content:{type:"AdaptiveCard",$schema:"http://adaptivecards.io/schemas/adaptive-card.json",version:"1.4",body:s}}]}}async function Tt(e,t,n,o){let r=Wn(e,t,o,n.filters?.sections);try{let l=await fetch(n.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});l.ok?x("Teams notification sent successfully"):C(`Teams notification failed: ${l.status} ${l.statusText}`)}catch(l){let s=l instanceof Error?l.message:String(l);C(`Teams notification failed: ${s}`)}}async function Me(e,t,n,o){if(!n.teams?.webhookUrl){$("No notification webhooks configured, skipping");return}A("Sending Teams notification\u2026"),await Tt(e,t,n.teams,o)}import{writeFileSync as Hn}from"node:fs";function Dt(e,t,n){let o=e.reduce((c,i)=>c+i.analysis.approved.length+i.analysis.needingReview.length+i.analysis.waitingOnAuthor.length,0),r,l=e.filter(c=>c.metrics).map(c=>c.metrics);if(l.length>0){let c=l.map(i=>i.aggregate);r={medianAgeInDays:c.reduce((i,p)=>i+p.medianAgeInDays,0)/c.length,avgTimeToFirstReviewInDays:Vn(c.map(i=>i.avgTimeToFirstReviewInDays)),avgReviewRounds:c.reduce((i,p)=>i+p.avgReviewRounds,0)/c.length,prsWithNoReviewActivity:c.reduce((i,p)=>i+p.prsWithNoReviewActivity,0),totalPrs:c.reduce((i,p)=>i+p.totalPrs,0)}}let s,a=e.filter(c=>c.staleness).map(c=>c.staleness);if(a.length>0){s={};for(let c of a)for(let[i,p]of Object.entries(c))s[i]=(s[i]??0)+p}let f={totalPrs:o,metrics:r,staleness:s};return{generatedAt:(n??new Date).toISOString(),version:t,repositories:e,aggregate:f}}async function Ct(e,t){let n=JSON.stringify(e,null,2);t==="-"?process.stdout.write(n+`
91
+ `):(Hn(t,n,"utf-8"),x(`JSON report written to ${t}`))}async function xt(e,t){let n=t.method??"POST";try{let o=await fetch(t.url,{method:n,headers:{"Content-Type":"application/json",...t.headers},body:JSON.stringify(e)});o.ok?x(`Webhook ${n} to ${t.url} succeeded (${o.status})`):C(`Webhook ${n} to ${t.url} returned ${o.status}: ${o.statusText}`)}catch(o){let r=o instanceof Error?o.message:String(o);C(`Webhook ${n} to ${t.url} failed: ${r}`)}}function Vn(e){let t=e.filter(n=>n!==null);return t.length===0?null:t.reduce((n,o)=>n+o,0)/t.length}import{readFileSync as Yn,writeFileSync as Jn,existsSync as Zn}from"node:fs";import{resolve as Et}from"node:path";function Kn(e){let t=Et(e);if(!Zn(t))return{entries:[]};try{let n=Yn(t,"utf-8"),o=JSON.parse(n);return o&&Array.isArray(o.entries)?o:{entries:[]}}catch{return C(`Failed to parse nudge history at ${t}, starting fresh`),{entries:[]}}}function Xn(e,t){let n=Et(e);Jn(n,JSON.stringify(t,null,2),"utf-8")}function Qn(e,t,n,o,r=new Date){let l=new Map;for(let f of o.entries)l.set(`${f.repoUrl}:${f.prId}`,f);let s=t.thresholds,a=-1;return n.minStalenessLevel&&(a=s.findIndex(f=>f.label===n.minStalenessLevel)),e.filter(f=>{let c=k(f.waitingSince,s,r);if(!c)return!1;if(a>=0){let u=s.findIndex(d=>d.label===c);if(u<0||u>a)return!1}let i=`${f.url}:${f.id}`,p=l.get(i);if(p){let u=new Date(p.lastNudgedAt);if((r.getTime()-u.getTime())/(1e3*60*60*24)<n.cooldownDays)return!1}return!0})}function eo(e,t,n){return t.commentTemplate.replace(/\{\{days\}\}/g,String(n)).replace(/\{\{reviewers\}\}/g,e.reviewerNames?.join(", ")??"Reviewers").replace(/\{\{title\}\}/g,e.title).replace(/\{\{author\}\}/g,e.author)}function to(e){let t=e.match(/^(https:\/\/dev\.azure\.com\/[^/]+)\/([^/]+)\/_git\/([^/]+)/);return t?{orgUrl:t[1],project:t[2],repoName:t[3]}:null}async function no(e,t,n,o,r){let l=`${e}/${t}/_apis/git/repositories/${n}/pullRequests/${o}/threads?api-version=7.1`,a=await fetch(l,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({comments:[{content:r,commentType:1}],status:"active"})});if(!a.ok)throw new Error(`ADO API returned ${a.status}: ${a.statusText}`)}async function Nt(e,t,n,o=new Date){let r=Kn(n.historyFile),l=Qn(e.needingReview,t,n,r,o),s={nudged:0,skipped:0,errors:0};if(l.length===0)return A("Auto-nudge: no PRs eligible for nudging"),s;A(`Auto-nudge: ${l.length} PR(s) eligible for nudging`);for(let a of l){let f=o.getTime()-a.waitingSince.getTime(),c=Math.floor(f/(1e3*60*60*24)),i=eo(a,n,c);if(n.dryRun){A(` [DRY RUN] Would nudge #${a.id} "${a.title}" (${c} days)`),s.nudged++;continue}let p=to(a.url);if(!p){C(` #${a.id} \u2014 could not parse ADO URL: ${a.url}`),s.errors++;continue}try{await no(p.orgUrl,p.project,p.repoName,a.id,i),x(` #${a.id} "${a.title}" \u2014 nudged (${c} days)`),s.nudged++;let u=r.entries.findIndex(g=>g.repoUrl===a.url&&g.prId===a.id),d={prId:a.id,repoUrl:a.url,lastNudgedAt:o.toISOString(),nudgeCount:u>=0?r.entries[u].nudgeCount+1:1};u>=0?r.entries[u]=d:r.entries.push(d)}catch(u){let d=u instanceof Error?u.message:String(u);C(` #${a.id} "${a.title}" \u2014 nudge failed: ${d}`),s.errors++}}return n.dryRun||Xn(n.historyFile,r),A(`Auto-nudge: ${s.nudged} nudged, ${s.skipped} skipped, ${s.errors} errors`),s}import{readFileSync as oo}from"node:fs";import{resolve as kt,dirname as ro}from"node:path";import{fileURLToPath as io}from"node:url";var so=ro(io(import.meta.url));function ao(){return`<!DOCTYPE html>
92
92
  <html lang="en">
93
93
  <head>
94
94
  <meta charset="UTF-8">
@@ -129,7 +129,7 @@ ${t}`,{cause:e})}}async function Ht(e,t){let n=await fetch(t,{headers:{Authoriza
129
129
  .policy-cell { position: relative; }
130
130
  .policy-popup-wrap { cursor: pointer; }
131
131
  .policy-popup { display: none; position: absolute; z-index: 100; top: 100%; left: 0; min-width: 320px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); padding: 0.75rem; font-size: 0.82rem; }
132
- .policy-popup-wrap:hover .policy-popup { display: block; }
132
+ .policy-popup.visible { display: block; }
133
133
  .policy-popup h4 { margin: 0 0 0.5rem 0; font-size: 0.85rem; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 0.4rem; }
134
134
  .policy-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0; border-bottom: 1px solid rgba(48,54,61,0.5); }
135
135
  .policy-row:last-child { border-bottom: none; }
@@ -398,7 +398,10 @@ function getPolicyPopup(ps) {
398
398
  const blockLabel = e.isBlocking
399
399
  ? '<span class="policy-blocking">required</span>'
400
400
  : '<span class="policy-optional">optional</span>';
401
- return \`<div class="policy-row"><span class="policy-icon">\${icon}</span><span class="policy-name">\${escapeHtml(e.displayName)}</span>\${blockLabel}</div>\`;
401
+ const nameHtml = e.buildUrl
402
+ ? \`<a href="\${escapeHtml(e.buildUrl)}" target="_blank" class="policy-name">\${escapeHtml(e.displayName)}</a>\`
403
+ : \`<span class="policy-name">\${escapeHtml(e.displayName)}</span>\`;
404
+ return \`<div class="policy-row"><span class="policy-icon">\${icon}</span>\${nameHtml}\${blockLabel}</div>\`;
402
405
  }).join('');
403
406
  return \`<div class="policy-popup"><h4>Policy Evaluations</h4>\${rows}</div>\`;
404
407
  }
@@ -453,9 +456,49 @@ function escapeHtml(s) {
453
456
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
454
457
  }
455
458
 
459
+ // Policy popup: show on hover/click, keep visible while mouse is over badge or popup
460
+ (function() {
461
+ let hideTimer = null;
462
+ function showPopup(wrap) {
463
+ const popup = wrap.querySelector('.policy-popup');
464
+ if (!popup) return;
465
+ // Close any other open popups
466
+ document.querySelectorAll('.policy-popup.visible').forEach(p => { if (p !== popup) p.classList.remove('visible'); });
467
+ clearTimeout(hideTimer);
468
+ popup.classList.add('visible');
469
+ }
470
+ function scheduleHide(wrap) {
471
+ hideTimer = setTimeout(() => {
472
+ const popup = wrap.querySelector('.policy-popup');
473
+ if (popup) popup.classList.remove('visible');
474
+ }, 300);
475
+ }
476
+ function cancelHide() { clearTimeout(hideTimer); }
477
+
478
+ document.addEventListener('mouseenter', e => {
479
+ const wrap = e.target.closest('.policy-popup-wrap');
480
+ if (wrap) showPopup(wrap);
481
+ if (e.target.closest('.policy-popup')) cancelHide();
482
+ }, true);
483
+ document.addEventListener('mouseleave', e => {
484
+ const wrap = e.target.closest('.policy-popup-wrap');
485
+ if (wrap) scheduleHide(wrap);
486
+ if (e.target.closest('.policy-popup')) {
487
+ const parentWrap = e.target.closest('.policy-popup-wrap');
488
+ if (parentWrap) scheduleHide(parentWrap);
489
+ }
490
+ }, true);
491
+ // Close on click outside
492
+ document.addEventListener('click', e => {
493
+ if (!e.target.closest('.policy-popup-wrap')) {
494
+ document.querySelectorAll('.policy-popup.visible').forEach(p => p.classList.remove('visible'));
495
+ }
496
+ });
497
+ })();
498
+
456
499
  init();
457
500
  </script>
458
501
  </body>
459
502
  </html>
460
- `}var no="{{DATA_PLACEHOLDER}}";function Nt(e){let t=to(),n=JSON.stringify(e);return t.replace(no,n)}var ao={$schema:"https://raw.githubusercontent.com/Meir017/ado-pr-review-needed/main/pr-review-config.schema.json",repositories:[{url:"https://dev.azure.com/{org}/{project}/_git/{repo}"}],orgManager:null,teamMembers:[],ignoreManagers:!1};function Ie(){let e=z(so(io(import.meta.url)),"..","package.json");try{return JSON.parse(ro(e,"utf-8")).version??"0.0.0"}catch{return"0.0.0"}}function Mt(){let e=z("pr-review-config.json");oo(e)&&(C(`Config file already exists: ${e}`),A("Remove or rename the existing file and try again."),process.exit(1)),Oe(e,JSON.stringify(ao,null,2)+`
461
- `,"utf-8"),x(`Created template config: ${e}`),A("Edit the file to add your Azure DevOps repository URLs and team members.")}function co(e,t){let n=ke(e.prs,t.botUsers,void 0,t.starredUsers),o=Me(e.prs,e.analysis,t.botUsers,void 0,t.aiBotUsers,t.starredUsers),r;if(t.staleness.enabled){r={};let l=[...e.analysis.approved.map(i=>i.createdDate),...e.analysis.needingReview.map(i=>i.waitingSince),...e.analysis.waitingOnAuthor.map(i=>i.lastReviewerActivityDate)];for(let i of l){let a=k(i,t.staleness.thresholds);a&&(r[a]=(r[a]??0)+1)}}return{repoLabel:e.repoLabel,analysis:e.analysis,metrics:n,workload:o,staleness:r,stats:be(e.repoLabel,e.analysis,e.restarted,e.restartFailed)}}async function lo(e,t,n,o,r){if(r.length===0)return;let l=new Set(r),i=o.filter(a=>l.has(a.id));A(`Refreshing merge status for ${i.length} restarted PR(s)\u2026`);for(let a of i)try{let m=await E(`Refresh merge status for PR #${a.id}`,()=>e.getPullRequestById(a.id,n));m.mergeStatus!==void 0&&(a.mergeStatus=m.mergeStatus)}catch(m){let c=m instanceof Error?m.message:String(m);C(` #${a.id} \u2014 failed to refresh merge status: ${c}`)}}async function uo(e){let{repo:t,isMultiRepo:n,restartMergeAfterDays:o,quantifierConfig:r,teamMembers:l,ignoredUsers:i,botUsers:a,aiBotUsers:m}=e,c=`${t.project}/${t.repository}`;A(`Fetching open PRs from ${c}\u2026`);let s=Date.now(),f=r;r&&t.patterns.ignore.length>0&&(f={...r,excludedPatterns:[...r.excludedPatterns,...t.patterns.ignore]});let u=await ve(t.orgUrl),d=await qe(t.orgUrl),g=await Ge(t.orgUrl),h=await We(t.orgUrl),p;try{p=(await E("Resolve project GUID",()=>h.getProject(t.project))).id}catch(w){let R=w instanceof Error?w.message:String(w);$(`Failed to resolve project GUID for ${t.project}: ${R}`)}let b=await it(u,t.repository,t.project,t.orgUrl,f,t.patterns,d,g,p);x(`Fetched ${b.length} candidate PRs from ${c} (${Date.now()-s}ms)`),await at(u,t.repository,t.project,b);let y=t.skipRestartMerge?-1:o;t.skipRestartMerge&&$(`Skipping restart-merge for ${c} (configured per repository)`);let S=await ct(u,t.repository,t.project,b,y);await lo(u,t.repository,t.project,b,S.restartedPrIds);let T=ut(b,l,n?c:void 0,i,a,m,e.starredUsers);return{repoLabel:c,prs:b,analysis:T,restarted:S.restarted,restartFailed:S.failed}}async function kt(e){A("Loading configuration\u2026");let t=await et(e),n=t.repos,o=n.length>1;A("Authenticating to Azure DevOps\u2026");let r=Date.now(),l=[...new Set(n.map(p=>p.orgUrl))];for(let p of l)await ve(p);x(`Authenticated to ${l.join(", ")} (${Date.now()-r}ms)`);let i=0,a=0,m=0;A(`Processing ${n.length} repo(s) (concurrency: ${10})\u2026`);let c=await re(n,10,p=>uo({repo:p,isMultiRepo:o,restartMergeAfterDays:t.restartMergeAfterDays,quantifierConfig:t.quantifier,teamMembers:t.teamMembers,ignoredUsers:t.ignoredUsers,botUsers:t.botUsers,aiBotUsers:t.aiBotUsers,starredUsers:t.starredUsers}));for(let p of c)i+=p.prs.length,a+=p.restarted,m+=p.restartFailed;let s=dt(c.map(p=>p.analysis)),f=c.map(p=>be(p.repoLabel,p.analysis,p.restarted,p.restartFailed)),u=Ke(s,a,m,f),d=c.flatMap(p=>p.prs),g=ke(d,t.botUsers,void 0,t.starredUsers),h=Me(d,s,t.botUsers,void 0,t.aiBotUsers,t.starredUsers);return{multiConfig:t,repos:n,isMultiRepo:o,results:c,merged:s,stats:u,allPrs:d,metrics:g,workload:h,totalPrs:i,totalRestarted:a,totalRestartFailed:m}}async function Lt(e){ze(e.verbose);let t=e.format??"markdown";if(t==="terminal"){let{multiConfig:u,repos:d,isMultiRepo:g,merged:h,stats:p,metrics:b,workload:y}=await kt(e.config),S=g?`${d.length} repositories`:`${d[0].project}/${d[0].repository}`,T=vt({analysis:h,repoLabel:S,multiRepo:g,stats:p,staleness:u.staleness,metrics:b,workload:y});console.log(T),u.notifications&&await Le(h,p,u.notifications,u.staleness);return}Re("PR Review Needed");let{multiConfig:n,repos:o,isMultiRepo:r,results:l,merged:i,stats:a,metrics:m,workload:c,totalPrs:s}=await kt(e.config);for(let u of l)x(`${u.repoLabel}: ${u.analysis.approved.length} approved, ${u.analysis.needingReview.length} needing review, ${u.analysis.waitingOnAuthor.length} waiting on author`);if(t==="json"||t==="html"){let u=l.map(h=>co(h,n)),d=Tt(u,Ie());if(t==="html"){A("Generating HTML report\u2026");let h=Nt(d),p=e.output==="pr-review-summary.md"?"pr-review-summary.html":e.output;Oe(z(p),h,"utf-8"),x(`HTML report written to ${z(p)}`)}else{A("Generating JSON report\u2026");let h=e.output==="pr-review-summary.md"?"pr-review-summary.json":e.output;await Pt(d,z(h))}let g=e.webhookUrl?{url:e.webhookUrl}:n.webhook;g&&await Dt(d,g)}else{A("Generating markdown\u2026");let u=$t({analysis:i,multiRepo:r,stats:a,staleness:n.staleness,metrics:m,workload:c}),d=z(e.output);Oe(d,u,"utf-8"),x(`Output written to ${d}`)}Re("Summary"),_("Repositories",o.length),_("PRs analyzed",s),_("Approved",i.approved.length),_("Needing review",i.needingReview.length),_("Waiting on author",i.waitingOnAuthor.length),_("Output file",z(e.output)),console.log(),e.notify!==!1&&n.notifications&&await Le(i,a,n.notifications,n.staleness);let f=n.autoNudge;f&&e.nudge!==!1&&(e.dryRun&&(f.dryRun=!0),await xt(i,n.staleness,f))}process.removeAllListeners("warning");process.on("warning",e=>{(e.name!=="DeprecationWarning"||e.code!=="DEP0169")&&console.warn(e)});var Fe=new go().name("pr-review-needed").description("Generates a markdown summary of Azure DevOps PRs needing review").version(Ie());Fe.command("setup").description("Generate a template pr-review-config.json in the current directory").action(()=>{Mt()});Fe.command("run").description("Analyze PRs and generate a markdown summary or dashboard").option("--output <path>","Output file path","pr-review-summary.md").option("--config <path>","Path to a custom config file").option("--format <type>","Output format: markdown, json, html, terminal","markdown").option("--webhook-url <url>","Send JSON report to webhook URL").option("--verbose","Enable debug logging",!1).option("--notify","Send notifications (default: true if webhooks configured)").option("--no-notify","Disable notifications").option("--nudge","Send nudge comments on stale PRs (default: true if configured)").option("--no-nudge","Disable auto-nudge comments").option("--dry-run","Log actions without making changes",!1).action(async e=>{await Lt(e)});Fe.parseAsync(process.argv).catch(e=>{K(e instanceof Error?e.message:String(e)),process.exit(1)});
503
+ `}var co="{{DATA_PLACEHOLDER}}";function Lt(e){let t=ao(),n=JSON.stringify(e);return t.replace(co,n)}var mo={$schema:"https://raw.githubusercontent.com/Meir017/ado-pr-review-needed/main/pr-review-config.schema.json",repositories:[{url:"https://dev.azure.com/{org}/{project}/_git/{repo}"}],orgManager:null,teamMembers:[],ignoreManagers:!1};function Ie(){let e=z(go(fo(import.meta.url)),"..","package.json");try{return JSON.parse(uo(e,"utf-8")).version??"0.0.0"}catch{return"0.0.0"}}function Ot(){let e=z("pr-review-config.json");lo(e)&&(C(`Config file already exists: ${e}`),A("Remove or rename the existing file and try again."),process.exit(1)),Oe(e,JSON.stringify(mo,null,2)+`
504
+ `,"utf-8"),x(`Created template config: ${e}`),A("Edit the file to add your Azure DevOps repository URLs and team members.")}function po(e,t){let n=ke(e.prs,t.botUsers,void 0,t.starredUsers),o=Le(e.prs,e.analysis,t.botUsers,void 0,t.aiBotUsers,t.starredUsers),r;if(t.staleness.enabled){r={};let l=[...e.analysis.approved.map(s=>s.createdDate),...e.analysis.needingReview.map(s=>s.waitingSince),...e.analysis.waitingOnAuthor.map(s=>s.lastReviewerActivityDate)];for(let s of l){let a=k(s,t.staleness.thresholds);a&&(r[a]=(r[a]??0)+1)}}return{repoLabel:e.repoLabel,analysis:e.analysis,metrics:n,workload:o,staleness:r,stats:be(e.repoLabel,e.analysis,e.restarted,e.restartFailed)}}async function ho(e,t,n,o,r){if(r.length===0)return;let l=new Set(r),s=o.filter(a=>l.has(a.id));A(`Refreshing merge status for ${s.length} restarted PR(s)\u2026`);for(let a of s)try{let f=await E(`Refresh merge status for PR #${a.id}`,()=>e.getPullRequestById(a.id,n));f.mergeStatus!==void 0&&(a.mergeStatus=f.mergeStatus)}catch(f){let c=f instanceof Error?f.message:String(f);C(` #${a.id} \u2014 failed to refresh merge status: ${c}`)}}async function $o(e){let{repo:t,isMultiRepo:n,restartMergeAfterDays:o,quantifierConfig:r,teamMembers:l,ignoredUsers:s,botUsers:a,aiBotUsers:f}=e,c=`${t.project}/${t.repository}`;A(`Fetching open PRs from ${c}\u2026`);let i=Date.now(),p=r;r&&t.patterns.ignore.length>0&&(p={...r,excludedPatterns:[...r.excludedPatterns,...t.patterns.ignore]});let u=await ve(t.orgUrl),d=await qe(t.orgUrl),g=await Ge(t.orgUrl),h=await We(t.orgUrl),m;try{m=(await E("Resolve project GUID",()=>h.getProject(t.project))).id}catch(y){let R=y instanceof Error?y.message:String(y);$(`Failed to resolve project GUID for ${t.project}: ${R}`)}let b=await ct(u,t.repository,t.project,t.orgUrl,p,t.patterns,d,g,m);x(`Fetched ${b.length} candidate PRs from ${c} (${Date.now()-i}ms)`),await lt(u,t.repository,t.project,b);let w=t.skipRestartMerge?-1:o;t.skipRestartMerge&&$(`Skipping restart-merge for ${c} (configured per repository)`);let S=await ut(u,t.repository,t.project,b,w);await ho(u,t.repository,t.project,b,S.restartedPrIds);let P=gt(b,l,n?c:void 0,s,a,f,e.starredUsers);return{repoLabel:c,prs:b,analysis:P,restarted:S.restarted,restartFailed:S.failed}}async function Mt(e){A("Loading configuration\u2026");let t=await et(e),n=t.repos,o=n.length>1;A("Authenticating to Azure DevOps\u2026");let r=Date.now(),l=[...new Set(n.map(m=>m.orgUrl))];for(let m of l)await ve(m);x(`Authenticated to ${l.join(", ")} (${Date.now()-r}ms)`);let s=0,a=0,f=0;A(`Processing ${n.length} repo(s) (concurrency: ${10})\u2026`);let c=await re(n,10,m=>$o({repo:m,isMultiRepo:o,restartMergeAfterDays:t.restartMergeAfterDays,quantifierConfig:t.quantifier,teamMembers:t.teamMembers,ignoredUsers:t.ignoredUsers,botUsers:t.botUsers,aiBotUsers:t.aiBotUsers,starredUsers:t.starredUsers}));for(let m of c)s+=m.prs.length,a+=m.restarted,f+=m.restartFailed;let i=ft(c.map(m=>m.analysis)),p=c.map(m=>be(m.repoLabel,m.analysis,m.restarted,m.restartFailed)),u=Ke(i,a,f,p),d=c.flatMap(m=>m.prs),g=ke(d,t.botUsers,void 0,t.starredUsers),h=Le(d,i,t.botUsers,void 0,t.aiBotUsers,t.starredUsers);return{multiConfig:t,repos:n,isMultiRepo:o,results:c,merged:i,stats:u,allPrs:d,metrics:g,workload:h,totalPrs:s,totalRestarted:a,totalRestartFailed:f}}async function It(e){ze(e.verbose);let t=e.format??"markdown";if(t==="terminal"){let{multiConfig:u,repos:d,isMultiRepo:g,merged:h,stats:m,metrics:b,workload:w}=await Mt(e.config),S=g?`${d.length} repositories`:`${d[0].project}/${d[0].repository}`,P=St({analysis:h,repoLabel:S,multiRepo:g,stats:m,staleness:u.staleness,metrics:b,workload:w});console.log(P),u.notifications&&await Me(h,m,u.notifications,u.staleness);return}Re("PR Review Needed");let{multiConfig:n,repos:o,isMultiRepo:r,results:l,merged:s,stats:a,metrics:f,workload:c,totalPrs:i}=await Mt(e.config);for(let u of l)x(`${u.repoLabel}: ${u.analysis.approved.length} approved, ${u.analysis.needingReview.length} needing review, ${u.analysis.waitingOnAuthor.length} waiting on author`);if(t==="json"||t==="html"){let u=l.map(h=>po(h,n)),d=Dt(u,Ie());if(t==="html"){A("Generating HTML report\u2026");let h=Lt(d),m=e.output==="pr-review-summary.md"?"pr-review-summary.html":e.output;Oe(z(m),h,"utf-8"),x(`HTML report written to ${z(m)}`)}else{A("Generating JSON report\u2026");let h=e.output==="pr-review-summary.md"?"pr-review-summary.json":e.output;await Ct(d,z(h))}let g=e.webhookUrl?{url:e.webhookUrl}:n.webhook;g&&await xt(d,g)}else{A("Generating markdown\u2026");let u=yt({analysis:s,multiRepo:r,stats:a,staleness:n.staleness,metrics:f,workload:c}),d=z(e.output);Oe(d,u,"utf-8"),x(`Output written to ${d}`)}Re("Summary"),U("Repositories",o.length),U("PRs analyzed",i),U("Approved",s.approved.length),U("Needing review",s.needingReview.length),U("Waiting on author",s.waitingOnAuthor.length),U("Output file",z(e.output)),console.log(),e.notify!==!1&&n.notifications&&await Me(s,a,n.notifications,n.staleness);let p=n.autoNudge;p&&e.nudge!==!1&&(e.dryRun&&(p.dryRun=!0),await Nt(s,n.staleness,p))}process.removeAllListeners("warning");process.on("warning",e=>{(e.name!=="DeprecationWarning"||e.code!=="DEP0169")&&console.warn(e)});var Fe=new wo().name("pr-review-needed").description("Generates a markdown summary of Azure DevOps PRs needing review").version(Ie());Fe.command("setup").description("Generate a template pr-review-config.json in the current directory").action(()=>{Ot()});Fe.command("run").description("Analyze PRs and generate a markdown summary or dashboard").option("--output <path>","Output file path","pr-review-summary.md").option("--config <path>","Path to a custom config file").option("--format <type>","Output format: markdown, json, html, terminal","markdown").option("--webhook-url <url>","Send JSON report to webhook URL").option("--verbose","Enable debug logging",!1).option("--notify","Send notifications (default: true if webhooks configured)").option("--no-notify","Disable notifications").option("--nudge","Send nudge comments on stale PRs (default: true if configured)").option("--no-nudge","Disable auto-nudge comments").option("--dry-run","Log actions without making changes",!1).action(async e=>{await It(e)});Fe.parseAsync(process.argv).catch(e=>{K(e instanceof Error?e.message:String(e)),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meirblachman/pr-review-needed",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Generates a markdown summary of Azure DevOps PRs needing review",
5
5
  "type": "module",
6
6
  "bin": {