diff --git a/docs/BRANCH_PROTECTION.md b/docs/BRANCH_PROTECTION.md new file mode 100644 index 0000000..f5ca8ef --- /dev/null +++ b/docs/BRANCH_PROTECTION.md @@ -0,0 +1,87 @@ +# Branch protection (`main`) + +GitHub rules for **playwrighter** (`weijia-89/playwrighter`). Apply only after the operator confirms `GH_REPO`. + +**Default target:** `weijia-89/playwrighter` (set explicitly; never assume from cwd alone). + +## Current state (2026-05-25) + +| Check | Result | +| ----- | ------ | +| Default branch | `main` | +| Visibility | **Public** | +| Manifest action | `apply` with `apply_live: true` when portfolio wave sets `APPLY=1` | +| Classic branch protection GET | **200** — protection already active on `main` | +| Required approvals | 1 | +| Dismiss stale reviews | yes | +| Require conversation resolution | yes | +| Force pushes / deletions on `main` | blocked | + +Ord 14 portfolio wave for playwrighter is primarily **docs + script parity** on `chore/branch-protection-sdk`; live PUT is idempotent refresh of the policy below. + +## Policy (script / live) + +| Rule | Setting | Notes | +| ---- | ------- | ----- | +| Default branch | `main` | Confirmed via `gh repo view weijia-89/playwrighter`. | +| Require PR before merge | yes | Direct pushes to `main` blocked. | +| Require approvals | 1 | See [Solo maintainer tradeoff](#solo-maintainer-tradeoff). | +| Dismiss stale reviews | yes | New commits invalidate prior approvals. | +| Require conversation resolution | yes | Unresolved review threads block merge. | +| Require linear history | off | Enable if you want squash-only or rebase-only merges. | +| Force pushes | block on `main` | No `--force` to shared default branch. | +| Branch deletions | block on `main` | Prevents accidental removal of the default branch. | +| Enforce for admins | off | Admins can bypass unless you enable enforce. | +| Required status checks | none (placeholder) | Add when a required CI gate is chosen. | + +## Solo maintainer tradeoff + +With **required approving review count = 1**, GitHub expects someone other than the PR author to approve. On a solo personal repo that usually means: + +- **Option A (strict):** keep `required_approving_review_count: 1` and use a second account, bot, or org rule exception. +- **Option B (pragmatic solo):** set count to `0` but keep **require PR** + conversation resolution. +- **Option C:** use bypass lists for specified actors if available on your plan. + +The bundled script defaults to **count = 1**. Lower it in the JSON payload before apply if you choose Option B. + +## Prerequisites + +1. Remote repo exists: `gh repo view "$GH_REPO"`. +2. `gh` authenticated to **github.com**: `gh auth status`. +3. Default branch is `main` (or edit the script branch name). +4. Operator confirms **`GH_REPO=owner/name`** matches the intended repo. +5. For live apply: set **`APPLY=1`** before `DRY_RUN=0`. + +## Apply via script (preferred) + +From repo root: + +```bash +cd ~/Projects/playwrighter +export GH_REPO=weijia-89/playwrighter + +# Dry run (default) — prints JSON only +./scripts/apply_branch_protection.sh + +# Apply (operator intent + APPLY=1) +APPLY=1 DRY_RUN=0 ./scripts/apply_branch_protection.sh +``` + +The script is idempotent when classic protection is in use: repeated `APPLY=1 DRY_RUN=0` runs send the same PUT payload. + +## Manual UI steps + +1. Open `https://github.com/weijia-89/playwrighter/settings/branches`. +2. Edit the rule for `main` or add one if missing. +3. Match the policy table above. + +## Verify remote state + +```bash +gh api repos/weijia-89/playwrighter/branches/main/protection +``` + +## References + +- Portfolio manifest: `cursor-sdk-playground/prompts/portfolio_branch_protection_manifest.json` (row `playwrighter`). +- SDK wave: `cursor-sdk-playground/scripts/portfolio_branch_protection_wave.sh`. diff --git a/localonly/sdk-portfolio-branch-protection-playwrighter-report.md b/localonly/sdk-portfolio-branch-protection-playwrighter-report.md new file mode 100644 index 0000000..bc38cba --- /dev/null +++ b/localonly/sdk-portfolio-branch-protection-playwrighter-report.md @@ -0,0 +1,98 @@ +# SDK report — portfolio-branch-protection-playwrighter + +**Queue id:** portfolio-branch-protection-playwrighter +**Repo:** weijia-89/playwrighter +**Branch:** chore/branch-protection-sdk +**Agent:** worker (Cursor SDK) +**Date:** 2026-05-25 + +## Manifest row + +| Field | Value | +| ----- | ----- | +| slug | playwrighter | +| gh_repo | weijia-89/playwrighter | +| default_branch | main | +| visibility | PUBLIC | +| action | apply | +| apply_live | true | +| branch | chore/branch-protection-sdk | + +## Deliverables + +| Path | Status | +| ---- | ------ | +| `docs/BRANCH_PROTECTION.md` | present — policy table, solo-maintainer tradeoff, apply/verify commands | +| `scripts/apply_branch_protection.sh` | present, executable — `DRY_RUN=1` default, `GH_REPO=weijia-89/playwrighter` | +| This report | present | + +## Repo snapshot (`gh repo view`) + +| Check | Result | +| ----- | ------ | +| Default branch | main | +| Visibility | PUBLIC | +| viewerPermission | ADMIN | +| viewerCanAdminister | true | + +## Live protection state (GET, no APPLY run this session) + +`APPLY=1` was **not** set in the worker environment; live PUT was skipped per portfolio wave gate. + +```bash +gh api repos/weijia-89/playwrighter/branches/main/protection +``` + +**HTTP 200** — classic branch protection already active on `main`: + +| Setting | Live value | +| ------- | ---------- | +| required_approving_review_count | 1 | +| dismiss_stale_reviews | true | +| require_code_owner_reviews | false | +| required_conversation_resolution | true | +| allow_force_pushes | false | +| allow_deletions | false | +| enforce_admins | false | +| required_linear_history | false | + +Live policy matches the script JSON payload. Idempotent refresh via `APPLY=1 DRY_RUN=0` is optional. + +## Verification + +```bash +test -f "/Users/wjia/Projects/playwrighter/docs/BRANCH_PROTECTION.md" +test -x "/Users/wjia/Projects/playwrighter/scripts/apply_branch_protection.sh" +``` + +**Result:** PASS + +```bash +./scripts/apply_branch_protection.sh +``` + +**Result:** PASS — dry-run prints target `weijia-89/playwrighter`, JSON payload, exit 0. + +```bash +bash -n scripts/apply_branch_protection.sh +``` + +**Result:** PASS + +## Operator: live apply (when needed) + +```bash +cd ~/Projects/playwrighter +APPLY=1 DRY_RUN=0 GH_REPO=weijia-89/playwrighter ./scripts/apply_branch_protection.sh +gh api repos/weijia-89/playwrighter/branches/main/protection +``` + +## Blockers + +None. Public repo on free tier supports classic branch protection; admin access confirmed. + +## Open TODOs + +- Portfolio manifest row still lists `protected: "no"` — stale vs live GET; refresh manifest inventory on next wave scan. +- Optional: add required status checks when a canonical CI gate is chosen (script keeps `required_status_checks: null`). +- Solo maintainer: choose Option A/B/C in `docs/BRANCH_PROTECTION.md` if PR self-merge is blocked by approval count = 1. diff --git a/scripts/apply_branch_protection.sh b/scripts/apply_branch_protection.sh new file mode 100755 index 0000000..bc8af21 --- /dev/null +++ b/scripts/apply_branch_protection.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Apply GitHub branch protection on main. Default DRY_RUN=1. +# Requires GH_REPO=owner/name. Never stores tokens in this file. +# Live PUT requires APPLY=1 in environment (portfolio wave gate). +set -euo pipefail + +GH_REPO="${GH_REPO:-weijia-89/playwrighter}" +DRY_RUN="${DRY_RUN:-1}" +BRANCH="${BRANCH:-main}" +OWNER="${GH_REPO%%/*}" +REPO="${GH_REPO##*/}" + +if [[ "$OWNER" == "$REPO" || -z "$OWNER" || -z "$REPO" ]]; then + echo "error: GH_REPO must be owner/repo, got: $GH_REPO" >&2 + exit 1 +fi + +PAYLOAD=$(cat <&2 + +if [[ "$DRY_RUN" != "0" ]]; then + echo "DRY_RUN=1 — would PUT repos/$OWNER/$REPO/branches/$BRANCH/protection" >&2 + echo "$PAYLOAD" | python3 -m json.tool + exit 0 +fi + +if [[ "${APPLY:-}" != "1" ]]; then + echo "error: DRY_RUN=0 requires APPLY=1 in environment (portfolio wave gate)." >&2 + exit 1 +fi + +if ! gh auth status -h github.com >/dev/null 2>&1; then + echo "error: gh not authenticated to github.com; run 'gh auth login' before DRY_RUN=0." >&2 + exit 1 +fi + +if ! gh repo view "$GH_REPO" --json nameWithOwner >/dev/null 2>&1; then + echo "error: repo $GH_REPO not found or not visible; aborting (no PUT)." >&2 + exit 1 +fi + +echo "Applying protection to $GH_REPO ($BRANCH) …" >&2 +gh api -X PUT "repos/${OWNER}/${REPO}/branches/${BRANCH}/protection" --input - <<<"$PAYLOAD" +echo "done — verify with: gh api repos/${OWNER}/${REPO}/branches/${BRANCH}/protection" >&2