Skip to content

Commit

Permalink
feat: prde-1918 Configure review request delegation for teams (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
stevoland authored Jan 13, 2025
2 parents d07de85 + 26398e1 commit d1b760f
Show file tree
Hide file tree
Showing 8 changed files with 914 additions and 104 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
## Internal changes

- Don't remove existing collaborators, teams on sync
- Added support for custom team roles
- Fixed test config, skipped failing unit tests
- Support is_alphanumeric on autolink references
- <strike>Fixed test config, skipped failing unit tests</strike> (Pushed upstream)
- <strike>Support is_alphanumeric on autolink references</strike> (Pushed upstream)
- Added metrics from github api requests
- Ignore archived repos to prevent errors trying to apply settings
- Disabled environments due to https://github.com/github/safe-settings/issues/551
- Disabled validator due to app crash if a validator throws
- Disabled milestones due to unit test indicating it's broken
- Added `reviewRequestDelegation` for teams

[![Create a release](https://github.com/github/safe-settings/actions/workflows/create-release.yml/badge.svg)](https://github.com/github/safe-settings/actions/workflows/create-release.yml)

Expand Down
88 changes: 84 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
}
}

async function syncTeamSettings (nop, context, repo = context.repo(), ref) {
try {
deploymentConfig = await loadYamlFileSystem()
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
const configManager = new ConfigManager(context, ref)
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
const config = Object.assign({}, deploymentConfig, runtimeConfig)
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
return Settings.syncTeams(nop, context, repo, config, ref)
} catch (e) {
if (nop) {
let filename = env.SETTINGS_FILE_PATH
if (!deploymentConfig) {
filename = env.DEPLOYMENT_CONFIG_FILE
deploymentConfig = {}
}
const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR')
robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`)
Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand)
} else {
throw e
}
}
}

/**
* Loads the deployment config file from file system
* Do this once when the app starts and then return the cached value
Expand Down Expand Up @@ -157,6 +182,27 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
return configs
}

function getAllChangedTeamConfigs (payload, owner) {
const settingPattern = new Glob('.github/teams/*.yml')
const added = payload.commits.map(c => {
return (c.added.filter(s => {
robot.log.debug(JSON.stringify(s))
return (s.search(settingPattern) >= 0)
}))
}).flat(2)
const modified = payload.commits.map(c => {
return (c.modified.filter(s => {
robot.log.debug(JSON.stringify(s))
return (s.search(settingPattern) >= 0)
}))
}).flat(2)
const changes = added.concat(modified)
return changes.map(file => {
robot.log.debug(`${JSON.stringify(file)}`)
return { team: file.match(settingPattern)[1], owner }
})
}

function getChangedRepoConfigName (glob, files, owner) {
const modifiedFiles = files.filter(s => {
robot.log.debug(JSON.stringify(s))
Expand Down Expand Up @@ -240,6 +286,13 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
return syncAllSettings(false, context)
}

const teamChanges = getAllChangedTeamConfigs(payload, context.repo().owner)
if (teamChanges.length > 0) {
return Promise.all(teamChanges.map(repo => {
return syncTeamSettings(false, context, repo)
}))
}

const repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner)
if (repoChanges.length > 0) {
return Promise.all(repoChanges.map(repo => {
Expand Down Expand Up @@ -310,8 +363,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
const member_change_events = [
'member',
'team.added_to_repository',
'team.removed_from_repository',
'team.edited'
'team.removed_from_repository'
]

robot.on(member_change_events, async context => {
Expand Down Expand Up @@ -471,17 +523,28 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
return syncAllSettings(true, context, context.repo(), pull_request.head.ref)
}

const teamChanges = getChangedRepoConfigName(new Glob('.github/teams/*.yml'), files, context.repo().owner)
const teamModified = teamChanges.length > 0

const repoChanges = getChangedRepoConfigName(new Glob('.github/repos/*.yml'), files, context.repo().owner)
const repoModified = repoChanges.length > 0

const subOrgChanges = getChangedSubOrgConfigName(new Glob('.github/suborgs/*.yml'), files, context.repo().owner)
const subOrgModified = subOrgChanges.length > 0

if (repoModified && subOrgModified) {
robot.log.debug('Both repo and subOrg change detected, doing a full synch...')
if (
(repoModified && subOrgModified) ||
(repoModified && teamModified) ||
(subOrgModified && teamModified)
) {
robot.log.debug('Changes detected across suborg/repos/teams folders, doing a full synch...')
return syncAllSettings(true, context, context.repo(), pull_request.head.ref)
}

if (teamModified) {
return syncTeamSettings(true, context, context.repo(), pull_request.head.ref)
}

if (repoModified) {
return Promise.all(repoChanges.map(repo => {
return syncSettings(true, context, repo, pull_request.head.ref)
Expand Down Expand Up @@ -515,6 +578,23 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
return syncSettings(false, context)
})

robot.on(['team.created', 'team.edited'], async context => {
robot.log.info(`team.${context.payload.action} event for ${context.payload.team.slug}`)
const repo = {
repo: env.ADMIN_REPO,
owner: context.payload.organization.login,
team: context.payload.team.slug
}

const enhancedContext = {
...context,
// Workaround: this is required deep in configManager
repo: () => repo
}

return syncTeamSettings(false, enhancedContext)
})

if (process.env.CRON) {
/*
# ┌────────────── second (optional)
Expand Down
12 changes: 6 additions & 6 deletions lib/commentmessage.eta
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
* Run on: `<%= new Date() %>`

* Number of repos that were considered: `<%= Object.keys(it.reposProcessed).length %>`
* Number of repos/teams that were considered: `<%= Object.keys(it.reposProcessed).length %>`

### Breakdown of changes
| Repo <% Object.keys(it.changes).forEach(plugin => { %> | <%= plugin %> settings <% }) %> |
| Repo/Team <% Object.keys(it.changes).forEach(plugin => { %> | <%= plugin %> settings <% }) %> |
| -- <% Object.keys(it.changes).forEach(plugin => { -%> | -- <% }) %>
|
|
<% Object.keys(it.reposProcessed).forEach( repo => { -%>
| <%= repo -%>
<%- Object.keys(it.changes).forEach(plugin => { -%>
<%_ if (it.changes[plugin][repo]) { -%> | :hand: <% } else { %> | :grey_exclamation: <% } -%>
<%_ }) -%> |
<% }) -%>

:hand: -> Changes to be applied to the GitHub repository.
:grey_exclamation: -> nothing to be changed in that particular GitHub repository.
:hand: -> Changes to be applied to the GitHub repository/team.
:grey_exclamation: -> nothing to be changed in that particular GitHub repository/team.

### Breakdown of errors

<% if (Object.keys(it.errors).length === 0) { %>
`None`
<% } else { %>
<% Object.keys(it.errors).forEach(repo => { %>
<%_= repo %>:
<%_= repo %>:
<% it.errors[repo].forEach(plugin => { %>
* <%= plugin.msg %>
<% }) %>
Expand Down
8 changes: 5 additions & 3 deletions lib/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ const metrics = require("@operate-first/probot-metrics");
const actionCounter = metrics.useCounter({
name: 'num_of_actions_total',
help: 'Total number of actions received',
labelNames: ['repository', 'result', 'action', 'nop'],
labelNames: ['repository', 'team', 'result', 'action', 'nop'],
});

const meteredPlugin = async (plugin, fn) => {
try {
const result = await fn()
actionCounter
.labels({
repository: plugin.repo.repo,
repository: plugin.repo?.repo,
team: plugin.team?.slug,
nop: plugin.nop,
result: "success",
action: plugin.constructor.name
Expand All @@ -22,7 +23,8 @@ const meteredPlugin = async (plugin, fn) => {
console.dir(e)
actionCounter
.labels({
repository: plugin.repo.repo,
repository: plugin.repo?.repo,
team: plugin.team?.slug,
result: "error",
nop: plugin.nop,
action: plugin.constructor.name
Expand Down
Loading

0 comments on commit d1b760f

Please sign in to comment.