Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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)

## 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
3 changes: 1 addition & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Maintained Versions

Actively maintained versions of contained software will vary from repository to repository, or may not be relevant at all.
Actively maintained versions of contained software will vary from repository to repository, or may not be relevant at all.
The developers of this repository will update this section if any actively maintained versions of the software need to be publicly disclosed. Otherwise, contact the developers directly for any version information.

## Vulnerability Disclosure:
Expand All @@ -16,4 +16,3 @@ Follow the instructions listed in the [HHS vulnerability disclosure policy](http
1. Click on the **Security and quality** tab of this repository.
2. Locate the **Report a vulnerability** button. If the button is not on the **Security and quality** landing page, look under the **Advisories** section in the side bar.
3. Click the **Report a vulnerability** button and submit the form. The developers will receive a notification of your submission.

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