@@ -7,8 +7,8 @@ import { fileURLToPath } from "url";
77
88const REPOS = [
99 { owner : "ArcadeAI" , repo : "docs" , private : false } ,
10- { owner : "ArcadeAI" , repo : "arcade-mcp" , private : false } ,
11- { owner : "ArcadeAI" , repo : "monorepo" , private : true } ,
10+ { owner : "ArcadeAI" , repo : "arcade-mcp" , private : false , productionBranch : "production" } ,
11+ { owner : "ArcadeAI" , repo : "monorepo" , private : true , productionBranch : "production" } ,
1212] ;
1313
1414const CATEGORIES = [
@@ -51,6 +51,7 @@ type PR = {
5151 labels : string [ ] ;
5252 merged_at : string ;
5353 is_private : boolean ;
54+ merge_commit_sha : string ;
5455} ;
5556
5657type CategorizedPR = {
@@ -63,12 +64,6 @@ type CategorizedPR = {
6364 is_private : boolean ;
6465} ;
6566
66- type FinalEntry = {
67- category : string ;
68- type : string ;
69- description : string ;
70- sources : { repo : string ; pr_number : number } [ ] ;
71- } ;
7267
7368// --- Step 1: Compute upcoming Friday (or today if Friday) ---
7469
@@ -105,7 +100,7 @@ async function fetchMergedPRs(
105100 let page = 1 ;
106101
107102 while ( true ) {
108- const url = `https://api.github.com/repos/${ owner } /${ repo } /pulls?state=closed&sort=updated&direction=desc&per_page=100&page=${ page } ` ;
103+ const url = `https://api.github.com/repos/${ owner } /${ repo } /pulls?state=closed&base=main& sort=updated&direction=desc&per_page=100&page=${ page } ` ;
109104 const res = await fetch ( url , {
110105 headers : {
111106 Accept : "application/vnd.github+json" ,
@@ -134,6 +129,7 @@ async function fetchMergedPRs(
134129 labels : pr . labels ?. map ( ( l : any ) => l . name ) || [ ] ,
135130 merged_at : pr . merged_at ,
136131 is_private : isPrivate ,
132+ merge_commit_sha : pr . merge_commit_sha ,
137133 } ) ;
138134 }
139135 }
@@ -199,89 +195,48 @@ async function categorizePR(pr: PR, openai: OpenAI): Promise<CategorizedPR> {
199195 } ;
200196}
201197
202- // --- Step 5: Final combining call ---
203198
204- const COMBINE_SCHEMA = {
205- name : "combined_changelog" ,
206- strict : true ,
207- schema : {
208- type : "object" as const ,
209- properties : {
210- entries : {
211- type : "array" as const ,
212- items : {
213- type : "object" as const ,
214- properties : {
215- category : { type : "string" as const , enum : [ ...CATEGORIES ] } ,
216- type : { type : "string" as const , enum : [ ...TYPES ] } ,
217- description : { type : "string" as const } ,
218- sources : {
219- type : "array" as const ,
220- items : {
221- type : "object" as const ,
222- properties : {
223- repo : { type : "string" as const } ,
224- pr_number : { type : "integer" as const } ,
225- } ,
226- required : [ "repo" , "pr_number" ] as const ,
227- additionalProperties : false ,
228- } ,
229- } ,
230- } ,
231- required : [ "category" , "type" , "description" , "sources" ] as const ,
232- additionalProperties : false ,
233- } ,
234- } ,
235- } ,
236- required : [ "entries" ] as const ,
237- additionalProperties : false ,
238- } ,
239- } ;
240-
241- async function combineEntries (
242- categorized : CategorizedPR [ ] ,
243- openai : OpenAI ,
244- ) : Promise < FinalEntry [ ] > {
245- const model = process . env . OPENAI_MODEL || "gpt-4o-mini" ;
199+ // --- Step 5a: Filter to production-deployed PRs ---
246200
247- const input = categorized
248- . sort ( ( a , b ) => a . merged_at . localeCompare ( b . merged_at ) )
249- . map ( ( pr ) => ( {
250- category : pr . category ,
251- type : pr . type ,
252- description : pr . description ,
253- repo : pr . repo ,
254- pr_number : pr . pr_number ,
255- merged_at : pr . merged_at ,
256- } ) ) ;
201+ async function getUndeployedSHAs (
202+ owner : string ,
203+ repo : string ,
204+ productionBranch : string ,
205+ ) : Promise < Set < string > > {
206+ const token = process . env . GITHUB_TOKEN ;
207+ if ( ! token ) throw new Error ( "GITHUB_TOKEN env var is required" ) ;
257208
258- const res = await openai . chat . completions . create ( {
259- model,
260- response_format : { type : "json_schema" , json_schema : COMBINE_SCHEMA } ,
261- messages : [
262- {
263- role : "system" ,
264- content : [
265- `You are combining changelog entries.` ,
266- `If a docs PR and a non-docs PR are about the same feature, combine them into one entry under the non-docs category.` ,
267- `Do not alter categories or types unless combining.` ,
268- `Keep descriptions concise. Return ALL entries.` ,
269- ] . join ( " " ) ,
270- } ,
271- { role : "user" , content : JSON . stringify ( input ) } ,
272- ] ,
209+ const url = `https://api.github.com/repos/${ owner } /${ repo } /compare/${ productionBranch } ...main` ;
210+ const res = await fetch ( url , {
211+ headers : {
212+ Accept : "application/vnd.github+json" ,
213+ Authorization : `Bearer ${ token } ` ,
214+ "X-GitHub-Api-Version" : "2022-11-28" ,
215+ } ,
273216 } ) ;
274217
275- return JSON . parse ( res . choices [ 0 ] . message . content ! ) . entries ;
218+ if ( ! res . ok ) {
219+ const body = await res . text ( ) ;
220+ throw new Error ( `GitHub compare API error for ${ owner } /${ repo } : ${ res . status } ${ body } ` ) ;
221+ }
222+
223+ const data : any = await res . json ( ) ;
224+ const shas = new Set < string > ( ) ;
225+ for ( const commit of data . commits ) {
226+ shas . add ( commit . sha ) ;
227+ }
228+ return shas ;
276229}
277230
278231// --- Step 6: Format the entry ---
279232
280- function formatEntry ( date : string , entries : FinalEntry [ ] ) : string {
233+ function formatEntry ( date : string , entries : CategorizedPR [ ] ) : string {
281234 const privateRepos = new Set ( REPOS . filter ( ( r ) => r . private ) . map ( ( r ) => r . repo ) ) ;
282235
283- const grouped : Record < string , FinalEntry [ ] > = { } ;
284- for ( const entry of entries ) {
236+ const sorted = [ ...entries ] . sort ( ( a , b ) => a . merged_at . localeCompare ( b . merged_at ) ) ;
237+
238+ const grouped : Record < string , CategorizedPR [ ] > = { } ;
239+ for ( const entry of sorted ) {
285240 if ( ! grouped [ entry . category ] ) grouped [ entry . category ] = [ ] ;
286241 grouped [ entry . category ] . push ( entry ) ;
287242 }
@@ -296,14 +251,7 @@ function formatEntry(date: string, entries: FinalEntry[]): string {
296251
297252 for ( const item of items ) {
298253 const emoji = EMOJI [ item . type ] || EMOJI . maintenance ;
299- const sources = item . sources
300- . map ( ( s ) => {
301- const prefix = privateRepos . has ( s . repo ) ? s . repo : s . repo ;
302- return `${ prefix } PR #${ s . pr_number } ` ;
303- } )
304- . join ( ", " ) ;
305-
306- result += `- \`[${ item . type } - ${ emoji } ]\` ${ item . description } (${ sources } )\n` ;
254+ result += `- \`[${ item . type } - ${ emoji } ]\` ${ item . description } (${ item . repo } PR #${ item . pr_number } )\n` ;
307255 }
308256 }
309257
@@ -351,21 +299,32 @@ async function main() {
351299 ) . flat ( ) ;
352300 console . log ( `Found ${ allPRs . length } merged PRs since ${ lastEntryDate } ` ) ;
353301
354- if ( allPRs . length === 0 ) {
355- console . log ( "No new PRs. Exiting." ) ;
302+ // Step 3a: Filter to only PRs deployed to production
303+ console . log ( "Filtering to production-deployed PRs..." ) ;
304+ const undeployedByRepo : Record < string , Set < string > > = { } ;
305+ await Promise . all (
306+ REPOS . filter ( ( r ) => r . productionBranch ) . map ( async ( r ) => {
307+ undeployedByRepo [ r . repo ] = await getUndeployedSHAs ( r . owner , r . repo , r . productionBranch ! ) ;
308+ } ) ,
309+ ) ;
310+ const deployedPRs = allPRs . filter ( ( pr ) => {
311+ const undeployed = undeployedByRepo [ pr . repo ] ;
312+ if ( ! undeployed ) return true ;
313+ return ! undeployed . has ( pr . merge_commit_sha ) ;
314+ } ) ;
315+ console . log ( `${ deployedPRs . length } PRs are on production` ) ;
316+
317+ if ( deployedPRs . length === 0 ) {
318+ console . log ( "No deployed PRs. Exiting." ) ;
356319 return ;
357320 }
358321
359322 // Step 4
360323 console . log ( "Categorizing PRs..." ) ;
361- const categorized = await Promise . all ( allPRs . map ( ( pr ) => categorizePR ( pr , openai ) ) ) ;
324+ const categorized = await Promise . all ( deployedPRs . map ( ( pr ) => categorizePR ( pr , openai ) ) ) ;
362325
363326 // Step 5
364- console . log ( "Combining related entries..." ) ;
365- const combined = await combineEntries ( categorized , openai ) ;
366-
367- // Step 6
368- const entry = formatEntry ( fridayDate , combined ) ;
327+ const entry = formatEntry ( fridayDate , categorized ) ;
369328 console . log ( "\nGenerated entry:\n" ) ;
370329 console . log ( entry ) ;
371330
0 commit comments