@@ -655,6 +655,15 @@ class Settings {
655655 await settings . loadConfigs ( )
656656 await settings . updateAll ( )
657657 }
658+
659+ // Sync app installations for affected apps (delta mode)
660+ await settings . syncAppInstallations ( {
661+ appGithub : context . appGithub ,
662+ enterpriseSlug : context . enterpriseSlug ,
663+ changedSubOrgs : subOrgs ,
664+ changedRepos : repos
665+ } )
666+
658667 await settings . handleResults ( )
659668 } catch ( error ) {
660669 settings . logError ( error . message )
@@ -1423,10 +1432,14 @@ class Settings {
14231432 * @param {Array } [options.appChanges] - Pre-computed per-app changes (delta mode)
14241433 */
14251434 async syncAppInstallations ( options = { } ) {
1426- const { appGithub, enterpriseSlug, appChanges } = options
1435+ const { appGithub, enterpriseSlug, appChanges, changedSubOrgs , changedRepos } = options
14271436
14281437 const appInstallationsConfig = this . config . app_installations
1429- if ( ! appInstallationsConfig || ! Array . isArray ( appInstallationsConfig ) || appInstallationsConfig . length === 0 ) {
1438+ // Check if any layer has app_installations config (org, suborg, or repo)
1439+ const hasOrgConfig = appInstallationsConfig && Array . isArray ( appInstallationsConfig ) && appInstallationsConfig . length > 0
1440+ const hasChangedConfigs = ( changedSubOrgs && changedSubOrgs . length > 0 ) || ( changedRepos && changedRepos . length > 0 )
1441+
1442+ if ( ! hasOrgConfig && ! hasChangedConfigs && ( ! appChanges || appChanges . length === 0 ) ) {
14301443 this . log . debug ( 'No app_installations config found, skipping' )
14311444 return
14321445 }
@@ -1462,8 +1475,16 @@ class Settings {
14621475
14631476 let results
14641477 if ( appChanges && appChanges . length > 0 ) {
1465- // Delta mode: process pre -computed changes
1478+ // Pre -computed delta mode
14661479 results = await plugin . syncDelta ( appChanges )
1480+ } else if ( hasChangedConfigs ) {
1481+ // Delta mode: build app changes from changed suborg/repo configs
1482+ const deltaChanges = await this . _buildAppChangesFromDelta ( appGithub , enterpriseSlug , changedSubOrgs , changedRepos )
1483+ if ( deltaChanges . length > 0 ) {
1484+ results = await plugin . syncDelta ( deltaChanges )
1485+ } else {
1486+ results = [ ]
1487+ }
14671488 } else {
14681489 // Full sync mode: compute desired state from all config layers
14691490 const desiredState = await this . _computeFullAppDesiredState ( appInstallationsConfig , appGithub , enterpriseSlug )
@@ -1476,6 +1497,98 @@ class Settings {
14761497 this . appendToResults ( results )
14771498 }
14781499
1500+ /**
1501+ * Build delta-based app changes from changed suborg/repo config files.
1502+ * Extracts app_installations from each changed config and computes
1503+ * repository_selection / repository_unselection per app.
1504+ * @private
1505+ */
1506+ async _buildAppChangesFromDelta ( appGithub , enterpriseSlug , changedSubOrgs = [ ] , changedRepos = [ ] ) {
1507+ const AppOctokitClient = require ( './appOctokitClient' )
1508+ const repoSelector = new RepoSelector ( this . github , this . repo . owner , this . log )
1509+ const appChangeMap = new Map ( ) // app_slug → { installation_id, repository_selection, repository_unselection }
1510+
1511+ // Get installation map (app_slug → installation_id)
1512+ let installationMap = new Map ( )
1513+ if ( appGithub && enterpriseSlug ) {
1514+ try {
1515+ const enterpriseClient = new AppOctokitClient ( { github : appGithub , enterpriseSlug, log : this . log } )
1516+ const orgInstallations = await enterpriseClient . listOrgInstallations ( this . repo . owner )
1517+ for ( const inst of orgInstallations ) {
1518+ installationMap . set ( inst . app_slug , inst . id )
1519+ }
1520+ } catch ( e ) {
1521+ this . log . error ( `Failed to list org installations for delta: ${ e . message } ` )
1522+ return [ ]
1523+ }
1524+ }
1525+
1526+ // Process changed suborg configs
1527+ for ( const suborg of changedSubOrgs ) {
1528+ const suborgConfig = this . subOrgConfigs && this . subOrgConfigs [ suborg . repo ]
1529+ if ( ! suborgConfig || ! suborgConfig . app_installations ) continue
1530+
1531+ // Resolve repos for this suborg's current targeting
1532+ const criteria = { }
1533+ if ( suborgConfig . suborgrepos ) criteria . names = suborgConfig . suborgrepos
1534+ if ( suborgConfig . suborgteams ) criteria . teams = suborgConfig . suborgteams
1535+ if ( suborgConfig . suborgproperties ) criteria . custom_properties = suborgConfig . suborgproperties
1536+
1537+ let suborgRepos = new Set ( )
1538+ try {
1539+ suborgRepos = await repoSelector . resolve ( criteria )
1540+ } catch ( e ) {
1541+ this . log . debug ( `Error resolving suborg repos for '${ suborg . repo } ': ${ e . message } ` )
1542+ }
1543+
1544+ for ( const appConfig of suborgConfig . app_installations ) {
1545+ const slug = appConfig . app_slug
1546+ if ( ! slug ) continue
1547+ const installationId = installationMap . get ( slug )
1548+ if ( ! installationId ) continue
1549+
1550+ if ( ! appChangeMap . has ( slug ) ) {
1551+ appChangeMap . set ( slug , {
1552+ app_slug : slug ,
1553+ installation_id : installationId ,
1554+ repository_selection : new Set ( ) ,
1555+ repository_unselection : new Set ( )
1556+ } )
1557+ }
1558+ const change = appChangeMap . get ( slug )
1559+ for ( const repo of suborgRepos ) {
1560+ change . repository_selection . add ( repo )
1561+ }
1562+ }
1563+ }
1564+
1565+ // Process changed repo configs
1566+ for ( const repo of changedRepos ) {
1567+ const repoConfig = this . repoConfigs &&
1568+ ( this . repoConfigs [ `${ repo . repo } .yml` ] || this . repoConfigs [ `${ repo . repo } .yaml` ] )
1569+ if ( ! repoConfig || ! repoConfig . app_installations ) continue
1570+
1571+ for ( const appConfig of repoConfig . app_installations ) {
1572+ const slug = appConfig . app_slug
1573+ if ( ! slug ) continue
1574+ const installationId = installationMap . get ( slug )
1575+ if ( ! installationId ) continue
1576+
1577+ if ( ! appChangeMap . has ( slug ) ) {
1578+ appChangeMap . set ( slug , {
1579+ app_slug : slug ,
1580+ installation_id : installationId ,
1581+ repository_selection : new Set ( ) ,
1582+ repository_unselection : new Set ( )
1583+ } )
1584+ }
1585+ appChangeMap . get ( slug ) . repository_selection . add ( repo . repo )
1586+ }
1587+ }
1588+
1589+ return [ ...appChangeMap . values ( ) ]
1590+ }
1591+
14791592 /**
14801593 * Compute the full desired state for all managed apps by merging
14811594 * org + suborg + repo level app_installations configs.
0 commit comments