molecule-core/tools/branch-protection/apply.sh
Hongming Wang 7cc1c39c49 ci: e2e coverage matrix + branch-protection-as-code
Closes #9.

Three pieces, all small:

1. **docs/e2e-coverage.md** — source of truth for which E2E suites
   guard which surfaces. Today three were running but informational
   only on staging; that's how the org-import silent-drop bug shipped
   without a test catching it pre-merge. Now the matrix shows what's
   required where + a follow-up note for the two suites that need an
   always-emit refactor before they can be required.

2. **tools/branch-protection/apply.sh** — branch protection as code.
   Lets `staging` and `main` required-checks live in a reviewable
   shell script instead of UI clicks that get lost between admins.
   This PR's net change: add `E2E API Smoke Test` and `Canvas tabs E2E`
   as required on staging. Both already use the always-emit path-filter
   pattern (no-op step emits SUCCESS when the workflow's paths weren't
   touched), so making them required can't deadlock unrelated PRs.

3. **branch-protection-drift.yml** — daily cron + drift_check.sh
   that compares live protection against apply.sh's desired state.
   Catches out-of-band UI edits before they drift further. Fails the
   workflow on mismatch; ops re-runs apply.sh or updates the script.

Out of scope (filed as follow-ups):
- e2e-staging-saas + e2e-staging-external use plain `paths:` filters
  and never trigger when paths are unchanged. They need refactoring
  to the always-emit shape (same as e2e-api / e2e-staging-canvas)
  before they can be required.
- main branch protection mirrors staging here; if main wants the
  E2E SaaS / External added later, do it in apply.sh and rerun.

Operator must apply once after merge:
  bash tools/branch-protection/apply.sh
The drift check picks it up from there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:21:59 -07:00

134 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# tools/branch-protection/apply.sh — idempotently apply branch
# protection to molecule-core's `staging` and `main` branches.
#
# Why a script: GitHub's branch protection lives in repo settings, so
# changes are usually clicked through the UI and lost between admins.
# This script makes the config reproducible — diff it against the live
# state, change the file, run it, done. Single source of truth that
# shows up in code review.
#
# Usage:
# tools/branch-protection/apply.sh # apply both branches
# tools/branch-protection/apply.sh --dry-run # show payload only
# tools/branch-protection/apply.sh --branch staging
#
# Requires: gh CLI authenticated as a repo admin. The script uses gh's
# token (no separate PAT needed).
set -euo pipefail
REPO="Molecule-AI/molecule-core"
DRY_RUN=0
ONLY_BRANCH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--branch) ONLY_BRANCH="$2"; shift 2 ;;
-h|--help)
echo "Usage: $0 [--dry-run] [--branch <name>]"
exit 0
;;
*) echo "Unknown arg: $1" >&2; exit 1 ;;
esac
done
# Required-check matrices. Each branch's set is the canonical list of
# check NAMES (from each workflow's job-name). Adding/removing a check
# here is the place to do it. Match docs/e2e-coverage.md.
#
# Why staging gets E2E API + Canvas E2E (this PR's addition): both
# already use the always-emit pattern (path-filter no-ops emit SUCCESS),
# so making them required can't deadlock a PR that doesn't touch their
# paths. The other E2Es (SaaS, External) need a refactor to that
# pattern before they can be required — tracked as follow-up.
read -r -d '' STAGING_CHECKS <<'EOF' || true
Analyze (go)
Analyze (javascript-typescript)
Analyze (python)
Canvas (Next.js)
Canvas tabs E2E
Detect changes
E2E API Smoke Test
Platform (Go)
Python Lint & Test
Scan diff for credential-shaped strings
Shellcheck (E2E scripts)
EOF
read -r -d '' MAIN_CHECKS <<'EOF' || true
Analyze (go)
Analyze (javascript-typescript)
Analyze (python)
Canvas (Next.js)
Canvas tabs E2E
Detect changes
E2E API Smoke Test
PR-built wheel + import smoke
Platform (Go)
Python Lint & Test
Scan diff for credential-shaped strings
Shellcheck (E2E scripts)
EOF
build_payload() {
local checks="$1"
local require_reviews="$2" # true / false
local checks_json
checks_json=$(printf '%s\n' "$checks" | jq -Rs '
split("\n")
| map(select(length > 0))
| map({context: ., app_id: -1})
')
jq -n \
--argjson checks "$checks_json" \
--argjson reviews "$require_reviews" \
'{
required_status_checks: {
strict: false,
checks: $checks
},
enforce_admins: false,
required_pull_request_reviews: (
if $reviews then
{ required_approving_review_count: 1, dismiss_stale_reviews: true }
else null end
),
restrictions: null,
allow_deletions: false,
allow_force_pushes: false,
block_creations: false,
required_conversation_resolution: true,
required_linear_history: false,
lock_branch: false,
allow_fork_syncing: true
}'
}
apply_branch() {
local branch="$1"
local checks="$2"
local require_reviews="$3"
local payload
payload=$(build_payload "$checks" "$require_reviews")
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "=== branch: $branch ==="
echo "$payload" | jq .
return
fi
echo "Applying branch protection on $branch..."
printf '%s' "$payload" | gh api -X PUT \
"repos/$REPO/branches/$branch/protection" \
--input -
echo "Applied: $branch"
}
if [[ -z "$ONLY_BRANCH" || "$ONLY_BRANCH" == "staging" ]]; then
apply_branch staging "$STAGING_CHECKS" true
fi
if [[ -z "$ONLY_BRANCH" || "$ONLY_BRANCH" == "main" ]]; then
apply_branch main "$MAIN_CHECKS" true
fi