diff --git a/README.md b/README.md index b6b865b..5c42905 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,23 @@ Run `make repo-settings` to reconcile your GitHub repository with the organizati It configures: - **Labels** — creates/updates the standard set of issue and PR labels -- **Merge strategy** — merge commits only (no squash or rebase), auto-merge enabled, delete branch on merge +- **Merge strategy** — merge commits only by default (configurable via `REPO_ALLOW_MERGE_COMMIT`, `REPO_ALLOW_SQUASH_MERGE`, `REPO_ALLOW_REBASE_MERGE`), auto-merge enabled, delete branch on merge - **Secret scanning** — enabled -- **Branch protection** — a "protect-main" ruleset on the default branch: requires PRs with 1 approval, dismisses stale reviews, requires review thread resolution, enforces commit signatures, prevents direct pushes/deletions/non-fast-forwards (org admins can bypass) +- **Branch protection** — a "protect-main" ruleset on the default branch (and any `REPO_RULESET_BRANCHES` patterns): requires PRs with 1 approval, dismisses stale reviews, requires review thread resolution, enforces commit signatures, prevents direct pushes/deletions/non-fast-forwards + +The repository settings are configurable via make variables (set them in your `Makefile` or pass them on the command line, e.g. `make repo-settings REPO_STATUS_CHECKS='["CI","lint"]' REPO_RULESET_BRANCHES='["release/*"]'`): + +| Variable | Default | Description | +| --- | --- | --- | +| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy | +| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging | +| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging | +| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset | +| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge | +| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners | +| `REPO_REQUIRE_BRANCH_UP_TO_DATE` | `false` | Require branches to be up to date before merging (needs at least one `REPO_STATUS_CHECKS` value; `repo-settings` fails otherwise) | +| `REPO_STATUS_CHECKS` | `[]` | JSON array of status-check contexts that must pass (e.g. `["CI","Check action pins"]`); each job name is used as-is, so contexts with spaces work; the `required_status_checks` rule is only added when this is non-empty | +| `REPO_RULESET_BRANCHES` | `[]` | JSON array of additional branch patterns the ruleset applies to (e.g. `["release/*"]`); short names are normalized to `refs/heads/...`; the default branch is always protected | ### Default git hooks diff --git a/common.mk b/common.mk index 19ac135..6c9d6f0 100644 --- a/common.mk +++ b/common.mk @@ -52,94 +52,40 @@ SETUP_ENVTEST ?= $(LOCALGOBIN)/setup-envtest ##@ Repository -define REPO_LABELS -bug;d73a4a;Something isn't working -documentation;0075ca;Improvements or additions to documentation -duplicate;cfd3d7;This issue or pull request already exists -enhancement;a2eeef;New feature or request -good first issue;7057ff;Good for newcomers -help wanted;008672;Extra attention is needed -invalid;e4e669;This doesn't seem right -question;37326e;Further information is requested -wontfix;ffffff;This will not be worked on -chore;ededed;A routine task or common potentially re-occurring task -go;16e2e2;Pull requests that update go code -ok-to-helm;0e8a16;PR is allowed to build an publish helm chart -dependencies;0366d6;Pull requests that update a dependency file -github-actions;80c4c6;PR created via GitHub action -needs-triage;eab668;Issue that has not been reviewed -ok-to-image;0e8a16;PR is allowed to run container build -ok-to-test;0e8a16;PR is allowed to be tested -spike;b23adb;A task to research a question and resolve problems -endef -export REPO_LABELS - -REPO_RULESET := { \ - "name": "protect-main", \ - "target": "branch", \ - "enforcement": "active", \ - "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, \ - "rules": [ \ - { "type": "deletion" }, \ - { "type": "non_fast_forward" }, \ - { "type": "creation" }, \ - { "type": "required_signatures" }, \ - { "type": "pull_request", "parameters": { \ - "required_approving_review_count": 1, \ - "dismiss_stale_reviews_on_push": true, \ - "required_reviewers": [], \ - "require_code_owner_review": false, \ - "require_last_push_approval": false, \ - "required_review_thread_resolution": true, \ - "allowed_merge_methods": ["squash", "rebase", "merge"] \ - }} \ - ], \ - "bypass_actors": [{ "actor_type": "OrganizationAdmin", "bypass_mode": "always" }] \ -} +# repo-settings — repository configuration, configurable per repository. +# Branch protection: the default branch is always protected; REPO_RULESET_BRANCHES +# adds further branch patterns (a JSON array, e.g. '["release/*"]'; short names are +# normalized to refs/heads/...). REPO_STATUS_CHECKS is a JSON array of status-check +# contexts that must pass (each job name is used as-is, so contexts with spaces work). +# REPO_ALLOW_MERGE_COMMIT/SQUASH_MERGE/REBASE_MERGE configure the merge strategy. +# Booleans must be `true` or `false`. Example: +# make repo-settings REPO_ADMIN_BYPASS=false REPO_STATUS_CHECKS='["CI","Check action pins"]' REPO_RULESET_BRANCHES='["release/*"]' +REPO_ADMIN_BYPASS ?= true +REPO_REQUIRED_APPROVING_REVIEW_COUNT ?= 1 +REPO_REQUIRE_CODE_OWNER_REVIEW ?= false +REPO_REQUIRE_BRANCH_UP_TO_DATE ?= false +REPO_STATUS_CHECKS ?= [] +REPO_RULESET_BRANCHES ?= [] +REPO_ALLOW_MERGE_COMMIT ?= true +REPO_ALLOW_SQUASH_MERGE ?= false +REPO_ALLOW_REBASE_MERGE ?= false .PHONY: repo-settings repo-settings: ## Reconcile GitHub repository settings (labels, merge strategy, branch protection, security) - @$(GH) auth status >/dev/null 2>&1 || { echo "error: gh is not authenticated; run 'gh auth login'"; exit 1; }; \ - REPO=$$($(GH) repo view --json nameWithOwner -q .nameWithOwner) || { echo "error: not a GitHub repository"; exit 1; }; \ - echo "Reconciling settings for $$REPO..."; \ - \ - echo " Syncing labels..."; \ - echo "$$REPO_LABELS" | while IFS=';' read -r name color desc; do \ - [ -z "$$name" ] && continue; \ - $(GH) label create "$$name" --repo "$$REPO" --color "$$color" --description "$$desc" --force 2>/dev/null; \ - done; \ - \ - echo " Configuring merge strategy..."; \ - $(GH) api "repos/$$REPO" -X PATCH \ - -f allow_merge_commit=true \ - -f allow_squash_merge=false \ - -f allow_rebase_merge=false \ - -f delete_branch_on_merge=true \ - -f allow_auto_merge=true > /dev/null; \ - \ - echo " Enabling secret scanning..."; \ - $(GH) api "repos/$$REPO" -X PATCH \ - --input <(echo '{"security_and_analysis":{"secret_scanning":{"status":"enabled"}}}') > /dev/null; \ - \ - echo " Configuring branch protection ruleset..."; \ - existing=$$($(GH) api "repos/$$REPO/rulesets" -q '.[] | select(.name=="protect-main") | .id' 2>/dev/null); \ - if [ -n "$$existing" ]; then \ - $(GH) api "repos/$$REPO/rulesets/$$existing" -X PUT --input <(echo '$(REPO_RULESET)') > /dev/null; \ - echo " Updated existing ruleset (id: $$existing)"; \ - else \ - $(GH) api "repos/$$REPO/rulesets" -X POST --input <(echo '$(REPO_RULESET)') > /dev/null; \ - echo " Created new ruleset"; \ - fi; \ - \ - echo " Installing update-action-pins workflow..."; \ - mkdir -p .github/workflows; \ - _dev_kit_ver=$${DEV_KIT_VERSION:-main}; \ - curl --fail -sSL \ - "https://raw.githubusercontent.com/opendefensecloud/dev-kit/$$_dev_kit_ver/.github/workflows/update-action-pins.yml" \ - -o .github/workflows/update-action-pins.yml; \ - echo " Wrote .github/workflows/update-action-pins.yml"; \ - \ - echo "Done." + @curl --fail -sSL \ + "https://raw.githubusercontent.com/opendefensecloud/dev-kit/$(DEV_KIT_VERSION)/scripts/repo-settings.sh" | \ + REPO_ADMIN_BYPASS='$(REPO_ADMIN_BYPASS)' \ + REPO_ALLOW_MERGE_COMMIT='$(REPO_ALLOW_MERGE_COMMIT)' \ + REPO_ALLOW_REBASE_MERGE='$(REPO_ALLOW_REBASE_MERGE)' \ + REPO_ALLOW_SQUASH_MERGE='$(REPO_ALLOW_SQUASH_MERGE)' \ + REPO_REQUIRE_BRANCH_UP_TO_DATE='$(REPO_REQUIRE_BRANCH_UP_TO_DATE)' \ + REPO_REQUIRE_CODE_OWNER_REVIEW='$(REPO_REQUIRE_CODE_OWNER_REVIEW)' \ + REPO_REQUIRED_APPROVING_REVIEW_COUNT='$(REPO_REQUIRED_APPROVING_REVIEW_COUNT)' \ + REPO_RULESET_BRANCHES='$(REPO_RULESET_BRANCHES)' \ + REPO_STATUS_CHECKS='$(REPO_STATUS_CHECKS)' \ + DEV_KIT_VERSION='$(DEV_KIT_VERSION)' \ + GH='$(GH)' JQ='$(JQ)' \ + bash .PHONY: update-action-pins update-action-pins: ## Update GitHub Action pins to their latest commit SHA diff --git a/docs/NEW_REPO.md b/docs/NEW_REPO.md index 8e88ec8..1796e52 100644 --- a/docs/NEW_REPO.md +++ b/docs/NEW_REPO.md @@ -33,7 +33,15 @@ Run: make repo-settings ``` -This reconciles labels, merge strategy (merge commits only, auto-merge enabled, delete branch on merge), secret scanning, and the `protect-main` branch ruleset. See `make help` for details. +This reconciles labels, merge strategy (merge commits only by default, configurable via the `REPO_ALLOW_*` variables; auto-merge enabled, delete branch on merge), secret scanning, and the `protect-main` branch ruleset. See `make help` for details. + +The branch protection ruleset is configurable via make variables, e.g.: + +```sh +make repo-settings REPO_STATUS_CHECKS='["CI","lint"]' REPO_REQUIRE_BRANCH_UP_TO_DATE=true REPO_RULESET_BRANCHES='["release/*"]' +``` + +See the README for the full list of `REPO_*` variables. ## 4. Set up GitHub organization secrets diff --git a/scripts/repo-settings.sh b/scripts/repo-settings.sh new file mode 100755 index 0000000..4e06e55 --- /dev/null +++ b/scripts/repo-settings.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Reconcile a repository's GitHub settings: labels, merge strategy, secret +# scanning, branch protection ruleset, and the update-action-pins workflow. +# +# All configuration is passed through environment variables. Defaults are the +# single source of truth in common.mk and are always passed by the make target; +# an unset variable here is a configuration error (set -u). +# REPO_ADMIN_BYPASS when "false", org admins cannot bypass the ruleset +# REPO_REQUIRED_APPROVING_REVIEW_COUNT number of approving reviews required to merge +# REPO_REQUIRE_CODE_OWNER_REVIEW require an approving review from code owners +# REPO_REQUIRE_BRANCH_UP_TO_DATE require branches up to date (requires status checks) +# REPO_STATUS_CHECKS JSON array of required status-check contexts +# REPO_RULESET_BRANCHES JSON array of additional branch patterns +# REPO_ALLOW_MERGE_COMMIT allow merge commits in the merge strategy +# REPO_ALLOW_SQUASH_MERGE allow squash merging +# REPO_ALLOW_REBASE_MERGE allow rebase merging +# DEV_KIT_VERSION dev-kit version to fetch the workflow from +# GH, JQ commands used to talk to GitHub and build JSON + +"$GH" auth status >/dev/null 2>&1 || { + echo "error: gh is not authenticated; run 'gh auth login'" >&2 + exit 1 +} + +REPO=$("$GH" repo view --json nameWithOwner -q .nameWithOwner) || { + echo "error: not a GitHub repository" >&2 + exit 1 +} + +REPO_STATUS_CHECKS_EFFECTIVE=$(echo "$REPO_STATUS_CHECKS" | "$JQ" -c '[.[] | select((type == "string") and (length > 0))]') +REPO_RULESET_BRANCHES_EFFECTIVE=$(echo "$REPO_RULESET_BRANCHES" | "$JQ" -c '[.[] | + select((type == "string") and (length > 0)) | + if startswith("~") or startswith("refs/") then . + else "refs/heads/" + . end] | + ["~DEFAULT_BRANCH"] + . | unique') + +if [ "$REPO_REQUIRE_BRANCH_UP_TO_DATE" = "true" ] && + [ "$REPO_STATUS_CHECKS_EFFECTIVE" = "[]" ]; then + echo "error: REPO_REQUIRE_BRANCH_UP_TO_DATE=true requires at least one value in REPO_STATUS_CHECKS" >&2 + exit 1 +fi + +echo "Reconciling settings for $REPO..." + +echo " Syncing labels..." +while IFS=';' read -r name color desc; do + [ -z "$name" ] && continue + "$GH" label create "$name" --repo "$REPO" --color "$color" --description "$desc" --force 2>/dev/null +done <<'EOF' +bug;d73a4a;Something isn't working +documentation;0075ca;Improvements or additions to documentation +duplicate;cfd3d7;This issue or pull request already exists +enhancement;a2eeef;New feature or request +good first issue;7057ff;Good for newcomers +help wanted;008672;Extra attention is needed +invalid;e4e669;This doesn't seem right +question;37326e;Further information is requested +wontfix;ffffff;This will not be worked on +chore;ededed;A routine task or common potentially re-occurring task +feature;a2eeef;New feature or request +go;16e2e2;Pull requests that update go code +ok-to-helm;0e8a16;PR is allowed to build an publish helm chart +dependencies;0366d6;Pull requests that update a dependency file +github-actions;80c4c6;PR created via GitHub action +help-wanted;811857;Extra attention is needed +good-first-issue;7057ff;Good for newcomers +needs-triage;eab668;Issue that has not been reviewed +ok-to-image;0e8a16;PR is allowed to run container build +ok-to-test;0e8a16;PR is allowed to be tested +spike;b23adb;A task to research a question and resolve problems +EOF + +echo " Configuring merge strategy..." +"$GH" api "repos/$REPO" -X PATCH \ + -f allow_merge_commit="$REPO_ALLOW_MERGE_COMMIT" \ + -f allow_squash_merge="$REPO_ALLOW_SQUASH_MERGE" \ + -f allow_rebase_merge="$REPO_ALLOW_REBASE_MERGE" \ + -f delete_branch_on_merge=true \ + -f allow_auto_merge=true >/dev/null + +echo " Enabling secret scanning..." +"$GH" api "repos/$REPO" -X PATCH \ + --input <(echo '{"security_and_analysis":{"secret_scanning":{"status":"enabled"}}}') >/dev/null + +RULESET_JSON=$( + # shellcheck disable=SC2016 # $variables belong to jq, not the shell + "$JQ" -cn \ + --argjson branches "$REPO_RULESET_BRANCHES_EFFECTIVE" \ + --argjson checks "$REPO_STATUS_CHECKS_EFFECTIVE" \ + --argjson approvals "$REPO_REQUIRED_APPROVING_REVIEW_COUNT" \ + --argjson codeOwner "$REPO_REQUIRE_CODE_OWNER_REVIEW" \ + --argjson adminBypass "$REPO_ADMIN_BYPASS" \ + --argjson upToDate "$REPO_REQUIRE_BRANCH_UP_TO_DATE" \ + '{ name: "protect-main", target: "branch", enforcement: "active", + conditions: { ref_name: { include: $branches, exclude: [] } }, + rules: ([ + { type: "deletion" }, + { type: "non_fast_forward" }, + { type: "creation" }, + { type: "required_signatures" }, + { type: "pull_request", parameters: { + required_approving_review_count: $approvals, + dismiss_stale_reviews_on_push: true, + required_reviewers: [], + require_code_owner_review: $codeOwner, + require_last_push_approval: false, + required_review_thread_resolution: true, + allowed_merge_methods: ["squash", "rebase", "merge"] + } } + ] + (if ($checks | length > 0) then + [{ type: "required_status_checks", parameters: { + strict_required_status_checks_policy: $upToDate, + required_status_checks: [$checks | .[] | { context: . }] + } }] else [] end)), + bypass_actors: (if $adminBypass then [{ actor_type: "OrganizationAdmin", bypass_mode: "always" }] else [] end) + }' +) + +echo " Configuring branch protection ruleset..." +echo " branches: $REPO_RULESET_BRANCHES_EFFECTIVE" +echo " approvals: $REPO_REQUIRED_APPROVING_REVIEW_COUNT" +echo " code owner review: $REPO_REQUIRE_CODE_OWNER_REVIEW" +echo " branch up-to-date: $REPO_REQUIRE_BRANCH_UP_TO_DATE" +echo " required status checks: $REPO_STATUS_CHECKS_EFFECTIVE" +echo " admin bypass: $REPO_ADMIN_BYPASS" + +existing=$("$GH" api "repos/$REPO/rulesets" -q '.[] | select(.name=="protect-main") | .id' 2>/dev/null || true) +if [ -n "$existing" ]; then + "$GH" api "repos/$REPO/rulesets/$existing" -X PUT --input <(echo "$RULESET_JSON") >/dev/null + echo " Updated existing ruleset (id: $existing)" +else + "$GH" api "repos/$REPO/rulesets" -X POST --input <(echo "$RULESET_JSON") >/dev/null + echo " Created new ruleset" +fi + +echo " Installing update-action-pins workflow..." +mkdir -p .github/workflows +curl --fail -sSL \ + "https://raw.githubusercontent.com/opendefensecloud/dev-kit/$DEV_KIT_VERSION/.github/workflows/update-action-pins.yml" \ + -o .github/workflows/update-action-pins.yml +echo " Wrote .github/workflows/update-action-pins.yml" + +echo "Done."