From 9f9c1a7909a5001416589924f2aba8ee1a2633ed Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 22 May 2026 09:19:41 -0400 Subject: [PATCH 1/2] chore: add branch protection ruleset specs (PR-1 of 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checks in JSON specs for three rulesets (main-protection, release-branches, tag-immutability), a CODEOWNERS file (advisory), and scripts/sync-rulesets.sh to apply them. Enforcement is `disabled` in all three files — PR-2 will apply with `evaluate` for a 1-week dry-run, PR-3 will flip to `active`. See docs/branch-protection.md for the full rollout plan. --- .github/rulesets/main-protection.json | 51 +++++++++++++++++++ .github/rulesets/release-branches.json | 48 ++++++++++++++++++ .github/rulesets/tag-immutability.json | 19 +++++++ docs/branch-protection.md | 68 ++++++++++++++++++++++++++ scripts/sync-rulesets.sh | 34 +++++++++++++ 5 files changed, 220 insertions(+) create mode 100644 .github/rulesets/main-protection.json create mode 100644 .github/rulesets/release-branches.json create mode 100644 .github/rulesets/tag-immutability.json create mode 100644 docs/branch-protection.md create mode 100755 scripts/sync-rulesets.sh diff --git a/.github/rulesets/main-protection.json b/.github/rulesets/main-protection.json new file mode 100644 index 000000000..8a91a3c84 --- /dev/null +++ b/.github/rulesets/main-protection.json @@ -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" } + ] + } + } + ] +} diff --git a/.github/rulesets/release-branches.json b/.github/rulesets/release-branches.json new file mode 100644 index 000000000..457eaf607 --- /dev/null +++ b/.github/rulesets/release-branches.json @@ -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" } + ] + } + } + ] +} diff --git a/.github/rulesets/tag-immutability.json b/.github/rulesets/tag-immutability.json new file mode 100644 index 000000000..582ad2640 --- /dev/null +++ b/.github/rulesets/tag-immutability.json @@ -0,0 +1,19 @@ +{ + "name": "tag-immutability", + "target": "tag", + "enforcement": "disabled", + "conditions": { + "ref_name": { + "include": ["~ALL"], + "exclude": [] + } + }, + "rules": [ + { + "type": "update" + }, + { + "type": "deletion" + } + ] +} diff --git a/docs/branch-protection.md b/docs/branch-protection.md new file mode 100644 index 000000000..03e2bcddb --- /dev/null +++ b/docs/branch-protection.md @@ -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. diff --git a/scripts/sync-rulesets.sh b/scripts/sync-rulesets.sh new file mode 100755 index 000000000..870023ef9 --- /dev/null +++ b/scripts/sync-rulesets.sh @@ -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 From aac93329a752cfac8c9e5e27224ac297f5cd3d8f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 22 May 2026 09:41:40 -0400 Subject: [PATCH 2/2] chore: replace CODEOWNERS reviewers (PR-1 of 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces legacy @glittercowboy entry with the active reviewer pool: @trek-e, @Solvely-Colin, @jeremymcs. CODEOWNERS is advisory only — the main-protection ruleset does not require CODEOWNERS approval (required_approving_review_count: 0). --- .github/CODEOWNERS | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 018c809f9..f90a8fd8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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