From 5deb6153c0cdd58cb71b1a8911200525ea231df3 Mon Sep 17 00:00:00 2001 From: drunkonjava Date: Thu, 31 Jul 2025 18:21:14 -0400 Subject: [PATCH] feat(scripts): Add hybrid workflow automation scripts Add two essential scripts for the hybrid workflow: 1. claude-branch.sh - Streamlined branch creation - Automatic branch prefix detection (feat/, fix/, docs/, etc.) - Base branch configuration (defaults to dev) - Optional draft PR creation with template - Clear next-steps guidance - Integration with commit size limits 2. apply-branch-protection.sh - GitHub branch protection setup - Different rules for main (strict) vs dev (relaxed) - Main: requires reviews, up-to-date branches, no force push - Dev: no reviews required, allows force push for rebasing - Dry-run mode for testing - Auto-detects repository from git remote These scripts reduce manual setup and ensure consistent workflow adoption. Part 3 of the hybrid workflow implementation. Co-authored-by: Claude --- scripts/apply-branch-protection.sh | 294 +++++++++++++++++++++++++++++ scripts/claude-branch.sh | 249 ++++++++++++++++++++++++ 2 files changed, 543 insertions(+) create mode 100755 scripts/apply-branch-protection.sh create mode 100755 scripts/claude-branch.sh diff --git a/scripts/apply-branch-protection.sh b/scripts/apply-branch-protection.sh new file mode 100755 index 00000000..0abc96a1 --- /dev/null +++ b/scripts/apply-branch-protection.sh @@ -0,0 +1,294 @@ +#!/bin/bash +# Apply Branch Protection Rules +# Configures GitHub branch protection for the hybrid workflow + +set -euo pipefail + +# Color codes for output +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +GITHUB_API="https://api.github.com" +REQUIRED_CHECKS=( + "build" + "test" + "claude-standards" +) + +# Function to display usage +usage() { + echo "Usage: $0 [options]" + echo "" + echo "Applies branch protection rules for the hybrid workflow." + echo "" + echo "Options:" + echo " --token TOKEN GitHub personal access token (or set GITHUB_TOKEN env var)" + echo " --owner OWNER Repository owner (default: detected from git remote)" + echo " --repo REPO Repository name (default: detected from git remote)" + echo " --dry-run Show what would be done without making changes" + echo " --help Show this help message" + echo "" + echo "Requirements:" + echo " - GitHub personal access token with repo permissions" + echo " - gh CLI tool installed (or provide --token)" + echo "" +} + +# Function to get repository info from git remote +get_repo_info() { + local remote_url=$(git remote get-url origin 2>/dev/null || echo "") + + if [[ -z "$remote_url" ]]; then + echo -e "${RED}❌ Error: No git remote found${NC}" + exit 1 + fi + + # Extract owner and repo from URL + # Works with both HTTPS and SSH URLs + if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+)(\.git)?$ ]]; then + OWNER="${BASH_REMATCH[1]}" + REPO="${BASH_REMATCH[2]}" + else + echo -e "${RED}❌ Error: Could not parse GitHub repository from remote URL${NC}" + exit 1 + fi +} + +# Function to get GitHub token +get_github_token() { + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + TOKEN="$GITHUB_TOKEN" + elif command -v gh &> /dev/null && gh auth status &>/dev/null; then + TOKEN=$(gh auth token) + else + echo -e "${RED}❌ Error: No GitHub token found${NC}" + echo "Please provide --token or set GITHUB_TOKEN environment variable" + exit 1 + fi +} + +# Function to apply protection to a branch +apply_branch_protection() { + local branch=$1 + local settings=$2 + + echo -e "${BLUE}🔒 Configuring protection for $branch branch...${NC}" + + if [[ "$DRY_RUN" == "true" ]]; then + echo -e "${YELLOW}[DRY RUN] Would apply these settings:${NC}" + echo "$settings" | jq '.' + return + fi + + # Apply protection rules + local response=$(curl -s -X PUT \ + -H "Authorization: token $TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Content-Type: application/json" \ + -d "$settings" \ + "$GITHUB_API/repos/$OWNER/$REPO/branches/$branch/protection") + + # Check if successful + if [[ $(echo "$response" | jq -r '.url // empty') ]]; then + echo -e "${GREEN}✅ Successfully protected $branch branch${NC}" + else + echo -e "${RED}❌ Failed to protect $branch branch${NC}" + echo "$response" | jq '.' + return 1 + fi +} + +# Function to create main branch protection settings +create_main_protection() { + cat < /dev/null; then + echo -e "${RED}❌ Error: jq is required but not installed${NC}" + echo "Install with: brew install jq" + exit 1 +fi + +if ! command -v curl &> /dev/null; then + echo -e "${RED}❌ Error: curl is required but not installed${NC}" + exit 1 +fi + +# Run main function +main "$@" \ No newline at end of file diff --git a/scripts/claude-branch.sh b/scripts/claude-branch.sh new file mode 100755 index 00000000..7db4d49a --- /dev/null +++ b/scripts/claude-branch.sh @@ -0,0 +1,249 @@ +#!/bin/bash +# Claude Branch Creation Script +# Creates feature branches and optionally draft PRs for the hybrid workflow + +set -euo pipefail + +# Color codes for output +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +DEFAULT_BASE_BRANCH="${CLAUDE_BASE_BRANCH:-dev}" +COMMIT_LIMIT="${CLAUDE_COMMIT_LIMIT:-30}" +LINE_LIMIT="${CLAUDE_LINE_LIMIT:-800}" + +# Function to display usage +usage() { + echo "Usage: $0 [options]" + echo "" + echo "Creates a new feature branch following Claude standards." + echo "" + echo "Arguments:" + echo " branch-name Name of the branch (without feat/ prefix)" + echo "" + echo "Options:" + echo " --base BRANCH Base branch (default: $DEFAULT_BASE_BRANCH)" + echo " --pr Create a draft PR immediately" + echo " --no-pr Don't create a PR (default)" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 add-search-feature" + echo " $0 fix-navigation-bug --base main --pr" + echo "" +} + +# Function to check if we're in a git repository +check_git_repo() { + if ! git rev-parse --git-dir >/dev/null 2>&1; then + echo -e "${RED}❌ Error: Not in a git repository${NC}" + exit 1 + fi +} + +# Function to check if branch already exists +check_branch_exists() { + local branch=$1 + if git show-ref --verify --quiet "refs/heads/$branch"; then + echo -e "${RED}❌ Error: Branch '$branch' already exists${NC}" + exit 1 + fi +} + +# Function to create branch +create_branch() { + local branch_name=$1 + local base_branch=$2 + + echo -e "${BLUE}📌 Creating branch from $base_branch...${NC}" + + # Fetch latest changes + git fetch origin "$base_branch" || { + echo -e "${RED}❌ Error: Failed to fetch $base_branch${NC}" + exit 1 + } + + # Create and checkout new branch + git checkout -b "$branch_name" "origin/$base_branch" || { + echo -e "${RED}❌ Error: Failed to create branch${NC}" + exit 1 + } + + echo -e "${GREEN}✅ Created and checked out branch: $branch_name${NC}" +} + +# Function to create draft PR +create_draft_pr() { + local branch_name=$1 + local base_branch=$2 + + echo -e "${BLUE}📝 Creating draft PR...${NC}" + + # Push the branch first + git push -u origin "$branch_name" || { + echo -e "${RED}❌ Error: Failed to push branch${NC}" + exit 1 + } + + # Create PR title based on branch name + local pr_title + if [[ $branch_name == feat/* ]]; then + pr_title="feat: ${branch_name#feat/}" + elif [[ $branch_name == fix/* ]]; then + pr_title="fix: ${branch_name#fix/}" + elif [[ $branch_name == docs/* ]]; then + pr_title="docs: ${branch_name#docs/}" + else + pr_title="$branch_name" + fi + + # Replace hyphens with spaces in title + pr_title="${pr_title//-/ }" + + # Create PR body + local pr_body="## 🚧 Work in Progress + +This is a draft PR for the \`$branch_name\` feature. + +### Description + + +### Changes + + +### Testing + + +### Checklist +- [ ] Tests pass +- [ ] Documentation updated +- [ ] Follows commit standards (max $COMMIT_LIMIT files, $LINE_LIMIT lines per commit) +- [ ] Ready for review + +--- +*Created by claude-branch.sh*" + + # Create the PR using gh CLI + if command -v gh &> /dev/null; then + gh pr create \ + --draft \ + --base "$base_branch" \ + --head "$branch_name" \ + --title "$pr_title" \ + --body "$pr_body" || { + echo -e "${YELLOW}⚠️ Failed to create PR. You can create it manually later.${NC}" + return 1 + } + echo -e "${GREEN}✅ Created draft PR${NC}" + else + echo -e "${YELLOW}⚠️ GitHub CLI (gh) not found. Install it to auto-create PRs.${NC}" + echo -e "${BLUE}Push complete. Create PR manually at:${NC}" + echo "https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')/pull/new/$branch_name" + fi +} + +# Function to show branch info +show_branch_info() { + local branch_name=$1 + local base_branch=$2 + + echo -e "\n${GREEN}✨ Branch created successfully!${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "Branch: ${YELLOW}$branch_name${NC}" + echo -e "Base: ${YELLOW}$base_branch${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e "${GREEN}📋 Next steps:${NC}" + echo "1. Make your changes" + echo "2. Commit with: git commit -m \"feat: your feature description\"" + echo "3. Push with: git push" + if [[ "$CREATE_PR" != "true" ]]; then + echo "4. Create PR when ready: gh pr create --draft" + fi + echo "" + echo -e "${YELLOW}⚡ Remember:${NC}" + echo "- Keep commits under $COMMIT_LIMIT files and $LINE_LIMIT lines" + echo "- Use conventional commit format" + echo "- Target $base_branch for your PR" + echo "" +} + +# Main script +main() { + # Check if we're in a git repo + check_git_repo + + # Parse arguments + if [[ $# -eq 0 ]] || [[ "$1" == "--help" ]]; then + usage + exit 0 + fi + + # Get branch name + BRANCH_INPUT=$1 + shift + + # Parse options + BASE_BRANCH=$DEFAULT_BASE_BRANCH + CREATE_PR=false + + while [[ $# -gt 0 ]]; do + case $1 in + --base) + BASE_BRANCH="$2" + shift 2 + ;; + --pr) + CREATE_PR=true + shift + ;; + --no-pr) + CREATE_PR=false + shift + ;; + *) + echo -e "${RED}❌ Unknown option: $1${NC}" + usage + exit 1 + ;; + esac + done + + # Determine branch prefix based on input + if [[ $BRANCH_INPUT == fix-* ]] || [[ $BRANCH_INPUT == *-fix ]]; then + BRANCH_NAME="fix/${BRANCH_INPUT#fix-}" + elif [[ $BRANCH_INPUT == docs-* ]] || [[ $BRANCH_INPUT == *-docs ]]; then + BRANCH_NAME="docs/${BRANCH_INPUT#docs-}" + elif [[ $BRANCH_INPUT == test-* ]] || [[ $BRANCH_INPUT == *-test ]]; then + BRANCH_NAME="test/${BRANCH_INPUT#test-}" + elif [[ $BRANCH_INPUT == chore-* ]] || [[ $BRANCH_INPUT == *-chore ]]; then + BRANCH_NAME="chore/${BRANCH_INPUT#chore-}" + else + # Default to feat/ prefix + BRANCH_NAME="feat/$BRANCH_INPUT" + fi + + # Remove any double slashes + BRANCH_NAME="${BRANCH_NAME//\/\//\/}" + + # Check if branch already exists + check_branch_exists "$BRANCH_NAME" + + # Create the branch + create_branch "$BRANCH_NAME" "$BASE_BRANCH" + + # Create PR if requested + if [[ "$CREATE_PR" == "true" ]]; then + create_draft_pr "$BRANCH_NAME" "$BASE_BRANCH" + fi + + # Show summary + show_branch_info "$BRANCH_NAME" "$BASE_BRANCH" +} + +# Run main function +main "$@" \ No newline at end of file