Skip to content

Commit

Permalink
Add code to revert back repo renames (#617)
Browse files Browse the repository at this point in the history
* Add to revert back repo renames

* add code to rename branch

* remove duplicate

* add feature flag, add code to create a config file

* Update README.md

* Update README.md
  • Loading branch information
decyjphr committed May 14, 2024
1 parent 8a83354 commit c00eb5f
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 212 deletions.
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,33 @@
`Safe-settings`– an app to manage policy-as-code and apply repository settings to repositories across an organization.

1. In `safe-settings` all the settings are stored centrally in an `admin` repo within the organization. This is important. Unlike [Settings Probot](https://github.com/probot/settings), the settings files cannot be in individual repositories.
> [!Note]
> It is possible to override this behavior and specify a custom repo instead of the `admin` repo.<br>
> This could be done by setting an `env` variable called `ADMIN_REPO`.
> [!Note]
> It is possible to override this behavior and specify a custom repo instead of the `admin` repo.<br>
> This could be done by setting an `env` variable called `ADMIN_REPO`.
1. The **settings** in the **default** branch is applied. If the settings are changed in a non-default branch and a PR is created to merge the changes, it would be run in a `dry-run` mode to evaluate and validate the settings, and checks would pass or fail based on that.
2. The **settings** in the **default** branch is applied. If the settings are changed in a non-default branch and a PR is created to merge the changes, it would be run in a `dry-run` mode to evaluate and validate the settings, and checks would pass or fail based on that.
2. In `safe-settings` the settings can have 2 types of targets:
1. `org` - These settings are applied to the `org`. `Org`-targeted settings are defined in `.github/settings.yml` . Currently, only `rulesets` are supported as `org`-targeted settings.
2. `repo` - These settings are applied to `repos`

3. For The `repo`-targeted settings there can be at 3 levels at which the settings could be managed:
1. Org-level settings are defined in `.github/settings.yml`
> [!Note]
> It is possible to override this behavior and specify a different filename for the `settings` yml repo.<br>
> This could be done by setting an `env` variable called `SETTINGS_FILE_PATH`.<br>
> Similarly, the `.github` directory can be overridden with an `env` variable called `CONFIG_PATH`.
> [!Note]
> It is possible to override this behavior and specify a different filename for the `settings` yml repo.<br>
> This could be done by setting an `env` variable called `SETTINGS_FILE_PATH`.<br>
> Similarly, the `.github` directory can be overridden with an `env` variable called `CONFIG_PATH`.
2. `Suborg` level settings. A `suborg` is an arbitrary collection of repos belonging to projects, business units, or teams. The `suborg` settings reside in a yaml file for each `suborg` in the `.github/suborgs` folder.

> [!Note]
> In `safe-settings`, sub orgs could be groups of repos based on `repo names`, or `teams` which the repos have collaborators from, or `custom property values` set for the repos
> [!Note]
> In `safe-settings`, sub orgs could be groups of repos based on `repo names`, or `teams` which the repos have collaborators from, or `custom property values` set for the repos
3. `Repo` level settings. They reside in a repo specific yaml in `.github/repos` folder
4. It is recommended to break the settings into org-level, suborg-level, and repo-level units. This will allow different teams to define and manage policies for their specific projects or business units. With `CODEOWNERS`, this will allow different people to be responsible for approving changes in different projects.

> [!Note]
> `Suborg` and `Repo` level settings directory structure cannot be customized.
> [!Note]
>
> The settings file must have a `.yml` extension only. `.yaml` extension is ignored, for now.
## How it works
Expand All @@ -46,14 +45,23 @@ The App listens to the following webhook events:

- **branch_protection_rule**: If a branch protection rule is modified or deleted, `safe-settings` will `sync` the settings to prevent any unauthorized changes.

- **repository.edited**: If the default branch is renamed, `safe-settings` will `sync` the settings, returning the default branch to the configured value for the repo.
- **repository.edited**: For e.g. If the default branch is renamed, or if topics change, `safe-settings` will `sync` the settings, to prevent any unauthorized changes.

- **repository.renamed**: If a repository is renamed, the default behavior is safe-settings will ignore this (for backward-compatibility). If `BLOCK_REPO_RENAME_BY_HUMAN` env variable is set to true, `safe-settings` will revert the repo to the previous name unless it is renamed using a `bot`. If it is renamed using a `bot`, it will try to copy the existing `<old-repo>.yml` to `<new-repo>.yml` so that the repo config yml stays consistent. If a <new-repo.yml> file already exists, it doesn't create a new one.

- **pull_request.opened**, **pull_request.reopened**, **check_suite.requested**: If the settings are changed, but it is not in the `default` branch, and there is an existing PR, the code will validate the settings changes by running safe-settings in `nop` mode and update the PR with the `dry-run` status.

- **repository_ruleset**: If the `ruleset` settings are modified in the UI manually, `safe-settings` will `sync` the settings to prevent any unauthorized changes.

- **member_change_events**: If a member is added or removed from a repository, `safe-settings` will `sync` the settings to prevent any unauthorized changes.


- **member**', __team.added_to_repository__, __team.removed_from_repository__, __team.edited__: `safe-settings` will `sync` the settings to prevent any unauthorized changes.

- __custom_property_values__: If new repository properties are set for a repository, `safe-settings` will run to so that if a sub-org config is defined by that property, it will be applied for the repo

### Use `safe-settings` to rename repos
If you rename a <repo.yml> that corresponds to a repo, safe-settings will rename the repo to the new name. This behavior will take effect whether the env variable `BLOCK_REPO_RENAME_BY_HUMAN` is set or not.

### Restricting `safe-settings` to specific repos
`safe-settings` can be turned on only to a subset of repos by specifying them in the runtime settings file, `deployment-settings.yml`.
If no file is specified, then the following repositories - `'admin', '.github', 'safe-settings'` are exempted by default.
Expand Down
120 changes: 113 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const env = require('./lib/env')

let deploymentConfig


module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => {
let appName = 'safe-settings'
let appSlug = 'safe-settings'
async function syncAllSettings (nop, context, repo = context.repo(), ref) {
try {
deploymentConfig = await loadYamlFileSystem()
Expand Down Expand Up @@ -89,6 +92,31 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
}
}

async function renameSync (nop, context, repo = context.repo(), rename, 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)
const renameConfig = Object.assign({}, config, rename)
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
return Settings.sync(nop, context, repo, renameConfig, 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 @@ -197,14 +225,11 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
robot.log.debug(`installations: ${JSON.stringify(installations)}`)
if (installations.length > 0) {
const installation = installations[0]
robot.log.debug(`Installation ID: ${installation.id}`)
robot.log.debug('Fetching the App Details')
const github = await robot.auth(installation.id)
const app = await github.apps.getAuthenticated()
appName = app.data.name
appSlug = app.data.slug
robot.log.debug(`Validated the app is configured properly = \n${JSON.stringify(app.data, null, 2)}`)
robot.log.debug(`Registered App name = ${app.data.slug}\n`)
robot.log.debug(`Permissions = ${JSON.stringify(app.data.permissions)}\n`)
robot.log.debug(`Events = ${app.data.events}\n`)
}
}

Expand Down Expand Up @@ -369,6 +394,87 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
return syncSettings(false, context)
})

robot.on('repository.renamed', async context => {
if (env.BLOCK_REPO_RENAME_BY_HUMAN!== 'true') {
robot.log.debug(`"env.BLOCK_REPO_RENAME_BY_HUMAN" is 'false' by default. Repo rename is not managed by Safe-settings. Continue with the default behavior.`)
return
}
const { payload } = context
const { sender } = payload

robot.log.debug(`repository renamed from ${payload.changes.repository.name.from} to ${payload.repository.name} by ', ${sender.login}`)

if (sender.type === 'Bot') {
robot.log.debug('Repository Edited by a Bot')
if (sender.login === `${appSlug}[bot]`) {
robot.log.debug('Renamed by safe-settings app')
return
}
const oldPath = `.github/repos/${payload.changes.repository.name.from}.yml`
const newPath = `.github/repos/${payload.repository.name}.yml`
robot.log.debug(oldPath)
try {
const repofile = await context.octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: payload.repository.owner.login,
repo: env.ADMIN_REPO,
path: oldPath,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
})
let content = Buffer.from(repofile.data.content, 'base64').toString()
robot.log.debug(content)
content = `# Repo Renamed and safe-settings renamed the file from ${payload.changes.repository.name.from} to ${payload.repository.name}\n# change the repo name in the config for consistency\n\n${content}`
content = Buffer.from(content).toString('base64')
try {
// Check if a config file already exists for the renamed repo name
await context.octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: payload.repository.owner.login,
repo: env.ADMIN_REPO,
path: newPath,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
})
} catch (error) {
if (error.status === 404) {
// if the a config file does not exist, create one from the old one
const update = await context.octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', {
owner: payload.repository.owner.login,
repo: env.ADMIN_REPO,
path: newPath,
name: `${payload.repository.name}.yml`,
content: content,
message: `Repo Renamed and safe-settings renamed the file from ${payload.changes.repository.name.from} to ${payload.repository.name}`,
sha: repofile.data.sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
})
robot.log.debug(`Created a new setting file ${newPath}`)
} else {
robot.log.error(error)
}
}

} catch (error) {
if (error.status === 404) {
//nop
} else {
robot.log.error(error)
}
}
return
} else {
robot.log.debug('Repository Edited by a Human')
// Create a repository config to reset the name back to the previous name
const rename = {repository: { name: payload.changes.repository.name.from, oldname: payload.repository.name}}
const repo = {repo: payload.changes.repository.name.from, owner: payload.repository.owner.login}
return renameSync(false, context, repo, rename)
}
})


robot.on('check_suite.requested', async context => {
const { payload } = context
const { repository } = payload
Expand Down Expand Up @@ -558,8 +664,8 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
})
}

//Uncomment below to get info about the app configuration
//info()
// Get info about the app
info()

return {
syncInstallation
Expand Down
3 changes: 2 additions & 1 deletion lib/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module.exports = {
SETTINGS_FILE_PATH: process.env.SETTINGS_FILE_PATH || 'settings.yml',
DEPLOYMENT_CONFIG_FILE: process.env.DEPLOYMENT_CONFIG_FILE || 'deployment-settings.yml',
CREATE_PR_COMMENT: process.env.CREATE_PR_COMMENT || 'true',
CREATE_ERROR_ISSUE: process.env.CREATE_ERROR_ISSUE || 'true'
CREATE_ERROR_ISSUE: process.env.CREATE_ERROR_ISSUE || 'true',
BLOCK_REPO_RENAME_BY_HUMAN: process.env.BLOCK_REPO_RENAME_BY_HUMAN || 'false'
}
Loading

0 comments on commit c00eb5f

Please sign in to comment.