Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/BRANCH_PROTECTION.md
Original file line number Diff line number Diff line change
@@ -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`.
98 changes: 98 additions & 0 deletions localonly/sdk-portfolio-branch-protection-playwrighter-report.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions scripts/apply_branch_protection.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOFJSON
{
"required_status_checks": null,
"enforce_admins": false,
"required_pull_request_reviews": {
"dismiss_stale_reviews": true,
"require_code_owner_reviews": false,
"required_approving_review_count": 1
},
"restrictions": null,
"required_linear_history": false,
"allow_force_pushes": false,
"allow_deletions": false,
"block_creations": false,
"required_conversation_resolution": true
}
EOFJSON
)

echo "target: $GH_REPO branch=$BRANCH DRY_RUN=$DRY_RUN" >&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
Loading