π’ 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
- π·οΈ Sync issue type definitions across organizations
- π§© Sync issue field definitions across organizations
- π§ Sync member privileges and repository policies across organizations
- π Sync
.githuband.github-privaterepository files across organizations (via PR) - π Sync code security configurations across organizations
- π Sync GitHub Actions security and policy settings 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 PRSync a local directory to the .github (and/or .github-private) repository across organizations. The action compares local files against the target repository and creates a PR with any creates/updates:
- name: Sync .github repo files
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org,my-other-org'
dot-github-source-dir: './dot-github-template'
dot-github-private-source-dir: './dot-github-private-template'
dry-run: ${{ github.event_name == 'pull_request' }}This sync is intentionally non-destructive: it creates or updates files present in the source directory, but it does not delete remote-only files from .github or .github-private.
For stronger security and higher rate limits, use a GitHub App:
-
Create a GitHub App with the following permissions:
Organization permissions:
- Administration: Read and write (required for managing organization settings, rulesets, and organization role team assignments)
- Custom properties: Admin (required for managing custom property definitions)
- Custom organization roles: Write (required for managing custom organization roles)
- Custom repository roles: Write (required for managing custom repository roles)
- Issue types: Write (required for managing issue type definitions)
- Issue fields: Write (required for managing issue field definitions)
Repository permissions (only required for
.github/.github-privaterepo sync):- Contents: Read and write
- Workflows: Read and write (required if syncing workflow files)
- Pull requests: Read and write
-
Install it to your organization(s)
-
Add
APP_CLIENT_IDas a repository variable andAPP_PRIVATE_KEYas a repository secret
If a sync step warns that it could not fetch existing settings with status 403 or 404, re-check the matching GitHub App permission above and re-approve the app installation. GitHub can return 404 for inaccessible organization resources, not only missing resources.
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.APP_CLIENT_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 action inputs (including member privilege inputs and 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 or member privileges.
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
# No member-privileges β inherits all base settings from action inputs
- 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_actors
member-privileges:
# Override specific member privilege settings for this org
members-can-fork-private-repositories: true
members-can-create-internal-repositories: true # GHEC/GHES onlyOptional: base-path
Use the base-path top-level property to avoid repeating a common directory prefix for all file-path settings (custom-properties-file, issue-types-file, issue-fields-file, rulesets-file). Relative paths in per-org overrides are resolved relative to base-path. Absolute paths are left unchanged.
base-path: './config/'
orgs:
- org: my-org
custom-properties-file: 'custom-properties/base.yml' # resolved to ./config/custom-properties/base.yml
issue-types-file: 'issue-types/base.yml' # resolved to ./config/issue-types/base.yml
rulesets-file: 'rulesets/branch-protection.json' # resolved to ./config/rulesets/branch-protection.json
- org: my-other-org
custom-properties-file: 'custom-properties/other-org.yml'Use 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 orgs
default-repository-permission: read # Base member privileges for all orgs
members-can-fork-private-repositories: falseSettings 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)The same merging applies to member privilege inputs and per-org member-privileges β per-org settings override base settings with the same key; base settings not overridden are preserved:
# action inputs (base): default-repository-permission=read, members-can-fork-private-repositories=false
# orgs.yml:
orgs:
- org: my-org # gets: read + no fork (base only)
- org: my-other-org
member-privileges:
members-can-fork-private-repositories: true # override β fork allowed
# gets: read (from base) + fork allowed (overridden)Most features that accept a base configuration file (e.g. custom-properties-file) also support per-org overrides in orgs.yml. For each of these features you can override per org in one of two equivalent ways:
- Inline β embed the config directly under the org entry (e.g.
custom-properties: [...]) - File-based β point the org at a different file (e.g.
custom-properties-file: './config/other-org.yml')
The file-based form lets you keep per-org config in separate files while still using a single orgs.yml to map orgs. Use whichever form is more convenient for each org β you can mix and match across orgs in the same orgs.yml.
| Feature | Inline key | File-path key | Per-org semantics |
|---|---|---|---|
| Custom properties | custom-properties |
custom-properties-file |
Merge by name with base |
| Issue types | issue-types |
issue-types-file |
Merge by name with base |
| Issue fields | issue-fields |
issue-fields-file |
Merge by name with base |
| Custom organization roles | custom-org-roles |
custom-org-roles-file |
Merge by name with base |
| Custom repository roles | custom-repo-roles |
custom-repo-roles-file |
Merge by name with base |
| Code security configurations | code-security-configurations |
code-security-configurations-file |
Merge by name with base |
| Organization role team assignments | organization-role-team-assignments |
organization-role-team-assignments-file |
Replaces base for that org |
| Member privileges | member-privileges |
(direct action inputs serve as base) | Per-key override of base |
| Organization profile | org-profile |
(direct action inputs serve as base) | Per-key override of base |
| Actions policy | actions-policy |
(direct action inputs serve as base) | Per-key override of base |
| Rulesets | (file only β no inline form) | rulesets-file (string or YAML array) |
Replaces base for that org |
| Actions allow list | (file only β no inline form) | actions-allow-list-file |
Replaces base for that org |
For features that support both forms, precedence is:
- Inline per-org config (highest)
- File-based per-org config (
*-fileunder the org entry) - Base input/file (lowest β applied to all orgs that don't override)
When both inline and file-based per-org overrides are set for the same org:
- For replace-semantics features (currently
organization-role-team-assignments), inline takes precedence and the per-org file is ignored. - For merge-by-name features (
custom-properties,issue-types,issue-fields,custom-org-roles,custom-repo-roles,code-security-configurations), the per-org file becomes that org's base and inline entries then merge on top byname.
- Merge by
nameβ per-org list items with the samenameoverride the base item; other base items are preserved. - Replaces base for that org β the entire per-org value replaces the base (no merge).
- Per-key override of base β keys present in the per-org block override base values for those keys only; other base keys are preserved.
Tip
π See full example: sample-configuration/orgs.yml
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 |
Note
GitHub API-style underscore aliases are also accepted for custom property fields:
value_type, default_value, allowed_values, and values_editable_by.
Hyphenated fields remain supported in v1 for backward compatibility, but are planned to be removed in v2.
In orgs.yml, override custom properties per org either inline or by pointing at a different file. Per-org properties are merged with the base by name; see Per-Org Overrides: Inline vs File-Based for precedence and merge rules.
orgs:
- org: my-org
# inherits base custom-properties-file as-is
- org: inline-org
custom-properties: # inline override (merges with base by name)
- name: team
value-type: single_select
required: true
allowed-values: [platform, frontend, backend, data-science]
values-editable-by: org_actors
delete-unmanaged-properties: true
- org: file-based-org
custom-properties-file: './config/custom-properties/file-based-org.yml' # file-based overrideBy 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
Rulesets are file-only β there is no inline form. Override per org by pointing rulesets-file at a different set of JSON files. Per-org rulesets replace the base for that org. See Per-Org Overrides: Inline vs File-Based for precedence.
orgs:
- org: my-org
# inherits base rulesets-file from action input
- org: my-other-org
rulesets-file: # YAML array of JSON file paths (replaces base for this org)
- './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
Sync organization-level issue type definitions across organizations. Issue types define the categories (e.g., Bug, Feature, Task) that can be assigned to issues within the organization.
Create an issue-types.yml file:
- name: Bug
description: 'Something is broken'
color: 'ff0000'
- name: Feature
description: 'A new feature request'
color: '0e8a16'
- name: Task
description: 'A unit of work'
color: 'fbca04'
is-enabled: trueUse in workflow:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
issue-types-file: './issue-types.yml'Behavior:
- If an issue type 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-issue-types: true, issue types not in the config are deleted
Each issue type supports these fields:
| Field | Description | Required | Default |
|---|---|---|---|
name |
Issue type name | Yes | |
description |
Human-readable description | No | |
color |
Named color (gray, blue, green, yellow, orange, red, pink, purple) or 6-character hex (without #) |
No | |
is-enabled |
Whether the issue type is enabled | No | true |
Color values are normalized case-insensitively before comparison to avoid unnecessary updates when only letter casing differs.
In orgs.yml, override issue types per org either inline or by pointing at a different file. Per-org issue types are merged with the base by name; see Per-Org Overrides: Inline vs File-Based for precedence and merge rules.
orgs:
- org: my-org
# inherits base issue-types-file from action input
- org: inline-org
issue-types: # inline override (merges with base by name)
- name: Bug
description: 'Critical bug for this org'
color: 'ff0000'
delete-unmanaged-issue-types: true
- org: file-based-org
issue-types-file: './config/issue-types/file-based-org.yml' # file-based overrideBy default, syncing issue types will create or update the specified types, but will not delete other issue types that may exist in the organization. To delete all other issue types not defined in the config, use delete-unmanaged-issue-types:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
issue-types-file: './issue-types.yml'
delete-unmanaged-issue-types: trueBehavior with delete-unmanaged-issue-types: true:
- Creates issue types that don't exist
- Updates issue types that differ from the config
- Deletes all other issue types not defined in the config
- In dry-run mode, shows which issue types would be deleted without actually deleting them
Sync organization-level issue field definitions across organizations. Issue fields let you add structured metadata (for example priority, effort, target date) to issues.
Tip
π See full example: sample-configuration/issue-fields.yml
Create an issue-fields.yml file:
- name: Priority
description: 'Issue priority'
data-type: single_select
visibility: organization_members_only
options:
- name: Urgent
color: red
priority: 1
- name: High
color: orange
priority: 2
- name: Medium
color: yellow
priority: 3
- name: Low
color: green
priority: 4
- name: Target date
description: 'Target completion date'
data-type: dateUse in workflow:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
issue-fields-file: './issue-fields.yml'Behavior:
- If an issue field 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-issue-fields: true, issue fields not in the config are deleted
Each issue field supports these fields:
| Field | Description | Required | Default |
|---|---|---|---|
name |
Issue field name | Yes | |
description |
Human-readable description | No | |
data-type |
Field type: text, date, single_select, number |
Yes | |
visibility |
Field visibility: organization_members_only or all |
No | API default |
options |
Required for single_select; list of options with name, color, and priority |
Conditional |
In orgs.yml, override issue fields per org either inline or by pointing at a different file. Per-org issue fields are merged with the base by name; see Per-Org Overrides: Inline vs File-Based for precedence and merge rules.
orgs:
- org: my-org
# inherits base issue-fields-file from action input
- org: inline-org
issue-fields: # inline override (merges with base by name)
- name: Priority
data-type: single_select
options:
- name: Critical
color: red
priority: 1
- name: Normal
color: yellow
priority: 2
delete-unmanaged-issue-fields: true
- org: file-based-org
issue-fields-file: './config/issue-fields/file-based-org.yml' # file-based overrideBy default, syncing issue fields will create or update the specified fields, but will not delete other issue fields that may exist in the organization. To delete all other issue fields not defined in the config, use delete-unmanaged-issue-fields:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
issue-fields-file: './issue-fields.yml'
delete-unmanaged-issue-fields: trueImportant
Custom organization roles require GitHub Enterprise Cloud (GHEC).
Sync custom organization roles across organizations. These define custom roles with specific organization-level permissions that can be assigned to members.
Tip
π See full examples: sample-configuration/custom-org-roles.yml
Create a YAML file defining your custom organization roles:
custom-org-roles.yml:
- name: Security Auditor
description: 'Can view security alerts and manage security settings'
permissions:
- read_audit_log
- manage_organization_security
- name: CI/CD Manager
description: 'Can manage Actions settings and self-hosted runners'
permissions:
- manage_organization_actions_settings
- manage_organization_runnersThen reference it in your workflow:
- 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-org-roles-file: './custom-org-roles.yml'
delete-unmanaged-org-roles: falseBehavior:
- Creates new roles that don't exist yet
- Updates roles that differ from the config (description or permissions)
- Only applies changes when the role definition differs from what's already configured
- In dry-run mode, shows what would be changed without applying
When delete-unmanaged-org-roles: true:
- Creates and updates roles from the config
- Deletes all other custom org roles not defined in the config
- In dry-run mode, shows which roles would be deleted without actually deleting them
In orgs.yml, override custom org roles per org either inline or by pointing at a different file. Per-org roles are merged with the base by name; see Per-Org Overrides: Inline vs File-Based for precedence and merge rules.
orgs:
- org: my-org
# inherits base custom-org-roles-file as-is
- org: inline-org
custom-org-roles: # inline override (merges with base by name)
- name: Security Auditor
description: 'Override for this org'
permissions:
- read_audit_log
delete-unmanaged-org-roles: true
- org: file-based-org
custom-org-roles-file: './config/custom-org-roles/file-based-org.yml' # file-based overrideSync organization profile/branding fields across organizations. These control the public-facing identity of the organization.
Set organization profile fields directly as action inputs:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
org-name: 'My Organization'
org-description: 'Building great things'
org-company: 'My Company Inc.'
org-location: 'San Francisco, CA'
org-email: 'contact@myorg.com'
org-twitter-username: 'myorg'
org-url: 'https://myorg.com'Note
org-blog is deprecated and still supported for backward compatibility. If both org-url and org-blog are set, org-url takes precedence.
In orgs.yml, use org-profile to override specific profile fields for an org. Per-org keys override base action inputs for those keys only; see Per-Org Overrides: Inline vs File-Based.
orgs:
- org: my-org
# inherits base org profile action inputs
- org: my-other-org
org-profile:
org-name: 'Different Name' # override base
org-description: 'Custom description for this org'Important
Custom repository roles require GitHub Enterprise Cloud (GHEC).
Sync custom repository roles across organizations. These define custom roles that extend a base role (read, triage, write, maintain, admin) with additional repository-level permissions.
Tip
π See full examples: sample-configuration/custom-repo-roles.yml
Create a YAML file defining your custom repository roles:
custom-repo-roles.yml:
- name: Contractor
description: 'Write access without sensitive settings'
base-role: write
permissions:
- delete_alerts_code_scanning
- name: Release Manager
description: 'Can manage releases and deployments'
base-role: maintain
permissions:
- manage_deploy_keys
- manage_webhooksThen reference it in your workflow:
- 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-repo-roles-file: './custom-repo-roles.yml'
delete-unmanaged-repo-roles: falseBehavior:
- Creates new roles that don't exist yet
- Updates roles that differ from the config (description, base role, or permissions)
- Only applies changes when the role definition differs from what's already configured
- In dry-run mode, shows what would be changed without applying
Note
GitHub API-style base_role is also accepted in custom repository role files.
base-role remains supported in v1 for backward compatibility, but is planned to be removed in v2.
When delete-unmanaged-repo-roles: true:
- Creates and updates roles from the config
- Deletes all other custom repo roles not defined in the config
- In dry-run mode, shows which roles would be deleted without actually deleting them
In orgs.yml, override custom repository roles per org either inline or by pointing at a different file. Per-org roles are merged with the base by name; see Per-Org Overrides: Inline vs File-Based for precedence and merge rules.
orgs:
- org: my-org
# inherits base custom-repo-roles-file as-is
- org: inline-org
custom-repo-roles: # inline override (merges with base by name)
- name: Contractor
description: 'Override for this org'
base-role: write
permissions:
- delete_alerts_code_scanning
delete-unmanaged-repo-roles: true
- org: file-based-org
custom-repo-roles-file: './config/custom-repo-roles/file-based-org.yml' # file-based overrideAssign built-in or custom organization roles to teams by slug.
This uses GitHub's organization roles APIs and supports built-in roles such as security_manager and CI/CD Admin, plus custom organization roles created by custom-org-roles-file. Custom organization role definitions are synced before team assignments, so newly created custom roles can be assigned in the same run.
- 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'
organization-role-team-assignments-file: './config/organization-role-team-assignments.yml'# config/organization-role-team-assignments.yml
- role: security_manager
teams:
- security-team
- appsec
delete-unmanaged: true
- role: CI/CD Admin
teams: platform-admins
- role: Security Auditor
teams:
- complianceBehavior:
- Adds configured team slugs that do not already have the organization role
- Leaves existing role team assignments alone unless
delete-unmanaged: trueis set for that role - When
delete-unmanaged: true, removes teams assigned to that role that are not in the configured desired set - In dry-run mode, shows which teams would be added or removed without applying changes
In orgs.yml, override organization role team assignments per org either inline or by pointing at a different file. Per-org assignments replace the base for that org (no merge); see Per-Org Overrides: Inline vs File-Based for precedence.
orgs:
- org: inline-org
organization-role-team-assignments: # inline override (replaces base for this org)
- role: security_manager
teams:
- security-team
- appsec
delete-unmanaged: true
- org: file-based-org
organization-role-team-assignments-file: './config/org-role-team-assignments/file-based-org.yml' # file-based overrideNote
If both inline organization-role-team-assignments and organization-role-team-assignments-file are specified for the same org, inline values take precedence and the file is ignored.
Sync organization-level member privilege settings (repository policies) across organizations. These control what members can do within the organization, such as creating repositories, forking private repos, and managing pages.
Set member privilege settings directly as action inputs:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
default-repository-permission: read
members-can-create-repositories: true
members-can-fork-private-repositories: false
members-can-create-internal-repositories: false # GHEC/GHES only
web-commit-signoff-required: true
default-repository-branch: mainBehavior:
- Only settings included in the config are managed β omitted settings remain unchanged
- If a setting already matches the config, no API call is made
- Settings are applied via a single
PATCH /orgs/{org}call per organization - In dry-run mode, shows which settings would be changed without applying them
| Setting | Type | Description |
|---|---|---|
default-repository-permission |
string | Default permission for org members: read, write, admin, none |
members-can-create-repositories |
boolean | Can members create repositories |
members-can-create-public-repositories |
boolean | Can members create public repositories |
members-can-create-private-repositories |
boolean | Can members create private repositories |
members-can-create-internal-repositories |
boolean | Can members create internal repositories (GHEC/GHES only) |
members-can-fork-private-repositories |
boolean | Can members fork private repositories |
web-commit-signoff-required |
boolean | Require web UI commits to be signed off |
members-can-create-pages |
boolean | Can members create GitHub Pages sites |
members-can-create-public-pages |
boolean | Can members create public GitHub Pages sites |
members-can-create-private-pages |
boolean | Can members create private GitHub Pages sites |
members-can-invite-outside-collaborators |
boolean | Can members invite outside collaborators |
members-can-create-teams |
boolean | Can members create teams |
members-can-delete-repositories |
boolean | Can members delete repositories |
members-can-change-repo-visibility |
boolean | Can members change repository visibility |
members-can-delete-issues |
boolean | Can members delete issues |
default-repository-branch |
string | Default branch name for new repositories |
deploy-keys-enabled-for-repositories |
boolean | Whether deploy keys can be added to repositories |
readers-can-create-discussions |
boolean | Can users with read access create discussions |
members-can-view-dependency-insights |
boolean | Can members view dependency insights |
display-commenter-full-name-setting-enabled |
boolean | Display commenter full name in issues and PRs |
In orgs.yml, use member-privileges to override specific settings for an org (per-key override of base inputs). See Per-Org Overrides: Inline vs File-Based for precedence.
orgs:
- org: my-org
# inherits base member privilege action inputs
- org: my-other-org
member-privileges:
members-can-fork-private-repositories: true # override base
members-can-create-internal-repositories: true # GHEC/GHES onlySync a local directory to the .github (and/or .github-private) repository across organizations. The action compares local files against the target repository and creates a PR with any creates/updates:
- name: Sync .github repo files
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org,my-other-org'
dot-github-source-dir: './dot-github-template'
dot-github-private-source-dir: './dot-github-private-template'
dry-run: ${{ github.event_name == 'pull_request' }}This sync is intentionally non-destructive: it creates or updates files present in the source directory, but it does not delete remote-only files from .github or .github-private.
Per-org overrides can be set in orgs.yml using the dot-github-source-dir and dot-github-private-source-dir keys.
By default, if the target .github or .github-private repository doesn't exist, the action skips it with a warning. To have the action bootstrap missing repos before syncing, opt in with create-missing-dot-github-repos: true. Only repos with a configured source-dir are affected. Created repos use auto_init: true so the sync flow has a default branch to PR against.
- name: Sync .github repo files (auto-create missing)
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org,my-other-org'
dot-github-source-dir: './dot-github-template'
dot-github-private-source-dir: './dot-github-private-template'
create-missing-dot-github-repos: true
# Defaults: dot-github-repo-visibility=public, dot-github-private-repo-visibility=private.
# EMU / restricted-GHEC orgs that disallow public repos must set this to 'internal'.
dot-github-repo-visibility: public
dot-github-private-repo-visibility: privateAll three settings (create-missing-dot-github-repos, dot-github-repo-visibility, dot-github-private-repo-visibility) can also be set per-org in orgs.yml. Allowed visibility values: public, private, internal.
Important
Creating repositories requires administration: write on the GitHub App at the organization level, in addition to the existing contents: write. If public is rejected by the organization (e.g. Enterprise Managed Users or a restricted GHEC org), the action emits an actionable warning suggesting the appropriate repo-specific visibility setting: dot-github-repo-visibility: internal for .github, or dot-github-private-repo-visibility: internal for .github-private.
Sync named code security configurations across organizations. These configurations define security feature enablement policies (e.g., Dependabot, secret scanning, code scanning) that can be applied to repositories.
- 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'
code-security-configurations-file: './code-security-configurations.yml'# code-security-configurations.yml
- name: High risk settings
description: Security configuration for high risk repositories
advanced_security: enabled
dependency_graph: enabled
dependabot_alerts: enabled
dependabot_security_updates: enabled
code_scanning_default_setup: enabled
secret_scanning: enabled
secret_scanning_push_protection: enabled
private_vulnerability_reporting: enabled
enforcement: enforced
- name: Standard settings
description: Security configuration for standard repositories
advanced_security: enabled
dependency_graph: enabled
dependabot_alerts: enabled
secret_scanning: enabled
secret_scanning_push_protection: enabled
private_vulnerability_reporting: enabled
enforcement: unenforcedAll enablement fields accept: enabled, disabled, or not_set. The enforcement field accepts: enforced or unenforced.
You can also configure how each code security configuration is applied:
attach-scope: attach toall,all_without_configurations,public,private_or_internal, orselectedrepositoriesselected-repository-ids: optional repository IDs whenattach-scope: selectedselected-repositories: optional repository names (for examplehigh-risk-serviceorapp-api) whenattach-scope: selectedselected-repositories-by-property: optional list of{property, value}filters; any repo in the org matching any filter is included whenattach-scope: selecteddefault-for-new-repos: set default for newly created repos (all,none,public,private_and_internal)
Example:
- name: High risk settings
description: Security configuration for high risk repositories
advanced_security: enabled
attach-scope: selected
selected-repositories: [high-risk-service, app-api]
default-for-new-repos: private_and_internalOr select repositories by custom property:
- name: High risk settings
description: Security configuration for high risk repositories
advanced_security: enabled
attach-scope: selected
selected-repositories-by-property:
- property: criticality
value: high
default-for-new-repos: private_and_internalselected-repository-ids, selected-repositories, and selected-repositories-by-property can all be combined β matching repos from all three sources are merged into one set.
If multiple configurations use attach-scope, broader scopes are applied first and selected is applied last, so selected repositories can override broad assignments.
For attach-scope, the following combinations are invalid and will fail the run:
- The same broad scope (
all,all_without_configurations,public,private_or_internal) cannot appear on more than one configuration. allcannot be combined withall_without_configurations,public, orprivate_or_internal.all_without_configurationscannot be combined withpublicorprivate_or_internal(unconfigured repos in those visibility categories would be targeted by both).selectedmay appear on multiple configurations, but each repository may only be targeted by one of them β overlapping repo sets acrossselected-scope configurations will fail the run.
For default-for-new-repos, values must not conflict:
nonecannot be combined with any other default assignment.allcannot be combined withpublicorprivate_and_internal.- You cannot define the same default target more than once.
In orgs.yml, override code security configurations per org either inline or by pointing at a different file. Per-org configurations are merged with the base by name; see Per-Org Overrides: Inline vs File-Based for precedence and merge rules.
orgs:
- org: my-org
# inherits base code-security-configurations-file as-is
- org: inline-org
code-security-configurations: # inline override (merges with base by name)
- name: High risk settings
description: Stricter settings for this org
advanced_security: enabled
secret_scanning: enabled
secret_scanning_push_protection: enabled
enforcement: enforced
attach-scope: all_without_configurations
default-for-new-repos: private_and_internal
delete-unmanaged-code-security-configurations: true
- org: file-based-org
code-security-configurations-file: './config/code-security/file-based-org.yml' # file-based overrideSet delete-unmanaged-code-security-configurations: true to remove code security configurations not defined in the configuration file. Only custom (organization-owned) configurations are deleted β global GitHub-managed configurations are never touched.
When attach-scope and/or default-for-new-repos are configured, the action also applies repository attachment and default assignment for that named configuration.
Note
Requires a GitHub Advanced Security (GHAS) license for advanced_security features. Available on GitHub.com (GHEC) and GHES 3.x+.
Sync organization-level GitHub Actions security and policy settings across organizations. These control which actions can run, workflow token permissions, and PR approval policies.
Set actions policy settings directly as action inputs:
- name: Sync Organization Settings
uses: joshjohanning/bulk-github-org-settings-sync-action@v1
with:
github-token: ${{ secrets.ORG_ADMIN_TOKEN }}
organizations: 'my-org'
actions-policy-allowed-actions: selected
actions-policy-github-owned-allowed: true
actions-policy-verified-allowed: true
actions-allow-list-file: './actions-allow-list.yml'
actions-policy-default-workflow-permissions: read
actions-policy-actions-can-approve-pull-request-reviews: falseBehavior:
- Only settings included in the config are managed β omitted settings remain unchanged
- If a setting already matches the config, no API call is made
- Settings are applied via
PUTcalls to the appropriate/orgs/{org}/actions/permissions/*endpoints - In dry-run mode, shows which settings would be changed without applying them
- The
github-owned-allowed,verified-allowed, andactions-allow-list-filesettings only apply whenallowed-actionsisselected
| Setting | Type | Description |
|---|---|---|
allowed-actions |
string | Allowed actions policy: all, local_only, or selected |
github-owned-allowed |
boolean | Allow GitHub-owned actions (when allowed-actions is selected) |
verified-allowed |
boolean | Allow GitHub Marketplace verified creator actions (when allowed-actions is selected) |
default-workflow-permissions |
string | Default GITHUB_TOKEN permissions for workflows: read or write |
actions-can-approve-pull-request-reviews |
boolean | Whether GitHub Actions can approve pull request reviews |
When allowed-actions is selected, use actions-allow-list-file to specify allowed action/reusable workflow patterns:
# actions-allow-list.yml
actions:
- actions/checkout@*
- actions/setup-node@*
- actions/cache@*
- myorg/* # all actions from an ownerIn orgs.yml, use actions-policy to override individual actions policy settings for an org (per-key override of base inputs). The actions-allow-list-file is file-only β there is no inline form; point it at a different file to override. See Per-Org Overrides: Inline vs File-Based for precedence.
orgs:
- org: my-org
# inherits base actions policy action inputs
- org: my-other-org
actions-policy:
allowed-actions: selected # override base
github-owned-allowed: true
verified-allowed: true
actions-allow-list-file: './config/actions-allow-list/other-org.yml' # file-only override| 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 |
issue-types-file |
Path to a YAML file defining issue type definitions | No | |
delete-unmanaged-issue-types |
Delete issue types not defined in the configuration file | No | false |
issue-fields-file |
Path to a YAML file defining issue field definitions | No | |
delete-unmanaged-issue-fields |
Delete issue fields not defined in the configuration file | No | false |
default-repository-permission |
Default permission for org members: read, write, admin, none |
No | |
members-can-create-repositories |
Whether members can create repositories | No | |
members-can-create-public-repositories |
Whether members can create public repositories | No | |
members-can-create-private-repositories |
Whether members can create private repositories | No | |
members-can-create-internal-repositories |
Whether members can create internal repositories (GHEC/GHES only) | No | |
members-can-fork-private-repositories |
Whether members can fork private repositories | No | |
web-commit-signoff-required |
Whether web UI commits require signoff | No | |
members-can-create-pages |
Whether members can create GitHub Pages sites | No | |
members-can-create-public-pages |
Whether members can create public GitHub Pages sites | No | |
members-can-create-private-pages |
Whether members can create private GitHub Pages sites | No | |
members-can-invite-outside-collaborators |
Whether members can invite outside collaborators | No | |
members-can-create-teams |
Whether members can create teams | No | |
members-can-delete-repositories |
Whether members can delete repositories | No | |
members-can-change-repo-visibility |
Whether members can change repository visibility | No | |
members-can-delete-issues |
Whether members can delete issues | No | |
default-repository-branch |
Default branch name for new repositories | No | |
deploy-keys-enabled-for-repositories |
Whether deploy keys can be added to repositories | No | |
readers-can-create-discussions |
Whether users with read access can create discussions | No | |
members-can-view-dependency-insights |
Whether members can view dependency insights | No | |
display-commenter-full-name-setting-enabled |
Whether to display commenter full name in issues and PRs | No | |
organization-role-team-assignments-file |
Path to a YAML file defining organization role team assignments | No | |
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 |
custom-org-roles-file |
Path to a YAML file defining custom organization role definitions (GHEC only) | No | |
delete-unmanaged-org-roles |
Delete custom org roles not defined in the configuration file | No | false |
custom-repo-roles-file |
Path to a YAML file defining custom repository role definitions (GHEC only) | No | |
delete-unmanaged-repo-roles |
Delete custom repo roles not defined in the configuration file | No | false |
dot-github-source-dir |
Path to a local directory to sync to the .github repo in each org (via PR) |
No | |
dot-github-private-source-dir |
Path to a local directory to sync to the .github-private repo in each org (via PR) |
No | |
create-missing-dot-github-repos |
Create missing .github / .github-private repos before syncing (requires administration: write) |
No | false |
dot-github-repo-visibility |
Visibility for newly created .github repo: public, private, or internal |
No | public |
dot-github-private-repo-visibility |
Visibility for newly created .github-private repo: public, private, or internal |
No | private |
actions-policy-allowed-actions |
Allowed GitHub Actions policy: all, local_only, or selected |
No | |
actions-policy-github-owned-allowed |
Whether GitHub-owned actions are allowed (when allowed-actions is selected) |
No | |
actions-policy-verified-allowed |
Whether verified creator actions are allowed (when allowed-actions is selected) |
No | |
actions-allow-list-file |
Path to YAML file with allowed action/reusable workflow patterns | No | |
actions-policy-default-workflow-permissions |
Default GITHUB_TOKEN permissions for workflows: read or write |
No | |
actions-policy-actions-can-approve-pull-request-reviews |
Whether GitHub Actions can approve pull request reviews | No | |
org-name |
Organization display name | No | |
org-description |
Organization description (max 160 chars) | No | |
org-company |
Company name | No | |
org-location |
Location | No | |
org-email |
Publicly visible email | No | |
org-twitter-username |
Twitter/X username | No | |
org-url |
Website URL | No | |
org-blog |
Blog/website URL (deprecated; use org-url) |
No | |
code-security-configurations-file |
Path to a YAML file defining code security configurations to sync | No | |
delete-unmanaged-code-security-configurations |
Delete code security configurations not defined in the configuration file | No | false |
dry-run |
Preview changes without applying them | No | false |
Note
You must provide either organizations or organizations-file. The custom-properties-file, issue-types-file, issue-fields-file, rulesets-file, custom-org-roles-file, custom-repo-roles-file, actions-allow-list-file, code-security-configurations-file, and organization-role-team-assignments-file inputs provide base settings for all orgs and can be combined with either approach. Member privilege settings can be provided as individual inputs (e.g., default-repository-permission). Actions policy settings can be provided as individual inputs (e.g., actions-policy-allowed-actions). Org profile settings can be provided as individual inputs (e.g., org-name). The dot-github-source-dir and dot-github-private-source-dir inputs sync a local directory to the respective special repositories via PR, and create-missing-dot-github-repos plus the repo-visibility inputs control optional repo bootstrapping before sync. Per-org overrides in organizations-file layer on top of the base unless otherwise noted β see Per-Org Overrides: Inline vs File-Based for which features support inline vs file-path overrides and their precedence and merge semantics.
| 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:
client-id: ${{ vars.APP_CLIENT_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 and member privileges 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 members-can-create-internal-repositoriesonly applies to organizations on GitHub Enterprise Cloud (GHEC) or GitHub Enterprise Server (GHES)
Contributions are welcome! See the Development section for setup instructions.