Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

278 Commits

Repository files navigation

GitHub Settings as Code

Apply declarative repository settings from .github/settings.yml: a loud, stateless replacement for the Probot Settings app that also manages rulesets (branch, tag, and push). Every apply is a visible workflow run that fails with the API's error message; nothing happens silently.

Usage

  1. Create a fine-grained PAT: the pre-filled token form starts you off with every repository permission the Sections table can need. Pick the resource owner and repositories, and add Members: read by hand when the owner is an organization; the form only offers organization permissions once one is selected. The default GITHUB_TOKEN can never hold these permissions.

  2. Save the token as a repository secret; ADMIN_TOKEN below.

  3. Declare your settings in .github/settings.yml (see the example below). One line at the top gives editor autocomplete and hover docs (agents can fetch the same URL):

    # yaml-language-server: $schema=https://raw.githubusercontent.com/Vivswan/github-settings-as-code/v2/lib/settings.schema.json # x-release-please-major
  4. Add the workflow. On a repository with existing labels, autolinks, collaborators, Actions variables, or Copilot agents variables, also set mode: check under with: for the first run: the drift report lists everything an apply would delete, and nothing is written.

    # .github/workflows/settings.ymlname: Apply Settingson:
    push:
    branches: [main]paths: [.github/settings.yml]workflow_dispatch:
    permissions:
    contents: readjobs:
    apply:
    runs-on: ubuntu-lateststeps:
    - uses: actions/checkout@v7
    - uses: Vivswan/github-settings-as-code@v2 # x-release-please-majorwith:
    token: ${{ secrets.ADMIN_TOKEN }}
  5. Run it once from the Actions tab (workflow_dispatch), review the run, and drop mode: check if you set it. From then on every push that touches .github/settings.yml applies it.

A JSON Schema describing every section and its structured fields is generated from the zod schemas in src/schema.ts (the single source of the config types; their .describe() strings become the published descriptions) and served at https://raw.githubusercontent.com/Vivswan/github-settings-as-code/<ref>/lib/settings.schema.json, where <ref> picks the version: v2 (canonical, the moving major tag, always the newest schema in the line) or vX.Y.Z (an exact release). The main ref still works but is deprecated and will be removed in a future major. Passthrough areas (the repository payload, branch protection, rule parameters) stay open objects on purpose.

The schema is documentation, not a gate: unknown fields validate on purpose, because payloads pass through to the API verbatim and declaring a field GitHub ships tomorrow must never read as an error (see Forward compatibility).

Guides

The guides live in docs/, in four groups:

Versioning

  • @v2 is a moving major tag: every release in that major line moves it, so fixes arrive without changing your pin.
  • Pinning exactly: pin @vX.Y.Z (or a commit SHA) when you need byte-stable behavior, and upgrade deliberately. Every version tag points at a packaged commit carrying the built action - its parent is the audited release commit on main - and is frozen by a ruleset.
  • v2 activates settings keys that were inert on v1: actions.oidc_customization_sub, actions.fork_pr_contributor_approval, actions.fork_pr_workflows_private_repos, and branches[].protection.required_signatures. Before moving a @v1 pin to @v2, audit any of those keys already in your settings files for intent; on v2 they act, and a stale required_signatures: false would remove a hand-enabled requirement.
  • Only the latest release is supported; fixes are not backported (see SECURITY.md).

Sections

SectionEndpointsPAT permissionUndeclared defaultNotes
repositoryPATCH repo, PUT topics, vulnerability-alerts, automated-security-fixes, private-vulnerability-reporting, lfs, immutable-releases, GraphQL RepositoryFeatures + UpdateRepositoryFeaturesAdministration: writeuntouchedProbot repository payload plus enable_* feature toggles; topics as string or list; enable_sponsorships and issue_creation_policy (all/collaborators_only) route through GraphQL - REST has no surface for them; declared fields only, undeclared siblings untouched
labelslabels CRUDIssues: writedeleted (settable)upsert by name (rename via new_name); the delete-by-default is Probot parity
rulesetsrepo rulesets CRUDAdministration: writekept (settable)branch, tag, and push targets; short ref names auto-prefixed (staging -> refs/heads/staging); deletion stays an explicit opt-in
environmentsPUT environments + per-environment variables, secrets, deployment branch policies, deployment protection rules, and pins (GraphQL EnvironmentPins + PinEnvironment + ReorderEnvironment)Environments: write; deployment_branch_policies and deployment_protection_rules additionally need Actions: read and Administration: writeuntouchedreviewers, wait timer, branch-policy flags; nested variables, secrets, deployment_branch_policies, and deployment_protection_rules keys reconcile per environment, each with its own undeclared: knob (within a declared key, undeclared variables and branch-policy patterns are deleted; secrets and protection rules are kept); a pinned key pins the environment on the home page's deployments sidebar over GraphQL (declaration order sets the pin order, max 10 pins; environments without the key are never unpinned)
branchesclassic branch protection + required-signatures sub-endpoint + app-by-slug actor lookup + GraphQL BranchProtectionRules + BranchProtectionRepository + BranchProtectionActorUser + BranchProtectionActorTeam + CreateBranchProtectionRule + UpdateBranchProtectionRule + DeleteBranchProtectionRuleAdministration: writeuntouchedprotection: null removes protection; the protection PUT drops required_signatures, so declare it on any branch already carrying it; force_push_bypassers (users, org/team, app/slug) and required_deployments ride the GraphQL rule mutation; wildcard entries (release/*) reconcile entirely through GraphQL with a fixed key set; add Contents: read so check mode can tell a missing branch from an unprotected one
autolinksautolinks CRUDAdministration: writedeleted (settable)immutable upstream, so changed entries are replaced
actionsactions permissions + selected-actions + workflow token + access level + artifact/log retention + cache limits + OIDC subject claim + fork PR policiesAdministration: write; oidc_customization_sub alone needs Actions: writeuntouchedkeys with their own sub-endpoint route there; everything else rides the base permissions PUT verbatim
actions_secretsactions secrets list + public-key + sealed PUT + deleteSecrets: writekept (settable){name, value: $NAME} sealed writes, re-sent every apply; existence-only checks, values unrecoverable
dependabot_secretsdependabot secrets list + public-key + sealed PUT + deleteDependabot secrets: writekept (settable)as actions_secrets, over the Dependabot secret store
codespaces_secretscodespaces secrets list + public-key + sealed PUT + deleteCodespaces secrets: writekept (settable)as actions_secrets, over the Codespaces secret store
agents_secretsagents secrets list + public-key + sealed PUT + deleteAgent secrets: writekept (settable)as actions_secrets, over the Copilot agents secret store
workflowsActions workflows list, enable/disableActions: writeuntouched{path, state: active or disabled}; bare file names match .github/workflows/
check_suite_preferencescheck-suites preferences PATCH (no read endpoint exists upstream)Checks: writeuntouchedper-app auto_trigger_checks toggles; write-only: check mode cannot verify them (one note, zero requests) and apply re-asserts them every run; the token owner must be a repository administrator
pagesPOST/PUT/DELETE pagesPages: writeuntouchedbuild_type: workflow or legacy + source, cname, https_enforced, public (GHEC site visibility); pages: null disables the site
code_scanning_default_setupcode scanning default setupAdministration or Code scanning alerts: writeuntouchedstate, query_suite, languages; needs Advanced Security on private repositories
code_quality_setupcode-quality setupAdministration: writeuntouchedstate, languages, runner and AI-findings options; a 202 means GitHub rolls the change out in a configuration run; needs code quality available on the repository
collaboratorsdirect collaborators + pending invitationsAdministration: writedeleted (settable)invitations for new users, pending ones reconciled (stale permission updated, expired re-sent, undeclared cancelled); the repository owner is never touched
teamsorg team repo permissionsMembers: read (org permission) + Administration: writeuntouchedorg repos only, skipped with a notice on personal accounts
milestonesmilestonesIssues: writekept (settable)upsert by title; deleting a milestone detaches it from every issue carrying it, which is why keep is the default
interaction_limitsinteraction-limits + pulls creation-cap/bypass-listAdministration: writeuntouchedre-arms the self-expiring limit every apply run; null clears it (base limit only); a 409 (org/user-level limit overrides) becomes a note; the PR creation cap is persistent (PATCHed only on divergence, 405 where unavailable) and its bypass logins reconcile add/remove
actions_variablesActions variables CRUDVariables: writedeleted (settable)plain-text variables upserted by name (case-insensitive); values read back in full, so check mode diffs them exactly
agents_variablesCopilot agents variables CRUDAgent variables: writedeleted (settable)as actions_variables, over the Copilot agents variable store
webhookshooks CRUD + hook config sub-endpointWebhooks: writekept (settable)one hook per config.url, the natural key; config.secret takes a $NAME reference and is re-sent every run
custom_propertiesGET/PATCH properties/values; probes GET /orgs/{owner}Custom properties: writekept (settable)values of org-defined properties (definitions are org-scoped); org repos only, skipped with a notice on personal accounts; value: null unsets
deploy_keysdeploy keys list/create/deleteAdministration: writekept (settable)matched by title; the declared material is a PUBLIC key; immutable upstream, so changed entries are replaced
secret_scanning_custom_patternssecret-scanning custom patterns: paginated list + bulk POST + PATCH by id + bulk DELETESecret scanning alerts: writekept (settable)matched by name (immutable upstream); state and push_protection_enabled are not declarable; deletes always resolve alerts

The Undeclared default column says what happens to live resources the settings file does not declare; (settable) means the wrapped undeclared: form can override it per file. The undeclared policy covers the knob and how it layers with a multi-repo defaults file.

The model in three lines: the engine is stateless and declared-keys-only (a key you do not declare is never touched or compared), applies are convergent (a check right after an apply reports clean), and every failure is loud, carrying the API's message verbatim. Semantics is the full model: softenable errors, retries, and the preflight barrier.

Payloads pass through to the API verbatim except for documented normalizations, so fields and rule types GitHub ships tomorrow work the day they exist; a handful of sections are instead closed to catch typos that would otherwise misconfigure silently. Forward compatibility draws that line section by section.

See COVERAGE.md for the full per-section detail: every row above expanded with its exact endpoints, semantics, and caveats, plus every repo-scoped gap and the user-scoped surface that is out of scope by design.

Example settings.yml

# yaml-language-server: $schema=https://raw.githubusercontent.com/Vivswan/github-settings-as-code/v2/lib/settings.schema.json # x-release-please-majorrepository:
description: My projecttopics: tooling, github-actionshas_wiki: falsedelete_branch_on_merge: truelabels:
- name: bugcolor: "d73a4a"description: Something isn't working

The examples cookbook is the full tour: a full-featured file exercising every section, classic branch protection, and what null means where it is meaningful.

Inputs

InputDefaultMeaning
tokengithub.tokenToken for the API calls (see Token permissions)
repositorycurrent repoTarget owner/name (single-repo mode only)
settings-file.github/settings.ymlSettings file path (single-repo mode only)
modeapplyapply mutates; check reports drift and exits 1 on any, making no settings changes (a private report may still be delivered)
on-missing-permissionfailwarn skips sections the token cannot access (partial success)
required-sections(empty)Sections that must fully apply even under warn
sections(all declared)Comma-separated allowlist of sections to process
api-version2022-11-28X-GitHub-Api-Version header; override to opt into a newer REST API version
repos(empty)Multi-repo remote mode: owner/name list (comma/newline), or * to discover owned repos
repos-dir(empty)Multi-repo central mode: directory of per-repo settings files in this repo
defaults-file(empty)YAML merged under every multi-repo target's settings (multi-repo mode only)
private-reposredactredact hides private and internal targets from public logs, summary, and outputs; show reveals them
private-reportnoneissue delivers each redacted target's full report to a reused issue on that target repository; issue-on-failure writes that issue only when the target fails or drifts, closing it once healthy; artifact uploads all reports as one age-encrypted workflow artifact; rejected with private-repos: show
report-public-key(empty)The age1... recipient the artifact channel encrypts reports to; required with private-report: artifact, rejected otherwise
visibilityallDiscovery-only: keep public, private, or internal repositories
archivedskipDiscovery-only: skip, include, or only archived repositories
forksincludeDiscovery-only: include, exclude, or only forks
exclude(empty)Discovery-only: * wildcard patterns (name, or owner/name if the pattern has a /) to drop
topics(empty)Discovery-only: keep repositories carrying at least one listed topic
affiliationownerDiscovery-only: owner, collaborator, organization_member (comma list)

Outputs: result (applied / partial / clean / drift / failed; worst-of across targets in multi-repo mode, where skipped can also appear), skipped-sections, and repos-result (multi-repo mode: a JSON map of owner/name to {result, source, skippedSections}). A redacted private target is keyed by its private repository #N placeholder instead of its slug; see Private repositories.

Multi-repo mode

One run in an admin repository can manage a whole fleet. Two sourcing modes are usable together: repos-dir names a directory of per-repo settings files in the admin repository, and repos lists targets applied from their own .github/settings.yml (repos: "*" discovers them). When both name the same repository, the central file wins. A defaults-file merges under every target, and a target's null section opts out of a section the defaults declare. Targets run independently and sequentially; one failure never stops the rest. The multi-repo guide owns the rules: sourcing precedence, the discovery filters, the merge, and the fleet patterns.

Private repositories

A public admin repository managing private targets would leak their slugs, live settings, and API error bodies into public logs. private-repos: redact (the default) hides every private or internal target behind a placeholder, and the private-report input can deliver each target's full report over a private channel. The private repositories guide covers what is hidden, what stays visible, and how to read the full detail.

Migrating from the Probot Settings app

This action started as a replacement for the Probot Settings app (repository-settings/app), so the schema is a superset of Probot's: an existing settings.yml keeps working, and migration is swapping the app installation for a workflow.

Compared to the Probot Settings app

Probot Settings appThis action
DeliveryGitHub App you install (hosted by a third party, or self-hosted)A step in your own workflow; no app installation, no third party
Failure visibilitySilent: no run log a repo owner can open; a misconfigured or uninstalled app just does nothingEvery apply is a workflow run with a log, annotations, a step summary, and a red X on failure
Drift detectionNonemode: check reports drift between the file and the live repo, exits 1 when it finds any, changes no settings
RulesetsExperimental upstream feature; schema may changeFirst class: branch, tag, and push targets, upsert by name; undeclared rulesets kept by default, undeclared: delete opts into deletion
Partial success policyNoneon-missing-permission: fail or warn, plus required-sections as a minimum-requirements floor
TokenApp installation token; its scope is invisible in the repoA PAT you mint and scope yourself; permission errors name the exact missing permission
Org-level shared configYes (org _settings repo with extends)Yes, as multi-repo mode: an admin repo with a defaults-file plus per-repo files (repos-dir) or each repo's own settings.yml (repos input); no hosted app needed
Call transparencyNoneEvery API call is traced as a debug line (method, path, payload, status, timing) when debug logging is on

The one Probot-family feature without a direct equivalent is suborg-level grouping (safe-settings' .github/suborgs layer); here the layers are the defaults-file and per-repo files. Everything else in Probot's schema is supported, plus the rows above.

Your existing settings.yml works as-is for repository, labels, branches, collaborators, teams, and milestones (for the list sections among them, the plain-array form remains Probot-compatible; the object-shaped sections keep their original Probot shapes). Uninstall the app, add the workflow above, and optionally move branch protection to rulesets. Differences: applies run visibly in Actions (loud failures instead of silent skips), rulesets are supported, and nothing except labels/autolinks/collaborators/Actions variables/Copilot agents variables - plus, WITHIN a declared per-environment key, that environment's variables and deployment branch-policy patterns, and WITHIN a declared pull_request_creation_bypass key, that list's undeclared logins - is ever deleted implicitly.

The step-by-step move, including an org-scale shadow run alongside the app, is the migration guide.

Contributing

The toolchain, the end-to-end harness, and the PR conventions are documented in CONTRIBUTING.md.

About

GitHub Action applying declarative repository settings: rulesets, labels, branch protection, and more. A loud, stateless Probot Settings replacement.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages