From 25e666ebad9b6d8fd493595757954b9f4af70742 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Mon, 18 May 2026 20:31:14 -0700 Subject: [PATCH 1/6] feat: include main branch ruleset in template Adapted from d-morrison/rpt#134. Exports the live `main` ruleset to .github/rulesets/main.json with the server-assigned fields stripped, adds apply-rulesets.sh that PUTs to update / POSTs to create (idempotent), and documents what's enforced in .github/rulesets/README.md. README.Rmd / README.md get a step pointing to the script under "Setup steps". qbt-specific ruleset: required PR + required status checks (link-checker, Spellcheck, check-chars, build-deploy; non-strict) + no force-push / no deletion, Maintain-role PR-only bypass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/rulesets/README.md | 45 ++++++++++++++++++++ .github/rulesets/main.json | 69 +++++++++++++++++++++++++++++++ .github/scripts/apply-rulesets.sh | 39 +++++++++++++++++ README.Rmd | 15 +++++-- README.md | 17 ++++++-- 5 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 .github/rulesets/README.md create mode 100644 .github/rulesets/main.json create mode 100755 .github/scripts/apply-rulesets.sh diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md new file mode 100644 index 0000000..d40cdb1 --- /dev/null +++ b/.github/rulesets/README.md @@ -0,0 +1,45 @@ +# Branch rulesets + +Each JSON file here is a GitHub branch ruleset exported from the template's +canonical configuration. Apply them to a new repo (after creating it from +the template) with: + +```sh +.github/scripts/apply-rulesets.sh # current repo +.github/scripts/apply-rulesets.sh owner/repo # explicit target +``` + +The script is idempotent — re-running it updates a ruleset in place when +one with the same `name` already exists, rather than creating a duplicate. + +Requirements: + +- `gh` CLI authenticated as a repo admin (org or user). Branch rulesets + require admin scope to create. +- `jq` available on PATH. + +## What's enforced (`main.json`) + +Applies to the default branch: + +- **Required PR before merging** — no direct pushes to `main`. +- **Required status checks** (not strict — branch does not need to be up + to date): link-checker, Spellcheck, check-chars, build-deploy. +- **No force-pushes, no branch deletion.** +- **Bypass** in `pull_request` mode for the Maintain role (role id 2) — + Maintainers can merge via a PR they authored, but cannot push directly. + +## Editing the ruleset + +Edit `main.json` here, then run `apply-rulesets.sh` to push the change to +the live repo. Or edit in the GitHub UI (Settings → Rules → Rulesets) and +re-export with: + +```sh +gh api repos/OWNER/REPO/rulesets/RULESET_ID \ + | jq 'del(.id, .node_id, .source, .source_type, .created_at, .updated_at, ._links, .current_user_can_bypass)' \ + > .github/rulesets/main.json +``` + +The fields stripped by `jq del(...)` are server-assigned and would either +be ignored or rejected by the create/update endpoints. diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json new file mode 100644 index 0000000..501f21b --- /dev/null +++ b/.github/rulesets/main.json @@ -0,0 +1,69 @@ +{ + "name": "main", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "~DEFAULT_BRANCH" + ] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "required_reviewers": [], + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": [ + "merge", + "squash", + "rebase" + ] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": false, + "required_status_checks": [ + { + "context": "link-checker", + "integration_id": 15368 + }, + { + "context": "Spellcheck", + "integration_id": 15368 + }, + { + "context": "check-chars", + "integration_id": 15368 + }, + { + "context": "build-deploy", + "integration_id": 15368 + } + ] + } + } + ], + "bypass_actors": [ + { + "actor_id": 2, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ] +} diff --git a/.github/scripts/apply-rulesets.sh b/.github/scripts/apply-rulesets.sh new file mode 100755 index 0000000..3460aee --- /dev/null +++ b/.github/scripts/apply-rulesets.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Apply branch rulesets from .github/rulesets/*.json to the current repo. +# Run once after creating a new repo from this template. Idempotent: a +# ruleset whose `name` already exists on the repo is updated in place rather +# than duplicated. +# +# Requires: gh CLI authenticated with admin access to the repo. +# Usage: .github/scripts/apply-rulesets.sh [owner/repo] +# Defaults to the gh-detected current repo. + +set -euo pipefail + +repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" +ruleset_dir="$(cd "$(dirname "$0")/../rulesets" && pwd)" + +shopt -s nullglob +files=("$ruleset_dir"/*.json) +if [ ${#files[@]} -eq 0 ]; then + echo "no rulesets found in $ruleset_dir" >&2 + exit 0 +fi + +# Map of existing ruleset name -> id, so we can PUT updates instead of +# creating duplicates. +existing=$(gh api "repos/$repo/rulesets" --jq '[.[] | {name, id}]') + +for f in "${files[@]}"; do + name=$(jq -r .name "$f") + id=$(jq -r --arg n "$name" 'map(select(.name == $n)) | .[0].id // empty' <<<"$existing") + if [ -n "$id" ]; then + echo "updating ruleset '$name' (id $id) on $repo" + gh api -X PUT "repos/$repo/rulesets/$id" --input "$f" >/dev/null + else + echo "creating ruleset '$name' on $repo" + gh api -X POST "repos/$repo/rulesets" --input "$f" >/dev/null + fi +done + +echo "done." diff --git a/README.Rmd b/README.Rmd index f7fe003..7e9323c 100644 --- a/README.Rmd +++ b/README.Rmd @@ -107,16 +107,25 @@ This template includes a GitHub Actions workflow (`.github/workflows/publish.yml - Go to Settings → Pages - Under "Build and deployment", set Source to "GitHub Actions" -2. **Push to main branch**: +2. **Apply branch rulesets** (requires admin access): + ```bash + .github/scripts/apply-rulesets.sh + ``` + This protects `main` against direct pushes / force-pushes / deletion, + requires a PR to merge, and gates the merge on the configured CI checks + (link-checker, Spellcheck, check-chars, build-deploy). See + `.github/rulesets/README.md` for details. + +3. **Push to main branch**: ```bash git add . git commit -m "Initial book setup" git push origin main ``` -3. **Wait for the workflow** to complete (check the Actions tab) +4. **Wait for the workflow** to complete (check the Actions tab) -4. **Access your book** at: `https://YOUR-USERNAME.github.io/YOUR-REPO/` +5. **Access your book** at: `https://YOUR-USERNAME.github.io/YOUR-REPO/` ## GitHub Actions Workflows diff --git a/README.md b/README.md index 7b4cf99..5b1248c 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,18 @@ publishes your book to GitHub Pages when you push to the main branch. - Go to Settings → Pages - Under “Build and deployment”, set Source to “GitHub Actions” -2. **Push to main branch**: +2. **Apply branch rulesets** (requires admin access): + + ``` bash + .github/scripts/apply-rulesets.sh + ``` + + This protects `main` against direct pushes / force-pushes / + deletion, requires a PR to merge, and gates the merge on the + configured CI checks (link-checker, Spellcheck, check-chars, + build-deploy). See `.github/rulesets/README.md` for details. + +3. **Push to main branch**: ``` bash git add . @@ -116,9 +127,9 @@ publishes your book to GitHub Pages when you push to the main branch. git push origin main ``` -3. **Wait for the workflow** to complete (check the Actions tab) +4. **Wait for the workflow** to complete (check the Actions tab) -4. **Access your book** at: +5. **Access your book** at: `https://YOUR-USERNAME.github.io/YOUR-REPO/` ## GitHub Actions Workflows From 5a14eee279fcc1ac6e71246f609668b96f0096d5 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Tue, 19 May 2026 00:56:17 -0700 Subject: [PATCH 2/6] fix: drop link-checker from required status checks The check-links.yml workflow declares `pull_request` as a trigger but doesn't actually produce a check run on PR events in this repo (workflow run history shows only `schedule` events firing it). Requiring it as a status check would make every PR wait forever for a context that never arrives. The live ruleset on the canonical qbt repo currently has the same gap; this commit removes it from the JSON shipped with the template so that new repos derived from the template don't inherit a broken gate. Apply the change to the live repo by running `.github/scripts/apply-rulesets.sh`. Also clarify in the rulesets README that the `build-deploy` PR check is satisfied by `preview.yml` (publish.yml is push-only), so future renames don't silently break the ruleset gate. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/rulesets/README.md | 7 ++++++- .github/rulesets/main.json | 4 ---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md index d40cdb1..a491789 100644 --- a/.github/rulesets/README.md +++ b/.github/rulesets/README.md @@ -24,7 +24,12 @@ Applies to the default branch: - **Required PR before merging** — no direct pushes to `main`. - **Required status checks** (not strict — branch does not need to be up - to date): link-checker, Spellcheck, check-chars, build-deploy. + to date): Spellcheck, check-chars, build-deploy. The `build-deploy` + context is produced by `preview.yml` on PRs (publish.yml only runs on + push-to-main, so it can't satisfy this); if you rename either job, the + ruleset gate will hang. `check-links.yml` is intentionally excluded — + it currently runs on a weekly schedule, not on PRs, and would otherwise + block every merge. - **No force-pushes, no branch deletion.** - **Bypass** in `pull_request` mode for the Maintain role (role id 2) — Maintainers can merge via a PR they authored, but cannot push directly. diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json index 501f21b..6b1e74c 100644 --- a/.github/rulesets/main.json +++ b/.github/rulesets/main.json @@ -39,10 +39,6 @@ "strict_required_status_checks_policy": false, "do_not_enforce_on_create": false, "required_status_checks": [ - { - "context": "link-checker", - "integration_id": 15368 - }, { "context": "Spellcheck", "integration_id": 15368 From 115c7649d8da5185f22610ce545923fbd6fc08e3 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Tue, 19 May 2026 00:57:58 -0700 Subject: [PATCH 3/6] docs: drop stale link-checker mention from README setup step The "Apply branch rulesets" step described the required-checks list as (link-checker, Spellcheck, check-chars, build-deploy). link-checker was removed from main.json in the previous commit; update the README to match so the docs and the actual ruleset don't disagree. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.Rmd | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.Rmd b/README.Rmd index 7e9323c..338e383 100644 --- a/README.Rmd +++ b/README.Rmd @@ -113,7 +113,7 @@ This template includes a GitHub Actions workflow (`.github/workflows/publish.yml ``` This protects `main` against direct pushes / force-pushes / deletion, requires a PR to merge, and gates the merge on the configured CI checks - (link-checker, Spellcheck, check-chars, build-deploy). See + (Spellcheck, check-chars, build-deploy). See `.github/rulesets/README.md` for details. 3. **Push to main branch**: diff --git a/README.md b/README.md index 5b1248c..3288413 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,8 @@ publishes your book to GitHub Pages when you push to the main branch. This protects `main` against direct pushes / force-pushes / deletion, requires a PR to merge, and gates the merge on the - configured CI checks (link-checker, Spellcheck, check-chars, - build-deploy). See `.github/rulesets/README.md` for details. + configured CI checks (Spellcheck, check-chars, build-deploy). See + `.github/rulesets/README.md` for details. 3. **Push to main branch**: From ae2874edb5bc003a7ee5388d1f252786fa77b1da Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Wed, 3 Jun 2026 15:18:19 -0700 Subject: [PATCH 4/6] Address review nits on branch-ruleset template - apply-rulesets.sh: fail fast with a clear message if jq is missing - apply-rulesets.sh: skip ruleset files lacking a .name field (avoid a bogus POST for a ruleset literally named "null") - rulesets/README.md: document the zero-approvals (self-merge) behavior and show how to look up the ruleset ID inline before re-export - README.Rmd/README.md: note the zero-approvals behavior in the apply step Co-Authored-By: Claude Opus 4.8 --- .github/rulesets/README.md | 11 +++++++++-- .github/scripts/apply-rulesets.sh | 6 ++++++ README.Rmd | 4 +++- README.md | 6 ++++-- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md index a491789..fa465f4 100644 --- a/.github/rulesets/README.md +++ b/.github/rulesets/README.md @@ -22,7 +22,11 @@ Requirements: Applies to the default branch: -- **Required PR before merging** — no direct pushes to `main`. +- **Required PR before merging** — no direct pushes to `main`. Note that + `required_approving_review_count` is `0`: a PR is required, but **zero + approvals** are needed, so authors can self-merge. This enforces process + (PR + status checks) but not peer review. Raise this value if you want to + require approvals before merge. - **Required status checks** (not strict — branch does not need to be up to date): Spellcheck, check-chars, build-deploy. The `build-deploy` context is produced by `preview.yml` on PRs (publish.yml only runs on @@ -41,7 +45,10 @@ the live repo. Or edit in the GitHub UI (Settings → Rules → Rulesets) and re-export with: ```sh -gh api repos/OWNER/REPO/rulesets/RULESET_ID \ +# Find the ruleset ID: +RULESET_ID=$(gh api repos/OWNER/REPO/rulesets | jq '.[] | select(.name == "main") | .id') + +gh api "repos/OWNER/REPO/rulesets/$RULESET_ID" \ | jq 'del(.id, .node_id, .source, .source_type, .created_at, .updated_at, ._links, .current_user_can_bypass)' \ > .github/rulesets/main.json ``` diff --git a/.github/scripts/apply-rulesets.sh b/.github/scripts/apply-rulesets.sh index 3460aee..5138d5c 100755 --- a/.github/scripts/apply-rulesets.sh +++ b/.github/scripts/apply-rulesets.sh @@ -10,6 +10,8 @@ set -euo pipefail +command -v jq >/dev/null 2>&1 || { echo "error: jq is required but not found" >&2; exit 1; } + repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" ruleset_dir="$(cd "$(dirname "$0")/../rulesets" && pwd)" @@ -26,6 +28,10 @@ existing=$(gh api "repos/$repo/rulesets" --jq '[.[] | {name, id}]') for f in "${files[@]}"; do name=$(jq -r .name "$f") + if [ -z "$name" ] || [ "$name" = "null" ]; then + echo "skipping $f: missing .name field" >&2 + continue + fi id=$(jq -r --arg n "$name" 'map(select(.name == $n)) | .[0].id // empty' <<<"$existing") if [ -n "$id" ]; then echo "updating ruleset '$name' (id $id) on $repo" diff --git a/README.Rmd b/README.Rmd index 338e383..241b7ad 100644 --- a/README.Rmd +++ b/README.Rmd @@ -113,7 +113,9 @@ This template includes a GitHub Actions workflow (`.github/workflows/publish.yml ``` This protects `main` against direct pushes / force-pushes / deletion, requires a PR to merge, and gates the merge on the configured CI checks - (Spellcheck, check-chars, build-deploy). See + (Spellcheck, check-chars, build-deploy). A PR is required but **zero + approvals** are needed, so authors can self-merge; raise + `required_approving_review_count` in `main.json` to require approvals. See `.github/rulesets/README.md` for details. 3. **Push to main branch**: diff --git a/README.md b/README.md index 3288413..69b5282 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,10 @@ publishes your book to GitHub Pages when you push to the main branch. This protects `main` against direct pushes / force-pushes / deletion, requires a PR to merge, and gates the merge on the - configured CI checks (Spellcheck, check-chars, build-deploy). See - `.github/rulesets/README.md` for details. + configured CI checks (Spellcheck, check-chars, build-deploy). A PR + is required but **zero approvals** are needed, so authors can + self-merge; raise `required_approving_review_count` in `main.json` + to require approvals. See `.github/rulesets/README.md` for details. 3. **Push to main branch**: From eb2886169e467984bb52b3f3286132f907e2af09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 09:25:37 +0000 Subject: [PATCH 5/6] fix(spell): add 'rulesets' to WORDLIST The spellcheck CI was failing because 'rulesets' (added to README.Rmd:110 and README.md:111 in this PR) is not in the dictionary. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01SNxbcZHy5tYHWAzeJ9XL4J --- inst/WORDLIST | 1 + 1 file changed, 1 insertion(+) diff --git a/inst/WORDLIST b/inst/WORDLIST index 9d5ea86..adcd9f2 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -7,4 +7,5 @@ emplate glitchy lintr ook +rulesets uarto From a99c62b42a5ea497918b36626391e6026bd0c892 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 09:34:29 +0000 Subject: [PATCH 6/6] fix: address Copilot review findings on apply-rulesets.sh and docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add `gh` dependency check alongside existing `jq` check - add --paginate to `gh api` rulesets listing so repos with >30 rulesets don't silently create duplicates - fix rulesets/README.md: check-links.yml is excluded because it checks external URLs (transient failures, not PR-related), not because it doesn't run on PRs — it does have a pull_request trigger - README.Rmd / README.md: use full path .github/rulesets/main.json Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01SNxbcZHy5tYHWAzeJ9XL4J --- .github/rulesets/README.md | 7 ++++--- .github/scripts/apply-rulesets.sh | 3 ++- README.Rmd | 2 +- README.md | 5 +++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md index fa465f4..4020bac 100644 --- a/.github/rulesets/README.md +++ b/.github/rulesets/README.md @@ -31,9 +31,10 @@ Applies to the default branch: to date): Spellcheck, check-chars, build-deploy. The `build-deploy` context is produced by `preview.yml` on PRs (publish.yml only runs on push-to-main, so it can't satisfy this); if you rename either job, the - ruleset gate will hang. `check-links.yml` is intentionally excluded — - it currently runs on a weekly schedule, not on PRs, and would otherwise - block every merge. + ruleset gate will hang. `check-links.yml` is intentionally excluded — it checks external URLs, + which can fail due to transient network issues or link rot unrelated to + the PR. Requiring it as a merge gate would block merges on external + failures outside the PR author's control. - **No force-pushes, no branch deletion.** - **Bypass** in `pull_request` mode for the Maintain role (role id 2) — Maintainers can merge via a PR they authored, but cannot push directly. diff --git a/.github/scripts/apply-rulesets.sh b/.github/scripts/apply-rulesets.sh index 5138d5c..aef855a 100755 --- a/.github/scripts/apply-rulesets.sh +++ b/.github/scripts/apply-rulesets.sh @@ -11,6 +11,7 @@ set -euo pipefail command -v jq >/dev/null 2>&1 || { echo "error: jq is required but not found" >&2; exit 1; } +command -v gh >/dev/null 2>&1 || { echo "error: gh is required but not found" >&2; exit 1; } repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" ruleset_dir="$(cd "$(dirname "$0")/../rulesets" && pwd)" @@ -24,7 +25,7 @@ fi # Map of existing ruleset name -> id, so we can PUT updates instead of # creating duplicates. -existing=$(gh api "repos/$repo/rulesets" --jq '[.[] | {name, id}]') +existing=$(gh api --paginate "repos/$repo/rulesets" | jq -s '[.[][] | {name, id}]') for f in "${files[@]}"; do name=$(jq -r .name "$f") diff --git a/README.Rmd b/README.Rmd index 212c3e0..eb8cc27 100644 --- a/README.Rmd +++ b/README.Rmd @@ -115,7 +115,7 @@ This template includes a GitHub Actions workflow (`.github/workflows/publish.yml requires a PR to merge, and gates the merge on the configured CI checks (Spellcheck, check-chars, build-deploy). A PR is required but **zero approvals** are needed, so authors can self-merge; raise - `required_approving_review_count` in `main.json` to require approvals. See + `required_approving_review_count` in `.github/rulesets/main.json` to require approvals. See `.github/rulesets/README.md` for details. 3. **Push to main branch**: diff --git a/README.md b/README.md index c2f619a..a3b7515 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,9 @@ publishes your book to GitHub Pages when you push to the main branch. deletion, requires a PR to merge, and gates the merge on the configured CI checks (Spellcheck, check-chars, build-deploy). A PR is required but **zero approvals** are needed, so authors can - self-merge; raise `required_approving_review_count` in `main.json` - to require approvals. See `.github/rulesets/README.md` for details. + self-merge; raise `required_approving_review_count` in + `.github/rulesets/main.json` to require approvals. See + `.github/rulesets/README.md` for details. 3. **Push to main branch**: