55 * Generates a monthly report measuring code owner responsiveness. We require (1) at least 75%
66 * of each component's PRs to be reviewed by at least one code owner, and (2) each code owner
77 * to have reviewed/replied to at least 75% / n of the PRs where they were requested (n =
8- * number of code owners for that component). The report can optionally list code owners with
9- * below 5% activity over the past 6 months. Uses the same component labels as the weekly
8+ * number of code owners for that component). Uses the same component labels as the weekly
109 * report (e.g. receiver/kubeletstats).
1110 */
1211
@@ -18,11 +17,6 @@ const REPO_NAME = 'opentelemetry-collector-contrib';
1817/** Target: at least this % of each component's PRs must be reviewed (by at least one code owner). */
1918const COMPONENT_TARGET_PCT = 75 ;
2019/** Per-code-owner target is COMPONENT_TARGET_PCT / number of code owners for that component (e.g. 75%/3 ≈ 25%). */
21- /** Code owners below this % activity over the past 6 months are listed in a separate section (when enabled). */
22- const LOW_ACTIVITY_THRESHOLD_PCT = 5 ;
23- const SIX_MONTHS_DAYS = 180 ;
24- /** Set to true to include the "Code owners below 5% activity (past 6 months)" section in the report. */
25- const REPORT_LOW_ACTIVITY_6MO = false ;
2620
2721/** PRs created by these logins are excluded from the report. */
2822const EXCLUDED_PR_AUTHORS = new Set ( [ 'otelbot' , 'renovate' ] ) ;
@@ -39,14 +33,11 @@ const FOCUS_COMPONENT_LABELS = new Set([
3933] ) ;
4034// Resourcedetection has sub-labels (e.g. processor/resourcedetection/internal/azure); include those too.
4135function isAllowedLabel ( label ) {
42- if ( FOCUS_COMPONENT_LABELS . size === 0 ) return true ;
4336 if ( FOCUS_COMPONENT_LABELS . has ( label ) ) return true ;
4437 if ( label . startsWith ( 'processor/resourcedetection/' ) ) return true ;
4538 return false ;
4639}
4740
48- // Optional: set LIMIT (e.g. 10) to only process that many PRs and issues (for quick local runs)
49- const PROCESS_LIMIT = process . env . LIMIT ? parseInt ( process . env . LIMIT , 10 ) : null ;
5041const PROGRESS_INTERVAL = 5 ;
5142
5243function debug ( msg ) {
@@ -72,9 +63,7 @@ function genLookbackDates() {
7263 ) ;
7364 const thirtyDaysAgo = new Date ( midnightYesterday ) ;
7465 thirtyDaysAgo . setDate ( midnightYesterday . getDate ( ) - 30 ) ;
75- const sixMonthsAgo = new Date ( midnightYesterday ) ;
76- sixMonthsAgo . setDate ( midnightYesterday . getDate ( ) - SIX_MONTHS_DAYS ) ;
77- return { thirtyDaysAgo, sixMonthsAgo, midnightYesterday } ;
66+ return { thirtyDaysAgo, midnightYesterday } ;
7867}
7968
8069function filterOnDateRange ( { created_at, thirtyDaysAgo, midnightYesterday } ) {
@@ -164,39 +153,6 @@ async function searchIssuesAndPrs(octokit, query, thirtyDaysAgo, midnightYesterd
164153 return items ;
165154}
166155
167- async function getPrsInWindow ( octokit , since , until ) {
168- return searchIssuesAndPrs ( octokit , 'is:pr' , since , until ) ;
169- }
170-
171- /**
172- * Fetch PRs in date-range chunks to avoid GitHub search 1000-result limit.
173- * Returns combined, deduplicated PRs for the full [since, until] window.
174- */
175- async function getPrsInWindowChunked ( octokit , since , until , chunkDays = 30 ) {
176- const results = [ ] ;
177- const seen = new Set ( ) ;
178- let start = new Date ( since ) ;
179- const end = new Date ( until ) ;
180- while ( start < end ) {
181- const chunkEnd = new Date ( start ) ;
182- chunkEnd . setDate ( chunkEnd . getDate ( ) + chunkDays ) ;
183- if ( chunkEnd > end ) chunkEnd . setTime ( end . getTime ( ) ) ;
184- progress ( `Fetching PRs ${ start . toISOString ( ) . slice ( 0 , 10 ) } – ${ chunkEnd . toISOString ( ) . slice ( 0 , 10 ) } ...` ) ;
185- const chunk = await searchIssuesAndPrs ( octokit , 'is:pr' , start , chunkEnd ) ;
186- for ( const pr of chunk ) {
187- if ( seen . has ( pr . id ) ) continue ;
188- seen . add ( pr . id ) ;
189- results . push ( pr ) ;
190- }
191- start = chunkEnd ;
192- }
193- return results ;
194- }
195-
196- async function getIssuesInWindow ( octokit , thirtyDaysAgo , midnightYesterday ) {
197- return searchIssuesAndPrs ( octokit , 'is:issue' , thirtyDaysAgo , midnightYesterday ) ;
198- }
199-
200156function getComponentLabelsOnItem ( item , componentLabels ) {
201157 const names = ( item . labels || [ ] ) . map ( ( l ) => l . name ) ;
202158 return names . filter ( ( name ) => componentLabels . has ( name ) && isAllowedLabel ( name ) ) ;
@@ -238,23 +194,6 @@ async function getReviewAndRequestedLogins(octokit, owner, repo, prNumber) {
238194 return { requested, respondents, draft } ;
239195}
240196
241- async function getIssueCommentLogins ( octokit , owner , repo , issueNumber ) {
242- const logins = new Set ( ) ;
243- try {
244- const { data : comments } = await octokit . issues . listComments ( {
245- owner,
246- repo,
247- issue_number : issueNumber ,
248- } ) ;
249- for ( const c of comments ) {
250- if ( c . user && c . user . login ) logins . add ( c . user . login ) ;
251- }
252- } catch ( e ) {
253- debug ( { msg : 'getIssueCommentLogins error' , owner, repo, issueNumber, error : e . message } ) ;
254- }
255- return logins ;
256- }
257-
258197/**
259198 * Get per-label code owners: for each label in labelsOnItem, add (label, login) pairs
260199 * to the given map. Returns Map<login, Set<label>> for quick lookup.
@@ -275,16 +214,13 @@ function getCodeOwnersByLabel(labelsOnItem, labelToOwners) {
275214/**
276215 * Stats aggregated by (code owner, component). Shape: byCodeOwner[login][componentLabel] = { total, responded }.
277216 * Also returns componentPrStats: per component, how many PRs had at least one code-owner review (for the 75% target).
278- * @param {number|null } [processLimitOverride] - If null, process all PRs; if undefined, use global PROCESS_LIMIT.
279217 */
280- async function computePrStats ( octokit , prs , labelToOwners , componentLabels , thirtyDaysAgo , midnightYesterday , processLimitOverride ) {
218+ async function computePrStats ( octokit , prs , labelToOwners , componentLabels , thirtyDaysAgo , midnightYesterday ) {
281219 const byCodeOwnerAndComponent = { } ;
282220 const componentPrStats = { } ; // { [component]: { prsTotal, prsWithResponse } }
283221 let processed = 0 ;
284- const limit = processLimitOverride !== undefined ? processLimitOverride : PROCESS_LIMIT ;
285222
286223 for ( const pr of prs ) {
287- if ( limit !== null && processed >= limit ) break ;
288224 if ( ! filterOnDateRange ( { created_at : pr . created_at , thirtyDaysAgo, midnightYesterday } ) ) continue ;
289225 const authorLogin = pr . user ?. login || null ;
290226 if ( authorLogin && EXCLUDED_PR_AUTHORS . has ( authorLogin ) ) continue ;
@@ -334,42 +270,6 @@ async function computePrStats(octokit, prs, labelToOwners, componentLabels, thir
334270 return { byCodeOwnerAndComponent, componentPrStats } ;
335271}
336272
337- async function computeIssueStats ( octokit , issues , labelToOwners , componentLabels , thirtyDaysAgo , midnightYesterday ) {
338- const byCodeOwnerAndComponent = { } ;
339- let processed = 0 ;
340-
341- for ( const issue of issues ) {
342- if ( PROCESS_LIMIT !== null && processed >= PROCESS_LIMIT ) break ;
343- if ( ! filterOnDateRange ( { created_at : issue . created_at , thirtyDaysAgo, midnightYesterday } ) ) continue ;
344- const labelsOnIssue = getComponentLabelsOnItem ( issue , componentLabels ) ;
345- if ( labelsOnIssue . length === 0 ) continue ;
346-
347- const ownerToLabels = getCodeOwnersByLabel ( labelsOnIssue , labelToOwners ) ;
348- if ( ownerToLabels . size === 0 ) continue ;
349-
350- if ( processed === 0 || processed % PROGRESS_INTERVAL === 0 ) progress ( `Issues: fetching #${ issue . number } (${ processed + 1 } )...` ) ;
351- const respondents = await getIssueCommentLogins ( octokit , REPO_OWNER , REPO_NAME , issue . number ) ;
352- processed ++ ;
353-
354- const authorLogin = issue . user ?. login || null ;
355- for ( const [ login , labels ] of ownerToLabels ) {
356- if ( authorLogin && login === authorLogin ) continue ; // do not count this issue for the author (they opened it)
357- for ( const label of labels ) {
358- if ( ! byCodeOwnerAndComponent [ login ] ) byCodeOwnerAndComponent [ login ] = { } ;
359- if ( ! byCodeOwnerAndComponent [ login ] [ label ] ) {
360- byCodeOwnerAndComponent [ login ] [ label ] = { total : 0 , responded : 0 } ;
361- }
362- byCodeOwnerAndComponent [ login ] [ label ] . total ++ ;
363- if ( respondents . has ( login ) ) {
364- byCodeOwnerAndComponent [ login ] [ label ] . responded ++ ;
365- }
366- }
367- }
368- }
369-
370- return byCodeOwnerAndComponent ;
371- }
372-
373273/**
374274 * Flatten byCodeOwnerAndComponent into rows: one row per (code owner, component).
375275 * Per-code-owner target is 75% / (number of code owners for that component).
@@ -447,59 +347,19 @@ ${content}
447347</details>` ;
448348}
449349
450- /**
451- * Code owners with < LOW_ACTIVITY_THRESHOLD_PCT response rate over the past 6 months, per component.
452- */
453- function formatLowActivityCodeOwners ( sixMonthByCodeOwner ) {
454- const rows = [ ] ;
455- for ( const [ login , byComponent ] of Object . entries ( sixMonthByCodeOwner ) ) {
456- for ( const [ component , v ] of Object . entries ( byComponent ) ) {
457- if ( v . total === 0 ) continue ;
458- const pct = ( 100 * v . responded ) / v . total ;
459- if ( pct >= LOW_ACTIVITY_THRESHOLD_PCT ) continue ;
460- rows . push ( [ login , component , String ( v . total ) , String ( v . responded ) , `${ pct . toFixed ( 1 ) } %` ] ) ;
461- }
462- }
463- rows . sort ( ( a , b ) => {
464- const cmpLogin = a [ 0 ] . localeCompare ( b [ 0 ] ) ;
465- if ( cmpLogin !== 0 ) return cmpLogin ;
466- return a [ 1 ] . localeCompare ( b [ 1 ] ) ;
467- } ) ;
468-
469- if ( rows . length === 0 ) {
470- return `No code owners below ${ LOW_ACTIVITY_THRESHOLD_PCT } % activity in the past 6 months.\n` ;
471- }
472- const header = `| Code owner | Component | Total requested | Responded | % |` ;
473- const sep = '| --- | --- | --- | --- | --- |' ;
474- const body = rows . map ( ( r ) => `| ${ r . join ( ' | ' ) } |` ) . join ( '\n' ) ;
475- return `${ header } \n${ sep } \n${ body } \n` ;
476- }
477-
478- function generateReport ( prStats , componentPrStats , issueStats , lookbackData , lowActivityMarkdown ) {
479- const focusNote = FOCUS_COMPONENT_LABELS . size > 0
480- ? `\n\n**Components in scope:** ${ [ ...FOCUS_COMPONENT_LABELS ] . sort ( ) . join ( ', ' ) } (and \`processor/resourcedetection/*\` sub-labels).\n`
481- : '' ;
350+ function generateReport ( prStats , componentPrStats , lookbackData ) {
482351 const out = [
483352 `## Code owner activity report` ,
484353 `` ,
485354 `Period: ${ lookbackData . thirtyDaysAgo . toISOString ( ) . slice ( 0 , 10 ) } – ${ lookbackData . midnightYesterday . toISOString ( ) . slice ( 0 , 10 ) } ` ,
486355 `` ,
487- `We target **at least ${ COMPONENT_TARGET_PCT } % of each component's PRs** to be reviewed by a code owner, and each code owner to respond to at least **${ COMPONENT_TARGET_PCT } % / n** of their requested PRs (n = number of code owners for that component) ([at least 3 code owners for components aiming for stable](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#beta-to-stable)).${ focusNote } ` ,
356+ `We target **at least ${ COMPONENT_TARGET_PCT } % of each component's PRs** to be reviewed by a code owner, and each code owner to respond to at least **${ COMPONENT_TARGET_PCT } % / n** of their requested PRs (n = number of code owners for that component) ([at least 3 code owners for components aiming for stable](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#beta-to-stable)).` ,
488357 `` ,
489358 `### Component PR review rate (${ COMPONENT_TARGET_PCT } % target)` ,
490359 `` ,
491360 formatComponentSummaryTable ( prStats , componentPrStats ) ,
492361 `` ,
493362 collapsibleSection ( 'PRs (per code owner)' , formatTable ( prStats , 'PRs' ) ) ,
494- ...( lowActivityMarkdown
495- ? [
496- `` ,
497- `### Code owners below ${ LOW_ACTIVITY_THRESHOLD_PCT } % activity (past 6 months)` ,
498- `` ,
499- lowActivityMarkdown ,
500- ]
501- : [ ] ) ,
502- // collapsibleSection('Issues', formatTable(issueStats, 'issues')),
503363 ] ;
504364 return out . join ( '\n' ) ;
505365}
@@ -534,28 +394,12 @@ async function main({ github, context }) {
534394 ? new Set ( [ ...allLabels ] . filter ( isAllowedLabel ) )
535395 : allLabels ;
536396
537- const prs30 = await getPrsInWindow ( octokit , lookbackData . thirtyDaysAgo , lookbackData . midnightYesterday ) ;
538- // const issues = await getIssuesInWindow(octokit, lookbackData.thirtyDaysAgo, lookbackData.midnightYesterday);
539- const issues = [ ] ;
540-
541- if ( PROCESS_LIMIT !== null ) {
542- progress ( `Limit set: processing up to ${ PROCESS_LIMIT } PRs for 30-day stats (set LIMIT= or unset for full run).` ) ;
543- }
397+ const prs30 = await searchIssuesAndPrs ( octokit , 'is:pr' , lookbackData . thirtyDaysAgo , lookbackData . midnightYesterday ) ;
544398 progress ( `Fetched ${ prs30 . length } PRs (30 days). Processing...` ) ;
545399
546400 const { byCodeOwnerAndComponent : prStats , componentPrStats } = await computePrStats ( octokit , prs30 , labelToOwners , componentLabels , lookbackData . thirtyDaysAgo , lookbackData . midnightYesterday ) ;
547401
548- let lowActivityMarkdown = '' ;
549- if ( REPORT_LOW_ACTIVITY_6MO ) {
550- const prs6Mo = await getPrsInWindowChunked ( octokit , lookbackData . sixMonthsAgo , lookbackData . midnightYesterday ) ;
551- progress ( `Fetched ${ prs6Mo . length } PRs (6 months). Computing low-activity stats...` ) ;
552- const { byCodeOwnerAndComponent : sixMonthPrStats } = await computePrStats ( octokit , prs6Mo , labelToOwners , componentLabels , lookbackData . sixMonthsAgo , lookbackData . midnightYesterday , null ) ;
553- lowActivityMarkdown = formatLowActivityCodeOwners ( sixMonthPrStats ) ;
554- }
555- // const issueStats = await computeIssueStats(octokit, issues, labelToOwners, componentLabels, lookbackData.thirtyDaysAgo, lookbackData.midnightYesterday);
556- const issueStats = { } ;
557-
558- const report = generateReport ( prStats , componentPrStats , issueStats , lookbackData , lowActivityMarkdown ) ;
402+ const report = generateReport ( prStats , componentPrStats , lookbackData ) ;
559403
560404 // Dry run: set DRY_RUN=true or DRY_RUN=1 to print the report without creating an issue.
561405 const dryRun = process . env . DRY_RUN === 'true' || process . env . DRY_RUN === '1' ;
0 commit comments