Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
53285fb
chore: ignore .posit/
kelly-sovacool Jun 25, 2026
5ee2b1f
feat: cli cmd + action to copy repo rulesets
kelly-sovacool Jun 25, 2026
4bcddd4
chore: update CHANGELOG.md
kelly-sovacool Jun 25, 2026
6688d04
Merge branch 'main' into copy-gh-ruleset
kelly-sovacool Jun 25, 2026
4306cc3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 25, 2026
104ffc8
ci: 🤖 render readme & format everything with pre-commit
ccbr-bot[bot] Jun 25, 2026
7afcf5e
ci: 🤖 render readme & format everything with pre-commit
ccbr-bot[bot] Jun 25, 2026
eee9822
chore: update changelog
kelly-sovacool Jun 25, 2026
20c4a6a
docs: session param desc
kelly-sovacool Jun 25, 2026
dbc05ad
style: fix python returns
kelly-sovacool Jun 25, 2026
cc9ca1f
refactor: use gh api directly
kelly-sovacool Jun 25, 2026
fcdbec4
chore: Merge branch 'copy-gh-ruleset' of github.com:CCBR/actions into…
kelly-sovacool Jun 25, 2026
a5344f5
ci: 🤖 render readme & format everything with pre-commit
ccbr-bot[bot] Jun 25, 2026
a565009
docs(copy-ruleset): add automatic workflow trigger examples
Copilot Jun 26, 2026
602467b
docs(copy-ruleset): guard automated trigger inputs
Copilot Jun 26, 2026
8ecec8c
ci: 🤖 render readme & format everything with pre-commit
ccbr-bot[bot] Jun 29, 2026
5b78616
docs(changelog): add copilot as author
kelly-sovacool Jul 2, 2026
61285d0
Merge branch 'main' into copy-gh-ruleset
kelly-sovacool Jul 2, 2026
c466be4
Merge branch 'main' into copy-gh-ruleset
kelly-sovacool Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,12 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# quarto
/.quarto/
/_site/
/objects.json
/reference
/_sidebar.yml

**/*.quarto_ipynb

/.posit/
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## actions development version

- New commands `list-rulesets` and `copy-ruleset`, plus a `copy-ruleset` action, for copying GitHub rulesets between repositories. (#183, @kelly-sovacool, @copilot)

## actions 0.7.1

- Fix `draft-release` to preserve custom keys in `CITATION.cff` in R packages. (#180, @kelly-sovacool)
Expand Down
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ them for your needs.
- [build-snakemake](examples/build-snakemake.yml)
- [changed-files](examples/changed-files.yml)
- [check-links](examples/check-links.yml)
- [copy-ruleset](examples/copy-ruleset.yml)
- [docs-mkdocs](examples/docs-mkdocs.yml)
- [docs-quarto](examples/docs-quarto.yml)
- [draft-release](examples/draft-release.yml)
Expand All @@ -54,6 +55,8 @@ Custom actions used in our github workflows.
guidelines
- [Changed Files](changed-files) - Get a list of changed files and
filter them by a list of path patterns similar to .gitignore
- [Copy Ruleset](copy-ruleset) - Copy a repository ruleset from one
GitHub repository to another.
- [draft-release](draft-release) - Draft a new release based on
conventional commits and prepare release notes
- [Install R + pak](install-r-pak) - Install R package dependencies with
Expand Down Expand Up @@ -115,7 +118,9 @@ pip install git+https://github.com/CCBR/actions@v0.7
-h, --help Show this message and exit.

Commands:
use-example Use a GitHub Actions workflow file from CCBR/actions.
use-example Use a GitHub Actions workflow file from CCBR/actions.
list-rulesets List all rulesets for a GitHub repository.
copy-ruleset Copy a ruleset from one GitHub repository to another.

#### use-example

Expand All @@ -136,6 +141,45 @@ pip install git+https://github.com/CCBR/actions@v0.7
Options:
-h, --help Show this message and exit.

#### list-rulesets

Usage: ccbr_actions list-rulesets [OPTIONS] REPO

List all rulesets for a GitHub repository.

Args:
repo (str): Repository in owner/repo format.

Examples:
ccbr_actions list-rulesets CCBR/actions
ccbr_actions list-rulesets CCBR/actions --token ghp_...

Options:
-t, --token TEXT GitHub token with repo scope. Defaults to the GH_TOKEN
environment variable.
-h, --help Show this message and exit.

#### copy-ruleset

Usage: ccbr_actions copy-ruleset [OPTIONS] SOURCE_REPO TARGET_REPO
RULESET_NAME

Copy a ruleset from one GitHub repository to another.

Args:
source-repo (str): Source repository in owner/repo format.
target-repo (str): Target repository in owner/repo format.
ruleset-name (str): Name of the ruleset to copy.

Examples:
ccbr_actions copy-ruleset CCBR/actions CCBR/other-repo "Require PR reviews"
ccbr_actions copy-ruleset CCBR/actions CCBR/other-repo "Require PR reviews" --token ghp_...

Options:
-t, --token TEXT GitHub token with repo scope. Defaults to the GH_TOKEN
environment variable.
-h, --help Show this message and exit.

## Help & Contributing

Come across a **bug**? Open an
Expand Down
124 changes: 124 additions & 0 deletions copy-ruleset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# copy-ruleset

**`Copy Ruleset`** - Copy a repository ruleset from one GitHub
repository to another.

Copy a named ruleset from one GitHub repository to another. This action
fetches the full ruleset definition from the source repository via the
GitHub API and creates an identical ruleset in the target repository. It
is useful for standardising branch protection rules across multiple
repositories in an organisation.

## Usage

### Basic example

[copy-ruleset.yml](/examples/copy-ruleset.yml)

```yaml
name: copy-ruleset

on:
workflow_dispatch:
inputs:
source-repo:
description: "Source repository (owner/repo) to copy the ruleset from."
required: true
target-repo:
description: "Target repository (owner/repo) to copy the ruleset to."
required: true
ruleset-name:
description: "Name of the ruleset to copy."
required: true
# Push to the default branch can handle first runs in repositories
# created from templates (initial commit push).
push:
branches:
- main
- master
# Allow central automation to trigger this workflow remotely.
repository_dispatch:
types:
- copy-ruleset
# Optional periodic sync to keep rulesets aligned over time.
schedule:
- cron: "0 5 * * 1"

permissions:
contents: read

jobs:
copy-ruleset:
if: >-
${{
github.event_name == 'workflow_dispatch' ||
(vars.RULESET_SOURCE_REPO != '' && vars.RULESET_NAME != '')
}}
runs-on: ubuntu-latest
steps:
- uses: CCBR/actions/copy-ruleset@latest
with:
source-repo: ${{ github.event_name == 'workflow_dispatch' && inputs['source-repo'] || vars.RULESET_SOURCE_REPO }}
target-repo: ${{ github.event_name == 'workflow_dispatch' && inputs['target-repo'] || vars.RULESET_TARGET_REPO || github.repository }}
ruleset-name: ${{ github.event_name == 'workflow_dispatch' && inputs['ruleset-name'] || vars.RULESET_NAME }}
token: ${{ secrets.ORG_PAT }}
```

### Using a PAT for cross-repository access

`github.token` is scoped to the repository running the workflow. To copy
rulesets to a repository outside the current one, supply a personal
access token (PAT) or a GitHub App token with `repo` scope on both
repositories:

```yaml
steps:
- uses: CCBR/actions/copy-ruleset@main
with:
source-repo: CCBR/actions
target-repo: CCBR/other-repo
ruleset-name: "Require PR reviews"
token: ${{ secrets.ORG_PAT }}
```

## Running locally

Both operations can also be performed directly in the terminal using the
[`gh` CLI](https://cli.github.com/) and `jq`, without installing any
additional packages.

**List rulesets** in a repository:

```bash
gh api repos/{owner}/{repo}/rulesets --jq '.[] | [.id, .enforcement, .name] | @tsv'
```

**Copy a ruleset** from one repository to another:

```bash
gh api repos/{source_owner}/{source_repo}/rulesets \
--jq '.[] | select(.name == "Require PR reviews") | .id' \
| xargs -I{} gh api repos/{source_owner}/{source_repo}/rulesets/{} \
| jq 'del(.id, .node_id, .created_at, .updated_at, ._links)' \
| gh api repos/{target_owner}/{target_repo}/rulesets \
--method POST \
--input -
```

Alternatively, using the `ccbr_actions` Python package:

```bash
# list rulesets
ccbr_actions list-rulesets owner/repo

# copy a ruleset
ccbr_actions copy-ruleset CCBR/actions CCBR/other-repo "Require PR reviews"
```

## Inputs

- `source-repo`: Source repository in owner/repo format. **Required.**
- `target-repo`: Target repository in owner/repo format. **Required.**
- `ruleset-name`: Name of the ruleset to copy. **Required.**
- `token`: GitHub token with repo scope on both repositories. Default:
`${{ github.token }}`.
83 changes: 83 additions & 0 deletions copy-ruleset/README.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: copy-ruleset
format: gfm
execute:
echo: false
output: asis
---

```{python}
import ccbr_actions.docs

action = ccbr_actions.docs.parse_action_yaml('action.yml')
print(ccbr_actions.docs.action_markdown_desc(action))
```

Copy a named ruleset from one GitHub repository to another.
This action fetches the full ruleset definition from the source repository via the GitHub API and creates an identical ruleset in the target repository.
It is useful for standardising branch protection rules across multiple repositories in an organisation.

## Usage

### Basic example

[copy-ruleset.yml](/examples/copy-ruleset.yml)

```{python}
yml_file = '../examples/copy-ruleset.yml'
print('```yaml')
with open(yml_file, 'r') as infile:
print(infile.read())
print('```\n')
```

### Using a PAT for cross-repository access

`github.token` is scoped to the repository running the workflow.
To copy rulesets to a repository outside the current one, supply a personal access token (PAT) or a GitHub App token with `repo` scope on both repositories:

```yaml
steps:
- uses: CCBR/actions/copy-ruleset@main
with:
source-repo: CCBR/actions
target-repo: CCBR/other-repo
ruleset-name: "Require PR reviews"
token: ${{ secrets.ORG_PAT }}
```

## Running locally

Both operations can also be performed directly in the terminal using the [`gh` CLI](https://cli.github.com/) and `jq`, without installing any additional packages.

**List rulesets** in a repository:

```bash
gh api repos/{owner}/{repo}/rulesets --jq '.[] | [.id, .enforcement, .name] | @tsv'
```

**Copy a ruleset** from one repository to another:

```bash
gh api repos/{source_owner}/{source_repo}/rulesets \
--jq '.[] | select(.name == "Require PR reviews") | .id' \
| xargs -I{} gh api repos/{source_owner}/{source_repo}/rulesets/{} \
| jq 'del(.id, .node_id, .created_at, .updated_at, ._links)' \
| gh api repos/{target_owner}/{target_repo}/rulesets \
--method POST \
--input -
```

Alternatively, using the `ccbr_actions` Python package:

```bash
# list rulesets
ccbr_actions list-rulesets owner/repo

# copy a ruleset
ccbr_actions copy-ruleset CCBR/actions CCBR/other-repo "Require PR reviews"
```

```{python}
print(ccbr_actions.docs.action_markdown_io(action))
```
55 changes: 55 additions & 0 deletions copy-ruleset/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: "Copy Ruleset"
author: "Kelly Sovacool"

description: |
Copy a repository ruleset from one GitHub repository to another.

branding:
icon: copy
color: purple

inputs:
source-repo:
description: "Source repository in owner/repo format."
required: true
target-repo:
description: "Target repository in owner/repo format."
required: true
ruleset-name:
description: "Name of the ruleset to copy."
required: true
token:
description: "GitHub token with repo scope on both repositories."
required: false
default: "${{ github.token }}"

runs:
using: composite
steps:
- name: Copy ruleset
shell: bash
env:
GH_TOKEN: "${{ inputs.token }}"
run: |
ruleset_id=$(
gh api "repos/${{ inputs.source-repo }}/rulesets" \
--jq --arg name "${{ inputs.ruleset-name }}" \
'.[] | select(.name == $name) | .id'
)

if [ -z "$ruleset_id" ]; then
echo "::error::Ruleset '${{ inputs.ruleset-name }}' not found in ${{ inputs.source-repo }}."
echo "Available rulesets:"
gh api "repos/${{ inputs.source-repo }}/rulesets" --jq '.[].name'
exit 1
fi

result=$(
gh api "repos/${{ inputs.source-repo }}/rulesets/${ruleset_id}" \
| jq 'del(.id, .node_id, .created_at, .updated_at, ._links)' \
| gh api "repos/${{ inputs.target-repo }}/rulesets" \
--method POST \
--input -
)

echo "Ruleset '$(echo "$result" | jq -r '.name')' (id=$(echo "$result" | jq -r '.id')) created in ${{ inputs.target-repo }}."
Loading
Loading