@@ -170,14 +170,19 @@ function filterActionByChangedNames (action, changedNames) {
170170
171171function buildChangeSections ( changes , baseConfig , config ) {
172172 return Object . keys ( changes ) . map ( plugin => {
173+ const isAppInstallations = plugin === 'app_installations'
173174 const repoSections = [ ]
174175 Object . keys ( changes [ plugin ] ) . forEach ( repo => {
175176 const targetMap = new Map ( )
176177 changes [ plugin ] [ repo ] . forEach ( action => {
177- targetsForAction ( plugin , repo , action , baseConfig , config ) . forEach ( target => {
178+ const actionTargets = isAppInstallations
179+ ? appInstallationTargets ( action )
180+ : targetsForAction ( plugin , repo , action , baseConfig , config )
181+ actionTargets . forEach ( target => {
178182 if ( ! targetMap . has ( target . target ) ) {
179183 targetMap . set ( target . target , {
180184 target : target . target ,
185+ flat : target . flat === true ,
181186 rows : [ ]
182187 } )
183188 }
@@ -194,11 +199,17 @@ function buildChangeSections (changes, baseConfig, config) {
194199 const changeCount = filteredRepoSections . reduce ( ( count , repoSection ) => {
195200 return count + repoSection . targets . reduce ( ( targetCount , target ) => targetCount + target . rows . length , 0 )
196201 } , 0 )
197- const targetCount = filteredRepoSections . reduce ( ( count , repoSection ) => count + repoSection . targets . length , 0 )
202+ // For flat targets (app_installations) each row is a distinct change; for
203+ // regular targets each target counts as one changed setting.
204+ const targetCount = filteredRepoSections . reduce ( ( count , repoSection ) => {
205+ return count + repoSection . targets . reduce ( ( tc , target ) => tc + ( target . flat ? target . rows . length : 1 ) , 0 )
206+ } , 0 )
198207 const repoCount = filteredRepoSections . length
208+ const subjectSingular = isAppInstallations ? 'app' : 'repo'
209+ const subjectPlural = isAppInstallations ? 'apps' : 'repos'
199210 const targetSingular = plugin . toLowerCase ( ) === 'rulesets' ? 'policy' : 'setting'
200211 const targetPlural = plugin . toLowerCase ( ) === 'rulesets' ? 'policies' : 'settings'
201- const impactSummary = `${ repoCount } ${ pluralize ( repoCount , 'repo' , 'repos' ) } , ${ targetCount } ${ pluralize ( targetCount , targetSingular , targetPlural ) } changed`
212+ const impactSummary = `${ repoCount } ${ pluralize ( repoCount , subjectSingular , subjectPlural ) } , ${ targetCount } ${ pluralize ( targetCount , targetSingular , targetPlural ) } changed`
202213 return {
203214 plugin,
204215 repoSections : filteredRepoSections ,
@@ -211,10 +222,40 @@ function buildChangeSections (changes, baseConfig, config) {
211222 } ) . filter ( section => section . repoSections . length > 0 )
212223}
213224
225+ // app_installations changes are presented with the GitHub App as the subject
226+ // (heading) and a flat list of repositories added/removed (or a toggle to
227+ // "all"). Returns a single flat target whose rows carry a `label` per change.
228+ function appInstallationTargets ( action ) {
229+ if ( ! action || typeof action === 'string' ) {
230+ return [ { target : '' , flat : true , rows : action ? [ { change : 'Info' , label : action } ] : [ ] } ]
231+ }
232+ const toList = value => {
233+ if ( value === null || value === undefined ) return [ ]
234+ const arr = Array . isArray ( value ) ? value : [ value ]
235+ return arr
236+ . filter ( entry => ! isDeepEmpty ( entry ) )
237+ . map ( entry => ( typeof entry === 'string' ? entry : ( getEntryIdentityValue ( entry ) || JSON . stringify ( entry ) ) ) )
238+ }
239+ const rows = [ ]
240+ toList ( action . additions ) . forEach ( label => rows . push ( { change : 'Added' , label } ) )
241+ toList ( action . modifications ) . forEach ( label => rows . push ( { change : 'Modified' , label } ) )
242+ toList ( action . deletions ) . forEach ( label => rows . push ( { change : 'Deleted' , label } ) )
243+ if ( rows . length === 0 && action . msg ) rows . push ( { change : 'Info' , label : action . msg } )
244+ return [ { target : '' , flat : true , rows } ]
245+ }
246+
214247function renderChangeSections ( changeSections ) {
215248 return changeSections . map ( section => {
216249 const repoBlocks = section . repoSections . map ( repoSection => {
217250 const targetBlocks = repoSection . targets . map ( target => {
251+ if ( target . flat ) {
252+ return target . rows . map ( row => {
253+ const marker = changeMarker ( row . change )
254+ return row . change === 'Info'
255+ ? `- ${ marker } ${ markdownText ( row . label ) } `
256+ : `- ${ marker } ${ markdownInlineCode ( row . label ) } `
257+ } ) . join ( '\n' )
258+ }
218259 return `- ${ markdownInlineCode ( target . target ) } \n${ renderFieldChangeList ( target . rows , ' ' ) } `
219260 } )
220261 return `**${ markdownText ( displayRepoName ( repoSection . repo ) ) } **\n${ targetBlocks . join ( '\n' ) } `
@@ -225,9 +266,13 @@ function renderChangeSections (changeSections) {
225266}
226267
227268function affectedRepoCount ( changeSections ) {
228- return new Set ( changeSections . flatMap ( section => {
229- return section . repoSections . map ( repoSection => displayRepoName ( repoSection . repo ) )
230- } ) ) . size
269+ return new Set ( changeSections
270+ // app_installations sections are keyed by app subject, not repositories,
271+ // so they must not inflate the "repos affected" count.
272+ . filter ( section => section . plugin !== 'app_installations' )
273+ . flatMap ( section => {
274+ return section . repoSections . map ( repoSection => displayRepoName ( repoSection . repo ) )
275+ } ) ) . size
231276}
232277
233278function displayRepoName ( repo ) {
@@ -1103,10 +1148,14 @@ class Settings {
11031148 if ( ! stats . changes [ res . plugin ] ) {
11041149 stats . changes [ res . plugin ] = { }
11051150 }
1106- if ( ! stats . changes [ res . plugin ] [ res . repo ] ) {
1107- stats . changes [ res . plugin ] [ res . repo ] = [ ]
1151+ // Group by the result's subject (defaults to the repo). Plugins that
1152+ // act on a non-repo entity — e.g. app_installations, whose subject is
1153+ // a GitHub App — group under that subject instead of the (org) repo.
1154+ const subject = res . subject || res . repo
1155+ if ( ! stats . changes [ res . plugin ] [ subject ] ) {
1156+ stats . changes [ res . plugin ] [ subject ] = [ ]
11081157 }
1109- stats . changes [ res . plugin ] [ res . repo ] . push ( res . action )
1158+ stats . changes [ res . plugin ] [ subject ] . push ( res . action )
11101159 }
11111160 }
11121161 } )
@@ -2185,6 +2234,11 @@ class Settings {
21852234 if ( section !== 'repositories' && section !== 'repository' ) {
21862235 // Ignore any config that is not a plugin
21872236 if ( section in Settings . PLUGINS ) {
2237+ // app_installations is not a per-repo Diffable plugin; it operates at
2238+ // the org level on app installations and is reconciled separately by
2239+ // syncAppInstallations(). Skip it here so the per-repo pipeline does
2240+ // not try to call the (non-existent) sync() on it.
2241+ if ( section === 'app_installations' ) continue
21882242 this . log . debug ( `Found section ${ section } in the config. Creating plugin...` )
21892243 const Plugin = Settings . PLUGINS [ section ]
21902244 // Include sectionName as 3rd element so callers can thread the
0 commit comments