@@ -6,11 +6,16 @@ import fetch from 'node-fetch'
66const ORG = process . env . ORG
77const REPO = process . env . REPO
88const GITHUB_TOKEN = process . env . GITHUB_TOKEN
9+ // Hardcoded endpoints
10+ const BASE_URL = 'https://gov.meshjs.dev/'
11+ const NETLIFY_FUNCTION_URL = 'https://glittering-chebakia-09bd42.netlify.app/.netlify/functions/github-stats-background'
912
1013// Debug logging
1114console . log ( '🔍 Environment variables:' )
1215console . log ( ` ORG: ${ process . env . ORG } ` )
1316console . log ( ` REPO: ${ process . env . REPO } ` )
17+ console . log ( ` BASE_URL: ${ BASE_URL } ` )
18+ console . log ( ` NETLIFY_FUNCTION_URL: ${ NETLIFY_FUNCTION_URL } ` )
1419console . log ( ` GITHUB_TOKEN: ${ GITHUB_TOKEN ? '[REDACTED]' : '(not provided)' } ` )
1520
1621// Input validation function
@@ -42,6 +47,8 @@ function validateInputs() {
4247 errors . push ( 'GITHUB_TOKEN must be a non-empty string' )
4348 }
4449
50+ // BASE_URL and NETLIFY_FUNCTION_URL are hardcoded
51+
4552 return errors
4653}
4754
@@ -58,13 +65,13 @@ if (validationErrors.length > 0) {
5865const parsedOrg = ORG . trim ( )
5966const parsedRepo = REPO . trim ( )
6067const parsedGithubToken = GITHUB_TOKEN . trim ( )
68+ const parsedBaseUrl = BASE_URL . trim ( ) . replace ( / \/ $ / , '' )
69+ const parsedFunctionUrl = NETLIFY_FUNCTION_URL . trim ( )
6170
6271console . log ( '📊 Resolved values:' )
6372console . log ( ` ORG: ${ parsedOrg } ` )
6473console . log ( ` REPO: ${ parsedRepo } ` )
65-
66- // Netlify function configuration - hardcoded URL
67- const NETLIFY_FUNCTION_URL = 'https://glittering-chebakia-09bd42.netlify.app/.netlify/functions/github-stats-background'
74+ console . log ( ` BASE_URL: ${ parsedBaseUrl } ` )
6875
6976// Helper function to make HTTP requests
7077async function makeRequest ( url , options = { } ) {
@@ -103,41 +110,167 @@ async function makeRequest(url, options = {}) {
103110 }
104111}
105112
113+ // Retry helper for GitHub API calls
114+ async function retryWithBackoff ( fn , maxRetries = 5 , initialDelayMs = 1000 ) {
115+ let attempt = 0
116+ let delay = initialDelayMs
117+ // eslint-disable-next-line no-constant-condition
118+ while ( true ) {
119+ try {
120+ return await fn ( )
121+ } catch ( error ) {
122+ const status = error . status || error . code || error ?. response ?. status
123+ if ( attempt < maxRetries && ( status === 403 || status === 429 || ( typeof status === 'number' && status >= 500 ) ) ) {
124+ attempt += 1
125+ console . warn ( `Retrying after error ${ status } (attempt ${ attempt } /${ maxRetries } )...` )
126+ await new Promise ( ( r ) => setTimeout ( r , delay ) )
127+ delay *= 2
128+ continue
129+ }
130+ throw error
131+ }
132+ }
133+ }
134+
135+ function ghHeaders ( ) {
136+ return {
137+ 'Accept' : 'application/vnd.github.v3+json' ,
138+ 'Authorization' : `token ${ parsedGithubToken } `
139+ }
140+ }
141+
142+ async function fetchExistingIds ( ) {
143+ const url = `${ parsedBaseUrl } /api/github/existing-ids?org=${ encodeURIComponent ( parsedOrg ) } &repo=${ encodeURIComponent ( parsedRepo ) } `
144+ return await makeRequest ( url , { method : 'GET' } )
145+ }
146+
147+ async function fetchAllMissingCommits ( existingShasSet ) {
148+ const results = [ ]
149+ let page = 1
150+ while ( true ) {
151+ const data = await retryWithBackoff ( async ( ) => {
152+ const resp = await fetch ( `https://api.github.com/repos/${ parsedOrg } /${ parsedRepo } /commits?per_page=100&page=${ page } ` , { headers : ghHeaders ( ) } )
153+ if ( ! resp . ok ) throw { status : resp . status , message : await resp . text ( ) }
154+ return resp . json ( )
155+ } )
156+ if ( ! Array . isArray ( data ) || data . length === 0 ) break
157+ for ( const item of data ) {
158+ if ( ! existingShasSet . has ( item . sha ) ) {
159+ // Ensure we have full commit details
160+ const details = await retryWithBackoff ( async ( ) => {
161+ const resp = await fetch ( `https://api.github.com/repos/${ parsedOrg } /${ parsedRepo } /commits/${ item . sha } ` , { headers : ghHeaders ( ) } )
162+ if ( ! resp . ok ) throw { status : resp . status , message : await resp . text ( ) }
163+ return resp . json ( )
164+ } )
165+ results . push ( details )
166+ }
167+ }
168+ page += 1
169+ }
170+ return results
171+ }
172+
173+ async function fetchAllMissingPulls ( existingNumbersSet ) {
174+ const results = [ ]
175+ let page = 1
176+ while ( true ) {
177+ const data = await retryWithBackoff ( async ( ) => {
178+ const resp = await fetch ( `https://api.github.com/repos/${ parsedOrg } /${ parsedRepo } /pulls?state=all&per_page=100&page=${ page } ` , { headers : ghHeaders ( ) } )
179+ if ( ! resp . ok ) throw { status : resp . status , message : await resp . text ( ) }
180+ return resp . json ( )
181+ } )
182+ if ( ! Array . isArray ( data ) || data . length === 0 ) break
183+ for ( const pr of data ) {
184+ if ( ! existingNumbersSet . has ( pr . number ) ) {
185+ const prDetails = await retryWithBackoff ( async ( ) => {
186+ const resp = await fetch ( `https://api.github.com/repos/${ parsedOrg } /${ parsedRepo } /pulls/${ pr . number } ` , { headers : ghHeaders ( ) } )
187+ if ( ! resp . ok ) throw { status : resp . status , message : await resp . text ( ) }
188+ return resp . json ( )
189+ } )
190+ const prCommits = await retryWithBackoff ( async ( ) => {
191+ const resp = await fetch ( `https://api.github.com/repos/${ parsedOrg } /${ parsedRepo } /pulls/${ pr . number } /commits` , { headers : ghHeaders ( ) } )
192+ if ( ! resp . ok ) throw { status : resp . status , message : await resp . text ( ) }
193+ return resp . json ( )
194+ } )
195+ results . push ( { details : prDetails , commits : prCommits } )
196+ }
197+ }
198+ page += 1
199+ }
200+ return results
201+ }
202+
203+ async function fetchAllMissingIssues ( existingNumbersSet ) {
204+ const results = [ ]
205+ let page = 1
206+ while ( true ) {
207+ const data = await retryWithBackoff ( async ( ) => {
208+ const resp = await fetch ( `https://api.github.com/repos/${ parsedOrg } /${ parsedRepo } /issues?state=all&per_page=100&page=${ page } ` , { headers : ghHeaders ( ) } )
209+ if ( ! resp . ok ) throw { status : resp . status , message : await resp . text ( ) }
210+ return resp . json ( )
211+ } )
212+ if ( ! Array . isArray ( data ) || data . length === 0 ) break
213+ for ( const issue of data ) {
214+ if ( issue . pull_request ) continue // skip PRs
215+ if ( ! existingNumbersSet . has ( issue . number ) ) {
216+ results . push ( issue )
217+ }
218+ }
219+ page += 1
220+ }
221+ return results
222+ }
223+
106224// Main function
107225async function main ( ) {
108226 console . log ( `🚀 Starting GitHub stats collection for repository: ${ parsedOrg } /${ parsedRepo } ` )
109227
110228 // Trigger the Netlify background function
111- console . log ( '📡 Triggering Netlify background function...' )
229+ console . log ( '📥 Fetching existing IDs from mesh-gov API...' )
230+ const existing = await fetchExistingIds ( )
231+ const existingCommitShas = new Set ( existing . commitShas || [ ] )
232+ const existingPrNumbers = new Set ( existing . prNumbers || [ ] )
233+ const existingIssueNumbers = new Set ( existing . issueNumbers || [ ] )
112234
113- const functionUrl = new URL ( NETLIFY_FUNCTION_URL )
114- functionUrl . searchParams . set ( 'org' , parsedOrg )
115- functionUrl . searchParams . set ( 'repo' , parsedRepo )
116- functionUrl . searchParams . set ( 'githubToken' , parsedGithubToken )
235+ console . log ( `🔎 Found existing: commits=${ existingCommitShas . size } , PRs=${ existingPrNumbers . size } , issues=${ existingIssueNumbers . size } ` )
117236
118- console . log ( `🌐 Calling URL: ${ functionUrl . toString ( ) . replace ( parsedGithubToken , '[REDACTED]' ) } ` )
237+ console . log ( '⬇️ Fetching missing commits from GitHub...' )
238+ const missingCommits = await fetchAllMissingCommits ( existingCommitShas )
239+ console . log ( `🧮 Missing commits: ${ missingCommits . length } ` )
119240
120- try {
121- const functionResponse = await makeRequest ( functionUrl . toString ( ) , {
122- method : 'GET'
123- } )
241+ console . log ( '⬇️ Fetching missing pull requests from GitHub...' )
242+ const missingPulls = await fetchAllMissingPulls ( existingPrNumbers )
243+ console . log ( `🧮 Missing PRs: ${ missingPulls . length } ` )
124244
125- console . log ( '✅ Background function triggered successfully' )
126- console . log ( `📊 Response: ${ JSON . stringify ( functionResponse , null , 2 ) } ` )
127-
128- // If the response indicates success (even if it's not JSON), we're done
129- if ( functionResponse . success !== false ) {
130- console . log ( '✅ Background function appears to have been triggered successfully' )
131- console . log ( '🎉 GitHub stats collection initiated - the background function will handle the rest' )
132- process . exit ( 0 )
133- } else {
134- console . error ( '❌ Background function returned an error' )
135- process . exit ( 1 )
136- }
137- } catch ( error ) {
138- console . error ( '❌ Failed to trigger background function:' , error . message )
139- process . exit ( 1 )
245+ console . log ( '⬇️ Fetching missing issues from GitHub...' )
246+ const missingIssues = await fetchAllMissingIssues ( existingIssueNumbers )
247+ console . log ( `🧮 Missing issues: ${ missingIssues . length } ` )
248+
249+ const payload = {
250+ org : parsedOrg ,
251+ repo : parsedRepo ,
252+ commits : missingCommits ,
253+ pulls : missingPulls ,
254+ issues : missingIssues ,
140255 }
256+
257+ if (
258+ ( missingCommits && missingCommits . length ) ||
259+ ( missingPulls && missingPulls . length ) ||
260+ ( missingIssues && missingIssues . length )
261+ ) {
262+ console . log ( '📡 Sending new data to Netlify background function...' )
263+ const response = await makeRequest ( parsedFunctionUrl , {
264+ method : 'POST' ,
265+ body : JSON . stringify ( payload ) ,
266+ } )
267+ console . log ( `✅ Background function response: ${ JSON . stringify ( response , null , 2 ) } ` )
268+ } else {
269+ console . log ( '✅ No new data to send' )
270+ }
271+
272+ console . log ( '🎉 Done' )
273+ process . exit ( 0 )
141274}
142275
143276// Run the main function
0 commit comments