Skip to content

Commit 99cd47e

Browse files
committed
Enhance app installations plugin: add subject handling and improve repo selection logic
1 parent 1364b09 commit 99cd47e

4 files changed

Lines changed: 498 additions & 17 deletions

File tree

lib/nopcommand.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
class NopCommand {
2-
constructor (pluginName, repo, endpoint, action, type = 'INFO') {
2+
constructor (pluginName, repo, endpoint, action, type = 'INFO', subject = null) {
3+
// Ergonomic overload: allow passing the subject in the `type` position so
4+
// callers can supply a subject while omitting the (default 'INFO') type,
5+
// e.g. new NopCommand(plugin, repo, endpoint, action, { name, type }).
6+
if (type !== null && typeof type === 'object') {
7+
subject = type
8+
type = 'INFO'
9+
}
310
this.type = type
411
this.plugin = pluginName
512
this.repo = repo.repo
13+
// Optional presentation subject. Some plugins (e.g. app_installations)
14+
// operate on a non-repo entity — the "subject" of the change is a GitHub
15+
// App, not a repository. `subject` overrides the repo as the heading in the
16+
// PR-comment/check-run report, while `repo` is still used for grouping and
17+
// repo counts. Defaults to the repo so existing plugins are unaffected.
18+
this.subject = (subject && subject.name) || repo.repo
19+
this.subjectType = (subject && subject.type) || 'repo'
620
this.endpoint = endpoint ? endpoint.url : ''
721
this.body = endpoint ? endpoint.body : ''
822
// check if action is a string

lib/plugins/appInstallations.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ class AppInstallations {
152152
additions: ['(all repositories)'],
153153
modifications: null,
154154
deletions: null
155-
}))
155+
}, { name: appSlug, type: 'app' }))
156156
return results
157157
}
158158
await this.enterpriseClient.setRepositorySelection(this.org, installation_id, 'all')
@@ -181,7 +181,7 @@ class AppInstallations {
181181
additions: [...desiredNames],
182182
modifications: null,
183183
deletions: ['(all repositories)']
184-
}))
184+
}, { name: appSlug, type: 'app' }))
185185
return results
186186
}
187187
await this.enterpriseClient.setRepositorySelection(this.org, installation_id, 'selected', [...desiredNames])
@@ -209,7 +209,7 @@ class AppInstallations {
209209
additions: toAdd.length > 0 ? toAdd : null,
210210
modifications: null,
211211
deletions: toRemove.length > 0 ? toRemove : null
212-
}))
212+
}, { name: appSlug, type: 'app' }))
213213
return results
214214
}
215215

@@ -265,7 +265,8 @@ class AppInstallations {
265265
additions: ['(all repositories)'],
266266
modifications: null,
267267
deletions: null
268-
}
268+
},
269+
{ name: app_slug, type: 'app' }
269270
))
270271
return results
271272
}
@@ -288,7 +289,8 @@ class AppInstallations {
288289
additions,
289290
modifications: null,
290291
deletions
291-
}
292+
},
293+
{ name: app_slug, type: 'app' }
292294
))
293295
return results
294296
}

lib/settings.js

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,19 @@ function filterActionByChangedNames (action, changedNames) {
170170

171171
function 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+
214247
function 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

227268
function 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

233278
function 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

Comments
 (0)