Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code to revert back repo renames #617

Merged
merged 7 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading