chore: introduce next integration branch (Phase 1 — additive) (#231)
Adds: - docs/branching.md — beginner contributor guide - docs/adr/XXXX-...md — ADR (will be renamed with issue#) - .github/workflows/auto-backmerge.yml — disabled in Phase 1 - .github/workflows/pr-target-validator.yml — warn-only in Phase 1 - scripts/setup-branch-protection.sh — idempotent gh api script Modifies: - .github/workflows/branch-naming.yml — recognize 'next' - CONTRIBUTING.md — 'Where Do I Open My PR?' section Phase 1 is additive: nothing operational changes until Phase 2 flips auto-backmerge.yml's if:false→true, flips pr-target-validator.yml's WARN_ONLY→false, creates the next branch, and switches the default branch. See the ADR for the migration plan.
This commit is contained in:
236
scripts/setup-branch-protection.sh
Executable file
236
scripts/setup-branch-protection.sh
Executable file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env bash
|
||||
# setup-branch-protection.sh
|
||||
#
|
||||
# Apply branch protection rules to `main` and `next` for the GSD repo.
|
||||
# Idempotent — run as many times as you like. Re-running brings the live
|
||||
# rules back to what this script declares, so the script IS the source of
|
||||
# truth for branch protection.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/setup-branch-protection.sh # apply both
|
||||
# bash scripts/setup-branch-protection.sh main # apply only main
|
||||
# bash scripts/setup-branch-protection.sh next # apply only next
|
||||
# DRY_RUN=1 bash scripts/setup-branch-protection.sh # show payloads, don't apply
|
||||
#
|
||||
# Requirements:
|
||||
# - gh CLI authenticated against open-gsd/get-shit-done-redux with admin scope
|
||||
# - jq installed
|
||||
#
|
||||
# What it sets:
|
||||
#
|
||||
# main (strict — production):
|
||||
# - 2 required approving reviews
|
||||
# - dismiss stale reviews on push
|
||||
# - require code-owner review when CODEOWNERS applies
|
||||
# - all required status checks must pass (defined in REQUIRED_CHECKS_MAIN below)
|
||||
# - require branches to be up to date before merging (ON — `main` is production)
|
||||
# - require linear history (OFF — release back-merges use merge commits)
|
||||
# - require conversation resolution
|
||||
# - require signed commits
|
||||
# - block force-push and deletion
|
||||
# - admins included
|
||||
#
|
||||
# next (loose — integration):
|
||||
# - 1 required approving review
|
||||
# - dismiss stale reviews on push
|
||||
# - require code-owner review when CODEOWNERS applies
|
||||
# - all required status checks must pass (defined in REQUIRED_CHECKS_NEXT below)
|
||||
# - require branches to be up to date before merging (OFF — this is the whole point)
|
||||
# - require linear history (OFF — auto-backmerge from main needs merge commits
|
||||
# to preserve the link from next's history to main's release tags;
|
||||
# feature PRs still squash-merge by repo merge-strategy setting)
|
||||
# - require conversation resolution
|
||||
# - require signed commits (OFF on next — easier for contributors)
|
||||
# - block force-push and deletion
|
||||
# - admins included
|
||||
#
|
||||
# See: docs/adr/XXXX-introduce-next-integration-branch.md
|
||||
# See: docs/branching.md
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${REPO:-open-gsd/get-shit-done-redux}"
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
|
||||
# Required status checks. Adjust as your CI suite evolves.
|
||||
# The names must match the JOB NAME (not the workflow name) that GitHub
|
||||
# records — check existing PRs to confirm.
|
||||
REQUIRED_CHECKS_MAIN=(
|
||||
"test"
|
||||
"install-smoke"
|
||||
"security-scan"
|
||||
"Changeset Required / changeset-lint"
|
||||
"Docs Required / docs-lint"
|
||||
"PR Gate / size-check"
|
||||
"Validate Branch Name / check-branch"
|
||||
)
|
||||
|
||||
REQUIRED_CHECKS_NEXT=(
|
||||
"test"
|
||||
"PR Gate / size-check"
|
||||
"Validate Branch Name / check-branch"
|
||||
"Changeset Required / changeset-lint"
|
||||
"Docs Required / docs-lint"
|
||||
"PR Target Validator / validate-target"
|
||||
)
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "ERROR: missing required command: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
require_cmd gh
|
||||
require_cmd jq
|
||||
|
||||
verify_auth() {
|
||||
if ! gh auth status >/dev/null 2>&1; then
|
||||
echo "ERROR: gh CLI is not authenticated. Run 'gh auth login' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
build_payload() {
|
||||
local branch="$1"
|
||||
shift
|
||||
local checks_array=("$@")
|
||||
|
||||
# Branch-specific knobs.
|
||||
local approvals require_up_to_date linear_history signed_commits
|
||||
case "$branch" in
|
||||
main)
|
||||
approvals=2
|
||||
require_up_to_date=true
|
||||
linear_history=false
|
||||
signed_commits=true
|
||||
;;
|
||||
next)
|
||||
approvals=1
|
||||
require_up_to_date=false
|
||||
# linear_history=false: auto-backmerge from main needs merge commits to
|
||||
# preserve the link to release tags. Feature PRs still produce one
|
||||
# commit each via repo-level "squash and merge" default — that gives
|
||||
# us a clean log without enforcing linearity at the protection layer.
|
||||
linear_history=false
|
||||
signed_commits=false
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown branch '$branch'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Build the contexts array via jq for safe quoting.
|
||||
local contexts_json
|
||||
contexts_json=$(printf '%s\n' "${checks_array[@]}" | jq -R . | jq -s .)
|
||||
|
||||
jq -n \
|
||||
--argjson contexts "$contexts_json" \
|
||||
--argjson approvals "$approvals" \
|
||||
--argjson require_up_to_date "$require_up_to_date" \
|
||||
--argjson linear_history "$linear_history" \
|
||||
--argjson signed_commits "$signed_commits" \
|
||||
'{
|
||||
required_status_checks: {
|
||||
strict: $require_up_to_date,
|
||||
contexts: $contexts
|
||||
},
|
||||
enforce_admins: true,
|
||||
required_pull_request_reviews: {
|
||||
dismiss_stale_reviews: true,
|
||||
require_code_owner_reviews: true,
|
||||
required_approving_review_count: $approvals,
|
||||
require_last_push_approval: false
|
||||
},
|
||||
restrictions: null,
|
||||
required_linear_history: $linear_history,
|
||||
allow_force_pushes: false,
|
||||
allow_deletions: false,
|
||||
required_conversation_resolution: true,
|
||||
required_signatures: $signed_commits,
|
||||
lock_branch: false,
|
||||
allow_fork_syncing: true
|
||||
}'
|
||||
}
|
||||
|
||||
apply_protection() {
|
||||
local branch="$1"
|
||||
local checks_var_name
|
||||
if [ "$branch" = "main" ]; then
|
||||
checks_var_name="REQUIRED_CHECKS_MAIN"
|
||||
else
|
||||
checks_var_name="REQUIRED_CHECKS_NEXT"
|
||||
fi
|
||||
|
||||
# Expand the array indirectly (bash 3 compatible — macOS default).
|
||||
eval "local checks=(\"\${${checks_var_name}[@]}\")"
|
||||
|
||||
local payload
|
||||
payload=$(build_payload "$branch" "${checks[@]}")
|
||||
|
||||
echo "──────────────────────────────────────────"
|
||||
echo "Branch: $branch"
|
||||
echo "Required checks (${#checks[@]}):"
|
||||
printf ' - %s\n' "${checks[@]}"
|
||||
echo "──────────────────────────────────────────"
|
||||
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "[DRY RUN] Would PUT to /repos/${REPO}/branches/${branch}/protection:"
|
||||
echo "$payload" | jq .
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Applying branch protection..."
|
||||
echo "$payload" | gh api \
|
||||
-X PUT \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--input - \
|
||||
"/repos/${REPO}/branches/${branch}/protection" \
|
||||
>/dev/null
|
||||
echo "✓ Protection rules applied to $branch."
|
||||
}
|
||||
|
||||
ensure_branch_exists() {
|
||||
local branch="$1"
|
||||
if ! gh api "/repos/${REPO}/branches/${branch}" >/dev/null 2>&1; then
|
||||
echo "ERROR: branch '$branch' does not exist in $REPO." >&2
|
||||
if [ "$branch" = "next" ]; then
|
||||
cat <<EOF >&2
|
||||
|
||||
Create the next branch first:
|
||||
git checkout main && git pull --ff-only
|
||||
git checkout -b next && git push -u origin next
|
||||
|
||||
Then re-run this script.
|
||||
EOF
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
verify_auth
|
||||
|
||||
local targets=()
|
||||
if [ $# -eq 0 ]; then
|
||||
targets=(main next)
|
||||
else
|
||||
targets=("$@")
|
||||
fi
|
||||
|
||||
for branch in "${targets[@]}"; do
|
||||
if [ "$branch" != "main" ] && [ "$branch" != "next" ]; then
|
||||
echo "ERROR: unsupported branch '$branch'. Use 'main' or 'next'." >&2
|
||||
exit 1
|
||||
fi
|
||||
ensure_branch_exists "$branch"
|
||||
apply_protection "$branch"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done. To verify: gh api /repos/${REPO}/branches/<branch>/protection | jq ."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user