π’ Bulk configure GitHub organization settings across multiple orgs using a declarative YAML config
Please refer to the release page for the latest release notes.
- π·οΈ Sync custom property definitions across organizations
- π Sync organization-level rulesets across organizations
- β
Support for all custom property types:
string,single_select,multi_select,true_false,url - π Dry-run mode with change preview and intelligent change detection
- π Per-organization overrides via YAML configuration
- π Rich job summary with per-organization status table
- π Support for GitHub.com, GHES, and GHEC
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org,my-other-org'
custom-properties-file: './custom-properties.yml'
delete-unmanaged-properties: true
dry-run: ${{ github.event_name == 'pull_request' }} # dry run if PRFor stronger security and higher rate limits, use a GitHub App:
- Create a GitHub App with the following permissions:
- Organization Custom Properties: Admin (required for managing custom property definitions)
- Organization Administration: Read and write (required for managing organization settings and rulesets)
- Install it to your organization(s)
- Add
APP_IDandAPP_PRIVATE_KEYas repository secrets
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ steps.app-token.outputs.token }}
# ... other inputsAlternatively, use a PAT with admin:org scope:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
# ... other inputsThis action supports two approaches for selecting which organizations to manage. Choose based on your needs:
| Approach | Best For | Configuration File |
|---|---|---|
| Option 1: Organization List | Simple setup, same settings applied to all orgs | custom-properties.yml |
| Option 2: Organizations File | Per-org overrides, different settings for different orgs | orgs.yml + custom-properties.yml |
List organizations directly via the organizations input. All orgs receive the same settings defined via custom-properties-file.
Best for: Applying identical settings across all organizations.
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org, my-other-org, my-third-org'
custom-properties-file: './custom-properties.yml'Define organizations in a YAML file with optional per-org setting overrides. Common settings can still be defined via custom-properties-file β per-org overrides layer on top (same pattern as bulk-github-repo-settings-sync-action where action inputs define global defaults and the YAML file provides per-item overrides).
Best for: Managing multiple orgs with different settings, or when specific orgs need additional/different custom properties.
Tip
π See full example: sample-configuration/orgs.yml
Create an orgs.yml file:
orgs:
- org: my-org
# No custom-properties β inherits all base properties from custom-properties-file
- org: my-other-org
custom-properties-file: './config/custom-properties/other-org.yml' # Override base file for this org
rulesets-file: # Override rulesets for this org (YAML array)
- './config/rulesets/branch-protection.json'
- './config/rulesets/tag-protection.json'
delete-unmanaged-rulesets: true # Delete rulesets not in the config for this org
delete-unmanaged-properties: true # Override the action input for this org
custom-properties:
# Override "team" to add extra allowed values for this org
- name: team
value-type: single_select
required: true
description: 'The team that owns this repository'
allowed-values:
- platform
- frontend
- backend
- data-science # extra team only in this org
values-editable-by: org_actorsUse in workflow:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations-file: './orgs.yml'
custom-properties-file: './config/custom-properties/base.yml' # Base properties for all orgs
rulesets-file: './config/rulesets/branch-protection.json, ./config/rulesets/tag-protection.json' # Base rulesets for all orgsSettings Merging:
When using both custom-properties-file (base) and per-org custom-properties in the organizations file, settings are merged by property name. Per-org definitions override base definitions for the same property name; base properties not overridden are preserved:
# custom-properties.yml (base):
- name: team # β applied to all orgs
- name: cost-center # β applied to all orgs
# orgs.yml:
orgs:
- org: my-org # gets: team + cost-center (base only)
- org: my-other-org
custom-properties:
- name: team # overrides base "team" with different allowed-values
# gets: team (overridden) + cost-center (from base)Sync custom property definitions (schemas) to organizations. Properties define the metadata that can be set on repositories within the organization.
Tip
π See full example: sample-configuration/custom-properties.yml
Create a custom-properties.yml file:
- name: team
value-type: single_select
required: true
description: 'The team that owns this repository'
allowed-values:
- platform
- frontend
- backend
- devops
- security
values-editable-by: org_actors
- name: environment
value-type: multi_select
required: false
description: 'Deployment environments for this repository'
allowed-values:
- production
- staging
- development
values-editable-by: org_and_repo_actors
- name: is-production
value-type: true_false
required: false
default-value: 'false'
description: 'Whether this repository is used in production'
values-editable-by: org_actors
- name: cost-center
value-type: string
required: false
description: 'Cost center code for billing'
values-editable-by: org_actorsBehavior:
- If a custom property doesn't exist in the org, it is created
- If it exists but differs from the config, it is updated
- If content is identical, no changes are made
- With
delete-unmanaged-properties: true, properties not in the config are deleted
| Type | Description | Requires allowed-values |
|---|---|---|
string |
Free-form text | No |
single_select |
Single selection from a list | Yes |
multi_select |
Multiple selections from a list | Yes |
true_false |
Boolean value | No |
url |
URL value | No |
Each custom property supports these fields:
| Field | Description | Required | Default |
|---|---|---|---|
name |
Property name | Yes | |
value-type |
Property type (string, single_select, etc.) |
Yes | |
required |
Whether a value is required for all repos | No | false |
description |
Human-readable description | No | |
default-value |
Default value for new repositories | No | |
allowed-values |
List of allowed values (required for select types) | Conditional | |
values-editable-by |
Who can edit: org_actors or org_and_repo_actors |
No | org_actors |
By default, syncing custom properties will create or update the specified properties, but will not delete other properties that may exist in the organization. To delete all other properties not defined in the config, use delete-unmanaged-properties:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
custom-properties-file: './custom-properties.yml'
delete-unmanaged-properties: trueBehavior with delete-unmanaged-properties: true:
- Creates properties that don't exist
- Updates properties that differ from the config
- Deletes all other properties not defined in the config
- In dry-run mode, shows which properties would be deleted without actually deleting them
Sync organization-level rulesets across organizations. Rulesets define rules that apply to repositories within the organization (e.g., branch protection rules, tag rules). Each ruleset is defined in its own JSON file, and rulesets-file accepts comma-separated paths to sync multiple rulesets.
Tip
π See full examples: sample-configuration/rulesets/
Create a JSON file for each ruleset (one ruleset per file):
rulesets/branch-protection.json:
{
"name": "org-branch-protection",
"target": "branch",
"enforcement": "active",
"bypass_actors": [
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "always"
}
],
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
},
"repository_name": {
"include": ["~ALL"],
"exclude": []
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 1,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"automatic_copilot_code_review_enabled": false
}
}
]
}rulesets/tag-protection.json:
{
"name": "org-tag-protection",
"target": "tag",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~ALL"],
"exclude": []
},
"repository_name": {
"include": ["~ALL"],
"exclude": []
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
}
]
}Sync both rulesets using comma-separated paths:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
rulesets-file: './rulesets/branch-protection.json, ./rulesets/tag-protection.json'Tip
The JSON format matches the GitHub REST API for organization rulesets. You can export an existing ruleset from your organization via the API as a starting point, but exported responses may include read-only fields (e.g., id, source, node_id) that are automatically stripped before create/update operations.
Behavior:
- If a ruleset with the same name doesn't exist, it is created
- If it exists but differs from the config, it is updated
- If content is identical, no changes are made
- With
delete-unmanaged-rulesets: true, rulesets not matching any managed name are deleted
In orgs.yml, use a YAML array to override rulesets for a specific org:
orgs:
- org: my-org
# inherits base rulesets-file from action input
- org: my-other-org
rulesets-file:
- './config/rulesets/branch-protection.json'
- './config/rulesets/tag-protection.json'
delete-unmanaged-rulesets: trueBy default, syncing rulesets will create or update the specified rulesets, but will not delete other rulesets that may exist in the organization. To delete all other rulesets besides those being synced, use delete-unmanaged-rulesets:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
rulesets-file: './rulesets/branch-protection.json, ./rulesets/tag-protection.json'
delete-unmanaged-rulesets: trueBehavior with delete-unmanaged-rulesets: true:
- Creates rulesets that don't exist
- Updates rulesets that differ from the config
- Deletes all other rulesets not matching any managed ruleset name
- In dry-run mode, shows which rulesets would be deleted without actually deleting them
| Input | Description | Required | Default |
|---|---|---|---|
github-token |
GitHub token for API access (requires admin:org scope) |
Yes | |
github-api-url |
GitHub API URL (e.g., https://api.github.com or https://ghes.domain.com/api/v3) |
No | ${{ github.api_url }} |
organizations |
Comma-separated list of organization names | No | |
organizations-file |
Path to YAML file containing organization settings configuration | No | |
custom-properties-file |
Path to a YAML file defining custom property schemas | No | |
delete-unmanaged-properties |
Delete custom properties not defined in the configuration file | No | false |
rulesets-file |
Comma-separated paths to JSON files, each with a single org ruleset config | No | |
delete-unmanaged-rulesets |
Delete all other rulesets besides those being synced | No | false |
dry-run |
Preview changes without applying them | No | false |
Note
You must provide either organizations or organizations-file. The custom-properties-file and rulesets-file inputs provide base settings for all orgs and can be combined with either approach. Per-org overrides in organizations-file layer on top of the base.
| Output | Description |
|---|---|
updated-organizations |
Number of organizations successfully processed (changed + unchanged) |
changed-organizations |
Number of organizations with changes (or would have in dry-run mode) |
unchanged-organizations |
Number of organizations with no changes |
failed-organizations |
Number of organizations that failed to update |
warning-organizations |
Number of organizations that emitted warnings |
results |
JSON array of update results for each organization |
Use dry-run: true to preview what changes would be made without actually applying them. The job summary will show all planned changes prefixed with "Would":
π DRY-RUN MODE: No changes will be applied
π Would Create custom property: team
π Would Create custom property: environment
π Would Update custom property: is-production (required: false β true)
npm installnpm test # Run tests
npm run lint # Check code quality with ESLint
npm run format:write # Run Prettier for formatting
npm run package # Bundle for distribution
npm run all # Run format, lint, test, coverage, and packageenv 'INPUT_GITHUB-TOKEN=ghp_xxx' \
'INPUT_ORGANIZATIONS=my-org' \
'INPUT_CUSTOM-PROPERTIES-FILE=./sample-configuration/custom-properties.yml' \
'INPUT_DRY-RUN=true' \
node "$(pwd)/src/index.js"For a complete working example of this action in use, see the sync-github-org-settings repository:
- orgs.yml - Example configuration file with per-org overrides
- sync-github-org-settings.yml - Example workflow using a GitHub App token
Example workflow:
name: sync-github-org-settings
on:
push:
branches: ['main']
pull_request:
branches: ['main']
workflow_dispatch:
jobs:
sync-github-org-settings:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ steps.app-token.outputs.token }}
organizations-file: 'orgs.yml'
custom-properties-file: './config/custom-properties/base.yml'
dry-run: ${{ github.event_name == 'pull_request' }} # dry run if PR- Settings not specified will remain unchanged
- Custom properties that already match the config are skipped (no unnecessary API calls)
- Failed updates are logged as warnings but don't fail the action; if one or more organizations fail entirely, the action is marked as failed
- With
delete-unmanaged-properties: true, properties not in the config are deleted from the organization
Contributions are welcome! See the Development section for setup instructions.