| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Policy-as-Code for GitHub Organizations
Centrally manage and enforce repository settings, branch protections, teams, and more across your entire GitHub organization.
Safe-settings – an app to manage policy-as-code and apply repository settings across an organization.
In safe-settings, all the settings are stored centrally in an admin repo within the organization. Unlike the GitHub Repository Settings App, the settings files cannot be in individual repositories.
It is possible specify a custom repo instead of the admin repo with ADMIN_REPO. See Environment variables for more details.
The settings in the default branch are applied. If the settings are changed on a non-default branch and a PR is created to merge the changes, the app runs in a dry-run mode to evaluate and validate the changes. Checks pass or fail based on the dry-run results.
In safe-settings the settings can have 2 types of targets:
For the repo-targeted settings, there can be 3 levels at which the settings are managed:
Org-level settings are defined in .github/settings.yml
It is possible to override this behavior and specify a different filename for the settings.yml file with SETTINGS_FILE_PATH. Similarly, the .github directory can be overridden with CONFIG_PATH. See Environment variables for more details.
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.
In safe-settings, suborgs 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
Repo level settings. They reside in a repo specific yaml in .github/repos folder
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
The suborg and repo level settings directory structure cannot be customized.
Choose your preferred deployment method:
👉 View all deployment options →
Create an admin repository in your organization to store all configuration files:
# Create admin repo in your organization
gh repo create your-org/admin --privateSet up your configuration files in the admin repository:
admin/ ├── .github/ │ ├── settings.yml # Organization-wide settings │ ├── suborgs/ # Sub-organization settings │ │ ├── frontend-team.yml │ │ └── backend-team.yml │ └── repos/ # Repository-specific settings │ ├── api-service.yml │ └── web-app.yml
Install the Safe-Settings GitHub App in your organization with the required permissions.
graph TD
A[Organization Settings<br/>.github/settings.yml] --> B[Sub-Organization Settings<br/>.github/suborgs/*.yml]
B --> C[Repository Settings<br/>.github/repos/*.yml]
style A fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000
style B fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000
style C fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px,color:#000
Precedence Order: Repository > Sub-Organization > Organization
sequenceDiagram
participant GH as GitHub
participant SS as Safe-Settings
participant AR as Admin Repo
participant TR as Target Repos
Note over GH,TR: Webhook Event Processing
GH->>+SS: Webhook Event<br/>(push, repo created, etc.)
SS->>SS: Validate Event Source
SS->>+AR: Fetch Configuration Files<br/>(.github/settings.yml, suborgs/, repos/)
AR-->>-SS: Return Config Files
SS->>SS: Merge Configurations<br/>(Org → Suborg → Repo)
SS->>SS: Compare with Current<br/>GitHub Settings
alt Configuration Changes Detected
SS->>+TR: Apply Settings<br/>(Branch Protection, Teams, etc.)
TR-->>-SS: Confirm Changes
SS->>GH: Create Check Run<br/>(Success/Failure)
else No Changes Needed
SS->>GH: Create Check Run<br/>(No Changes)
end
SS-->>-GH: HTTP 200 Response
Note over GH,TR: Pull Request Validation (Dry-Run Mode)
GH->>+SS: PR Event<br/>(opened, synchronize)
SS->>+AR: Fetch PR Changes<br/>(Modified Config Files)
AR-->>-SS: Return Changed Configs
SS->>SS: Validate Changes<br/>(Dry-Run Mode)
SS->>SS: Run Custom Validators<br/>(if configured)
alt Validation Passes
SS->>GH: ✅ Check Success<br/>+ PR Comment (optional)
else Validation Fails
SS->>GH: ❌ Check Failure<br/>+ Error Details
end
SS-->>-GH: HTTP 200 Response
Note over GH,TR: Scheduled Sync (Drift Prevention)
SS->>SS: Cron Trigger<br/>(if configured)
SS->>+AR: Fetch All Configurations
AR-->>-SS: Return All Configs
SS->>+TR: Sync All Repositories<br/>(Prevent Drift)
TR-->>-SS: Confirm Sync
SS->>GH: Create Check Run<br/>(Sync Results)
Safe-settings is designed to run as a service listening for webhook events or as a scheduled job running on some regular cadence. It can also be triggered through GitHub Actions. (See the How to use section for details on deploying and configuring.)
The App listens to the following webhook events:
push: If the settings are created or modified, that is, if push happens in the default branch of the admin repo and the file added or changed is .github/settings.yml or .github/repos/*.ymlor .github/suborgs/*.yml, then the settings would be applied either globally to all the repos, or specific repos. For each repo, the settings that are actually applied depend on the default settings for the org, overlaid with settings for the suborg that the repo belongs to, overlaid with the settings for that specific repo.
repository.created: If a repository is created in the org, the settings for the repo - the default settings for the org, overlaid with settings for the suborg that the repo belongs to, overlaid with the settings for that specific repo - is applied.
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: 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
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.
To restrict which repositories safe-settings can manage, create a deployment-settings.yml file. This file controls the app's scope through the restrictedRepos configuration:
# Using include/exclude
restrictedRepos:
include:
- api
- core-* # Matches `core-api`, `core-service`, etc.
exclude:
- admin
- .github
- safe-settings
- test-* # Matches `test-repo`, etc.
# Or using simple array syntax for includes
restrictedRepos:
- admin
- .github
# ...Note
Pattern matching uses glob expressions, e.g use * for wildcards.
When using include and exclude:
By default, if no configuration file is provided, safe-settings will excludes these repos: admin, .github and safe-settings.
See our deployment-settings.yml sample.
Admins setting up safe-settings can include custom rules that would be validated before applying a setting or overriding a broader scoped setting.
The code has to return true if validation is successful, or false if it isn't.
If the validation fails, the error attribute specified would be used to create the error message in the logs or in the PR checks.
The first use case is where a custom rule has to be applied for a setting on its own. For e.g. No collaborator should be given admin permissions.
For this type of validation, admins can provide custom code as configvalidators which validates the setting by itself.
For e.g. for the case above, it would look like:
configvalidators:
- plugin: collaborators
error: |
`Admin role cannot be assigned to collaborators`
script: |
console.log(`baseConfig ${JSON.stringify(baseconfig)}`)
return baseconfig.permission != 'admin'For convenience this script has access to a variable, baseconfig, that contains the setting that is be applied.
The second use case is where custom rule has to be applied when a setting in the org or suborg level is being overridden. Such as, when default branch protection is being overridden.
For this type of validation, admins can provide custom code as overridevalidators. The script can access two variables, baseconfig and overrideconfig which represent the base setting and the setting that is overriding it.
A sample would look like:
overridevalidators:
- plugin: branches
error: |
`Branch protection required_approving_review_count cannot be overidden to a lower value`
script: |
console.log(`baseConfig ${JSON.stringify(baseconfig)}`)
console.log(`overrideConfig ${JSON.stringify(overrideconfig)}`)
if (baseconfig.protection.required_pull_request_reviews.required_approving_review_count && overrideconfig.protection.required_pull_request_reviews.required_approving_review_count ) {
return overrideconfig.protection.required_pull_request_reviews.required_approving_review_count >= baseconfig.protection.required_pull_request_reviews.required_approving_review_count
}
return trueA sample of deployment-settings file is found here.
For branch protection rules and rulesets, you can allow for status checks to be defined outside of safe-settings together with your usual safe settings.
This can be defined at the org, sub-org, and repo level.
To configure this for branch protection rules, specify {{EXTERNALLY_DEFINED}} under the contexts keyword:
branches:
- name: main
protection:
...
required_status_checks:
contexts:
- "{{EXTERNALLY_DEFINED}}"For rulesets, specify {{EXTERNALLY_DEFINED}} under the required_status_checks keyword:
rulesets:
- name: Status Checks
...
rules:
- type: required_status_checks
parameters:
required_status_checks:
- context: "{{EXTERNALLY_DEFINED}}"Notes:
⚠️ Warning: When {{EXTERNALLY_DEFINED}} is removed from an existing branch protection rule or ruleset configuration, the status checks in the existing rules in GitHub will revert to the checks that are defined in safe-settings. From this point onwards, all status checks configured through the GitHub UI will be reverted back to the safe-settings configuration.
Refer to Status checks.
When there are 1000s of repos to be managed -- and there is a global settings change -- safe-settings will have to work efficiently and only make the necessary API calls.
The app also has to complete the work within an hour: the lifetime of the GitHub app token.
To address these constraints the following design decisions have been implemented:
To determine if there are real changes, the code will generate a detailed list of additions, modifications, and deletions compared to the settings in GitHub:
For e.g:
If the settings is:
{
"branches": [
{
"name": "master",
"protection": {
"required_pull_request_reviews": {
"required_approving_review_count": 2,
"dismiss_stale_reviews": false,
"require_code_owner_reviews": true,
"dismissal_restrictions": {}
},
"required_status_checks": {
"strict": true,
"contexts": []
},
"enforce_admins": false
}
}
]
}and the settings in GitHub is:
{
"branches": [
{
"name": "master",
"protection": {
"url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection",
"required_status_checks": {
"url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/required_status_checks",
"strict": true,
"contexts": [],
"contexts_url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/required_status_checks/contexts",
"checks": []
},
"restrictions": {
"url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/restrictions",
"users_url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/restrictions/users",
"teams_url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/restrictions/teams",
"apps_url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/restrictions/apps",
"users": [],
"teams": [],
"apps": []
},
"required_pull_request_reviews": {
"url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/required_pull_request_reviews",
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true,
"required_approving_review_count": 2,
"dismissal_restrictions": {
"url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/dismissal_restrictions",
"users_url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/dismissal_restrictions/users",
"teams_url": "https://api.github.com/repos/decyjphr-org/test/branches/develop/protection/dismissal_restrictions/teams",
"users": [],
"teams": []
}
},
"required_signatures": false,
"enforce_admins": false,
"required_linear_history": false,
"allow_force_pushes": {
"enabled": false
},
"allow_deletions": false,
"required_conversation_resolution": false
}
}
]
}the results of comparison would be:
{
"additions": {},
"modifications": {
"branches": [
{
"protection": {
"required_pull_request_reviews": {
"dismiss_stale_reviews": false
}
},
"name": "master"
}
]
},
"deletions": {},
"hasChanges": true
}The App can be configured to apply the settings on a schedule. This could be a way to address configuration drift since webhooks are not always guaranteed to be delivered.
To periodically converge the settings to the configuration, set the CRON environment variable. See Environment variables for more details.
Safe-settings explicitly looks in the admin repo in the organization for the settings files. The admin repo could be a restricted repository with branch protections and CODEOWNERS
In that set up, when changes happen to the settings files and there is a PR for merging the changes back to the default branch in the admin repo, safe-settings will run checks – which will run in nop mode and produce a report of the changes that would happen, including the API calls and the payload.
For e.g. If we have override validators that will fail if org-level branch protections are overridden at the repo or suborg level with a lesser number of required approvers, here is an screenshot of what users will see in the PR.
Note
If you don't want the PR message to have these details, they can be turned off with CREATE_PR_COMMENT. See Environment variables for more details.
Here is a screenshot of what the users will see in the checkrun page:
The app creates a Check at the end of its processing to indicate if there were any errors. The Check is called safe-settings and corresponds to the latest commit on the default branch of the admin repo.
Here is an example of a checkrun result:
And the checkrun page will look like this:
The settings files can be used to set the policies at the org, suborg or repo level.
The following can be configured:
See docs/sample-settings/settings.yml for a sample settings file.
Note
When using collaborators, teams or labels, you can control which repositories they apply to using include and exclude:
Pattern matching uses glob expressions, e.g use * for wildcards. For example:
teams:
- name: Myteam-admins
permission: admin
- name: Myteam-developers
permission: push
- name: Other-team
permission: push
include:
- '*-config'In addition to the values in the file above, the settings file can have some additional values:
You can pass environment variables; the easiest way to do it is via a .env file.
# ┌────────────── second (optional) # │ ┌──────────── minute # │ │ ┌────────── hour # │ │ │ ┌──────── day of month # │ │ │ │ ┌────── month # │ │ │ │ │ ┌──── day of week # │ │ │ │ │ │ # │ │ │ │ │ │ # * * * * * * CRON=* * * * * # Run every minute
LOG_LEVEL=trace
ADMIN_REPO=safe-settings-config
CONFIG_PATH=.github
SETTINGS_FILE_PATH=settings.yml
DEPLOYMENT_CONFIG_FILE=deployment-settings.yml
ENABLE_PR_COMMENT=true
BLOCK_REPO_RENAME_BY_HUMAN=true
Create an admin repo (or an alternative of your choosing) within your organization. Remember to set ADMIN_REPO if you choose something other than admin. See Environment variables for more details.
Add the settings for the org, suborgs, and repos. Sample files can be found here.
Deploy and install the app. Alternatively, the GitHub Actions Guide describes how to run safe-settings with GitHub Actions.
safe-settings is licensed under the ISC license
safe-settings uses 3rd party libraries, each with their own license. These are found here.
| Back | FazBrowse Home | New Git URL |