@meirblachman/pr-review-needed 0.1.24 → 0.1.26
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.
- package/dist/index.min.js +386 -110
- package/package.json +2 -2
package/dist/index.min.js
CHANGED
|
@@ -1,117 +1,393 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
${o}`)}boxWrap(e,t){if(t<this.minWidthToWrap)return e;let n=e.split(/\r\n|\n/),r=/[\s]*[^\s]+/g,s=[];return n.forEach(o=>{let l=o.match(r);if(l===null){s.push("");return}let h=[l.shift()],u=this.displayWidth(h[0]);l.forEach(a=>{let d=this.displayWidth(a);if(u+d<=t){h.push(a),u+=d;return}s.push(h.join(""));let c=a.trimStart();h=[c],u=this.displayWidth(c)}),s.push(h.join(""))}),s.join(`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
${e}`,{cause:i})}}async function Xn(i,e){let t=await fetch(e,{headers:{Authorization:`Bearer ${i}`}});if(!t.ok){let n=await t.text();throw t.status>=400&&t.status<500?new V(`Graph API ${t.status}: ${n}`):new Error(`Graph API ${t.status}: ${n}`)}return await t.json()}async function xt(i,e){let t=[],n=`${Zn}/users/${encodeURIComponent(e)}/directReports?$select=mail,userPrincipalName,displayName&$top=999`;for(;n;){let r=await E(`Fetch direct reports for ${e}`,()=>Xn(i,n));for(let s of r.value){let o=s.mail?.toLowerCase(),l=s.userPrincipalName?.toLowerCase();o&&t.push(o),l&&l!==o&&t.push(l)}n=r["@odata.nextLink"]}return t}async function Tt(i){S(`Fetching direct reports for ${i} from Microsoft Graph\u2026`);let e=await Rt(),t=await xt(e,i);x(`Found ${t.length} direct reports for ${i}`);for(let n of t)$(` ${n}`);return t}async function Et(i){S(`Fetching full org tree under ${i} from Microsoft Graph\u2026`);let e=await Rt(),t=10,n=[],r=[i.toLowerCase()],s=[i],o=new Set;for(;s.length>0;){let l=s.filter(u=>!o.has(u));for(let u of l)o.add(u);if(l.length===0)break;$(` Processing ${l.length} users (concurrency: ${t})\u2026`);let h=[];for(let u=0;u<l.length;u+=t){let a=l.slice(u,u+t),d=await Promise.all(a.map(async c=>{try{let m=await xt(e,c);return $(` ${c} \u2192 ${m.length} direct reports`),{upn:c,reports:m}}catch(m){if(m instanceof V&&m.message.includes("404"))return O(` ${c} \u2014 not found in directory, keeping as member`),{upn:c,reports:[c]};throw m}}));for(let{upn:c,reports:m}of d){m.length>0&&!(m.length===1&&m[0]===c)&&r.push(c.toLowerCase());for(let p of m)n.push(p),h.push(p)}}s=h}return x(`Found ${n.length} total org members under ${i} (${r.length} managers)`),{members:n,managers:r}}function Dt(i){let e=i.match(/https?:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/\s]+)/);if(e)return{orgUrl:`https://dev.azure.com/${e[1]}`,project:e[2],repository:e[3]};let t=i.match(/https?:\/\/(?:[^@]+@)?([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/\s]+)/);if(t)return{orgUrl:`https://dev.azure.com/${t[1]}`,project:t[2],repository:t[3]};let n=i.match(/ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/\s]+)/);if(n)return{orgUrl:`https://dev.azure.com/${n[1]}`,project:n[2],repository:n[3]};let r=i.match(/vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/\s]+)/);return r?{orgUrl:`https://dev.azure.com/${r[1]}`,project:r[2],repository:r[3]}:null}var se=[{label:"XS",maxChanges:10},{label:"S",maxChanges:40},{label:"M",maxChanges:100},{label:"L",maxChanges:400},{label:"XL",maxChanges:1e3}];function oe(i){return i?.uniqueName?.toLowerCase()??""}function Pt(i,e,t,n){return{totalConflicts:[...i.approved,...i.needingReview,...i.waitingOnAuthor].filter(o=>o.hasMergeConflict).length,mergeRestarted:e,mergeRestartFailed:t,repoStats:n}}function We(i,e,t,n){let r=[...e.approved,...e.needingReview,...e.waitingOnAuthor];return{repoLabel:i,approved:e.approved.length,needingReview:e.needingReview.length,waitingOnAuthor:e.waitingOnAuthor.length,conflicts:r.filter(s=>s.hasMergeConflict).length,mergeRestarted:t,mergeRestartFailed:n}}var kt=[{label:"\u26A0\uFE0F Aging",minDays:7},{label:"\u{1F534} Stale",minDays:14},{label:"\u{1F480} Abandoned",minDays:30}];function ni(i){let e=i?Nt(i):Nt(ei(ti(import.meta.url)),"..","pr-review-config.json"),t=Qn(e,"utf-8");return JSON.parse(t)}function ii(i){if(!i.repositories||i.repositories.length===0)throw new Error("Config must specify 'repositories' (array of repository objects with a 'url' field).");return i.repositories.map(e=>{let t=Dt(e.url);if(!t)throw new Error(`Invalid ADO repository URL: ${e.url}`);return{...t,skipRestartMerge:e.skipRestartMerge??!1,patterns:{ignore:e.patterns?.ignore??[],labels:e.patterns?.labels??{}}}})}async function ri(i){let e=new Set((i.teamMembers??[]).map(n=>n.toLowerCase())),t=new Set;if(i.orgManager){let n=await Et(i.orgManager);for(let r of n.members)e.add(r);if(e.add(i.orgManager.toLowerCase()),i.ignoreManagers)for(let r of n.managers)t.add(r.toLowerCase())}if(i.manager){let n=await Tt(i.manager);for(let r of n)e.add(r);e.add(i.manager.toLowerCase()),i.ignoreManagers&&t.add(i.manager.toLowerCase())}return{teamMembers:e,ignoredUsers:t}}function si(i){if(i.quantifier?.enabled===!1)return;let e=i.quantifier?.excludedPatterns??[],t=i.quantifier?.thresholds?i.quantifier.thresholds.map(n=>({label:n.label,maxChanges:n.maxChanges})):se;return{enabled:!0,excludedPatterns:e,thresholds:t}}function oi(i){return i.staleness?.enabled===!1?{enabled:!1,thresholds:[]}:{enabled:!0,thresholds:i.staleness?.thresholds?i.staleness.thresholds.map(t=>({label:t.label,minDays:t.minDays})).sort((t,n)=>n.minDays-t.minDays):kt.slice().sort((t,n)=>n.minDays-t.minDays)}}function ai(i){if(!(!i.autoNudge||i.autoNudge.enabled===!1))return{enabled:!0,minStalenessLevel:i.autoNudge.minStalenessLevel,cooldownDays:i.autoNudge.cooldownDays??7,commentTemplate:i.autoNudge.commentTemplate??"\u23F0 This PR has been waiting for review for {{days}} days. Reviewers: {{reviewers}}. Please take a look!",dryRun:i.autoNudge.dryRun??!1,historyFile:i.autoNudge.historyFile??".pr-nudge-history.json"}}async function Mt(i){let e=ni(i),t=ii(e),{teamMembers:n,ignoredUsers:r}=await ri(e),s=new Set((e.botUsers??[]).map(p=>p.toLowerCase())),o=new Set((e.aiBotUsers??[]).map(p=>p.toLowerCase())),l=new Set((e.starredUsers??[]).map(p=>p.toLowerCase())),h=si(e),u=e.restartMergeAfterDays??30,a=oi(e),d=e.notifications,c=e.webhook,m=ai(e);return{repos:t,teamMembers:n,ignoredUsers:r,botUsers:s,aiBotUsers:o,starredUsers:l,quantifier:h,restartMergeAfterDays:u,staleness:a,notifications:d,webhook:c,autoNudge:m}}import{PullRequestStatus as ci}from"azure-devops-node-api/interfaces/GitInterfaces.js";import{BuildResult as ce,BuildStatus as de}from"azure-devops-node-api/interfaces/BuildInterfaces.js";import{LineDiffBlockChangeType as ae,VersionControlChangeType as Lt}from"azure-devops-node-api/interfaces/GitInterfaces.js";import li from"picomatch";function le(i,e=se){let t=[...e].sort((n,r)=>n.maxChanges-r.maxChanges);for(let n of t)if(i<=n.maxChanges)return n.label;return t[t.length-1].label}function ui(i,e){if(e.length===0)return!1;let t=i.replace(/^\//,"");return e.some(n=>n(t))}function It(i){let e=0,t=0;for(let n of i){let r=n.changeType??ae.None;r===ae.Add?e+=n.modifiedLinesCount??0:r===ae.Delete?t+=n.originalLinesCount??0:r===ae.Edit&&(e+=n.modifiedLinesCount??0,t+=n.originalLinesCount??0)}return{added:e,deleted:t}}async function Ft(i,e,t,n,r){let s=r.excludedPatterns.map(w=>li(w,{dot:!0})),o=await E(`Fetch iterations for PR #${n}`,()=>i.getPullRequestIterations(e,n,t));if(!o||o.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:le(0,r.thresholds)};let l=o[o.length-1],h=l.id,u=[],a=0,d=100;for(;;){let w=await E(`Fetch iteration changes for PR #${n} iter ${h}`,()=>i.getPullRequestIterationChanges(e,n,h,t,d,a)),y=w.changeEntries??[];for(let T of y){let I=T.item?.path??"",U=T.originalPath;I&&!ui(I,s)&&u.push({path:I,originalPath:U??void 0,changeType:T.changeType??0})}if((w.nextSkip??0)===0&&(w.nextTop??0)===0||(a=w.nextSkip??a+d,y.length===0))break}if(u.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:le(0,r.thresholds)};let c=l.sourceRefCommit?.commitId,m=l.targetRefCommit?.commitId;if(!c||!m){$(` PR #${n} \u2014 no commit refs, using file count as proxy`);let w=u.length;return{linesAdded:w,linesDeleted:0,totalChanges:w,label:le(w,r.thresholds)}}let p=u.map(w=>{let y=(w.changeType&Lt.Add)!==0,T=(w.changeType&Lt.Delete)!==0;return{originalPath:y?void 0:w.originalPath??w.path,path:T?void 0:w.path}}),g=0,f=0,A=10,b=0;for(let w=0;w<p.length;w+=A){let y=p.slice(w,w+A),T={baseVersionCommit:m,targetVersionCommit:c,fileDiffParams:y};try{let I=await i.getFileDiffs(T,t,e);for(let U of I){let{added:z,deleted:J}=It(U.lineDiffBlocks??[]);g+=z,f+=J}}catch{for(let I of y)try{let U={baseVersionCommit:m,targetVersionCommit:c,fileDiffParams:[I]},z=await i.getFileDiffs(U,t,e);for(let J of z){let{added:wn,deleted:$n}=It(J.lineDiffBlocks??[]);g+=wn,f+=$n}}catch{b++}}}b>0&&$(` PR #${n} \u2014 skipped ${b} files (not found at specified version)`);let v=g+f,R=le(v,r.thresholds);return $(` PR #${n} \u2014 +${g} -${f} = ${v} (${R})`),{linesAdded:g,linesDeleted:f,totalChanges:v,label:R}}async function ue(i,e,t){let n=new Array(i.length),r=0;async function s(){for(;r<i.length;){let l=r++;n[l]=await t(i[l])}}let o=Array.from({length:Math.min(e,i.length)},()=>s());return await Promise.all(o),n}import Ht from"picomatch";function Vt(i,e,t){if(Object.keys(t).length===0)return[];let n=e.map(o=>Ht(o,{dot:!0})),r=i.map(o=>o.replace(/^\//,"")).filter(o=>!n.some(l=>l(o)));if(r.length===0)return[];let s=[];for(let[o,l]of Object.entries(t)){let h=l.map(u=>Ht(u,{dot:!0}));r.some(u=>h.some(a=>a(u)))&&s.push(o)}return s}function di(i){let e=[],t=0,n=0;for(let r of i){if(r.isDraft){t++,$(` #${r.pullRequestId} \u2014 draft, skipping`);continue}if((r.labels??[]).map(o=>o.name??"").some(o=>o.toUpperCase()==="NO-MERGE")){n++,$(` #${r.pullRequestId} \u2014 NO-MERGE label, skipping`);continue}e.push(r)}return t>0&&$(`Skipped ${t} draft PRs`),n>0&&$(`Skipped ${n} NO-MERGE PRs`),e}async function hi(i,e,t,n){let r=await E(`Fetch iterations for PR #${n} (file patterns)`,()=>i.getPullRequestIterations(e,n,t));if(!r||r.length===0)return[];let o=r[r.length-1].id;if(o==null)return[];let l=[],h=0,u=100;for(;;){let a=await E(`Fetch iteration changes for PR #${n} iter ${o} (file patterns)`,()=>i.getPullRequestIterationChanges(e,n,o,t,u,h));for(let d of a.changeEntries??[]){let c=d.item?.path??"";c&&l.push(c)}if((a.nextSkip??0)===0&&(a.nextTop??0)===0||(h=a.nextSkip??h+u,(a.changeEntries??[]).length===0))break}return l}function mi(i,e){if(i===de.InProgress)return"inProgress";if(i===de.NotStarted)return"notStarted";switch(e){case ce.Succeeded:return"succeeded";case ce.Failed:return"failed";case ce.PartiallySucceeded:return"partiallySucceeded";case ce.Canceled:return"canceled";default:return"none"}}async function pi(i,e,t,n){try{let r=`refs/pull/${n}/merge`,s=await E(`Fetch builds for PR #${n}`,()=>i.getBuilds(t,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,e,"TfsGit"));if(!s||s.length===0)return;let o=new Map;for(let c of s){let m=c.definition?.id??0;o.has(m)||o.set(m,c)}let l=[],h=0,u=0,a=0,d=0;for(let c of o.values()){let m=mi(c.status,c.result);switch(l.push({id:c.id??0,name:c.definition?.name??"Unknown",status:de[c.status??de.None]??"None",result:m}),m){case"succeeded":h++;break;case"failed":u++;break;case"inProgress":case"notStarted":a++;break;default:d++;break}}return{total:l.length,succeeded:h,failed:u,inProgress:a,other:d,runs:l}}catch(r){let s=r instanceof Error?r.message:String(r);$(` #${n} \u2014 failed to fetch pipeline status: ${s}`);return}}async function Ut(i,e,t,n,r,s={ignore:[],labels:{}},o){let l;try{let c=new URL(n);c.hostname.endsWith(".visualstudio.com")?l=`https://dev.azure.com/${c.hostname.replace(".visualstudio.com","")}`:l=n.replace(/\/$/,"")}catch{l=n.replace(/\/$/,"")}let h=await E("Fetch pull requests",()=>i.getPullRequests(e,{status:ci.Active},t));$(`API returned ${h.length} active pull requests`);let u;if(o)try{u=(await E("Resolve repository GUID",()=>i.getRepository(e,t))).id}catch(c){let m=c instanceof Error?c.message:String(c);$(`Failed to resolve repository GUID for ${e}: ${m}`)}let a=di(h);S(`Fetching threads for ${a.length} PRs in ${t}/${e} (concurrency: ${10})\u2026`);let d=await ue(a,10,async c=>{let m=c.pullRequestId;$(` #${m} \u2014 fetching threads\u2026`);let g=(await E(`Fetch threads for PR #${m}`,()=>i.getThreads(e,m,t))).map(w=>({id:w.id??0,publishedDate:new Date(w.publishedDate??0),comments:(w.comments??[]).filter(y=>!y.isDeleted).map(y=>({authorUniqueName:oe(y.author),publishedDate:new Date(y.publishedDate??0)}))})),f=(c.reviewers??[]).map(w=>({displayName:w.displayName??"",uniqueName:oe(w),vote:w.vote??0})),A=`${l}/${t}/_git/${e}/pullrequest/${m}`,b=(c.labels??[]).map(w=>w.name??""),v=[];if(Object.keys(s.labels).length>0){let w=await hi(i,e,t,m);v=Vt(w,s.ignore,s.labels),v.length>0&&$(` #${m} \u2014 detected labels: ${v.join(", ")}`)}let R=o&&u?await pi(o,u,t,m):void 0;return{id:m,title:c.title??"(no title)",author:c.createdBy?.displayName??"Unknown",authorUniqueName:oe(c.createdBy),url:A,createdDate:new Date(c.creationDate??0),reviewers:f,threads:g,labels:b,detectedLabels:v,mergeStatus:c.mergeStatus??0,lastSourcePushDate:c.lastMergeSourceCommit?.committer?.date?new Date(c.lastMergeSourceCommit.committer.date):void 0,size:r?await Ft(i,e,t,m,r):void 0,description:c.description??void 0,sourceBranch:c.sourceRefName??void 0,targetBranch:c.targetRefName??void 0,pipelineStatus:R}});return $(`${d.length} PRs remain after filtering`),d}async function Wt(i,e,t,n){for(let r of n)if(r.detectedLabels.length!==0){for(let s of r.detectedLabels)if(!r.labels.some(o=>o.toLowerCase()===s.toLowerCase()))try{await E(`Add label '${s}' to PR #${r.id}`,()=>i.createPullRequestLabel({name:s},e,r.id,t)),r.labels.push(s),$(` #${r.id} \u2014 added label '${s}'`)}catch(o){let l=o instanceof Error?o.message:String(o);$(` #${r.id} \u2014 failed to add label '${s}': ${l}`)}}}var fi=1440*60*1e3,gi=["TF401398","TF401027"],wi=["TF401027"];async function Gt(i,e,t,n,r,s=new Date){if(r<0)return $("Restart merge is disabled (restartMergeAfterDays < 0)"),{restarted:0,failed:0,restartedPrIds:[]};let o=new Date(s.getTime()-r*fi),l=n.filter(d=>d.createdDate<o);if(l.length===0)return $("No PRs older than the restart-merge threshold"),{restarted:0,failed:0,restartedPrIds:[]};S(`Restarting merge for ${l.length} PR(s) older than ${r} days\u2026`);let h=0,u=0,a=[];for(let d of l)try{await E(`Restart merge for PR #${d.id}`,async()=>{try{return await i.updatePullRequest({mergeStatus:1},e,d.id,t)}catch(c){let m=c instanceof Error?c.message:String(c);throw gi.some(p=>m.includes(p))?new V(m):c}}),$(` #${d.id} "${d.title}" \u2014 merge restarted`),h++,a.push(d.id)}catch(c){let m=c instanceof Error?c.message:String(c);if(O(` #${d.id} "${d.title}" \u2014 failed to restart merge: ${m}`),u++,wi.some(p=>m.includes(p))){O("Stopping restart-merge for this repository due to permission error"),u+=l.length-l.indexOf(d)-1;break}}return x(`Restarted merge for ${h}/${l.length} PR(s)`),{restarted:h,failed:u,restartedPrIds:a}}import{PullRequestAsyncStatus as Ge}from"azure-devops-node-api/interfaces/GitInterfaces.js";var $i=["build","[bot]","team foundation","microsoft.visualstudio.com"],yi=["dependabot[bot]","renovate[bot]","github-actions[bot]","snyk-bot","greenkeeper[bot]","depfu[bot]","imgbot[bot]","allcontributors[bot]"],Bt=["github copilot","copilot[bot]","claude","codex"];function qe(i,e=new Set,t){let n=i.toLowerCase();return!!(e.has(n)||$i.some(r=>n.includes(r))||t&&e.has(t.toLowerCase()))}function he(i,e=new Set,t){let n=i.toLowerCase();if(e.has(n)||Bt.some(r=>n.includes(r)))return!0;if(t){let r=t.toLowerCase();if(e.has(r)||Bt.some(s=>r.includes(s)))return!0}return!1}function me(i,e=new Set,t,n=new Set){let r=i.toLowerCase();return yi.some(s=>r.includes(s))||qe(r,e,t)||he(r,n,t)}function bi(i,e=new Set,t){return he(i,e,t)}function Be(i,e,t=new Set,n,r=new Set){if(bi(e,r,n))switch(i){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}if(me(e,t,n,r))return"APPROVE";switch(i){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}}function je(i,e=new Set,t=new Set){let n=i.authorUniqueName,r=[];for(let s of i.threads)for(let o of s.comments)qe(o.authorUniqueName,e)||he(o.authorUniqueName,t)||r.push({date:o.publishedDate,isAuthor:o.authorUniqueName===n});return i.lastSourcePushDate&&r.push({date:i.lastSourcePushDate,isAuthor:!0}),r}function qt(i,e=new Set,t,n=new Set,r=new Set,s=new Set,o=new Set){let l=[],h=[],u=[];for(let a of i){if(n.has(a.authorUniqueName)){$(` #${a.id} "${a.title}" \u2014 author ${a.authorUniqueName} is ignored, skipping`);continue}let d=e.size===0||e.has(a.authorUniqueName),c=o.has(a.authorUniqueName);if(a.reviewers.some(y=>y.vote>=5&&!qe(y.uniqueName,r,y.displayName)&&!he(y.uniqueName,s,y.displayName))){let y=a.mergeStatus===Ge.Conflicts;$(` #${a.id} "${a.title}" \u2014 approved`),l.push({id:a.id,title:a.title,author:a.author,url:a.url,createdDate:a.createdDate,hasMergeConflict:y,isTeamMember:d,isStarred:c,action:Be("approved",a.authorUniqueName,r,a.author,s),repository:t,size:a.size,detectedLabels:a.detectedLabels.length>0?a.detectedLabels:void 0,pipelineStatus:a.pipelineStatus});continue}let p=je(a,r,s),g=p.filter(y=>y.isAuthor).sort((y,T)=>y.date.getTime()-T.date.getTime()),f=p.filter(y=>!y.isAuthor).sort((y,T)=>y.date.getTime()-T.date.getTime()),A=g.length>0?g[g.length-1]:null,b=f.length>0?f[f.length-1]:null,v=!1;if(A&&b?v=A.date.getTime()>b.date.getTime():(A&&!b||!A&&!b)&&(v=!0),!v){let y=a.mergeStatus===Ge.Conflicts;$(` #${a.id} "${a.title}" \u2014 reviewer acted last`),u.push({id:a.id,title:a.title,author:a.author,url:a.url,lastReviewerActivityDate:b.date,hasMergeConflict:y,isTeamMember:d,isStarred:c,action:Be("waitingOnAuthor",a.authorUniqueName,r,a.author,s),repository:t,size:a.size,detectedLabels:a.detectedLabels.length>0?a.detectedLabels:void 0,pipelineStatus:a.pipelineStatus});continue}let R;b?R=g.find(T=>T.date.getTime()>b.date.getTime())?.date??a.createdDate:R=a.createdDate;let w=a.mergeStatus===Ge.Conflicts;$(` #${a.id} "${a.title}" \u2014 needs review (waiting since ${R.toISOString()}${w?", has conflicts":""})`),h.push({id:a.id,title:a.title,author:a.author,url:a.url,waitingSince:R,hasMergeConflict:w,isTeamMember:d,isStarred:c,action:Be("needingReview",a.authorUniqueName,r,a.author,s),repository:t,size:a.size,detectedLabels:a.detectedLabels.length>0?a.detectedLabels:void 0,reviewerNames:a.reviewers.map(y=>y.displayName),pipelineStatus:a.pipelineStatus})}return $(`${l.length} approved PRs`),$(`${u.length} PRs waiting on author`),l.sort((a,d)=>a.createdDate.getTime()-d.createdDate.getTime()),h.sort((a,d)=>a.waitingSince.getTime()-d.waitingSince.getTime()),u.sort((a,d)=>a.lastReviewerActivityDate.getTime()-d.lastReviewerActivityDate.getTime()),{approved:l,needingReview:h,waitingOnAuthor:u}}function jt(i){let e=i.flatMap(r=>r.approved),t=i.flatMap(r=>r.needingReview),n=i.flatMap(r=>r.waitingOnAuthor);return e.sort((r,s)=>r.createdDate.getTime()-s.createdDate.getTime()),t.sort((r,s)=>r.waitingSince.getTime()-s.waitingSince.getTime()),n.sort((r,s)=>r.lastReviewerActivityDate.getTime()-s.lastReviewerActivityDate.getTime()),{approved:e,needingReview:t,waitingOnAuthor:n}}function P(i,e,t=new Date){if(e.length===0)return null;let n=t.getTime()-i.getTime(),r=Math.floor(n/(1e3*60*60*24));for(let s of e)if(r>=s.minDays)return s.label;return null}function pe(i,e=new Date){let t=e.getTime()-i.getTime(),n=Math.floor(t/(1e3*60*60*24)),r=Math.floor(t/(1e3*60*60)),s=Math.floor(t/(1e3*60)),o;return n>3?o="high":n>1?o="medium":o="low",{days:n,hours:r,minutes:s,urgency:o}}function fe(i){return i==="XS"||i==="S"?"low":i==="M"?"medium":"high"}function ge(i,e){let{approved:t,needingReview:n,waitingOnAuthor:r}=i,s=t.length+n.length+r.length,o=`Total: ${s} open PR${s===1?"":"s"} \u2014 ${t.length} approved, ${n.length} needing review, ${r.length} waiting on author`;return e&&(o+=`, ${e.totalConflicts} with conflicts`,(e.mergeRestarted>0||e.mergeRestartFailed>0)&&(o+=`, ${e.mergeRestarted} merge restarted`,e.mergeRestartFailed>0&&(o+=` (${e.mergeRestartFailed} failed)`))),o}function Q(i){return i?i.failed>0?`\u{1F534} ${i.failed}/${i.total} failed`:i.inProgress>0?`\u{1F7E1} ${i.inProgress}/${i.total} running`:i.succeeded===i.total?`\u{1F7E2} ${i.total}/${i.total} passed`:`\u26AA ${i.total} pipeline(s)`:""}function zt(i,e=new Date){let t=pe(i,e),n=t.urgency==="high"?"\u{1F534}":t.urgency==="medium"?"\u{1F7E1}":"\u{1F7E2}",r;return t.days>0?r=`${t.days} day${t.days===1?"":"s"} ago`:t.hours>0?r=`${t.hours} hour${t.hours===1?"":"s"} ago`:r=`${t.minutes} minute${t.minutes===1?"":"s"} ago`,`${n} ${r}`}function Jt(i){let e=fe(i.label);return`${e==="low"?"\u{1F7E2}":e==="medium"?"\u{1F7E1}":"\u{1F534}"} ${i.label}`}function Yt(i){switch(i){case"APPROVE":return"\u{1F7E2} APPROVE";case"REVIEW":return"\u{1F50D} REVIEW";case"PENDING":return"\u23F3 PENDING"}}function Kt(i){return!i||i.length===0?"":" "+i.map(e=>`\`${L(e)}\``).join(" ")}function L(i){return i.replace(/[\r\n]+/g," ").trim().replace(/\[/g,"\\[").replace(/\]/g,"\\]").replace(/\|/g,"\\|")}function Zt(i,e){return e?`\u2B50 ${i}`:i}function ze(i,e,t,n,r=!1){if(i.length===0)return`_${t}_
|
|
31
|
-
|
|
32
|
-
`;let s=i.some(a=>a.size!=null),o=i.some(a=>a.stalenessBadge),l=i.some(a=>a.pipelineStatus!=null);if(r){let a=["PR","Repository","Author","Action"];s&&a.push("Size"),l&&a.push("Pipelines"),o&&a.push("Staleness"),a.push(e);let d=`| ${a.join(" | ")} |
|
|
33
|
-
|${a.map(()=>"---").join("|")}|
|
|
34
|
-
`;for(let c of i){let m=c.hasMergeConflict?" \u274C":"",p=L(c.title),g=L(Zt(c.author,c.isStarred)),f=L(c.repository??"Unknown"),A=Kt(c.detectedLabels),b=`[#${c.id} - ${p}](${c.url})${m}${A}`,v=zt(c.dateColumn,n),R=Yt(c.action),w=s?` ${c.size?Jt(c.size):""} |`:"",y=l?` ${Q(c.pipelineStatus)} |`:"",T=o?` ${c.stalenessBadge??""} |`:"";d+=`| ${b} | ${f} | ${g} | ${R} |${w}${y}${T} ${v} |
|
|
35
|
-
`}return d+`
|
|
36
|
-
`}let h=["PR","Author","Action"];s&&h.push("Size"),l&&h.push("Pipelines"),o&&h.push("Staleness"),h.push(e);let u=`| ${h.join(" | ")} |
|
|
37
|
-
|${h.map(()=>"---").join("|")}|
|
|
38
|
-
`;for(let a of i){let d=a.hasMergeConflict?" \u274C":"",c=L(a.title),m=L(Zt(a.author,a.isStarred)),p=Kt(a.detectedLabels),g=`[#${a.id} - ${c}](${a.url})${d}${p}`,f=zt(a.dateColumn,n),A=Yt(a.action),b=s?` ${a.size?Jt(a.size):""} |`:"",v=l?` ${Q(a.pipelineStatus)} |`:"",R=o?` ${a.stalenessBadge??""} |`:"";u+=`| ${g} | ${m} | ${A} |${b}${v}${R} ${f} |
|
|
39
|
-
`}return u+`
|
|
40
|
-
`}function _i(i){return i.every(t=>t.isTeamMember)?{team:i,community:[]}:{team:i.filter(t=>t.isTeamMember),community:i.filter(t=>!t.isTeamMember)}}function Je(i,e,t,n,r,s,o=!1){let l=`## ${i}
|
|
41
|
-
|
|
42
|
-
`,{team:h,community:u}=_i(e);return u.length>0?(l+=`### Team PRs
|
|
43
|
-
|
|
44
|
-
`,l+=ze(h.map(t),n,"No team PRs.",s,o),l+=`### Community Contributions
|
|
45
|
-
|
|
46
|
-
`,l+=ze(u.map(t),n,"No community PRs.",s,o)):l+=ze(e.map(t),n,r,s,o),l}function Ci(i){let e=`## \u{1F4CA} Statistics per Repository
|
|
47
|
-
|
|
48
|
-
`;e+=`| Repository | Open PRs | \u2705 Approved | \u{1F440} Needs Review | \u270D\uFE0F Waiting on Author | \u274C Conflicts | \u{1F504} Merge Restarted |
|
|
49
|
-
`,e+=`|---|---|---|---|---|---|---|
|
|
50
|
-
`;for(let t of i){let n=t.approved+t.needingReview+t.waitingOnAuthor,r=t.mergeRestarted>0?t.mergeRestartFailed>0?`${t.mergeRestarted} (${t.mergeRestartFailed} failed)`:`${t.mergeRestarted}`:"0";e+=`| ${L(t.repoLabel)} | ${n} | ${t.approved} | ${t.needingReview} | ${t.waitingOnAuthor} | ${t.conflicts} | ${r} |
|
|
51
|
-
`}return e+`
|
|
52
|
-
`}function ee(i){return i<1?"< 1 day":`${i} day${i===1?"":"s"}`}function vi(i){let e=`## \u{1F4C8} Review Metrics
|
|
53
|
-
|
|
54
|
-
`;if(e+=`### Summary
|
|
55
|
-
|
|
56
|
-
`,e+=`| Metric | Value |
|
|
2
|
+
import{Command as so}from"commander";import{existsSync as Kn,readFileSync as Xn,writeFileSync as xe}from"node:fs";import{resolve as B,dirname as Qn}from"node:path";import{fileURLToPath as eo}from"node:url";import{DefaultAzureCredential as kt}from"@azure/identity";import*as Ie from"azure-devops-node-api";import{BearerCredentialHandler as Ot}from"azure-devops-node-api/handlers/bearertoken.js";var I="\x1B[0m",fe="\x1B[2m",Me="\x1B[36m",Nt="\x1B[32m",Mt="\x1B[33m",Lt="\x1B[31m",Le="\x1B[1m",ke=!1;function Oe(e){ke=e}function W(){return fe+new Date().toISOString().slice(11,19)+I}function A(e){console.log(`${W()} ${Me}\u2139${I} ${e}`)}function P(e){console.log(`${W()} ${Nt}\u2714${I} ${e}`)}function T(e){console.log(`${W()} ${Mt}\u26A0${I} ${e}`)}function Y(e){console.error(`${W()} ${Lt}\u2716${I} ${e}`)}function w(e){ke&&console.log(`${W()} ${fe}\xB7 ${e}${I}`)}function me(e){console.log(`
|
|
3
|
+
${Le}${Me}\u25B8 ${e}${I}`)}function _(e,t){console.log(` ${fe}${e}:${I} ${Le}${t}${I}`)}var It="499b84ac-1321-427f-aa17-267ca6975798";async function Fe(){let e=process.env.SYSTEM_ACCESSTOKEN;if(e)return w("Using SYSTEM_ACCESSTOKEN from Azure Pipelines environment"),e;try{w("Requesting token for Azure DevOps resource via DefaultAzureCredential\u2026");let n=await new kt().getToken(`${It}/.default`);return w("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 Ue(e,t){w(`Connecting to ${e}\u2026`);let n=new Ot(t),o=new Ie.WebApi(e,n),r=await o.getGitApi(),u=await o.getBuildApi(),i=await o.getCoreApi();return{gitApi:r,buildApi:u,coreApi:i}}var z=new Map;async function pe(e){if(!z.has(e)){let t=await Fe(),n=await Ue(e,t);z.set(e,n)}return z.get(e).gitApi}async function _e(e){if(!z.has(e)){let t=await Fe(),n=await Ue(e,t);z.set(e,n)}return z.get(e).buildApi}import{readFileSync as qt}from"node:fs";import{resolve as Ve,dirname as Gt}from"node:path";import{fileURLToPath as Wt}from"node:url";import{DefaultAzureCredential as _t}from"@azure/identity";var F=class extends Error{constructor(t){super(t),this.name="NonRetryableError"}},Ft={maxAttempts:4,baseDelayMs:2e3,maxDelayMs:3e4};function Ut(e){return new Promise(t=>setTimeout(t,e))}async function x(e,t,n){let{maxAttempts:o,baseDelayMs:r,maxDelayMs:u}={...Ft,...n};for(let i=1;i<=o;i++)try{return await t()}catch(a){if(a instanceof F)throw a;let g=a instanceof Error?a.message:String(a);if(i===o)throw Y(`${e} failed after ${o} attempts: ${g}`),a;let s=Math.min(r*2**(i-1),u);T(`${e} failed (attempt ${i}/${o}), retrying in ${(s/1e3).toFixed(1)}s\u2026 \u2014 ${g}`),await Ut(s)}throw new Error("unreachable")}var Bt="https://graph.microsoft.com/.default",zt="https://graph.microsoft.com/v1.0";async function Be(){try{return(await new _t().getToken(Bt)).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 jt(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 F(`Graph API ${n.status}: ${o}`):new Error(`Graph API ${n.status}: ${o}`)}return await n.json()}async function ze(e,t){let n=[],o=`${zt}/users/${encodeURIComponent(t)}/directReports?$select=mail,userPrincipalName,displayName&$top=999`;for(;o;){let r=await x(`Fetch direct reports for ${t}`,()=>jt(e,o));for(let u of r.value){let i=u.mail?.toLowerCase(),a=u.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 Be(),n=await ze(t,e);P(`Found ${n.length} direct reports for ${e}`);for(let o of n)w(` ${o}`);return n}async function qe(e){A(`Fetching full org tree under ${e} from Microsoft Graph\u2026`);let t=await Be(),n=10,o=[],r=[e.toLowerCase()],u=[e],i=new Set;for(;u.length>0;){let a=u.filter(c=>!i.has(c));for(let c of a)i.add(c);if(a.length===0)break;w(` Processing ${a.length} users (concurrency: ${n})\u2026`);let g=[];for(let c=0;c<a.length;c+=n){let s=a.slice(c,c+n),f=await Promise.all(s.map(async l=>{try{let d=await ze(t,l);return w(` ${l} \u2192 ${d.length} direct reports`),{upn:l,reports:d}}catch(d){if(d instanceof F&&d.message.includes("404"))return T(` ${l} \u2014 not found in directory, keeping as member`),{upn:l,reports:[l]};throw d}}));for(let{upn:l,reports:d}of f){d.length>0&&!(d.length===1&&d[0]===l)&&r.push(l.toLowerCase());for(let m of d)o.push(m),g.push(m)}}u=g}return P(`Found ${o.length} total org members under ${e} (${r.length} managers)`),{members:o,managers:r}}function Ge(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 Z=[{label:"XS",maxChanges:10},{label:"S",maxChanges:40},{label:"M",maxChanges:100},{label:"L",maxChanges:400},{label:"XL",maxChanges:1e3}];function K(e){return e?.uniqueName?.toLowerCase()??""}function We(e,t,n,o){return{totalConflicts:[...e.approved,...e.needingReview,...e.waitingOnAuthor].filter(i=>i.hasMergeConflict).length,mergeRestarted:t,mergeRestartFailed:n,repoStats:o}}function he(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(u=>u.hasMergeConflict).length,mergeRestarted:n,mergeRestartFailed:o}}var He=[{label:"\u26A0\uFE0F Aging",minDays:7},{label:"\u{1F534} Stale",minDays:14},{label:"\u{1F480} Abandoned",minDays:30}];function Ht(e){let t=e?Ve(e):Ve(Gt(Wt(import.meta.url)),"..","pr-review-config.json"),n=qt(t,"utf-8");return JSON.parse(n)}function Vt(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=Ge(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 Jt(e){let t=new Set((e.teamMembers??[]).map(o=>o.toLowerCase())),n=new Set;if(e.orgManager){let o=await qe(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 Yt(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})):Z;return{enabled:!0,excludedPatterns:t,thresholds:n}}function Zt(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):He.slice().sort((n,o)=>o.minDays-n.minDays)}}function Kt(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 Je(e){let t=Ht(e),n=Vt(t),{teamMembers:o,ignoredUsers:r}=await Jt(t),u=new Set((t.botUsers??[]).map(m=>m.toLowerCase())),i=new Set((t.aiBotUsers??[]).map(m=>m.toLowerCase())),a=new Set((t.starredUsers??[]).map(m=>m.toLowerCase())),g=Yt(t),c=t.restartMergeAfterDays??30,s=Zt(t),f=t.notifications,l=t.webhook,d=Kt(t);return{repos:n,teamMembers:o,ignoredUsers:r,botUsers:u,aiBotUsers:i,starredUsers:a,quantifier:g,restartMergeAfterDays:c,staleness:s,notifications:f,webhook:l,autoNudge:d}}import{PullRequestStatus as en}from"azure-devops-node-api/interfaces/GitInterfaces.js";import{BuildResult as te,BuildStatus as ne}from"azure-devops-node-api/interfaces/BuildInterfaces.js";import{LineDiffBlockChangeType as X,VersionControlChangeType as Ye}from"azure-devops-node-api/interfaces/GitInterfaces.js";import Xt from"picomatch";function Q(e,t=Z){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 Qt(e,t){if(t.length===0)return!1;let n=e.replace(/^\//,"");return t.some(o=>o(n))}function Ze(e){let t=0,n=0;for(let o of e){let r=o.changeType??X.None;r===X.Add?t+=o.modifiedLinesCount??0:r===X.Delete?n+=o.originalLinesCount??0:r===X.Edit&&(t+=o.modifiedLinesCount??0,n+=o.originalLinesCount??0)}return{added:t,deleted:n}}async function Ke(e,t,n,o,r){let u=r.excludedPatterns.map($=>Xt($,{dot:!0})),i=await x(`Fetch iterations for PR #${o}`,()=>e.getPullRequestIterations(t,o,n));if(!i||i.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:Q(0,r.thresholds)};let a=i[i.length-1],g=a.id,c=[],s=0,f=100;for(;;){let $=await x(`Fetch iteration changes for PR #${o} iter ${g}`,()=>e.getPullRequestIterationChanges(t,o,g,n,f,s)),y=$.changeEntries??[];for(let C of y){let O=C.item?.path??"",U=C.originalPath;O&&!Qt(O,u)&&c.push({path:O,originalPath:U??void 0,changeType:C.changeType??0})}if(($.nextSkip??0)===0&&($.nextTop??0)===0||(s=$.nextSkip??s+f,y.length===0))break}if(c.length===0)return{linesAdded:0,linesDeleted:0,totalChanges:0,label:Q(0,r.thresholds)};let l=a.sourceRefCommit?.commitId,d=a.targetRefCommit?.commitId;if(!l||!d){w(` PR #${o} \u2014 no commit refs, using file count as proxy`);let $=c.length;return{linesAdded:$,linesDeleted:0,totalChanges:$,label:Q($,r.thresholds)}}let m=c.map($=>{let y=($.changeType&Ye.Add)!==0,C=($.changeType&Ye.Delete)!==0;return{originalPath:y?void 0:$.originalPath??$.path,path:C?void 0:$.path}}),h=0,p=0,S=10,R=0;for(let $=0;$<m.length;$+=S){let y=m.slice($,$+S),C={baseVersionCommit:d,targetVersionCommit:l,fileDiffParams:y};try{let O=await e.getFileDiffs(C,n,t);for(let U of O){let{added:q,deleted:G}=Ze(U.lineDiffBlocks??[]);h+=q,p+=G}}catch{for(let O of y)try{let U={baseVersionCommit:d,targetVersionCommit:l,fileDiffParams:[O]},q=await e.getFileDiffs(U,n,t);for(let G of q){let{added:xt,deleted:Et}=Ze(G.lineDiffBlocks??[]);h+=xt,p+=Et}}catch{R++}}}R>0&&w(` PR #${o} \u2014 skipped ${R} files (not found at specified version)`);let b=h+p,D=Q(b,r.thresholds);return w(` PR #${o} \u2014 +${h} -${p} = ${b} (${D})`),{linesAdded:h,linesDeleted:p,totalChanges:b,label:D}}async function ee(e,t,n){let o=new Array(e.length),r=0;async function u(){for(;r<e.length;){let a=r++;o[a]=await n(e[a])}}let i=Array.from({length:Math.min(t,e.length)},()=>u());return await Promise.all(i),o}import Xe from"picomatch";function Qe(e,t,n){if(Object.keys(n).length===0)return[];let o=t.map(i=>Xe(i,{dot:!0})),r=e.map(i=>i.replace(/^\//,"")).filter(i=>!o.some(a=>a(i)));if(r.length===0)return[];let u=[];for(let[i,a]of Object.entries(n)){let g=a.map(c=>Xe(c,{dot:!0}));r.some(c=>g.some(s=>s(c)))&&u.push(i)}return u}function tn(e){let t=[],n=0,o=0;for(let r of e){if(r.isDraft){n++,w(` #${r.pullRequestId} \u2014 draft, skipping`);continue}if((r.labels??[]).map(i=>i.name??"").some(i=>i.toUpperCase()==="NO-MERGE")){o++,w(` #${r.pullRequestId} \u2014 NO-MERGE label, skipping`);continue}t.push(r)}return n>0&&w(`Skipped ${n} draft PRs`),o>0&&w(`Skipped ${o} NO-MERGE PRs`),t}async function nn(e,t,n,o){let r=await x(`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=[],g=0,c=100;for(;;){let s=await x(`Fetch iteration changes for PR #${o} iter ${i} (file patterns)`,()=>e.getPullRequestIterationChanges(t,o,i,n,c,g));for(let f of s.changeEntries??[]){let l=f.item?.path??"";l&&a.push(l)}if((s.nextSkip??0)===0&&(s.nextTop??0)===0||(g=s.nextSkip??g+c,(s.changeEntries??[]).length===0))break}return a}function on(e,t){if(e===ne.InProgress)return"inProgress";if(e===ne.NotStarted)return"notStarted";switch(t){case te.Succeeded:return"succeeded";case te.Failed:return"failed";case te.PartiallySucceeded:return"partiallySucceeded";case te.Canceled:return"canceled";default:return"none"}}async function rn(e,t,n,o){try{let r=`refs/pull/${o}/merge`,u=await x(`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(!u||u.length===0)return;let i=new Map;for(let l of u){let d=l.definition?.id??0;i.has(d)||i.set(d,l)}let a=[],g=0,c=0,s=0,f=0;for(let l of i.values()){let d=on(l.status,l.result);switch(a.push({id:l.id??0,name:l.definition?.name??"Unknown",status:ne[l.status??ne.None]??"None",result:d}),d){case"succeeded":g++;break;case"failed":c++;break;case"inProgress":case"notStarted":s++;break;default:f++;break}}return{total:a.length,succeeded:g,failed:c,inProgress:s,other:f,runs:a}}catch(r){let u=r instanceof Error?r.message:String(r);w(` #${o} \u2014 failed to fetch pipeline status: ${u}`);return}}async function et(e,t,n,o,r,u={ignore:[],labels:{}},i){let a;try{let l=new URL(o);l.hostname.endsWith(".visualstudio.com")?a=`https://dev.azure.com/${l.hostname.replace(".visualstudio.com","")}`:a=o.replace(/\/$/,"")}catch{a=o.replace(/\/$/,"")}let g=await x("Fetch pull requests",()=>e.getPullRequests(t,{status:en.Active},n));w(`API returned ${g.length} active pull requests`);let c;if(i)try{c=(await x("Resolve repository GUID",()=>e.getRepository(t,n))).id}catch(l){let d=l instanceof Error?l.message:String(l);w(`Failed to resolve repository GUID for ${t}: ${d}`)}let s=tn(g);A(`Fetching threads for ${s.length} PRs in ${n}/${t} (concurrency: ${10})\u2026`);let f=await ee(s,10,async l=>{let d=l.pullRequestId;w(` #${d} \u2014 fetching threads\u2026`);let h=(await x(`Fetch threads for PR #${d}`,()=>e.getThreads(t,d,n))).map($=>({id:$.id??0,publishedDate:new Date($.publishedDate??0),comments:($.comments??[]).filter(y=>!y.isDeleted).map(y=>({authorUniqueName:K(y.author),publishedDate:new Date(y.publishedDate??0)}))})),p=(l.reviewers??[]).map($=>({displayName:$.displayName??"",uniqueName:K($),vote:$.vote??0})),S=`${a}/${n}/_git/${t}/pullrequest/${d}`,R=(l.labels??[]).map($=>$.name??""),b=[];if(Object.keys(u.labels).length>0){let $=await nn(e,t,n,d);b=Qe($,u.ignore,u.labels),b.length>0&&w(` #${d} \u2014 detected labels: ${b.join(", ")}`)}let D=i&&c?await rn(i,c,n,d):void 0;return{id:d,title:l.title??"(no title)",author:l.createdBy?.displayName??"Unknown",authorUniqueName:K(l.createdBy),url:S,createdDate:new Date(l.creationDate??0),reviewers:p,threads:h,labels:R,detectedLabels:b,mergeStatus:l.mergeStatus??0,lastSourcePushDate:l.lastMergeSourceCommit?.committer?.date?new Date(l.lastMergeSourceCommit.committer.date):void 0,size:r?await Ke(e,t,n,d,r):void 0,description:l.description??void 0,sourceBranch:l.sourceRefName??void 0,targetBranch:l.targetRefName??void 0,pipelineStatus:D}});return w(`${f.length} PRs remain after filtering`),f}async function tt(e,t,n,o){for(let r of o)if(r.detectedLabels.length!==0){for(let u of r.detectedLabels)if(!r.labels.some(i=>i.toLowerCase()===u.toLowerCase()))try{await x(`Add label '${u}' to PR #${r.id}`,()=>e.createPullRequestLabel({name:u},t,r.id,n)),r.labels.push(u),w(` #${r.id} \u2014 added label '${u}'`)}catch(i){let a=i instanceof Error?i.message:String(i);w(` #${r.id} \u2014 failed to add label '${u}': ${a}`)}}}var sn=1440*60*1e3,an=["TF401398","TF401027"],cn=["TF401027"];async function nt(e,t,n,o,r,u=new Date){if(r<0)return w("Restart merge is disabled (restartMergeAfterDays < 0)"),{restarted:0,failed:0,restartedPrIds:[]};let i=new Date(u.getTime()-r*sn),a=o.filter(f=>f.createdDate<i);if(a.length===0)return w("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 g=0,c=0,s=[];for(let f of a)try{await x(`Restart merge for PR #${f.id}`,async()=>{try{return await e.updatePullRequest({mergeStatus:1},t,f.id,n)}catch(l){let d=l instanceof Error?l.message:String(l);throw an.some(m=>d.includes(m))?new F(d):l}}),w(` #${f.id} "${f.title}" \u2014 merge restarted`),g++,s.push(f.id)}catch(l){let d=l instanceof Error?l.message:String(l);if(T(` #${f.id} "${f.title}" \u2014 failed to restart merge: ${d}`),c++,cn.some(m=>d.includes(m))){T("Stopping restart-merge for this repository due to permission error"),c+=a.length-a.indexOf(f)-1;break}}return P(`Restarted merge for ${g}/${a.length} PR(s)`),{restarted:g,failed:c,restartedPrIds:s}}import{PullRequestAsyncStatus as $e}from"azure-devops-node-api/interfaces/GitInterfaces.js";var ln=["build","[bot]","team foundation","microsoft.visualstudio.com"],un=["dependabot[bot]","renovate[bot]","github-actions[bot]","snyk-bot","greenkeeper[bot]","depfu[bot]","imgbot[bot]","allcontributors[bot]"],ot=["github copilot","copilot[bot]","claude","codex"];function ye(e,t=new Set,n){let o=e.toLowerCase();return!!(t.has(o)||ln.some(r=>o.includes(r))||n&&t.has(n.toLowerCase()))}function oe(e,t=new Set,n){let o=e.toLowerCase();if(t.has(o)||ot.some(r=>o.includes(r)))return!0;if(n){let r=n.toLowerCase();if(t.has(r)||ot.some(u=>r.includes(u)))return!0}return!1}function re(e,t=new Set,n,o=new Set){let r=e.toLowerCase();return un.some(u=>r.includes(u))||ye(r,t,n)||oe(r,o,n)}function dn(e,t=new Set,n){return oe(e,t,n)}function we(e,t,n=new Set,o,r=new Set){if(dn(t,r,o))switch(e){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}if(re(t,n,o,r))return"APPROVE";switch(e){case"approved":return"APPROVE";case"needingReview":return"REVIEW";case"waitingOnAuthor":return"PENDING"}}function Re(e,t=new Set,n=new Set){let o=e.authorUniqueName,r=[];for(let u of e.threads)for(let i of u.comments)ye(i.authorUniqueName,t)||oe(i.authorUniqueName,n)||r.push({date:i.publishedDate,isAuthor:i.authorUniqueName===o});return e.lastSourcePushDate&&r.push({date:e.lastSourcePushDate,isAuthor:!0}),r}function rt(e,t=new Set,n,o=new Set,r=new Set,u=new Set,i=new Set){let a=[],g=[],c=[];for(let s of e){if(o.has(s.authorUniqueName)){w(` #${s.id} "${s.title}" \u2014 author ${s.authorUniqueName} is ignored, skipping`);continue}let f=t.size===0||t.has(s.authorUniqueName),l=i.has(s.authorUniqueName);if(s.reviewers.some(y=>y.vote>=5&&!ye(y.uniqueName,r,y.displayName)&&!oe(y.uniqueName,u,y.displayName))){let y=s.mergeStatus===$e.Conflicts;w(` #${s.id} "${s.title}" \u2014 approved`),a.push({id:s.id,title:s.title,author:s.author,url:s.url,createdDate:s.createdDate,hasMergeConflict:y,isTeamMember:f,isStarred:l,action:we("approved",s.authorUniqueName,r,s.author,u),repository:n,size:s.size,detectedLabels:s.detectedLabels.length>0?s.detectedLabels:void 0,pipelineStatus:s.pipelineStatus});continue}let m=Re(s,r,u),h=m.filter(y=>y.isAuthor).sort((y,C)=>y.date.getTime()-C.date.getTime()),p=m.filter(y=>!y.isAuthor).sort((y,C)=>y.date.getTime()-C.date.getTime()),S=h.length>0?h[h.length-1]:null,R=p.length>0?p[p.length-1]:null,b=!1;if(S&&R?b=S.date.getTime()>R.date.getTime():(S&&!R||!S&&!R)&&(b=!0),!b){let y=s.mergeStatus===$e.Conflicts;w(` #${s.id} "${s.title}" \u2014 reviewer acted last`),c.push({id:s.id,title:s.title,author:s.author,url:s.url,lastReviewerActivityDate:R.date,hasMergeConflict:y,isTeamMember:f,isStarred:l,action:we("waitingOnAuthor",s.authorUniqueName,r,s.author,u),repository:n,size:s.size,detectedLabels:s.detectedLabels.length>0?s.detectedLabels:void 0,pipelineStatus:s.pipelineStatus});continue}let D;R?D=h.find(C=>C.date.getTime()>R.date.getTime())?.date??s.createdDate:D=s.createdDate;let $=s.mergeStatus===$e.Conflicts;w(` #${s.id} "${s.title}" \u2014 needs review (waiting since ${D.toISOString()}${$?", has conflicts":""})`),g.push({id:s.id,title:s.title,author:s.author,url:s.url,waitingSince:D,hasMergeConflict:$,isTeamMember:f,isStarred:l,action:we("needingReview",s.authorUniqueName,r,s.author,u),repository:n,size:s.size,detectedLabels:s.detectedLabels.length>0?s.detectedLabels:void 0,reviewerNames:s.reviewers.map(y=>y.displayName),pipelineStatus:s.pipelineStatus})}return w(`${a.length} approved PRs`),w(`${c.length} PRs waiting on author`),a.sort((s,f)=>s.createdDate.getTime()-f.createdDate.getTime()),g.sort((s,f)=>s.waitingSince.getTime()-f.waitingSince.getTime()),c.sort((s,f)=>s.lastReviewerActivityDate.getTime()-f.lastReviewerActivityDate.getTime()),{approved:a,needingReview:g,waitingOnAuthor:c}}function st(e){let t=e.flatMap(r=>r.approved),n=e.flatMap(r=>r.needingReview),o=e.flatMap(r=>r.waitingOnAuthor);return t.sort((r,u)=>r.createdDate.getTime()-u.createdDate.getTime()),n.sort((r,u)=>r.waitingSince.getTime()-u.waitingSince.getTime()),o.sort((r,u)=>r.lastReviewerActivityDate.getTime()-u.lastReviewerActivityDate.getTime()),{approved:t,needingReview:n,waitingOnAuthor:o}}function N(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 u of t)if(r>=u.minDays)return u.label;return null}function se(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)),u=Math.floor(n/(1e3*60)),i;return o>3?i="high":o>1?i="medium":i="low",{days:o,hours:r,minutes:u,urgency:i}}function ie(e){return e==="XS"||e==="S"?"low":e==="M"?"medium":"high"}function ae(e,t){let{approved:n,needingReview:o,waitingOnAuthor:r}=e,u=n.length+o.length+r.length,i=`Total: ${u} open PR${u===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 V(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 it(e,t=new Date){let n=se(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 at(e){let t=ie(e.label);return`${t==="low"?"\u{1F7E2}":t==="medium"?"\u{1F7E1}":"\u{1F534}"} ${e.label}`}function ct(e){switch(e){case"APPROVE":return"\u{1F7E2} APPROVE";case"REVIEW":return"\u{1F50D} REVIEW";case"PENDING":return"\u23F3 PENDING"}}function lt(e){return!e||e.length===0?"":" "+e.map(t=>`\`${k(t)}\``).join(" ")}function k(e){return e.replace(/[\r\n]+/g," ").trim().replace(/\[/g,"\\[").replace(/\]/g,"\\]").replace(/\|/g,"\\|")}function ut(e,t){return t?`\u2B50 ${e}`:e}function ve(e,t,n,o,r=!1){if(e.length===0)return`_${n}_
|
|
6
|
+
|
|
7
|
+
`;let u=e.some(s=>s.size!=null),i=e.some(s=>s.stalenessBadge),a=e.some(s=>s.pipelineStatus!=null);if(r){let s=["PR","Repository","Author","Action"];u&&s.push("Size"),a&&s.push("Pipelines"),i&&s.push("Staleness"),s.push(t);let f=`| ${s.join(" | ")} |
|
|
8
|
+
|${s.map(()=>"---").join("|")}|
|
|
9
|
+
`;for(let l of e){let d=l.hasMergeConflict?" \u274C":"",m=k(l.title),h=k(ut(l.author,l.isStarred)),p=k(l.repository??"Unknown"),S=lt(l.detectedLabels),R=`[#${l.id} - ${m}](${l.url})${d}${S}`,b=it(l.dateColumn,o),D=ct(l.action),$=u?` ${l.size?at(l.size):""} |`:"",y=a?` ${V(l.pipelineStatus)} |`:"",C=i?` ${l.stalenessBadge??""} |`:"";f+=`| ${R} | ${p} | ${h} | ${D} |${$}${y}${C} ${b} |
|
|
10
|
+
`}return f+`
|
|
11
|
+
`}let g=["PR","Author","Action"];u&&g.push("Size"),a&&g.push("Pipelines"),i&&g.push("Staleness"),g.push(t);let c=`| ${g.join(" | ")} |
|
|
12
|
+
|${g.map(()=>"---").join("|")}|
|
|
13
|
+
`;for(let s of e){let f=s.hasMergeConflict?" \u274C":"",l=k(s.title),d=k(ut(s.author,s.isStarred)),m=lt(s.detectedLabels),h=`[#${s.id} - ${l}](${s.url})${f}${m}`,p=it(s.dateColumn,o),S=ct(s.action),R=u?` ${s.size?at(s.size):""} |`:"",b=a?` ${V(s.pipelineStatus)} |`:"",D=i?` ${s.stalenessBadge??""} |`:"";c+=`| ${h} | ${d} | ${S} |${R}${b}${D} ${p} |
|
|
14
|
+
`}return c+`
|
|
15
|
+
`}function gn(e){return e.every(n=>n.isTeamMember)?{team:e,community:[]}:{team:e.filter(n=>n.isTeamMember),community:e.filter(n=>!n.isTeamMember)}}function be(e,t,n,o,r,u,i=!1){let a=`## ${e}
|
|
16
|
+
|
|
17
|
+
`,{team:g,community:c}=gn(t);return c.length>0?(a+=`### Team PRs
|
|
18
|
+
|
|
19
|
+
`,a+=ve(g.map(n),o,"No team PRs.",u,i),a+=`### Community Contributions
|
|
20
|
+
|
|
21
|
+
`,a+=ve(c.map(n),o,"No community PRs.",u,i)):a+=ve(t.map(n),o,r,u,i),a}function fn(e){let t=`## \u{1F4CA} Statistics per Repository
|
|
22
|
+
|
|
23
|
+
`;t+=`| Repository | Open PRs | \u2705 Approved | \u{1F440} Needs Review | \u270D\uFE0F Waiting on Author | \u274C Conflicts | \u{1F504} Merge Restarted |
|
|
24
|
+
`,t+=`|---|---|---|---|---|---|---|
|
|
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+=`| ${k(n.repoLabel)} | ${o} | ${n.approved} | ${n.needingReview} | ${n.waitingOnAuthor} | ${n.conflicts} | ${r} |
|
|
26
|
+
`}return t+`
|
|
27
|
+
`}function J(e){return e<1?"< 1 day":`${e} day${e===1?"":"s"}`}function mn(e){let t=`## \u{1F4C8} Review Metrics
|
|
28
|
+
|
|
29
|
+
`;if(t+=`### Summary
|
|
30
|
+
|
|
31
|
+
`,t+=`| Metric | Value |
|
|
57
32
|
|---|---|
|
|
58
|
-
`,
|
|
59
|
-
`,
|
|
60
|
-
`,
|
|
61
|
-
`,
|
|
62
|
-
`,
|
|
33
|
+
`,t+=`| Total open PRs | ${e.aggregate.totalPrs} |
|
|
34
|
+
`,t+=`| Median PR age | ${J(e.aggregate.medianAgeInDays)} |
|
|
35
|
+
`,t+=`| Avg time to first review | ${e.aggregate.avgTimeToFirstReviewInDays!==null?J(e.aggregate.avgTimeToFirstReviewInDays):"N/A"} |
|
|
36
|
+
`,t+=`| Avg review rounds | ${e.aggregate.avgReviewRounds} |
|
|
37
|
+
`,t+=`| PRs with no review activity | ${e.aggregate.prsWithNoReviewActivity} |
|
|
63
38
|
|
|
64
|
-
`,
|
|
39
|
+
`,e.perAuthor.length>0){t+=`### Per-Author Summary
|
|
65
40
|
|
|
66
|
-
`,
|
|
41
|
+
`,t+=`| Author | Open PRs | Avg Age | Avg Rounds | Fastest Review |
|
|
67
42
|
|---|---|---|---|---|
|
|
68
|
-
`;for(let
|
|
69
|
-
`}
|
|
70
|
-
`}return
|
|
43
|
+
`;for(let n of e.perAuthor){let o=n.fastestReviewInDays!==null?J(n.fastestReviewInDays):"N/A",r=n.isStarred?`\u2B50 ${k(n.author)}`:k(n.author);t+=`| ${r} | ${n.openPrCount} | ${J(n.avgAgeInDays)} | ${n.avgReviewRounds} | ${o} |
|
|
44
|
+
`}t+=`
|
|
45
|
+
`}return t}function pn(e){if(e.length===0)return"";let t=`## \u{1F465} Reviewer Workload
|
|
71
46
|
|
|
72
|
-
`;
|
|
47
|
+
`;t+=`| Reviewer | Assigned | Pending | Completed | Avg Response | Load |
|
|
73
48
|
|---|---|---|---|---|---|
|
|
74
|
-
`;for(let
|
|
75
|
-
`}return
|
|
76
|
-
`}function
|
|
77
|
-
|
|
78
|
-
`;if(
|
|
79
|
-
|
|
80
|
-
`,
|
|
81
|
-
`,
|
|
82
|
-
`;for(let
|
|
83
|
-
`}
|
|
84
|
-
`}if(
|
|
85
|
-
|
|
86
|
-
`,
|
|
87
|
-
`,
|
|
88
|
-
`;for(let
|
|
89
|
-
`);
|
|
90
|
-
`}return
|
|
91
|
-
|
|
92
|
-
`;return
|
|
93
|
-
`,
|
|
94
|
-
`,
|
|
95
|
-
`,
|
|
96
|
-
`,
|
|
97
|
-
`,
|
|
98
|
-
`,
|
|
99
|
-
`,
|
|
100
|
-
|
|
101
|
-
`;return
|
|
102
|
-
`,
|
|
103
|
-
${
|
|
104
|
-
`),
|
|
105
|
-
`),
|
|
106
|
-
`);if(
|
|
107
|
-
`)}function
|
|
108
|
-
${
|
|
109
|
-
`),
|
|
110
|
-
`)}function
|
|
111
|
-
${
|
|
112
|
-
`);let
|
|
113
|
-
`)}function
|
|
114
|
-
`)}function
|
|
115
|
-
`)}function
|
|
116
|
-
`):(
|
|
117
|
-
|
|
49
|
+
`;for(let n of e){let o=n.avgResponseTimeInDays!==null?J(n.avgResponseTimeInDays):"N/A",r=n.isStarred?`\u2B50 ${k(n.displayName)}`:k(n.displayName);t+=`| ${r} | ${n.assignedPrCount} | ${n.pendingReviewCount} | ${n.completedReviewCount} | ${o} | ${n.loadIndicator} |
|
|
50
|
+
`}return t+`
|
|
51
|
+
`}function hn(e){let t=`## \u{1F517} PR Dependencies
|
|
52
|
+
|
|
53
|
+
`;if(e.chains.length>0){t+=`### Dependency Chains
|
|
54
|
+
|
|
55
|
+
`,t+=`| Chain | PRs | Status |
|
|
56
|
+
`,t+=`|-------|-----|--------|
|
|
57
|
+
`;for(let n of e.chains){let o=n.prIds.map(u=>`#${u}`).join(" \u2192 "),r=n.status==="blocked"?`\u26A0\uFE0F ${n.blockerDescription??"Blocked"}`:"\u2705 Ready to merge in order";t+=`| ${n.chainId} | ${o} | ${r} |
|
|
58
|
+
`}t+=`
|
|
59
|
+
`}if(e.blockedPrIds.length>0){t+=`### Blocked PRs
|
|
60
|
+
|
|
61
|
+
`,t+=`| PR | Blocked By | Reason |
|
|
62
|
+
`,t+=`|----|-----------|--------|
|
|
63
|
+
`;for(let n of e.dependencies)e.blockedPrIds.includes(n.fromPrId)&&(t+=`| #${n.fromPrId} | #${n.toPrId} | ${n.details} |
|
|
64
|
+
`);t+=`
|
|
65
|
+
`}return t}function ce(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 le(e){return e===null?"\u2014":e===0?"\u2192 stable":e>0?`\u2197\uFE0F +${e}`:`\u2198\uFE0F ${e}`}function $n(e){let t=e.current,n=`## \u{1F4C8} DORA Metrics
|
|
66
|
+
|
|
67
|
+
`;return n+=`| Metric | Value | Rating | Trend |
|
|
68
|
+
`,n+=`|--------|-------|--------|-------|
|
|
69
|
+
`,n+=`| Change Lead Time | ${t.changeLeadTime.medianDays} days | ${ce(t.changeLeadTime.rating)} | ${le(e.deltas.changeLeadTime)} |
|
|
70
|
+
`,n+=`| Deployment Frequency | ${t.deploymentFrequency.perWeek}/week | ${ce(t.deploymentFrequency.rating)} | ${le(e.deltas.deploymentFrequency)} |
|
|
71
|
+
`,n+=`| Change Failure Rate | ${t.changeFailureRate.percentage}% | ${ce(t.changeFailureRate.rating)} | ${le(e.deltas.changeFailureRate)} |
|
|
72
|
+
`,n+=`| Mean Time to Restore | ${t.meanTimeToRestore.medianHours}h | ${ce(t.meanTimeToRestore.rating)} | ${le(e.deltas.meanTimeToRestore)} |
|
|
73
|
+
`,n+=`
|
|
74
|
+
`,n}function dt(e){let{analysis:t,multiRepo:n,stats:o,staleness:r,metrics:u,workload:i,dependencyGraph:a,doraTrend:g}=e,c=new Date,{approved:s,needingReview:f,waitingOnAuthor:l}=t,d=r?.enabled!==!1?r?.thresholds??[]:[],m=`_Last updated: ${c.toISOString()}_
|
|
75
|
+
|
|
76
|
+
`;return m+=be("\u2705 Approved",s,h=>({...h,dateColumn:h.createdDate,stalenessBadge:N(h.createdDate,d,c)}),"Created","No approved PRs.",c,n),m+=be("\u{1F440} PRs Needing Review",f,h=>({...h,dateColumn:h.waitingSince,stalenessBadge:N(h.waitingSince,d,c)}),"Waiting for feedback","No PRs currently need review.",c,n),m+=be("\u270D\uFE0F Waiting on Author",l,h=>({...h,dateColumn:h.lastReviewerActivityDate,stalenessBadge:N(h.lastReviewerActivityDate,d,c)}),"Last reviewer activity","No PRs waiting on author.",c,n),o?.repoStats&&o.repoStats.length>1&&(m+=fn(o.repoStats)),u&&(m+=mn(u)),i&&i.length>0&&(m+=pn(i)),a&&a.dependencies.length>0&&(m+=hn(a)),g&&(m+=$n(g)),m+=`_${ae(t,o)}._
|
|
77
|
+
`,m}var v="\x1B[0m",M="\x1B[2m",E="\x1B[1m",ue="\x1B[31m",de="\x1B[32m",ge="\x1B[33m",j="\x1B[36m",wn="\x1B[37m",ft="\x1B[41m",yn="\x1B[42m",mt="\x1B[43m";function Rn(e,t){return`\x1B]8;;${t}\x1B\\${e}\x1B]8;;\x1B\\`}function vn(e,t){let n=se(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:ue,label:o}:n.urgency==="medium"?{color:ge,label:o}:{color:de,label:o}}function Te(e,t){return`${e}${E} ${t} ${v}`}function bn(e){return e?` ${ue}\u26A0 conflict${v}`:""}function Sn(e){if(!e)return"";let t=ie(e.label);return` ${t==="low"?de:t==="medium"?ge:ue}${E}${e.label}${v}`}function An(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 Ae(e,t){return e.length>t?e.slice(0,t-1)+"\u2026":e}function Tn(e){switch(e){case"APPROVE":return`${de}${E}APPROVE${v}`;case"REVIEW":return`${ge}${E}REVIEW${v}`;case"PENDING":return`${M}PENDING${v}`}}function gt(e,t,n,o,r,u,i,a,g,c,s,f,l){let{color:d,label:m}=vn(r,i),h=`${d}${E}${m}${v}`,p=`${M}#${e}${v}`,S=Ae(t.replace(/[\r\n]+/g," ").trim(),60),R=Rn(`${p} ${wn}${S}${v}`,o),D=`${M}${l?"\u2B50 ":""}${Ae(n,20)}${v}`,$=bn(u),y=Sn(g),C=Tn(a),O=c&&c.length>0?" "+c.map(G=>`${M}[${G}]${v}`).join(" "):"",U=s?` ${ge}${s}${v}`:"",q=f?` ${V(f)}`:"";return` ${h} ${An(R,80)} ${D}${y} ${C}${$}${q}${U}${O}`}function Se(e,t,n,o,r,u){let i=[];if(i.push(`
|
|
78
|
+
${Te(t,`${e} (${n.length})`)}
|
|
79
|
+
`),n.length===0)return i.push(` ${M}No PRs${v}
|
|
80
|
+
`),i.join(`
|
|
81
|
+
`);if(u){let a=new Map;for(let g of n){let c=u(g)??"Unknown";a.has(c)||a.set(c,[]),a.get(c).push(g)}for(let[g,c]of a){i.push(` ${E}${j}\u{1F4C2} ${g}${v}`);for(let s of c){let{id:f,title:l,author:d,url:m,date:h,hasMergeConflict:p,action:S,size:R,detectedLabels:b,stalenessBadge:D,pipelineStatus:$,isStarred:y}=o(s);i.push(gt(f,l,d,m,h,p,r,S,R,b,D,$,y))}i.push("")}}else{for(let a of n){let{id:g,title:c,author:s,url:f,date:l,hasMergeConflict:d,action:m,size:h,detectedLabels:p,stalenessBadge:S,pipelineStatus:R,isStarred:b}=o(a);i.push(gt(g,c,s,f,l,d,r,m,h,p,S,R,b))}i.push("")}return i.join(`
|
|
82
|
+
`)}function Dn(e){let t=[];return t.push(`
|
|
83
|
+
${Te(mt,"\u{1F4C8} Review Metrics")}
|
|
84
|
+
`),t.push(` ${M}Median PR age:${v} ${E}${e.aggregate.medianAgeInDays}d${v} ${M}Avg first review:${v} ${E}${e.aggregate.avgTimeToFirstReviewInDays??"N/A"}d${v} ${M}Avg rounds:${v} ${E}${e.aggregate.avgReviewRounds}${v} ${M}No review:${v} ${E}${e.aggregate.prsWithNoReviewActivity}${v}`),t.push(""),t.join(`
|
|
85
|
+
`)}function Pn(e){if(e.length===0)return"";let t=[];t.push(`
|
|
86
|
+
${Te(ft,"\u{1F465} Top Reviewer Bottlenecks")}
|
|
87
|
+
`);let n=e.slice(0,5);for(let o of n){let r=o.avgResponseTimeInDays!==null?`${o.avgResponseTimeInDays}d avg`:"no response",u=o.isStarred?"\u2B50 ":"";t.push(` ${o.loadIndicator} ${E}${u}${o.displayName}${v} \u2014 ${o.pendingReviewCount} pending / ${o.assignedPrCount} assigned (${r})`)}return t.push(""),t.join(`
|
|
88
|
+
`)}function pt(e){let{analysis:t,repoLabel:n,multiRepo:o=!1,stats:r,staleness:u,metrics:i,workload:a,dependencyGraph:g}=e,c=new Date,{approved:s,needingReview:f,waitingOnAuthor:l}=t,d=u?.enabled!==!1?u?.thresholds??[]:[],m=[];m.push(""),m.push(`${E}${j} \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}`),m.push(`${E}${j} \u2502 \u{1F4CB} PR Review Dashboard \u2502${v}`),m.push(`${E}${j} \u2502 ${v}${M}${Ae(n,42)}${v}${E}${j}${" ".repeat(Math.max(0,42-n.length))}\u2502${v}`),m.push(`${E}${j} \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}`),m.push("");let h=o?p=>p.repository:void 0;return m.push(Se("\u2705 Approved",yn,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:N(p.createdDate,d,c),pipelineStatus:p.pipelineStatus,isStarred:p.isStarred}),c,h)),m.push(Se("\u{1F440} Needing Review",mt,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:N(p.waitingSince,d,c),pipelineStatus:p.pipelineStatus,isStarred:p.isStarred}),c,h)),m.push(Se("\u270D\uFE0F Waiting on Author",ft,l,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:N(p.lastReviewerActivityDate,d,c),pipelineStatus:p.pipelineStatus,isStarred:p.isStarred}),c,h)),i&&m.push(Dn(i)),a&&a.length>0&&m.push(Pn(a)),g&&g.dependencies.length>0&&m.push(Cn(g)),m.push(` ${M}${ae(t,r)}${v}`),m.push(` ${M}Updated: ${c.toLocaleString()}${v}`),m.push(""),m.join(`
|
|
89
|
+
`)}function Cn(e){let t=[];t.push(` ${E}\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(u=>`#${u}`).join(" \u2192 "),r=n.status==="blocked"?`${ue}\u26A0\uFE0F blocked${v}`:`${de}\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 ht(e,t){return Math.abs(t.getTime()-e.getTime())/(1e3*60*60*24)}function xn(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 En(e,t,n){let o=Re(e,t),r=o.filter(f=>!f.isAuthor).sort((f,l)=>f.date.getTime()-l.date.getTime()),u=ht(e.createdDate,n),i=r.length>0?ht(e.createdDate,r[0].date):null,a=o.sort((f,l)=>f.date.getTime()-l.date.getTime()),g=0,c=!1;for(let f of a)f.isAuthor?c=!0:c&&(g++,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(u*10)/10,timeToFirstReviewInDays:i!==null?Math.round(i*10)/10:null,reviewRounds:g,lastActivityDate:s}}function De(e,t=new Set,n=new Date,o=new Set){let r=e.map(d=>En(d,t,n)),u=r.map(d=>d.ageInDays),i=r.map(d=>d.timeToFirstReviewInDays).filter(d=>d!==null),a=r.map(d=>d.reviewRounds),g=r.filter(d=>d.timeToFirstReviewInDays===null).length,c={medianAgeInDays:Math.round(xn(u)*10)/10,avgTimeToFirstReviewInDays:i.length>0?Math.round(i.reduce((d,m)=>d+m,0)/i.length*10)/10:null,avgReviewRounds:a.length>0?Math.round(a.reduce((d,m)=>d+m,0)/a.length*10)/10:0,prsWithNoReviewActivity:g,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 l=[];for(let[d,m]of s){let h=m.map(R=>R.ageInDays),p=m.map(R=>R.reviewRounds),S=m.map(R=>R.timeToFirstReviewInDays).filter(R=>R!==null);l.push({author:d,isStarred:o.has(f.get(d)??""),openPrCount:m.length,avgAgeInDays:Math.round(h.reduce((R,b)=>R+b,0)/h.length*10)/10,avgReviewRounds:Math.round(p.reduce((R,b)=>R+b,0)/p.length*10)/10,fastestReviewInDays:S.length>0?Math.round(Math.min(...S)*10)/10:null})}return l.sort((d,m)=>m.openPrCount-d.openPrCount),{perPr:r,aggregate:c,perAuthor:l}}var Nn={light:{maxPending:10,maxAvgResponseDays:2},medium:{maxPending:20,maxAvgResponseDays:4}};function Mn(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 Pe(e,t,n=new Set,o=Nn,r=new Set,u=new Set){let i=new Set(t.needingReview.map(c=>c.id)),a=new Map;for(let c of e)if(!re(c.authorUniqueName,n,c.author,r))for(let s of c.reviewers){if(re(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 l=a.get(f);l.assignedPrCount++,s.vote>=5?l.completedReviewCount++:i.has(c.id)&&l.pendingReviewCount++;let d=c.threads.flatMap(m=>m.comments).filter(m=>m.authorUniqueName.toLowerCase()===f).sort((m,h)=>m.publishedDate.getTime()-h.publishedDate.getTime());if(d.length>0){let m=(d[0].publishedDate.getTime()-c.createdDate.getTime())/864e5;m>=0&&l.responseTimes.push(m)}}let g=[];for(let[c,s]of a){let f=s.responseTimes.length>0?Math.round(s.responseTimes.reduce((l,d)=>l+d,0)/s.responseTimes.length*10)/10:null;g.push({reviewer:c,displayName:s.displayName,isStarred:u.has(c),assignedPrCount:s.assignedPrCount,pendingReviewCount:s.pendingReviewCount,completedReviewCount:s.completedReviewCount,avgResponseTimeInDays:f,loadIndicator:Mn(s.pendingReviewCount,f,o)})}return g.sort((c,s)=>s.pendingReviewCount-c.pendingReviewCount),g}function $t(e,t){let n=t?` ${t}`:"",o=e.isStarred?"\u2B50 ":"";return`[#${e.id} - ${e.title}](${e.url}) \u2014 ${o}${e.author}${n}`}function Ln(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=g=>!o||o.includes(g);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 g=e.needingReview.slice(0,15);for(let c of g){let s=N(c.waitingSince,r);i.push({type:"TextBlock",text:$t(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 g=e.waitingOnAuthor.slice(0,10);for(let c of g){let s=N(c.lastReviewerActivityDate,r);i.push({type:"TextBlock",text:$t(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 wt(e,t,n,o){let r=Ln(e,t,o,n.filters?.sections);try{let u=await fetch(n.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});u.ok?P("Teams notification sent successfully"):T(`Teams notification failed: ${u.status} ${u.statusText}`)}catch(u){let i=u instanceof Error?u.message:String(u);T(`Teams notification failed: ${i}`)}}async function Ce(e,t,n,o){if(!n.teams?.webhookUrl){w("No notification webhooks configured, skipping");return}A("Sending Teams notification\u2026"),await wt(e,t,n.teams,o)}import{writeFileSync as kn}from"node:fs";function yt(e,t,n){let o=e.reduce((c,s)=>c+s.analysis.approved.length+s.analysis.needingReview.length+s.analysis.waitingOnAuthor.length,0),r,u=e.filter(c=>c.metrics).map(c=>c.metrics);if(u.length>0){let c=u.map(s=>s.aggregate);r={medianAgeInDays:c.reduce((s,f)=>s+f.medianAgeInDays,0)/c.length,avgTimeToFirstReviewInDays:On(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 g={totalPrs:o,metrics:r,staleness:i};return{generatedAt:(n??new Date).toISOString(),version:t,repositories:e,aggregate:g}}async function Rt(e,t){let n=JSON.stringify(e,null,2);t==="-"?process.stdout.write(n+`
|
|
91
|
+
`):(kn(t,n,"utf-8"),P(`JSON report written to ${t}`))}async function vt(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?P(`Webhook ${n} to ${t.url} succeeded (${o.status})`):T(`Webhook ${n} to ${t.url} returned ${o.status}: ${o.statusText}`)}catch(o){let r=o instanceof Error?o.message:String(o);T(`Webhook ${n} to ${t.url} failed: ${r}`)}}function On(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 In,writeFileSync as Fn,existsSync as Un}from"node:fs";import{resolve as bt}from"node:path";function _n(e){let t=bt(e);if(!Un(t))return{entries:[]};try{let n=In(t,"utf-8"),o=JSON.parse(n);return o&&Array.isArray(o.entries)?o:{entries:[]}}catch{return T(`Failed to parse nudge history at ${t}, starting fresh`),{entries:[]}}}function Bn(e,t){let n=bt(e);Fn(n,JSON.stringify(t,null,2),"utf-8")}function zn(e,t,n,o,r=new Date){let u=new Map;for(let g of o.entries)u.set(`${g.repoUrl}:${g.prId}`,g);let i=t.thresholds,a=-1;return n.minStalenessLevel&&(a=i.findIndex(g=>g.label===n.minStalenessLevel)),e.filter(g=>{let c=N(g.waitingSince,i,r);if(!c)return!1;if(a>=0){let l=i.findIndex(d=>d.label===c);if(l<0||l>a)return!1}let s=`${g.url}:${g.id}`,f=u.get(s);if(f){let l=new Date(f.lastNudgedAt);if((r.getTime()-l.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 qn(e){let t=e.match(/^(https:\/\/dev\.azure\.com\/[^/]+)\/([^/]+)\/_git\/([^/]+)/);return t?{orgUrl:t[1],project:t[2],repoName:t[3]}:null}async function Gn(e,t,n,o,r){let u=`${e}/${t}/_apis/git/repositories/${n}/pullRequests/${o}/threads?api-version=7.1`,a=await fetch(u,{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 St(e,t,n,o=new Date){let r=_n(n.historyFile),u=zn(e.needingReview,t,n,r,o),i={nudged:0,skipped:0,errors:0};if(u.length===0)return A("Auto-nudge: no PRs eligible for nudging"),i;A(`Auto-nudge: ${u.length} PR(s) eligible for nudging`);for(let a of u){let g=o.getTime()-a.waitingSince.getTime(),c=Math.floor(g/(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=qn(a.url);if(!f){T(` #${a.id} \u2014 could not parse ADO URL: ${a.url}`),i.errors++;continue}try{await Gn(f.orgUrl,f.project,f.repoName,a.id,s),P(` #${a.id} "${a.title}" \u2014 nudged (${c} days)`),i.nudged++;let l=r.entries.findIndex(m=>m.repoUrl===a.url&&m.prId===a.id),d={prId:a.id,repoUrl:a.url,lastNudgedAt:o.toISOString(),nudgeCount:l>=0?r.entries[l].nudgeCount+1:1};l>=0?r.entries[l]=d:r.entries.push(d)}catch(l){let d=l instanceof Error?l.message:String(l);T(` #${a.id} "${a.title}" \u2014 nudge failed: ${d}`),i.errors++}}return n.dryRun||Bn(n.historyFile,r),A(`Auto-nudge: ${i.nudged} nudged, ${i.skipped} skipped, ${i.errors} errors`),i}import{readFileSync as Wn}from"node:fs";import{resolve as At,dirname as Hn}from"node:path";import{fileURLToPath as Vn}from"node:url";var Jn=Hn(Vn(import.meta.url));function Yn(){return`<!DOCTYPE html>
|
|
92
|
+
<html lang="en">
|
|
93
|
+
<head>
|
|
94
|
+
<meta charset="UTF-8">
|
|
95
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
96
|
+
<title>PR Review Dashboard</title>
|
|
97
|
+
<style>
|
|
98
|
+
:root { --bg: #0d1117; --surface: #161b22; --border: #30363d; --text: #e6edf3; --text-dim: #8b949e; --accent: #58a6ff; --green: #3fb950; --yellow: #d29922; --red: #f85149; }
|
|
99
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
100
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; padding: 1rem; }
|
|
101
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
102
|
+
header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 0; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; }
|
|
103
|
+
header h1 { font-size: 1.5rem; }
|
|
104
|
+
header .meta { color: var(--text-dim); font-size: 0.85rem; }
|
|
105
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
|
106
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; }
|
|
107
|
+
.card .label { color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
108
|
+
.card .value { font-size: 1.8rem; font-weight: 600; margin-top: 0.25rem; }
|
|
109
|
+
.card.green .value { color: var(--green); }
|
|
110
|
+
.card.yellow .value { color: var(--yellow); }
|
|
111
|
+
.card.red .value { color: var(--red); }
|
|
112
|
+
.tabs { display: flex; gap: 0.25rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
|
113
|
+
.tab { background: var(--surface); border: 1px solid var(--border); border-radius: 6px 6px 0 0; padding: 0.5rem 1rem; cursor: pointer; color: var(--text-dim); font-size: 0.9rem; }
|
|
114
|
+
.tab.active { background: var(--border); color: var(--text); border-bottom-color: var(--border); }
|
|
115
|
+
.search-bar { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
|
116
|
+
.search-bar input, .search-bar select { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.75rem; color: var(--text); font-size: 0.9rem; }
|
|
117
|
+
.search-bar input { flex: 1; min-width: 200px; }
|
|
118
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; }
|
|
119
|
+
th { background: var(--surface); color: var(--text-dim); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; cursor: pointer; user-select: none; }
|
|
120
|
+
th:hover { color: var(--accent); }
|
|
121
|
+
th, td { padding: 0.6rem 0.75rem; text-align: left; border-bottom: 1px solid var(--border); }
|
|
122
|
+
tr:hover { background: rgba(88,166,255,0.04); }
|
|
123
|
+
a { color: var(--accent); text-decoration: none; }
|
|
124
|
+
a:hover { text-decoration: underline; }
|
|
125
|
+
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
|
|
126
|
+
.badge-green { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
127
|
+
.badge-yellow { background: rgba(210,153,34,0.15); color: var(--yellow); }
|
|
128
|
+
.badge-red { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
129
|
+
.section { margin-bottom: 2rem; }
|
|
130
|
+
.section h2 { font-size: 1.1rem; margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); }
|
|
131
|
+
.export-bar { display: flex; gap: 0.5rem; margin-top: 1rem; }
|
|
132
|
+
.btn { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 0.4rem 0.75rem; color: var(--text); cursor: pointer; font-size: 0.85rem; }
|
|
133
|
+
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
134
|
+
.empty { color: var(--text-dim); text-align: center; padding: 2rem; }
|
|
135
|
+
@media print { body { background: #fff; color: #000; } .card { border: 1px solid #ccc; } th { background: #f5f5f5; } .search-bar, .tabs, .export-bar { display: none; } }
|
|
136
|
+
@media (max-width: 768px) { .cards { grid-template-columns: 1fr 1fr; } }
|
|
137
|
+
</style>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<div class="container">
|
|
141
|
+
<header>
|
|
142
|
+
<h1>\u{1F4CB} PR Review Dashboard</h1>
|
|
143
|
+
<div class="meta">Generated: <span id="timestamp"></span> \xB7 <span id="repo-count"></span> repos</div>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<div class="cards" id="summary-cards"></div>
|
|
147
|
+
|
|
148
|
+
<div class="search-bar">
|
|
149
|
+
<input type="text" id="search" placeholder="Search PRs by title, author, ID..." oninput="filterTable()">
|
|
150
|
+
<select id="status-filter" onchange="filterTable()">
|
|
151
|
+
<option value="all">All Status</option>
|
|
152
|
+
<option value="needingReview">Needing Review</option>
|
|
153
|
+
<option value="waitingOnAuthor">Waiting on Author</option>
|
|
154
|
+
<option value="approved">Approved</option>
|
|
155
|
+
</select>
|
|
156
|
+
<select id="repo-filter" onchange="filterTable()"></select>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="tabs" id="repo-tabs"></div>
|
|
160
|
+
|
|
161
|
+
<div class="section">
|
|
162
|
+
<h2>Pull Requests</h2>
|
|
163
|
+
<table>
|
|
164
|
+
<thead>
|
|
165
|
+
<tr>
|
|
166
|
+
<th onclick="sortTable(0)">ID \u2195</th>
|
|
167
|
+
<th onclick="sortTable(1)">Title \u2195</th>
|
|
168
|
+
<th onclick="sortTable(2)">Author \u2195</th>
|
|
169
|
+
<th onclick="sortTable(3)">Status \u2195</th>
|
|
170
|
+
<th onclick="sortTable(4)">Repo \u2195</th>
|
|
171
|
+
<th onclick="sortTable(5)">Size \u2195</th>
|
|
172
|
+
<th onclick="sortTable(6)">Pipelines \u2195</th>
|
|
173
|
+
<th onclick="sortTable(7)">Staleness \u2195</th>
|
|
174
|
+
</tr>
|
|
175
|
+
</thead>
|
|
176
|
+
<tbody id="pr-table"></tbody>
|
|
177
|
+
</table>
|
|
178
|
+
<div id="empty-message" class="empty" style="display:none">No PRs match your filters.</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="section" id="metrics-section" style="display:none">
|
|
182
|
+
<h2>\u{1F4CA} Aggregate Metrics</h2>
|
|
183
|
+
<div class="cards" id="metrics-cards"></div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="export-bar">
|
|
187
|
+
<button class="btn" onclick="exportCsv()">\u{1F4E5} Export CSV</button>
|
|
188
|
+
<button class="btn" onclick="window.print()">\u{1F5A8}\uFE0F Print</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<script>
|
|
193
|
+
const DATA = {{DATA_PLACEHOLDER}};
|
|
194
|
+
|
|
195
|
+
function init() {
|
|
196
|
+
document.getElementById('timestamp').textContent = new Date(DATA.generatedAt).toLocaleString();
|
|
197
|
+
document.getElementById('repo-count').textContent = DATA.repositories.length;
|
|
198
|
+
|
|
199
|
+
renderSummaryCards();
|
|
200
|
+
renderRepoFilter();
|
|
201
|
+
renderPrTable();
|
|
202
|
+
renderMetrics();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderSummaryCards() {
|
|
206
|
+
const agg = DATA.aggregate;
|
|
207
|
+
const total = agg.totalPrs;
|
|
208
|
+
let needingReview = 0, approved = 0, waitingOnAuthor = 0;
|
|
209
|
+
for (const repo of DATA.repositories) {
|
|
210
|
+
needingReview += repo.analysis.needingReview.length;
|
|
211
|
+
approved += repo.analysis.approved.length;
|
|
212
|
+
waitingOnAuthor += repo.analysis.waitingOnAuthor.length;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const cards = [
|
|
216
|
+
{ label: 'Total PRs', value: total, cls: '' },
|
|
217
|
+
{ label: 'Needing Review', value: needingReview, cls: 'yellow' },
|
|
218
|
+
{ label: 'Approved', value: approved, cls: 'green' },
|
|
219
|
+
{ label: 'Waiting on Author', value: waitingOnAuthor, cls: 'red' },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
let conflicts = 0;
|
|
223
|
+
for (const repo of DATA.repositories) {
|
|
224
|
+
for (const list of [repo.analysis.needingReview, repo.analysis.approved, repo.analysis.waitingOnAuthor]) {
|
|
225
|
+
for (const pr of list) { if (pr.hasMergeConflict) conflicts++; }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (conflicts > 0) {
|
|
229
|
+
cards.push({ label: 'Conflicts', value: conflicts, cls: 'red' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (agg.metrics) {
|
|
233
|
+
cards.push({ label: 'Median Age', value: agg.metrics.medianAgeInDays.toFixed(1) + 'd', cls: '' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const el = document.getElementById('summary-cards');
|
|
237
|
+
el.innerHTML = cards.map(c => \`<div class="card \${c.cls}"><div class="label">\${c.label}</div><div class="value">\${c.value}</div></div>\`).join('');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderRepoFilter() {
|
|
241
|
+
const sel = document.getElementById('repo-filter');
|
|
242
|
+
sel.innerHTML = '<option value="all">All Repos</option>';
|
|
243
|
+
for (const repo of DATA.repositories) {
|
|
244
|
+
sel.innerHTML += \`<option value="\${repo.repoLabel}">\${repo.repoLabel}</option>\`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getAllPrs() {
|
|
249
|
+
const rows = [];
|
|
250
|
+
for (const repo of DATA.repositories) {
|
|
251
|
+
for (const pr of repo.analysis.needingReview) rows.push({ ...pr, status: 'needingReview', repo: repo.repoLabel, date: pr.waitingSince });
|
|
252
|
+
for (const pr of repo.analysis.approved) rows.push({ ...pr, status: 'approved', repo: repo.repoLabel, date: pr.createdDate });
|
|
253
|
+
for (const pr of repo.analysis.waitingOnAuthor) rows.push({ ...pr, status: 'waitingOnAuthor', repo: repo.repoLabel, date: pr.lastReviewerActivityDate });
|
|
254
|
+
}
|
|
255
|
+
return rows;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let allPrs = [];
|
|
259
|
+
let sortCol = 0, sortDir = 1;
|
|
260
|
+
|
|
261
|
+
function renderPrTable() {
|
|
262
|
+
allPrs = getAllPrs();
|
|
263
|
+
filterTable();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function filterTable() {
|
|
267
|
+
const q = document.getElementById('search').value.toLowerCase();
|
|
268
|
+
const sf = document.getElementById('status-filter').value;
|
|
269
|
+
const rf = document.getElementById('repo-filter').value;
|
|
270
|
+
|
|
271
|
+
let filtered = allPrs.filter(pr => {
|
|
272
|
+
if (q && !\`\${pr.id} \${pr.title} \${pr.author}\`.toLowerCase().includes(q)) return false;
|
|
273
|
+
if (sf !== 'all' && pr.status !== sf) return false;
|
|
274
|
+
if (rf !== 'all' && pr.repo !== rf) return false;
|
|
275
|
+
return true;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
filtered.sort((a, b) => {
|
|
279
|
+
const va = getCellValue(a, sortCol);
|
|
280
|
+
const vb = getCellValue(b, sortCol);
|
|
281
|
+
return (va < vb ? -1 : va > vb ? 1 : 0) * sortDir;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const tbody = document.getElementById('pr-table');
|
|
285
|
+
const empty = document.getElementById('empty-message');
|
|
286
|
+
|
|
287
|
+
if (filtered.length === 0) {
|
|
288
|
+
tbody.innerHTML = '';
|
|
289
|
+
empty.style.display = 'block';
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
empty.style.display = 'none';
|
|
293
|
+
|
|
294
|
+
tbody.innerHTML = filtered.map(pr => {
|
|
295
|
+
const statusBadge = pr.status === 'needingReview' ? '<span class="badge badge-yellow">Needs Review</span>'
|
|
296
|
+
: pr.status === 'approved' ? '<span class="badge badge-green">Approved</span>'
|
|
297
|
+
: '<span class="badge badge-red">Waiting</span>';
|
|
298
|
+
const size = pr.size ? pr.size.label : '\u2014';
|
|
299
|
+
const staleness = getStaleness(pr);
|
|
300
|
+
const pipeline = getPipelineBadge(pr.pipelineStatus);
|
|
301
|
+
const conflict = pr.hasMergeConflict ? ' <span class="badge badge-red">\u274C conflict</span>' : '';
|
|
302
|
+
return \`<tr>
|
|
303
|
+
<td><a href="\${pr.url}" target="_blank">#\${pr.id}</a></td>
|
|
304
|
+
<td>\${escapeHtml(pr.title)}\${conflict}</td>
|
|
305
|
+
<td>\${pr.isStarred ? '\u2B50 ' : ''}\${escapeHtml(pr.author)}</td>
|
|
306
|
+
<td>\${statusBadge}</td>
|
|
307
|
+
<td>\${escapeHtml(pr.repo)}</td>
|
|
308
|
+
<td>\${size}</td>
|
|
309
|
+
<td>\${pipeline}</td>
|
|
310
|
+
<td>\${staleness}</td>
|
|
311
|
+
</tr>\`;
|
|
312
|
+
}).join('');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function getCellValue(pr, col) {
|
|
316
|
+
switch(col) {
|
|
317
|
+
case 0: return pr.id;
|
|
318
|
+
case 1: return pr.title.toLowerCase();
|
|
319
|
+
case 2: return pr.author.toLowerCase();
|
|
320
|
+
case 3: return pr.status;
|
|
321
|
+
case 4: return pr.repo;
|
|
322
|
+
case 5: return pr.size ? pr.size.totalChanges : 0;
|
|
323
|
+
case 6: return pr.pipelineStatus ? (pr.pipelineStatus.failed > 0 ? 2 : pr.pipelineStatus.inProgress > 0 ? 1 : 0) : -1;
|
|
324
|
+
case 7: return pr.date ? new Date(pr.date).getTime() : 0;
|
|
325
|
+
default: return '';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function sortTable(col) {
|
|
330
|
+
if (sortCol === col) sortDir *= -1;
|
|
331
|
+
else { sortCol = col; sortDir = 1; }
|
|
332
|
+
filterTable();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getStaleness(pr) {
|
|
336
|
+
if (!pr.date) return '\u2014';
|
|
337
|
+
const days = Math.floor((Date.now() - new Date(pr.date).getTime()) / 86400000);
|
|
338
|
+
if (days >= 30) return '<span class="badge badge-red">\u{1F480} ' + days + 'd</span>';
|
|
339
|
+
if (days >= 14) return '<span class="badge badge-red">\u{1F534} ' + days + 'd</span>';
|
|
340
|
+
if (days >= 7) return '<span class="badge badge-yellow">\u26A0\uFE0F ' + days + 'd</span>';
|
|
341
|
+
return days + 'd';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getPipelineBadge(ps) {
|
|
345
|
+
if (!ps) return '\u2014';
|
|
346
|
+
if (ps.failed > 0) return \`<span class="badge badge-red">\u{1F534} \${ps.failed}/\${ps.total} failed</span>\`;
|
|
347
|
+
if (ps.inProgress > 0) return \`<span class="badge badge-yellow">\u{1F7E1} \${ps.inProgress}/\${ps.total} running</span>\`;
|
|
348
|
+
if (ps.succeeded === ps.total) return \`<span class="badge badge-green">\u{1F7E2} \${ps.total}/\${ps.total} passed</span>\`;
|
|
349
|
+
return \`\${ps.total} pipeline(s)\`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function renderMetrics() {
|
|
353
|
+
const agg = DATA.aggregate;
|
|
354
|
+
if (!agg.metrics) return;
|
|
355
|
+
const el = document.getElementById('metrics-section');
|
|
356
|
+
el.style.display = 'block';
|
|
357
|
+
const m = agg.metrics;
|
|
358
|
+
const cards = [
|
|
359
|
+
{ label: 'Median PR Age', value: m.medianAgeInDays.toFixed(1) + ' days' },
|
|
360
|
+
{ label: 'Avg First Review', value: m.avgTimeToFirstReviewInDays !== null ? m.avgTimeToFirstReviewInDays.toFixed(1) + ' days' : 'N/A' },
|
|
361
|
+
{ label: 'Avg Review Rounds', value: m.avgReviewRounds.toFixed(1) },
|
|
362
|
+
{ label: 'No Review Activity', value: m.prsWithNoReviewActivity },
|
|
363
|
+
];
|
|
364
|
+
document.getElementById('metrics-cards').innerHTML = cards.map(c =>
|
|
365
|
+
\`<div class="card"><div class="label">\${c.label}</div><div class="value">\${c.value}</div></div>\`
|
|
366
|
+
).join('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function exportCsv() {
|
|
370
|
+
const rows = [['ID','Title','Author','Status','Repo','Size','Pipelines','Age (days)']];
|
|
371
|
+
for (const pr of allPrs) {
|
|
372
|
+
const days = pr.date ? Math.floor((Date.now() - new Date(pr.date).getTime()) / 86400000) : '';
|
|
373
|
+
const pipeline = pr.pipelineStatus ? \`\${pr.pipelineStatus.succeeded}/\${pr.pipelineStatus.total} passed\` : '';
|
|
374
|
+
rows.push([pr.id, \`"\${pr.title.replace(/"/g,'""')}"\`, (pr.isStarred ? '\u2B50 ' : '') + pr.author, pr.status, pr.repo, pr.size?.label || '', pipeline, days]);
|
|
375
|
+
}
|
|
376
|
+
const csv = rows.map(r => r.join(',')).join('\\n');
|
|
377
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
378
|
+
const a = document.createElement('a');
|
|
379
|
+
a.href = URL.createObjectURL(blob);
|
|
380
|
+
a.download = 'pr-review-report.csv';
|
|
381
|
+
a.click();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function escapeHtml(s) {
|
|
385
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
init();
|
|
389
|
+
</script>
|
|
390
|
+
</body>
|
|
391
|
+
</html>
|
|
392
|
+
`}var Zn="{{DATA_PLACEHOLDER}}";function Tt(e){let t=Yn(),n=JSON.stringify(e);return t.replace(Zn,n)}var to={$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 Ee(){let e=B(Qn(eo(import.meta.url)),"..","package.json");try{return JSON.parse(Xn(e,"utf-8")).version??"0.0.0"}catch{return"0.0.0"}}function Pt(){let e=B("pr-review-config.json");Kn(e)&&(T(`Config file already exists: ${e}`),A("Remove or rename the existing file and try again."),process.exit(1)),xe(e,JSON.stringify(to,null,2)+`
|
|
393
|
+
`,"utf-8"),P(`Created template config: ${e}`),A("Edit the file to add your Azure DevOps repository URLs and team members.")}function no(e,t){let n=De(e.prs,t.botUsers,void 0,t.starredUsers),o=Pe(e.prs,e.analysis,t.botUsers,void 0,t.aiBotUsers,t.starredUsers),r;if(t.staleness.enabled){r={};let u=[...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 u){let a=N(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:he(e.repoLabel,e.analysis,e.restarted,e.restartFailed)}}async function oo(e,t,n,o,r){if(r.length===0)return;let u=new Set(r),i=o.filter(a=>u.has(a.id));A(`Refreshing merge status for ${i.length} restarted PR(s)\u2026`);for(let a of i)try{let g=await x(`Refresh merge status for PR #${a.id}`,()=>e.getPullRequestById(a.id,n));g.mergeStatus!==void 0&&(a.mergeStatus=g.mergeStatus)}catch(g){let c=g instanceof Error?g.message:String(g);T(` #${a.id} \u2014 failed to refresh merge status: ${c}`)}}async function ro(e){let{repo:t,isMultiRepo:n,restartMergeAfterDays:o,quantifierConfig:r,teamMembers:u,ignoredUsers:i,botUsers:a,aiBotUsers:g}=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 l=await pe(t.orgUrl),d=await _e(t.orgUrl),m=await et(l,t.repository,t.project,t.orgUrl,f,t.patterns,d);P(`Fetched ${m.length} candidate PRs from ${c} (${Date.now()-s}ms)`),await tt(l,t.repository,t.project,m);let h=t.skipRestartMerge?-1:o;t.skipRestartMerge&&w(`Skipping restart-merge for ${c} (configured per repository)`);let p=await nt(l,t.repository,t.project,m,h);await oo(l,t.repository,t.project,m,p.restartedPrIds);let S=rt(m,u,n?c:void 0,i,a,g,e.starredUsers);return{repoLabel:c,prs:m,analysis:S,restarted:p.restarted,restartFailed:p.failed}}async function Dt(e){A("Loading configuration\u2026");let t=await Je(e),n=t.repos,o=n.length>1;A("Authenticating to Azure DevOps\u2026");let r=Date.now(),u=[...new Set(n.map(p=>p.orgUrl))];for(let p of u)await pe(p);P(`Authenticated to ${u.join(", ")} (${Date.now()-r}ms)`);let i=0,a=0,g=0;A(`Processing ${n.length} repo(s) (concurrency: ${10})\u2026`);let c=await ee(n,10,p=>ro({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,g+=p.restartFailed;let s=st(c.map(p=>p.analysis)),f=c.map(p=>he(p.repoLabel,p.analysis,p.restarted,p.restartFailed)),l=We(s,a,g,f),d=c.flatMap(p=>p.prs),m=De(d,t.botUsers,void 0,t.starredUsers),h=Pe(d,s,t.botUsers,void 0,t.aiBotUsers,t.starredUsers);return{multiConfig:t,repos:n,isMultiRepo:o,results:c,merged:s,stats:l,allPrs:d,metrics:m,workload:h,totalPrs:i,totalRestarted:a,totalRestartFailed:g}}async function Ct(e){Oe(e.verbose);let t=e.format??"markdown";if(t==="terminal"){let{multiConfig:l,repos:d,isMultiRepo:m,merged:h,stats:p,metrics:S,workload:R}=await Dt(e.config),b=m?`${d.length} repositories`:`${d[0].project}/${d[0].repository}`,D=pt({analysis:h,repoLabel:b,multiRepo:m,stats:p,staleness:l.staleness,metrics:S,workload:R});console.log(D),l.notifications&&await Ce(h,p,l.notifications,l.staleness);return}me("PR Review Needed");let{multiConfig:n,repos:o,isMultiRepo:r,results:u,merged:i,stats:a,metrics:g,workload:c,totalPrs:s}=await Dt(e.config);for(let l of u)P(`${l.repoLabel}: ${l.analysis.approved.length} approved, ${l.analysis.needingReview.length} needing review, ${l.analysis.waitingOnAuthor.length} waiting on author`);if(t==="json"||t==="html"){let l=u.map(h=>no(h,n)),d=yt(l,Ee());if(t==="html"){A("Generating HTML report\u2026");let h=Tt(d),p=e.output==="pr-review-summary.md"?"pr-review-summary.html":e.output;xe(B(p),h,"utf-8"),P(`HTML report written to ${B(p)}`)}else{A("Generating JSON report\u2026");let h=e.output==="pr-review-summary.md"?"pr-review-summary.json":e.output;await Rt(d,B(h))}let m=e.webhookUrl?{url:e.webhookUrl}:n.webhook;m&&await vt(d,m)}else{A("Generating markdown\u2026");let l=dt({analysis:i,multiRepo:r,stats:a,staleness:n.staleness,metrics:g,workload:c}),d=B(e.output);xe(d,l,"utf-8"),P(`Output written to ${d}`)}me("Summary"),_("Repositories",o.length),_("PRs analyzed",s),_("Approved",i.approved.length),_("Needing review",i.needingReview.length),_("Waiting on author",i.waitingOnAuthor.length),_("Output file",B(e.output)),console.log(),e.notify!==!1&&n.notifications&&await Ce(i,a,n.notifications,n.staleness);let f=n.autoNudge;f&&e.nudge!==!1&&(e.dryRun&&(f.dryRun=!0),await St(i,n.staleness,f))}process.removeAllListeners("warning");process.on("warning",e=>{(e.name!=="DeprecationWarning"||e.code!=="DEP0169")&&console.warn(e)});var Ne=new so().name("pr-review-needed").description("Generates a markdown summary of Azure DevOps PRs needing review").version(Ee());Ne.command("setup").description("Generate a template pr-review-config.json in the current directory").action(()=>{Pt()});Ne.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 Ct(e)});Ne.parseAsync(process.argv).catch(e=>{Y(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.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Generates a markdown summary of Azure DevOps PRs needing review",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc",
|
|
22
|
-
"bundle": "
|
|
22
|
+
"bundle": "node scripts/bundle.mjs",
|
|
23
23
|
"prepublishOnly": "npm run build && npm run bundle",
|
|
24
24
|
"start": "tsx src/index.ts run",
|
|
25
25
|
"lint": "eslint src/",
|