Skip to content

Commit 3dbedb8

Browse files
committed
refactor: move repo-settings into a dedicated script
1 parent 79e847f commit 3dbedb8

4 files changed

Lines changed: 175 additions & 114 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,17 @@ Run `make repo-settings` to reconcile your GitHub repository with the organizati
100100
It configures:
101101

102102
- **Labels** — creates/updates the standard set of issue and PR labels
103-
- **Merge strategy** — merge commits only (no squash or rebase), auto-merge enabled, delete branch on merge
103+
- **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
104104
- **Secret scanning** — enabled
105105
- **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
106106

107-
The branch protection ruleset is 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/*"]'`):
107+
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/*"]'`):
108108

109109
| Variable | Default | Description |
110110
| --- | --- | --- |
111+
| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy |
112+
| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging |
113+
| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging |
111114
| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset |
112115
| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge |
113116
| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners |

common.mk

Lines changed: 23 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -52,33 +52,12 @@ SETUP_ENVTEST ?= $(LOCALGOBIN)/setup-envtest
5252

5353
##@ Repository
5454

55-
define REPO_LABELS
56-
bug;d73a4a;Something isn't working
57-
documentation;0075ca;Improvements or additions to documentation
58-
duplicate;cfd3d7;This issue or pull request already exists
59-
enhancement;a2eeef;New feature or request
60-
good first issue;7057ff;Good for newcomers
61-
help wanted;008672;Extra attention is needed
62-
invalid;e4e669;This doesn't seem right
63-
question;37326e;Further information is requested
64-
wontfix;ffffff;This will not be worked on
65-
chore;ededed;A routine task or common potentially re-occurring task
66-
go;16e2e2;Pull requests that update go code
67-
ok-to-helm;0e8a16;PR is allowed to build an publish helm chart
68-
dependencies;0366d6;Pull requests that update a dependency file
69-
github-actions;80c4c6;PR created via GitHub action
70-
needs-triage;eab668;Issue that has not been reviewed
71-
ok-to-image;0e8a16;PR is allowed to run container build
72-
ok-to-test;0e8a16;PR is allowed to be tested
73-
spike;b23adb;A task to research a question and resolve problems
74-
endef
75-
export REPO_LABELS
76-
77-
# Branch protection ruleset — configurable per repository. The default branch
78-
# is always protected; REPO_RULESET_BRANCHES adds further branch patterns
79-
# (a JSON array, e.g. '["release/*"]'; short names are normalized to refs/heads/...).
80-
# REPO_STATUS_CHECKS is a JSON array of status-check contexts that must pass
81-
# (each job name is used as-is, so contexts with spaces work).
55+
# repo-settings — repository configuration, configurable per repository.
56+
# Branch protection: the default branch is always protected; REPO_RULESET_BRANCHES
57+
# adds further branch patterns (a JSON array, e.g. '["release/*"]'; short names are
58+
# normalized to refs/heads/...). REPO_STATUS_CHECKS is a JSON array of status-check
59+
# contexts that must pass (each job name is used as-is, so contexts with spaces work).
60+
# REPO_ALLOW_MERGE_COMMIT/SQUASH_MERGE/REBASE_MERGE configure the merge strategy.
8261
# Booleans must be `true` or `false`. Example:
8362
# make repo-settings REPO_ADMIN_BYPASS=false REPO_STATUS_CHECKS='["CI","Check action pins"]' REPO_RULESET_BRANCHES='["release/*"]'
8463
REPO_ADMIN_BYPASS ?= true
@@ -87,93 +66,26 @@ REPO_REQUIRE_CODE_OWNER_REVIEW ?= false
8766
REPO_REQUIRE_BRANCH_UP_TO_DATE ?= false
8867
REPO_STATUS_CHECKS ?= []
8968
REPO_RULESET_BRANCHES ?= []
69+
REPO_ALLOW_MERGE_COMMIT ?= true
70+
REPO_ALLOW_SQUASH_MERGE ?= false
71+
REPO_ALLOW_REBASE_MERGE ?= false
9072

9173
.PHONY: repo-settings
9274
repo-settings: ## Reconcile GitHub repository settings (labels, merge strategy, branch protection, security)
93-
@$(GH) auth status >/dev/null 2>&1 || { echo "error: gh is not authenticated; run 'gh auth login'"; exit 1; }; \
94-
REPO=$$($(GH) repo view --json nameWithOwner -q .nameWithOwner) || { echo "error: not a GitHub repository"; exit 1; }; \
95-
echo "Reconciling settings for $$REPO..."; \
96-
\
97-
echo " Syncing labels..."; \
98-
echo "$$REPO_LABELS" | while IFS=';' read -r name color desc; do \
99-
[ -z "$$name" ] && continue; \
100-
$(GH) label create "$$name" --repo "$$REPO" --color "$$color" --description "$$desc" --force 2>/dev/null; \
101-
done; \
102-
\
103-
echo " Configuring merge strategy..."; \
104-
$(GH) api "repos/$$REPO" -X PATCH \
105-
-f allow_merge_commit=true \
106-
-f allow_squash_merge=false \
107-
-f allow_rebase_merge=false \
108-
-f delete_branch_on_merge=true \
109-
-f allow_auto_merge=true > /dev/null; \
110-
\
111-
echo " Enabling secret scanning..."; \
112-
$(GH) api "repos/$$REPO" -X PATCH \
113-
--input <(echo '{"security_and_analysis":{"secret_scanning":{"status":"enabled"}}}') > /dev/null; \
114-
\
115-
echo " Configuring branch protection ruleset..."; \
116-
if [ "$(REPO_REQUIRE_BRANCH_UP_TO_DATE)" = "true" ] && ! echo '$(REPO_STATUS_CHECKS)' | $(JQ) -e 'length > 0' > /dev/null 2>&1; then \
117-
echo "error: REPO_REQUIRE_BRANCH_UP_TO_DATE=true requires at least one value in REPO_STATUS_CHECKS"; \
118-
exit 1; \
119-
fi; \
120-
RULESET_JSON=$$($(JQ) -cn \
121-
--argjson branches '$(REPO_RULESET_BRANCHES)' \
122-
--argjson checks '$(REPO_STATUS_CHECKS)' \
123-
--argjson approvals '$(REPO_REQUIRED_APPROVING_REVIEW_COUNT)' \
124-
--argjson codeOwner '$(REPO_REQUIRE_CODE_OWNER_REVIEW)' \
125-
--argjson adminBypass '$(REPO_ADMIN_BYPASS)' \
126-
--argjson upToDate '$(REPO_REQUIRE_BRANCH_UP_TO_DATE)' \
127-
'($$branches | map(select(length > 0)) | map(if startswith("~") or startswith("refs/") then . else "refs/heads/" + . end)) as $$bs | \
128-
($$checks | map(select(length > 0))) as $$cs | \
129-
{ name: "protect-main", target: "branch", enforcement: "active", \
130-
conditions: { ref_name: { include: (["~DEFAULT_BRANCH"] + $$bs | unique), exclude: [] } }, \
131-
rules: ([ \
132-
{ type: "deletion" }, \
133-
{ type: "non_fast_forward" }, \
134-
{ type: "creation" }, \
135-
{ type: "required_signatures" }, \
136-
{ type: "pull_request", parameters: { \
137-
required_approving_review_count: $$approvals, \
138-
dismiss_stale_reviews_on_push: true, \
139-
required_reviewers: [], \
140-
require_code_owner_review: $$codeOwner, \
141-
require_last_push_approval: false, \
142-
required_review_thread_resolution: true, \
143-
allowed_merge_methods: ["squash", "rebase", "merge"] \
144-
} } \
145-
] + (if ($$cs | length > 0) then \
146-
[{ type: "required_status_checks", parameters: { \
147-
strict_required_status_checks_policy: $$upToDate, \
148-
required_status_checks: [$$cs | .[] | { context: . }] \
149-
} }] else [] end)), \
150-
bypass_actors: (if $$adminBypass then [{ actor_type: "OrganizationAdmin", bypass_mode: "always" }] else [] end) \
151-
}'); \
152-
echo " branches: $$(echo "$$RULESET_JSON" | $(JQ) -r '.conditions.ref_name.include | join(", ")')"; \
153-
echo " approvals: $(REPO_REQUIRED_APPROVING_REVIEW_COUNT)"; \
154-
echo " code owner review: $(REPO_REQUIRE_CODE_OWNER_REVIEW)"; \
155-
echo " branch up-to-date: $(REPO_REQUIRE_BRANCH_UP_TO_DATE)"; \
156-
echo " required status checks: $(REPO_STATUS_CHECKS)"; \
157-
echo " admin bypass: $(REPO_ADMIN_BYPASS)"; \
158-
\
159-
existing=$$($(GH) api "repos/$$REPO/rulesets" -q '.[] | select(.name=="protect-main") | .id' 2>/dev/null); \
160-
if [ -n "$$existing" ]; then \
161-
$(GH) api "repos/$$REPO/rulesets/$$existing" -X PUT --input <(echo "$$RULESET_JSON") > /dev/null; \
162-
echo " Updated existing ruleset (id: $$existing)"; \
163-
else \
164-
$(GH) api "repos/$$REPO/rulesets" -X POST --input <(echo "$$RULESET_JSON") > /dev/null; \
165-
echo " Created new ruleset"; \
166-
fi; \
167-
\
168-
echo " Installing update-action-pins workflow..."; \
169-
mkdir -p .github/workflows; \
170-
_dev_kit_ver=$${DEV_KIT_VERSION:-main}; \
171-
curl --fail -sSL \
172-
"https://raw.githubusercontent.com/opendefensecloud/dev-kit/$$_dev_kit_ver/.github/workflows/update-action-pins.yml" \
173-
-o .github/workflows/update-action-pins.yml; \
174-
echo " Wrote .github/workflows/update-action-pins.yml"; \
175-
\
176-
echo "Done."
75+
@curl --fail -sSL \
76+
"https://raw.githubusercontent.com/opendefensecloud/dev-kit/$(DEV_KIT_VERSION)/scripts/repo-settings.sh" | \
77+
REPO_ADMIN_BYPASS='$(REPO_ADMIN_BYPASS)' \
78+
REPO_ALLOW_MERGE_COMMIT='$(REPO_ALLOW_MERGE_COMMIT)' \
79+
REPO_ALLOW_REBASE_MERGE='$(REPO_ALLOW_REBASE_MERGE)' \
80+
REPO_ALLOW_SQUASH_MERGE='$(REPO_ALLOW_SQUASH_MERGE)' \
81+
REPO_REQUIRE_BRANCH_UP_TO_DATE='$(REPO_REQUIRE_BRANCH_UP_TO_DATE)' \
82+
REPO_REQUIRE_CODE_OWNER_REVIEW='$(REPO_REQUIRE_CODE_OWNER_REVIEW)' \
83+
REPO_REQUIRED_APPROVING_REVIEW_COUNT='$(REPO_REQUIRED_APPROVING_REVIEW_COUNT)' \
84+
REPO_RULESET_BRANCHES='$(REPO_RULESET_BRANCHES)' \
85+
REPO_STATUS_CHECKS='$(REPO_STATUS_CHECKS)' \
86+
DEV_KIT_VERSION='$(DEV_KIT_VERSION)' \
87+
GH='$(GH)' JQ='$(JQ)' \
88+
bash
17789

17890
.PHONY: update-action-pins
17991
update-action-pins: ## Update GitHub Action pins to their latest commit SHA

docs/NEW_REPO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Run:
3333
make repo-settings
3434
```
3535

36-
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.
36+
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.
3737

3838
The branch protection ruleset is configurable via make variables, e.g.:
3939

scripts/repo-settings.sh

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Reconcile a repository's GitHub settings: labels, merge strategy, secret
6+
# scanning, branch protection ruleset, and the update-action-pins workflow.
7+
#
8+
# All configuration is passed through environment variables. Defaults are the
9+
# single source of truth in common.mk and are always passed by the make target;
10+
# an unset variable here is a configuration error (set -u).
11+
# REPO_ADMIN_BYPASS when "false", org admins cannot bypass the ruleset
12+
# REPO_REQUIRED_APPROVING_REVIEW_COUNT number of approving reviews required to merge
13+
# REPO_REQUIRE_CODE_OWNER_REVIEW require an approving review from code owners
14+
# REPO_REQUIRE_BRANCH_UP_TO_DATE require branches up to date (requires status checks)
15+
# REPO_STATUS_CHECKS JSON array of required status-check contexts
16+
# REPO_RULESET_BRANCHES JSON array of additional branch patterns
17+
# REPO_ALLOW_MERGE_COMMIT allow merge commits in the merge strategy
18+
# REPO_ALLOW_SQUASH_MERGE allow squash merging
19+
# REPO_ALLOW_REBASE_MERGE allow rebase merging
20+
# DEV_KIT_VERSION dev-kit version to fetch the workflow from
21+
# GH, JQ commands used to talk to GitHub and build JSON
22+
23+
"$GH" auth status >/dev/null 2>&1 || {
24+
echo "error: gh is not authenticated; run 'gh auth login'" >&2
25+
exit 1
26+
}
27+
28+
REPO=$("$GH" repo view --json nameWithOwner -q .nameWithOwner) || {
29+
echo "error: not a GitHub repository" >&2
30+
exit 1
31+
}
32+
33+
REPO_STATUS_CHECKS_EFFECTIVE=$(echo "$REPO_STATUS_CHECKS" | "$JQ" -c '[.[] | select((type == "string") and (length > 0))]')
34+
REPO_RULESET_BRANCHES_EFFECTIVE=$(echo "$REPO_RULESET_BRANCHES" | "$JQ" -c '[.[] |
35+
select((type == "string") and (length > 0)) |
36+
if startswith("~") or startswith("refs/") then .
37+
else "refs/heads/" + . end] |
38+
["~DEFAULT_BRANCH"] + . | unique')
39+
40+
if [ "$REPO_REQUIRE_BRANCH_UP_TO_DATE" = "true" ] &&
41+
[ "$REPO_STATUS_CHECKS_EFFECTIVE" = "[]" ]; then
42+
echo "error: REPO_REQUIRE_BRANCH_UP_TO_DATE=true requires at least one value in REPO_STATUS_CHECKS" >&2
43+
exit 1
44+
fi
45+
46+
echo "Reconciling settings for $REPO..."
47+
48+
echo " Syncing labels..."
49+
while IFS=';' read -r name color desc; do
50+
[ -z "$name" ] && continue
51+
"$GH" label create "$name" --repo "$REPO" --color "$color" --description "$desc" --force 2>/dev/null
52+
done <<'EOF'
53+
bug;d73a4a;Something isn't working
54+
documentation;0075ca;Improvements or additions to documentation
55+
duplicate;cfd3d7;This issue or pull request already exists
56+
enhancement;a2eeef;New feature or request
57+
good first issue;7057ff;Good for newcomers
58+
help wanted;008672;Extra attention is needed
59+
invalid;e4e669;This doesn't seem right
60+
question;37326e;Further information is requested
61+
wontfix;ffffff;This will not be worked on
62+
chore;ededed;A routine task or common potentially re-occurring task
63+
feature;a2eeef;New feature or request
64+
go;16e2e2;Pull requests that update go code
65+
ok-to-helm;0e8a16;PR is allowed to build an publish helm chart
66+
dependencies;0366d6;Pull requests that update a dependency file
67+
github-actions;80c4c6;PR created via GitHub action
68+
help-wanted;811857;Extra attention is needed
69+
good-first-issue;7057ff;Good for newcomers
70+
needs-triage;eab668;Issue that has not been reviewed
71+
ok-to-image;0e8a16;PR is allowed to run container build
72+
ok-to-test;0e8a16;PR is allowed to be tested
73+
spike;b23adb;A task to research a question and resolve problems
74+
EOF
75+
76+
echo " Configuring merge strategy..."
77+
"$GH" api "repos/$REPO" -X PATCH \
78+
-f allow_merge_commit="$REPO_ALLOW_MERGE_COMMIT" \
79+
-f allow_squash_merge="$REPO_ALLOW_SQUASH_MERGE" \
80+
-f allow_rebase_merge="$REPO_ALLOW_REBASE_MERGE" \
81+
-f delete_branch_on_merge=true \
82+
-f allow_auto_merge=true >/dev/null
83+
84+
echo " Enabling secret scanning..."
85+
"$GH" api "repos/$REPO" -X PATCH \
86+
--input <(echo '{"security_and_analysis":{"secret_scanning":{"status":"enabled"}}}') >/dev/null
87+
88+
RULESET_JSON=$(
89+
# shellcheck disable=SC2016 # $variables belong to jq, not the shell
90+
"$JQ" -cn \
91+
--argjson branches "$REPO_RULESET_BRANCHES_EFFECTIVE" \
92+
--argjson checks "$REPO_STATUS_CHECKS_EFFECTIVE" \
93+
--argjson approvals "$REPO_REQUIRED_APPROVING_REVIEW_COUNT" \
94+
--argjson codeOwner "$REPO_REQUIRE_CODE_OWNER_REVIEW" \
95+
--argjson adminBypass "$REPO_ADMIN_BYPASS" \
96+
--argjson upToDate "$REPO_REQUIRE_BRANCH_UP_TO_DATE" \
97+
'{ name: "protect-main", target: "branch", enforcement: "active",
98+
conditions: { ref_name: { include: $branches, exclude: [] } },
99+
rules: ([
100+
{ type: "deletion" },
101+
{ type: "non_fast_forward" },
102+
{ type: "creation" },
103+
{ type: "required_signatures" },
104+
{ type: "pull_request", parameters: {
105+
required_approving_review_count: $approvals,
106+
dismiss_stale_reviews_on_push: true,
107+
required_reviewers: [],
108+
require_code_owner_review: $codeOwner,
109+
require_last_push_approval: false,
110+
required_review_thread_resolution: true,
111+
allowed_merge_methods: ["squash", "rebase", "merge"]
112+
} }
113+
] + (if ($checks | length > 0) then
114+
[{ type: "required_status_checks", parameters: {
115+
strict_required_status_checks_policy: $upToDate,
116+
required_status_checks: [$checks | .[] | { context: . }]
117+
} }] else [] end)),
118+
bypass_actors: (if $adminBypass then [{ actor_type: "OrganizationAdmin", bypass_mode: "always" }] else [] end)
119+
}'
120+
)
121+
122+
echo " Configuring branch protection ruleset..."
123+
echo " branches: $REPO_RULESET_BRANCHES_EFFECTIVE"
124+
echo " approvals: $REPO_REQUIRED_APPROVING_REVIEW_COUNT"
125+
echo " code owner review: $REPO_REQUIRE_CODE_OWNER_REVIEW"
126+
echo " branch up-to-date: $REPO_REQUIRE_BRANCH_UP_TO_DATE"
127+
echo " required status checks: $REPO_STATUS_CHECKS_EFFECTIVE"
128+
echo " admin bypass: $REPO_ADMIN_BYPASS"
129+
130+
existing=$("$GH" api "repos/$REPO/rulesets" -q '.[] | select(.name=="protect-main") | .id' 2>/dev/null || true)
131+
if [ -n "$existing" ]; then
132+
"$GH" api "repos/$REPO/rulesets/$existing" -X PUT --input <(echo "$RULESET_JSON") >/dev/null
133+
echo " Updated existing ruleset (id: $existing)"
134+
else
135+
"$GH" api "repos/$REPO/rulesets" -X POST --input <(echo "$RULESET_JSON") >/dev/null
136+
echo " Created new ruleset"
137+
fi
138+
139+
echo " Installing update-action-pins workflow..."
140+
mkdir -p .github/workflows
141+
curl --fail -sSL \
142+
"https://raw.githubusercontent.com/opendefensecloud/dev-kit/$DEV_KIT_VERSION/.github/workflows/update-action-pins.yml" \
143+
-o .github/workflows/update-action-pins.yml
144+
echo " Wrote .github/workflows/update-action-pins.yml"
145+
146+
echo "Done."

0 commit comments

Comments
 (0)