Merge pull request #106 from gsd-redux/chore/branch-protection-specs

chore: add branch protection ruleset specs (PR-1 of 3)
This commit is contained in:
Tom Boucher
2026-05-22 09:52:09 -04:00
committed by GitHub
6 changed files with 224 additions and 21 deletions

25
.github/CODEOWNERS vendored
View File

@@ -1,21 +1,4 @@
# All changes require review from project owner
* @glittercowboy
# Phase 6 of #3524 — source-of-truth files require architecture-team review.
# See docs/agents/cjs-sdk-seam.md for context.
# The blanket rule above already covers everything; these specific rules make
# the architectural intent explicit and would still apply if the blanket rule
# is later relaxed.
/sdk/src/state-document/ @glittercowboy
/sdk/src/configuration/ @glittercowboy
/sdk/src/workstream-inventory/ @glittercowboy
/sdk/src/project-root/ @glittercowboy
/sdk/src/runtime-bridge-sync/ @glittercowboy
/sdk/shared/config-defaults.manifest.json @glittercowboy
/sdk/shared/config-schema.manifest.json @glittercowboy
/sdk/shared/model-catalog.json @glittercowboy
/sdk/src/query/query-runtime-bridge.ts @glittercowboy
/scripts/lint-shared-module-handsync.cjs @glittercowboy
/scripts/shared-module-handsync-allowlist.json @glittercowboy
/sdk/src/query/decisions.ts @glittercowboy
/sdk/src/workstream-name-policy.ts @glittercowboy
# CODEOWNERS is advisory only — the main-protection ruleset does not require
# CODEOWNERS approval (required_approving_review_count: 0).
# All paths: active reviewer pool as of 2026-05.
* @trek-e @Solvely-Colin @jeremymcs

51
.github/rulesets/main-protection.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "main-protection",
"target": "branch",
"enforcement": "disabled",
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "required_linear_history"
},
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": true,
"allowed_merge_methods": ["squash", "rebase"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [
{ "context": "lint-tests" },
{ "context": "test (22, ubuntu-latest)" },
{ "context": "test (22, macos-latest)" },
{ "context": "test (22, windows-latest)" },
{ "context": "test (24, ubuntu-latest)" },
{ "context": "test (24, macos-latest)" },
{ "context": "test (24, windows-latest)" },
{ "context": "Changeset Required" },
{ "context": "require-issue-link" },
{ "context": "pr-template-format" }
]
}
}
]
}

48
.github/rulesets/release-branches.json vendored Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "release-branches",
"target": "branch",
"enforcement": "disabled",
"conditions": {
"ref_name": {
"include": ["refs/heads/release/**", "refs/heads/hotfix/**"],
"exclude": []
}
},
"rules": [
{
"type": "deletion"
},
{
"type": "non_fast_forward"
},
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": true,
"allowed_merge_methods": ["squash", "rebase"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [
{ "context": "lint-tests" },
{ "context": "test (22, ubuntu-latest)" },
{ "context": "test (22, macos-latest)" },
{ "context": "test (22, windows-latest)" },
{ "context": "test (24, ubuntu-latest)" },
{ "context": "test (24, macos-latest)" },
{ "context": "test (24, windows-latest)" },
{ "context": "Changeset Required" },
{ "context": "require-issue-link" },
{ "context": "pr-template-format" }
]
}
}
]
}

19
.github/rulesets/tag-immutability.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "tag-immutability",
"target": "tag",
"enforcement": "disabled",
"conditions": {
"ref_name": {
"include": ["~ALL"],
"exclude": []
}
},
"rules": [
{
"type": "update"
},
{
"type": "deletion"
}
]
}

68
docs/branch-protection.md Normal file
View File

@@ -0,0 +1,68 @@
# Branch Protection Rollout
## Rulesets
Three ruleset specs live under `.github/rulesets/`. All are committed with
`enforcement: disabled` and activated in stages via the 3-PR rollout below.
### `main-protection`
Targets `~DEFAULT_BRANCH` (main). Enforces:
- No deletions or force pushes
- Required linear history (no merge commits)
- All changes via pull request (0 required approvals, stale-review dismissal, thread resolution required, squash/rebase only)
- 10 required status checks: lint, Node 22/24 matrix (ubuntu/mac/windows), Changeset Required, require-issue-link, pr-template-format
### `release-branches`
Targets `refs/heads/release/**` and `refs/heads/hotfix/**`. Same rules as
`main-protection` except `required_linear_history` is omitted (merge commits
are permitted on release/hotfix branches).
### `tag-immutability`
Targets all tags (`~ALL`). Blocks tag updates and deletions — tags are
immutable once created. Tag creation is unrestricted.
## 3-PR Rollout Plan
| PR | Branch | Action |
|----|--------|--------|
| PR-1 (this PR) | `chore/branch-protection-specs` | Check in spec files; `enforcement: disabled` — no effect on repo |
| PR-2 | `chore/branch-protection-evaluate` | Run `sync-rulesets.sh` with `ENFORCEMENT=evaluate`; 1-week dry-run via rule-suite logs |
| PR-3 | `chore/branch-protection-active` | Run `sync-rulesets.sh` with `ENFORCEMENT=active`; protection live |
## Running `sync-rulesets.sh`
**Prerequisites:** `gh` authenticated with repo-admin scope, `jq` installed.
```bash
# Dry-run (evaluate mode — logs violations, does not block)
REPO=GSD-redux/get-shit-done-redux ENFORCEMENT=evaluate bash scripts/sync-rulesets.sh
# Activate protection
REPO=GSD-redux/get-shit-done-redux ENFORCEMENT=active bash scripts/sync-rulesets.sh
# Roll back to disabled
REPO=GSD-redux/get-shit-done-redux ENFORCEMENT=disabled bash scripts/sync-rulesets.sh
```
The script is idempotent: running it twice with the same `ENFORCEMENT` value
is a no-op semantically (PUT with identical body).
## Reading evaluate-mode logs
After applying with `evaluate`, check which PRs/pushes would have been blocked:
```bash
REPO=GSD-redux/get-shit-done-redux
RULESET_ID=$(gh api repos/$REPO/rulesets --jq '.[] | select(.name=="main-protection") | .id')
gh api repos/$REPO/rulesets/$RULESET_ID/rule-suites
```
Each entry shows the actor, ref, result (`pass`/`fail`), and which rules
triggered. Use this to validate no legitimate workflows are broken before
flipping to `active` in PR-3.
## Phase-2 TODO
Enable the `required_signatures` rule (signed commits) once agent commits sign
uniformly. As of PR-1 this rule is intentionally omitted — unsigned agent
commits would be blocked by it. Track readiness in the issue linked to PR-3.

34
scripts/sync-rulesets.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
REPO="${REPO:-GSD-redux/get-shit-done-redux}"
ENFORCEMENT="${ENFORCEMENT:-evaluate}"
case "$ENFORCEMENT" in
disabled|evaluate|active) ;;
*)
echo "ERROR: ENFORCEMENT must be one of: disabled|evaluate|active" >&2
exit 1
;;
esac
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
RULESETS_DIR="$SCRIPT_DIR/../.github/rulesets"
for ruleset_file in "$RULESETS_DIR"/*.json; do
name="$(jq -r '.name' "$ruleset_file")"
body="$(jq --arg enforcement "$ENFORCEMENT" '.enforcement = $enforcement' "$ruleset_file")"
existing_id="$(gh api "repos/$REPO/rulesets" --jq ".[] | select(.name==\"$name\") | .id" 2>/dev/null || true)"
if [ -n "$existing_id" ]; then
gh api --method PUT "repos/$REPO/rulesets/$existing_id" \
--input - <<< "$body" > /dev/null
echo "[update] $name -> enforcement=$ENFORCEMENT"
else
gh api --method POST "repos/$REPO/rulesets" \
--input - <<< "$body" > /dev/null
echo "[create] $name -> enforcement=$ENFORCEMENT"
fi
done