@@ -255,7 +255,7 @@ export default async function deployGitHubRoutes(server: FastifyInstance) {
255255 schema : {
256256 tags : [ 'Deployment' ] ,
257257 summary : 'Check GitHub App installation status' ,
258- description : 'Returns whether the team has installed the GitHub App' ,
258+ description : 'Returns whether the team has an active GitHub App installation. Verifies against GitHub API and auto-cleans stale records. ' ,
259259 security : [ { cookieAuth : [ ] } ] ,
260260 params : {
261261 type : 'object' ,
@@ -294,11 +294,126 @@ export default async function deployGitHubRoutes(server: FastifyInstance) {
294294
295295 const { teamId } = request . params as { teamId : string } ;
296296
297- const hasInstallation = await credentialService . hasInstallation ( teamId , 'github' ) ;
297+ // Step 1: Check database for installation record
298+ let installation = await credentialService . getInstallation ( teamId , 'github' ) ;
299+
300+ if ( ! installation ) {
301+ // No database record - try to auto-detect and link
302+ try {
303+ const installations = await githubService . listInstallations ( ) ;
304+
305+ if ( installations . length > 0 ) {
306+ // Found installations - select the best one (most recent, not suspended)
307+ const selectedInstallation = installations [ 0 ] ;
308+
309+ // Auto-link to this team
310+ await credentialService . storeInstallation ( {
311+ teamId,
312+ source : 'github' ,
313+ installationId : selectedInstallation . id . toString ( )
314+ } ) ;
315+
316+ server . log . info ( {
317+ teamId,
318+ installation_id : selectedInstallation . id ,
319+ account_login : selectedInstallation . account . login ,
320+ total_installations_found : installations . length ,
321+ operation : 'auto_linked_installation'
322+ } , 'Auto-linked GitHub installation to team' ) ;
323+
324+ if ( installations . length > 1 ) {
325+ server . log . info ( {
326+ teamId,
327+ total_installations : installations . length ,
328+ selected_installation_id : selectedInstallation . id ,
329+ selected_account : selectedInstallation . account . login ,
330+ operation : 'multiple_installations_found'
331+ } , 'Multiple GitHub installations found, selected most recent' ) ;
332+ }
333+
334+ // Return connected
335+ const response : ConnectionStatusResponse = { connected : true } ;
336+ const jsonString = JSON . stringify ( response ) ;
337+ return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
338+ }
298339
299- const response : ConnectionStatusResponse = { connected : hasInstallation } ;
300- const jsonString = JSON . stringify ( response ) ;
301- return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
340+ // No installations found
341+ server . log . info ( {
342+ teamId,
343+ operation : 'no_installations_found'
344+ } , 'No GitHub installations found for user' ) ;
345+
346+ const response : ConnectionStatusResponse = { connected : false } ;
347+ const jsonString = JSON . stringify ( response ) ;
348+ return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
349+ } catch ( error ) {
350+ // Failed to list installations - log and return false
351+ server . log . warn ( { error, teamId } , 'Failed to list GitHub installations during auto-detection' ) ;
352+ const response : ConnectionStatusResponse = { connected : false } ;
353+ const jsonString = JSON . stringify ( response ) ;
354+ return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
355+ }
356+ }
357+
358+ // Step 2: Verify installation still exists on GitHub
359+ try {
360+ const isValid = await githubService . verifyInstallation ( installation . installationId ) ;
361+
362+ if ( ! isValid ) {
363+ // Installation no longer exists on GitHub - clean up stale record
364+ await credentialService . deleteInstallation ( teamId , 'github' ) ;
365+
366+ server . log . info ( {
367+ teamId,
368+ installation_id : installation . installationId ,
369+ operation : 'stale_installation_cleaned'
370+ } , 'Cleaned up stale GitHub installation record' ) ;
371+
372+ // After cleaning up stale record, retry auto-detection
373+ try {
374+ const installations = await githubService . listInstallations ( ) ;
375+
376+ if ( installations . length > 0 ) {
377+ const selectedInstallation = installations [ 0 ] ;
378+
379+ await credentialService . storeInstallation ( {
380+ teamId,
381+ source : 'github' ,
382+ installationId : selectedInstallation . id . toString ( )
383+ } ) ;
384+
385+ server . log . info ( {
386+ teamId,
387+ installation_id : selectedInstallation . id ,
388+ account_login : selectedInstallation . account . login ,
389+ total_installations_found : installations . length ,
390+ operation : 'auto_linked_installation_after_cleanup'
391+ } , 'Auto-linked GitHub installation to team after cleaning stale record' ) ;
392+
393+ const response : ConnectionStatusResponse = { connected : true } ;
394+ const jsonString = JSON . stringify ( response ) ;
395+ return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
396+ }
397+ } catch ( retryError ) {
398+ server . log . warn ( { error : retryError , teamId } , 'Failed to auto-detect installation after cleanup' ) ;
399+ }
400+
401+ const response : ConnectionStatusResponse = { connected : false } ;
402+ const jsonString = JSON . stringify ( response ) ;
403+ return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
404+ }
405+
406+ // Installation is valid and active
407+ const response : ConnectionStatusResponse = { connected : true } ;
408+ const jsonString = JSON . stringify ( response ) ;
409+ return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
410+ } catch ( error ) {
411+ // GitHub API error - return cached state optimistically
412+ server . log . warn ( { error, teamId } , 'GitHub API verification failed, returning cached state' ) ;
413+ const response : ConnectionStatusResponse = { connected : true } ;
414+ const jsonString = JSON . stringify ( response ) ;
415+ return reply . status ( 200 ) . type ( 'application/json' ) . send ( jsonString ) ;
416+ }
302417 } ) ;
303418
304419 // GET /api/teams/{teamId}/deploy/github/repositories
0 commit comments