diff --git a/bin/disinto b/bin/disinto index 9c80add..e30240c 100755 --- a/bin/disinto +++ b/bin/disinto @@ -1986,6 +1986,14 @@ p.write_text(text) # Create labels on remote create_labels "$forge_repo" "$forge_url" + # Set up branch protection on project repo (#10) + # This enforces PR flow: no direct pushes, 1 approval required, dev-bot can merge after CI + if setup_project_branch_protection "$forge_repo" "$branch"; then + echo "Branch protection: project protection configured on ${forge_repo}" + else + echo "Warning: failed to set up project branch protection" >&2 + fi + # Generate VISION.md template generate_vision "$repo_root" "$project_name" diff --git a/lib/branch-protection.sh b/lib/branch-protection.sh index 52a9181..81a2be1 100644 --- a/lib/branch-protection.sh +++ b/lib/branch-protection.sh @@ -369,6 +369,115 @@ remove_branch_protection() { return 0 } +# ----------------------------------------------------------------------------- +# setup_project_branch_protection — Set up branch protection for project repos +# +# Configures the following protection rules: +# - Block direct pushes to main (all changes must go through PR) +# - Require 1 approval before merge +# - Allow merge only via dev-bot (for auto-merge after review+CI) +# - Allow review-bot to approve PRs +# +# Args: +# $1 - Repo path in format 'owner/repo' (e.g., 'johba/disinto') +# $2 - Branch to protect (default: main) +# +# Returns: 0 on success, 1 on failure +# ----------------------------------------------------------------------------- +setup_project_branch_protection() { + local repo="${1:-}" + local branch="${2:-main}" + + if [ -z "$repo" ]; then + _bp_log "ERROR: repo path required (format: owner/repo)" + return 1 + fi + + _bp_log "Setting up branch protection for ${branch} on ${repo}" + + local api_url + api_url="${FORGE_URL}/api/v1/repos/${repo}" + + # Check if branch exists + local branch_exists + branch_exists=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/git/branches/${branch}" 2>/dev/null || echo "0") + + if [ "$branch_exists" != "200" ]; then + _bp_log "ERROR: Branch ${branch} does not exist on ${repo}" + return 1 + fi + + # Check if protection already exists + local protection_exists + protection_exists=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") + + if [ "$protection_exists" = "200" ]; then + _bp_log "Branch protection already exists for ${branch}" + _bp_log "Updating existing protection rules" + fi + + # Create/update branch protection + # Forgejo API for branch protection (factory mode): + # - enable_push: false (block direct pushes) + # - enable_merge_whitelist: true (only whitelisted users can merge) + # - merge_whitelist_usernames: ["dev-bot"] (dev-bot merges after CI) + # - required_approvals: 1 (review-bot must approve) + local protection_json + protection_json=$(cat <&2 + exit 1 + fi + setup_project_branch_protection "${2}" "${3:-main}" + ;; verify) verify_branch_protection "${2:-main}" ;; @@ -408,11 +524,12 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then remove_branch_protection "${2:-main}" ;; help|*) - echo "Usage: $0 {setup|setup-profile|verify|remove} [args...]" + echo "Usage: $0 {setup|setup-profile|setup-project|verify|remove} [args...]" echo "" echo "Commands:" echo " setup [branch] Set up branch protection on ops repo (default: main)" echo " setup-profile [branch] Set up branch protection on .profile repo" + echo " setup-project [branch] Set up branch protection on project repo" echo " verify [branch] Verify branch protection is configured correctly" echo " remove [branch] Remove branch protection (for cleanup/testing)" echo ""