diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md new file mode 100644 index 0000000..4020bac --- /dev/null +++ b/.github/rulesets/README.md @@ -0,0 +1,58 @@ +# 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`. 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 + 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 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. + +## 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 +# 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 +``` + +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..6b1e74c --- /dev/null +++ b/.github/rulesets/main.json @@ -0,0 +1,65 @@ +{ + "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": "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..aef855a --- /dev/null +++ b/.github/scripts/apply-rulesets.sh @@ -0,0 +1,46 @@ +#!/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 + +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)" + +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 --paginate "repos/$repo/rulesets" | jq -s '[.[][] | {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" + 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 c8b2950..eb8cc27 100644 --- a/README.Rmd +++ b/README.Rmd @@ -107,16 +107,27 @@ 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 + (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 `.github/rulesets/main.json` to require approvals. 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 99af1e0..a3b7515 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,21 @@ 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 (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 + `.github/rulesets/main.json` to require approvals. See + `.github/rulesets/README.md` for details. + +3. **Push to main branch**: ``` bash git add . @@ -116,9 +130,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 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