66 * Runs on the first Monday of each month
77 */
88
9- import { fetchProjectItems , filterByStatus } from "./lib/graphql-queries.js" ;
9+ import {
10+ fetchProjectItems ,
11+ filterByStatus ,
12+ fetchClosedItemsFromRepo ,
13+ } from "./lib/graphql-queries.js" ;
1014import { updateContributorHistory , isBot } from "./lib/contributor-tracker.js" ;
1115import { generateReportMarkdown } from "./lib/markdown-generator.js" ;
1216import { getCategoryForLabel } from "./lib/label-mapping.js" ;
17+ import { MONITORED_REPOS } from "./lib/monitored-repos.js" ;
1318import { format , parseISO , isWithinInterval } from "date-fns" ;
1419import { writeFile } from "fs/promises" ;
1520
@@ -137,11 +142,11 @@ async function generateReport() {
137142 try {
138143 // Fetch project board data
139144 log . info ( "Fetching project board data..." ) ;
140- const allItems = await fetchProjectItems ( "projectbluefin" , 2 ) ;
141- log . info ( `Total items on board: ${ allItems . length } ` ) ;
145+ const boardItems = await fetchProjectItems ( "projectbluefin" , 2 ) ;
146+ log . info ( `Total items on board: ${ boardItems . length } ` ) ;
142147
143148 // Filter by Status="Done" column
144- const doneItems = filterByStatus ( allItems , "Done" ) ;
149+ const doneItems = filterByStatus ( boardItems , "Done" ) ;
145150 log . info ( `Items in "Done" column: ${ doneItems . length } ` ) ;
146151
147152 // Filter by date range (items updated within window)
@@ -150,34 +155,92 @@ async function generateReport() {
150155 . filter ( ( item ) => item . content && item . content . title && item . content . url ) ; // Skip items without valid content
151156 log . info ( `Items completed in window: ${ itemsInWindow . length } ` ) ;
152157
158+ // Fetch opportunistic work from monitored repositories
159+ log . info ( "Fetching opportunistic work from monitored repositories..." ) ;
160+ const allOpportunisticItems = [ ] ;
161+
162+ for ( const repo of MONITORED_REPOS ) {
163+ const [ owner , name ] = repo . split ( "/" ) ;
164+ log . info ( ` Fetching from ${ repo } ...` ) ;
165+ const repoItems = await fetchClosedItemsFromRepo (
166+ owner ,
167+ name ,
168+ startDate ,
169+ endDate ,
170+ ) ;
171+ allOpportunisticItems . push ( ...repoItems ) ;
172+ }
173+
174+ log . info (
175+ `Total closed items from monitored repos: ${ allOpportunisticItems . length } ` ,
176+ ) ;
177+
178+ // Extract URLs from project board items to identify opportunistic work
179+ const boardItemUrls = new Set (
180+ itemsInWindow . map ( ( item ) => item . content ?. url ) . filter ( Boolean ) ,
181+ ) ;
182+
183+ // Filter opportunistic items (not on project board)
184+ const opportunisticItems = allOpportunisticItems
185+ . filter ( ( item ) => ! boardItemUrls . has ( item . url ) )
186+ . map ( ( item ) => {
187+ // Transform to match board item structure for consistency
188+ return {
189+ content : {
190+ __typename : item . type , // "Issue" or "PullRequest"
191+ number : item . number ,
192+ title : item . title ,
193+ url : item . url ,
194+ repository : { nameWithOwner : item . repository } ,
195+ labels : { nodes : item . labels } ,
196+ author : { login : item . author } ,
197+ } ,
198+ } ;
199+ } ) ;
200+
201+ log . info (
202+ `Opportunistic items (not on board): ${ opportunisticItems . length } ` ,
203+ ) ;
204+
153205 // Handle empty data period
154- if ( itemsInWindow . length === 0 ) {
206+ if ( itemsInWindow . length === 0 && opportunisticItems . length === 0 ) {
155207 log . warn (
156208 "No items completed in this period - generating quiet period report" ,
157209 ) ;
158210 github . warning ( "This was a quiet period with no completed items" ) ;
159211 }
160212
161- // Separate human contributions from bot activity
162- const humanItems = itemsInWindow . filter (
213+ // Separate human contributions from bot activity (both planned and opportunistic)
214+ const allItems = [ ...itemsInWindow , ...opportunisticItems ] ;
215+ const humanItems = allItems . filter (
163216 ( item ) => ! isBot ( item . content ?. author ?. login || "" ) ,
164217 ) ;
165- const botItems = itemsInWindow . filter ( ( item ) =>
218+ const botItems = allItems . filter ( ( item ) =>
166219 isBot ( item . content ?. author ?. login || "" ) ,
167220 ) ;
168221
169- log . info ( `Human contributions: ${ humanItems . length } ` ) ;
222+ // Separate planned vs opportunistic within human items
223+ const plannedHumanItems = itemsInWindow . filter (
224+ ( item ) => ! isBot ( item . content ?. author ?. login || "" ) ,
225+ ) ;
226+ const opportunisticHumanItems = opportunisticItems . filter (
227+ ( item ) => ! isBot ( item . content ?. author ?. login || "" ) ,
228+ ) ;
229+
230+ log . info ( `Planned work (human): ${ plannedHumanItems . length } ` ) ;
231+ log . info ( `Opportunistic work (human): ${ opportunisticHumanItems . length } ` ) ;
170232 log . info ( `Bot contributions: ${ botItems . length } ` ) ;
171233
172- // Extract contributor usernames (human only)
234+ // Extract contributor usernames (human only, PRs only - people who wrote code )
173235 const contributors = [
174236 ...new Set (
175237 humanItems
238+ . filter ( ( item ) => item . content ?. __typename === "PullRequest" )
176239 . map ( ( item ) => item . content ?. author ?. login )
177240 . filter ( ( login ) => login ) ,
178241 ) ,
179242 ] ;
180- log . info ( `Unique contributors: ${ contributors . length } ` ) ;
243+ log . info ( `Unique contributors (PR authors) : ${ contributors . length } ` ) ;
181244
182245 // Track contributors and identify new ones (with error handling)
183246 log . info ( "Updating contributor history..." ) ;
@@ -204,7 +267,8 @@ async function generateReport() {
204267 // Generate markdown
205268 log . info ( "Generating markdown..." ) ;
206269 const markdown = generateReportMarkdown (
207- humanItems ,
270+ plannedHumanItems ,
271+ opportunisticHumanItems ,
208272 contributors ,
209273 newContributors ,
210274 botActivity ,
@@ -217,14 +281,15 @@ async function generateReport() {
217281 await writeFile ( filename , markdown , "utf8" ) ;
218282
219283 log . info ( `✅ Report generated: ${ filename } ` ) ;
220- log . info ( ` ${ humanItems . length } items completed` ) ;
284+ log . info ( ` ${ plannedHumanItems . length } planned work items` ) ;
285+ log . info ( ` ${ opportunisticHumanItems . length } opportunistic work items` ) ;
221286 log . info ( ` ${ contributors . length } contributors` ) ;
222287 log . info ( ` ${ newContributors . length } new contributors` ) ;
223288 log . info ( ` ${ botItems . length } bot PRs` ) ;
224289
225290 // GitHub Actions summary annotation
226291 github . notice (
227- `Report generated: ${ humanItems . length } items , ${ contributors . length } contributors, ${ newContributors . length } new` ,
292+ `Report generated: ${ plannedHumanItems . length } planned + ${ opportunisticHumanItems . length } opportunistic , ${ contributors . length } contributors, ${ newContributors . length } new` ,
228293 ) ;
229294 } catch ( error ) {
230295 log . error ( "Report generation failed" ) ;
0 commit comments