From bb0e44bb9eb60d66a3b60093c03374a3a7ab1607 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:20:15 -0500 Subject: [PATCH 01/10] feat(just): Adding justfile entries for protecting repository --- .../integration-branch-protection.json | 31 +++ .github/rulesets/main-branch-protection.json | 34 ++++ justfile | 186 ++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 .github/rulesets/integration-branch-protection.json create mode 100644 .github/rulesets/main-branch-protection.json diff --git a/.github/rulesets/integration-branch-protection.json b/.github/rulesets/integration-branch-protection.json new file mode 100644 index 0000000..39cf277 --- /dev/null +++ b/.github/rulesets/integration-branch-protection.json @@ -0,0 +1,31 @@ +{ + "name": "integration-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/integration"], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + } + ], + "bypass_actors": [] +} diff --git a/.github/rulesets/main-branch-protection.json b/.github/rulesets/main-branch-protection.json new file mode 100644 index 0000000..1773f86 --- /dev/null +++ b/.github/rulesets/main-branch-protection.json @@ -0,0 +1,34 @@ +{ + "name": "main-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + }, + { + "type": "required_linear_history" + } + ], + "bypass_actors": [] +} diff --git a/justfile b/justfile index c971fc5..f602347 100644 --- a/justfile +++ b/justfile @@ -748,3 +748,189 @@ issues: @echo "" @echo "=== Rust Rewrite Issues ===" @gh search issues --owner "{{ github_org }}" --state open --json repository,title,labels --jq '.[] | select(.repository.name | endswith("-rs")) | "[\(.repository.name)] \(.title)"' 2>/dev/null | head -20 || echo "No issues found" + +# ============================================================================= +# Repository Protection +# ============================================================================= + +# Apply all branch protection rulesets to a repository +protect-repo repo: + #!/usr/bin/env bash + set -euo pipefail + + REPO="{{ repo }}" + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + RULESETS_DIR="$SCRIPT_DIR/.github/rulesets" + + echo "╔════════════════════════════════════════════════════════════════════╗" + echo "║ Protecting Repository: ${REPO}" + echo "╚════════════════════════════════════════════════════════════════════╝" + echo "" + + # Check if rulesets directory exists + if [[ ! -d "$RULESETS_DIR" ]]; then + echo "Error: Rulesets directory not found at $RULESETS_DIR" + exit 1 + fi + + # Ensure integration branch exists + echo "→ Ensuring 'integration' branch exists..." + if ! gh api "repos/$REPO/branches/integration" &>/dev/null; then + echo " Creating 'integration' branch from 'main'..." + DEFAULT_SHA=$(gh api "repos/$REPO/git/ref/heads/main" --jq '.object.sha') + gh api "repos/$REPO/git/refs" \ + -X POST \ + -f ref="refs/heads/integration" \ + -f sha="$DEFAULT_SHA" \ + && echo " ✓ Created 'integration' branch" \ + || echo " ⚠ Could not create 'integration' branch" + else + echo " ✓ 'integration' branch already exists" + fi + + # Apply each ruleset + echo "" + echo "→ Applying branch protection rulesets..." + for ruleset_file in "$RULESETS_DIR"/*.json; do + if [[ -f "$ruleset_file" ]]; then + RULESET_NAME=$(basename "$ruleset_file" .json) + echo " Applying: $RULESET_NAME" + just _apply-ruleset "$REPO" "$ruleset_file" + fi + done + + echo "" + echo "╔════════════════════════════════════════════════════════════════════╗" + echo "║ Repository Protection Complete!" + echo "╚════════════════════════════════════════════════════════════════════╝" + +# Apply a single ruleset to a repository (internal) +_apply-ruleset repo ruleset_file: + #!/usr/bin/env bash + set -euo pipefail + + REPO="{{ repo }}" + RULESET_FILE="{{ ruleset_file }}" + RULESET_NAME=$(jq -r '.name' "$RULESET_FILE") + + # Check if ruleset already exists + EXISTING_ID=$(gh api "repos/$REPO/rulesets" --jq ".[] | select(.name == \"$RULESET_NAME\") | .id" 2>/dev/null || echo "") + + if [[ -n "$EXISTING_ID" ]]; then + # Update existing ruleset + gh api "repos/$REPO/rulesets/$EXISTING_ID" \ + -X PUT \ + --input "$RULESET_FILE" \ + && echo " ✓ Updated ruleset: $RULESET_NAME" \ + || echo " ⚠ Failed to update ruleset: $RULESET_NAME" + else + # Create new ruleset + gh api "repos/$REPO/rulesets" \ + -X POST \ + --input "$RULESET_FILE" \ + && echo " ✓ Created ruleset: $RULESET_NAME" \ + || echo " ⚠ Failed to create ruleset: $RULESET_NAME" + fi + +# Protect only the main branch +protect-main repo: + #!/usr/bin/env bash + set -euo pipefail + + REPO="{{ repo }}" + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + RULESET_FILE="$SCRIPT_DIR/.github/rulesets/main-branch-protection.json" + + echo "→ Protecting 'main' branch for ${REPO}..." + + if [[ ! -f "$RULESET_FILE" ]]; then + echo "Error: Main branch ruleset not found" + exit 1 + fi + + just _apply-ruleset "$REPO" "$RULESET_FILE" + +# Protect only the integration branch +protect-integration repo: + #!/usr/bin/env bash + set -euo pipefail + + REPO="{{ repo }}" + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + RULESET_FILE="$SCRIPT_DIR/.github/rulesets/integration-branch-protection.json" + + echo "→ Protecting 'integration' branch for ${REPO}..." + + # Ensure integration branch exists first + if ! gh api "repos/$REPO/branches/integration" &>/dev/null; then + echo " Creating 'integration' branch from 'main'..." + DEFAULT_SHA=$(gh api "repos/$REPO/git/ref/heads/main" --jq '.object.sha') + gh api "repos/$REPO/git/refs" \ + -X POST \ + -f ref="refs/heads/integration" \ + -f sha="$DEFAULT_SHA" \ + && echo " ✓ Created 'integration' branch" \ + || { echo " ✗ Could not create 'integration' branch"; exit 1; } + fi + + if [[ ! -f "$RULESET_FILE" ]]; then + echo "Error: Integration branch ruleset not found" + exit 1 + fi + + just _apply-ruleset "$REPO" "$RULESET_FILE" + +# List all rulesets for a repository +list-rulesets repo: + #!/usr/bin/env bash + set -euo pipefail + + REPO="{{ repo }}" + + echo "Rulesets for ${REPO}:" + echo "" + gh api "repos/$REPO/rulesets" --jq '.[] | "• \(.name) (ID: \(.id)) - \(.enforcement)"' 2>/dev/null || echo "No rulesets found or insufficient permissions" + +# Remove all rulesets from a repository +unprotect-repo repo: + #!/usr/bin/env bash + set -euo pipefail + + REPO="{{ repo }}" + + echo "⚠ Removing all rulesets from ${REPO}..." + echo "" + + RULESET_IDS=$(gh api "repos/$REPO/rulesets" --jq '.[].id' 2>/dev/null || echo "") + + if [[ -z "$RULESET_IDS" ]]; then + echo "No rulesets found" + exit 0 + fi + + for id in $RULESET_IDS; do + NAME=$(gh api "repos/$REPO/rulesets/$id" --jq '.name' 2>/dev/null || echo "unknown") + gh api "repos/$REPO/rulesets/$id" -X DELETE \ + && echo "✓ Removed ruleset: $NAME (ID: $id)" \ + || echo "⚠ Failed to remove ruleset: $NAME (ID: $id)" + done + + echo "" + echo "✓ All rulesets removed" + +# Show ruleset details for a repository +show-ruleset repo name: + #!/usr/bin/env bash + set -euo pipefail + + REPO="{{ repo }}" + NAME="{{ name }}" + + RULESET_ID=$(gh api "repos/$REPO/rulesets" --jq ".[] | select(.name == \"$NAME\") | .id" 2>/dev/null || echo "") + + if [[ -z "$RULESET_ID" ]]; then + echo "Ruleset '$NAME' not found in $REPO" + exit 1 + fi + + gh api "repos/$REPO/rulesets/$RULESET_ID" | jq . From dd6e9d8e430cda4030998c038bdfcfa85244461f Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:21:37 -0500 Subject: [PATCH 02/10] docs(just): Documenting justfile entries for protecting repository --- .ai/docs/strategies/protect-github-repo.md | 248 +++++++++++++++++++++ justfile | 232 ++++++------------- 2 files changed, 319 insertions(+), 161 deletions(-) create mode 100644 .ai/docs/strategies/protect-github-repo.md diff --git a/.ai/docs/strategies/protect-github-repo.md b/.ai/docs/strategies/protect-github-repo.md new file mode 100644 index 0000000..be61cb1 --- /dev/null +++ b/.ai/docs/strategies/protect-github-repo.md @@ -0,0 +1,248 @@ +--- +id: 2769C84B-479C-4EE3-9970-0AF4A973C82E +title: "GitHub Repository Protection Strategy" +status: "✅ Active" +date: 2025-01-13 +author: aRustyDev +type: strategy +related: + - ../adr/frontmatter-standard.md +--- + +# GitHub Repository Protection Strategy + +## Overview + +This document defines the strategy for protecting GitHub repositories using branch rulesets. The goal is to enforce a consistent, secure workflow across all repositories that prevents accidental or unauthorized changes to critical branches. + +--- + +## Branch Model + +### Protected Branches + +| Branch | Purpose | Protection Level | +| ------------- | ---------------------------------------- | ---------------- | +| `main` | Production-ready code, release source | Maximum | +| `integration` | Pre-release staging, feature integration | High | + +### Workflow + +``` +feature/* ──────┐ +bugfix/* ──────┼──► PR ──► integration ──► PR ──► main +hotfix/* ──────┘ +``` + +1. **Feature Development**: Work on `feature/*`, `bugfix/*`, or `hotfix/*` branches +2. **Integration**: Merge to `integration` via Pull Request with review +3. **Release**: Merge `integration` to `main` via Pull Request with review + +--- + +## Protection Rules + +### Main Branch Protection + +The `main` branch receives the highest level of protection: + +| Rule | Setting | Rationale | +| --------------------- | ------------- | ------------------------------------------- | +| Direct pushes | ❌ Blocked | All changes must go through PR | +| Force pushes | ❌ Blocked | Preserve history integrity | +| Branch deletion | ❌ Blocked | Prevent accidental loss | +| Required PR reviews | ✅ 1 reviewer | Ensure code quality and knowledge sharing | +| Dismiss stale reviews | ✅ Enabled | Reviews must be current with latest changes | +| Resolve conversations | ✅ Required | All feedback must be addressed | +| Linear history | ✅ Required | Clean, traceable commit history | + +### Integration Branch Protection + +The `integration` branch serves as a staging area before production: + +| Rule | Setting | Rationale | +| --------------------- | ------------- | ------------------------------------------- | +| Direct pushes | ❌ Blocked | All changes must go through PR | +| Force pushes | ❌ Blocked | Preserve history integrity | +| Branch deletion | ❌ Blocked | Prevent accidental loss | +| Required PR reviews | ✅ 1 reviewer | Ensure code quality | +| Dismiss stale reviews | ✅ Enabled | Reviews must be current with latest changes | +| Resolve conversations | ✅ Required | All feedback must be addressed | + +--- + +## Implementation + +### Ruleset Files + +Rulesets are stored as JSON files in `.github/rulesets/`: + +``` +.github/rulesets/ +├── main-branch-protection.json +└── integration-branch-protection.json +``` + +### Justfile Recipes + +The following recipes are available for managing repository protection: + +| Recipe | Description | +| ---------------------------------- | -------------------------------------- | +| `just protect-repo ` | Apply all branch protection rulesets | +| `just apply-ruleset ` | Apply a single ruleset from JSON file | +| `just unprotect-repo ` | Remove all rulesets (use with caution) | +| `just list-rulesets ` | List all rulesets for a repository | + +### Usage Examples + +```bash +# Protect a repository (applies all rulesets) +just protect-repo aRustyDev/my-repo + +# Apply a single ruleset +just apply-ruleset aRustyDev/my-repo .github/rulesets/main-branch-protection.json + +# View current protection status +just list-rulesets aRustyDev/my-repo + +# Remove all protections +just unprotect-repo aRustyDev/my-repo +``` + +--- + +## GitHub Rulesets API + +### Why Rulesets Over Branch Protection Rules? + +GitHub Repository Rulesets (introduced 2023) offer advantages over legacy branch protection rules: + +| Feature | Legacy Protection | Rulesets | +| ------------------------ | ----------------- | ---------------- | +| Target multiple branches | ❌ One at a time | ✅ Pattern-based | +| Organization-wide rules | ❌ Repo-only | ✅ Org-level | +| Import/Export as JSON | ❌ API only | ✅ Native JSON | +| Bypass permissions | Limited | ✅ Fine-grained | +| Audit logging | Basic | ✅ Enhanced | + +### API Endpoints + +| Operation | Method | Endpoint | +| -------------- | ------ | ------------------------------------- | +| List rulesets | GET | `/repos/{owner}/{repo}/rulesets` | +| Create ruleset | POST | `/repos/{owner}/{repo}/rulesets` | +| Get ruleset | GET | `/repos/{owner}/{repo}/rulesets/{id}` | +| Update ruleset | PUT | `/repos/{owner}/{repo}/rulesets/{id}` | +| Delete ruleset | DELETE | `/repos/{owner}/{repo}/rulesets/{id}` | + +### Ruleset JSON Schema + +```json +{ + "name": "branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true + } + } + ], + "bypass_actors": [] +} +``` + +--- + +## Available Rule Types + +| Rule Type | Description | +| ------------------------- | ------------------------------------------------ | +| `deletion` | Prevent branch deletion | +| `non_fast_forward` | Prevent force pushes (history rewrite) | +| `pull_request` | Require pull requests with configurable reviews | +| `required_linear_history` | Require linear commit history (no merge commits) | +| `required_signatures` | Require signed commits | +| `required_status_checks` | Require CI/CD checks to pass | +| `update` | Prevent non-admin updates | + +--- + +## Prerequisites + +1. **GitHub CLI**: Authenticated with admin access to the target repository + + ```bash + gh auth status + ``` + +2. **jq**: For JSON processing + + ```bash + jq --version + ``` + +3. **Repository Admin Access**: Required to create/modify rulesets + +--- + +## Troubleshooting + +### Common Issues + +| Issue | Solution | +| ------------------------- | ----------------------------------------------- | +| "Resource not accessible" | Ensure you have admin access to the repository | +| "Ruleset already exists" | The recipe will update existing rulesets | +| "Branch doesn't exist" | `protect-integration` creates branch if missing | +| "Invalid ruleset format" | Validate JSON in `.github/rulesets/` files | + +### Verification + +After applying protection, verify with: + +```bash +# List applied rulesets +just list-rulesets owner/repo + +# Test protection (should fail) +git push origin main # Should be rejected + +# Verify in GitHub UI +# Settings → Rules → Rulesets +``` + +--- + +## Security Considerations + +1. **Bypass Actors**: By default, no bypass actors are configured. Add administrator exceptions only when necessary. + +2. **Enforcement Level**: Use `"enforcement": "active"` for production. Use `"enforcement": "evaluate"` for testing without blocking. + +3. **Review Requirements**: Minimum 1 reviewer is recommended. Increase for sensitive repositories. + +4. **Emergency Access**: Document procedures for legitimate emergency direct pushes (requires temporary ruleset modification). + +--- + +## References + +- [GitHub Rulesets Documentation](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets) +- [GitHub REST API - Rulesets](https://docs.github.com/en/rest/repos/rules) +- [Migrating from Branch Protection to Rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets#about-rulesets-branch-protection-rules-and-protected-tags) diff --git a/justfile b/justfile index f602347..70274c5 100644 --- a/justfile +++ b/justfile @@ -752,185 +752,95 @@ issues: # ============================================================================= # Repository Protection # ============================================================================= +# Recipes for protecting GitHub repository branches using rulesets. +# +# Overview: +# These recipes use GitHub's Repository Rulesets API to protect branches +# from direct pushes, force pushes, and deletion. They enforce a PR-based +# workflow for code changes. +# +# Available Recipes: +# protect-repo - Apply all branch protection rulesets +# apply-ruleset - Apply a single ruleset from JSON file +# unprotect-repo - Remove all rulesets from a repository +# list-rulesets - List all rulesets for a repository +# +# Ruleset Files: +# .github/rulesets/main-branch-protection.json +# .github/rulesets/integration-branch-protection.json +# +# Branch Protection Strategy: +# - 'main' branch: Protected from pushes, force pushes, deletion. +# Requires PRs (typically from 'integration'). +# - 'integration' branch: Protected from pushes, force pushes, deletion. +# Requires PRs for changes. +# +# Usage Examples: +# just protect-repo owner/repo +# just apply-ruleset owner/repo .github/rulesets/main-branch-protection.json +# just list-rulesets owner/repo +# just unprotect-repo owner/repo +# +# Prerequisites: +# - GitHub CLI (gh) authenticated with admin access to the repository +# - Repository must exist on GitHub +# - Target branches must already exist +# - jq installed for JSON processing +# ============================================================================= -# Apply all branch protection rulesets to a repository -protect-repo repo: - #!/usr/bin/env bash - set -euo pipefail - - REPO="{{ repo }}" - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - RULESETS_DIR="$SCRIPT_DIR/.github/rulesets" - - echo "╔════════════════════════════════════════════════════════════════════╗" - echo "║ Protecting Repository: ${REPO}" - echo "╚════════════════════════════════════════════════════════════════════╝" - echo "" - - # Check if rulesets directory exists - if [[ ! -d "$RULESETS_DIR" ]]; then - echo "Error: Rulesets directory not found at $RULESETS_DIR" - exit 1 - fi - - # Ensure integration branch exists - echo "→ Ensuring 'integration' branch exists..." - if ! gh api "repos/$REPO/branches/integration" &>/dev/null; then - echo " Creating 'integration' branch from 'main'..." - DEFAULT_SHA=$(gh api "repos/$REPO/git/ref/heads/main" --jq '.object.sha') - gh api "repos/$REPO/git/refs" \ - -X POST \ - -f ref="refs/heads/integration" \ - -f sha="$DEFAULT_SHA" \ - && echo " ✓ Created 'integration' branch" \ - || echo " ⚠ Could not create 'integration' branch" - else - echo " ✓ 'integration' branch already exists" - fi - - # Apply each ruleset - echo "" - echo "→ Applying branch protection rulesets..." - for ruleset_file in "$RULESETS_DIR"/*.json; do - if [[ -f "$ruleset_file" ]]; then - RULESET_NAME=$(basename "$ruleset_file" .json) - echo " Applying: $RULESET_NAME" - just _apply-ruleset "$REPO" "$ruleset_file" - fi - done - - echo "" - echo "╔════════════════════════════════════════════════════════════════════╗" - echo "║ Repository Protection Complete!" - echo "╚════════════════════════════════════════════════════════════════════╝" +# Rulesets directory path +rulesets_dir := justfile_directory() / ".github/rulesets" -# Apply a single ruleset to a repository (internal) -_apply-ruleset repo ruleset_file: +# Apply a single ruleset to a repository from a JSON file +# Creates a new ruleset or updates an existing one with the same name. +apply-ruleset repo ruleset_file: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - RULESET_FILE="{{ ruleset_file }}" - RULESET_NAME=$(jq -r '.name' "$RULESET_FILE") - - # Check if ruleset already exists - EXISTING_ID=$(gh api "repos/$REPO/rulesets" --jq ".[] | select(.name == \"$RULESET_NAME\") | .id" 2>/dev/null || echo "") - + RULESET_NAME=$(jq -r '.name' "{{ ruleset_file }}") + EXISTING_ID=$(gh api "repos/{{ repo }}/rulesets" --jq ".[] | select(.name == \"$RULESET_NAME\") | .id" 2>/dev/null || echo "") if [[ -n "$EXISTING_ID" ]]; then - # Update existing ruleset - gh api "repos/$REPO/rulesets/$EXISTING_ID" \ - -X PUT \ - --input "$RULESET_FILE" \ - && echo " ✓ Updated ruleset: $RULESET_NAME" \ - || echo " ⚠ Failed to update ruleset: $RULESET_NAME" + gh api "repos/{{ repo }}/rulesets/$EXISTING_ID" -X PUT --input "{{ ruleset_file }}" \ + && echo "✓ Updated ruleset: $RULESET_NAME" \ + || echo "⚠ Failed to update ruleset: $RULESET_NAME" else - # Create new ruleset - gh api "repos/$REPO/rulesets" \ - -X POST \ - --input "$RULESET_FILE" \ - && echo " ✓ Created ruleset: $RULESET_NAME" \ - || echo " ⚠ Failed to create ruleset: $RULESET_NAME" - fi - -# Protect only the main branch -protect-main repo: - #!/usr/bin/env bash - set -euo pipefail - - REPO="{{ repo }}" - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - RULESET_FILE="$SCRIPT_DIR/.github/rulesets/main-branch-protection.json" - - echo "→ Protecting 'main' branch for ${REPO}..." - - if [[ ! -f "$RULESET_FILE" ]]; then - echo "Error: Main branch ruleset not found" - exit 1 + gh api "repos/{{ repo }}/rulesets" -X POST --input "{{ ruleset_file }}" \ + && echo "✓ Created ruleset: $RULESET_NAME" \ + || echo "⚠ Failed to create ruleset: $RULESET_NAME" fi - just _apply-ruleset "$REPO" "$RULESET_FILE" - -# Protect only the integration branch -protect-integration repo: - #!/usr/bin/env bash - set -euo pipefail - - REPO="{{ repo }}" - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - RULESET_FILE="$SCRIPT_DIR/.github/rulesets/integration-branch-protection.json" - - echo "→ Protecting 'integration' branch for ${REPO}..." - - # Ensure integration branch exists first - if ! gh api "repos/$REPO/branches/integration" &>/dev/null; then - echo " Creating 'integration' branch from 'main'..." - DEFAULT_SHA=$(gh api "repos/$REPO/git/ref/heads/main" --jq '.object.sha') - gh api "repos/$REPO/git/refs" \ - -X POST \ - -f ref="refs/heads/integration" \ - -f sha="$DEFAULT_SHA" \ - && echo " ✓ Created 'integration' branch" \ - || { echo " ✗ Could not create 'integration' branch"; exit 1; } - fi - - if [[ ! -f "$RULESET_FILE" ]]; then - echo "Error: Integration branch ruleset not found" - exit 1 - fi - - just _apply-ruleset "$REPO" "$RULESET_FILE" - -# List all rulesets for a repository -list-rulesets repo: +# Apply all branch protection rulesets to a repository +protect-repo repo: (_check-rulesets-dir) #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - - echo "Rulesets for ${REPO}:" - echo "" - gh api "repos/$REPO/rulesets" --jq '.[] | "• \(.name) (ID: \(.id)) - \(.enforcement)"' 2>/dev/null || echo "No rulesets found or insufficient permissions" + echo "╔════════════════════════════════════════════════════════════════════╗" + echo "║ Protecting Repository: {{ repo }}" + echo "╚════════════════════════════════════════════════════════════════════╝" + echo "→ Applying branch protection rulesets..." + just apply-ruleset "{{ repo }}" "{{ rulesets_dir }}/main-branch-protection.json" + just apply-ruleset "{{ repo }}" "{{ rulesets_dir }}/integration-branch-protection.json" + echo "╔════════════════════════════════════════════════════════════════════╗" + echo "║ Repository Protection Complete!" + echo "╚════════════════════════════════════════════════════════════════════╝" # Remove all rulesets from a repository +# WARNING: This removes ALL branch protections - use with caution! unprotect-repo repo: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - - echo "⚠ Removing all rulesets from ${REPO}..." - echo "" - - RULESET_IDS=$(gh api "repos/$REPO/rulesets" --jq '.[].id' 2>/dev/null || echo "") - - if [[ -z "$RULESET_IDS" ]]; then - echo "No rulesets found" - exit 0 - fi - - for id in $RULESET_IDS; do - NAME=$(gh api "repos/$REPO/rulesets/$id" --jq '.name' 2>/dev/null || echo "unknown") - gh api "repos/$REPO/rulesets/$id" -X DELETE \ + echo "⚠ Removing all rulesets from {{ repo }}..." + for id in $(gh api "repos/{{ repo }}/rulesets" --jq '.[].id' 2>/dev/null); do + NAME=$(gh api "repos/{{ repo }}/rulesets/$id" --jq '.name' 2>/dev/null || echo "unknown") + gh api "repos/{{ repo }}/rulesets/$id" -X DELETE \ && echo "✓ Removed ruleset: $NAME (ID: $id)" \ || echo "⚠ Failed to remove ruleset: $NAME (ID: $id)" done - - echo "" echo "✓ All rulesets removed" -# Show ruleset details for a repository -show-ruleset repo name: - #!/usr/bin/env bash - set -euo pipefail - - REPO="{{ repo }}" - NAME="{{ name }}" - - RULESET_ID=$(gh api "repos/$REPO/rulesets" --jq ".[] | select(.name == \"$NAME\") | .id" 2>/dev/null || echo "") - - if [[ -z "$RULESET_ID" ]]; then - echo "Ruleset '$NAME' not found in $REPO" - exit 1 - fi +# List all rulesets for a repository +list-rulesets repo: + @gh api "repos/{{ repo }}/rulesets" --jq '.[] | "• \(.name) (ID: \(.id)) - \(.enforcement)"' 2>/dev/null || echo "No rulesets found or insufficient permissions" - gh api "repos/$REPO/rulesets/$RULESET_ID" | jq . +# Internal: Check that rulesets directory exists +[private] +_check-rulesets-dir: + {{ if path_exists(rulesets_dir) == "false" { error("Rulesets directory not found: " + rulesets_dir) } else { "" } }} From 25ec5b17842a80ed075f5709c7bbf2078a781781 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:08:08 -0500 Subject: [PATCH 03/10] refactor(just): Use built-in functions, add init recipe, and target_repo default - Add `init` recipe for interactive first-time setup - Add `set-target` recipe for non-interactive target_repo changes - Add `target_repo` global variable as default for all repo parameters - Replace shell `$(dirname "$0")` with `justfile_directory()` - Replace shell path checks with `path_exists()` - Add `_require-repo`, `_require-file`, `_require-dir` validation helpers - Remove redundant `echo ""` statements throughout - Remove unnecessary shell variable assignments (use just params directly) - Mark internal recipes with `[private]` attribute - Simplify heredocs to inline strings for just compatibility --- justfile | 592 +++++++++++++++++-------------------------------------- 1 file changed, 179 insertions(+), 413 deletions(-) diff --git a/justfile b/justfile index 70274c5..2756765 100644 --- a/justfile +++ b/justfile @@ -3,10 +3,14 @@ # ============================================================================= # Automates forking, templating, and onboarding of MCP servers # +# First-time setup: +# just init +# # Usage: # just fork-mcp modelcontextprotocol/servers # just list-forks -# just sync-labels +# just sync-labels +# just protect-repo # ============================================================================= # Configuration @@ -15,10 +19,58 @@ rust_template := "aRustyDev/tmpl-rust" project_number := env("MCP_PROJECT_NUMBER", "") project_url := "https://github.com/orgs/" + github_org + "/projects/" + project_number +# Target repository - set via 'just init' or override per-command +target_repo := "" + +# Paths +justfile_dir := justfile_directory() +labels_file := justfile_dir / ".github/labels.yml" +rulesets_dir := justfile_dir / ".github/rulesets" +bundles_dir := justfile_dir / "bundles" + # Default recipe - show help default: @just --list +# ============================================================================= +# Initialization +# ============================================================================= + +# Initialize justfile with target repository (interactive on first run) +init: + #!/usr/bin/env bash + set -euo pipefail + CURRENT_TARGET="{{ target_repo }}" + if [[ -n "$CURRENT_TARGET" ]]; then + echo "Already initialized with target_repo: $CURRENT_TARGET" + echo "To change, manually edit the justfile or run:" + echo " just set-target " + exit 0 + fi + echo "╔════════════════════════════════════════════════════════════════════╗" + echo "║ MCP Server Justfile Initialization" + echo "╚════════════════════════════════════════════════════════════════════╝" + read -rp "Enter target repository (owner/repo): " REPO + if [[ -z "$REPO" || ! "$REPO" =~ ^[^/]+/[^/]+$ ]]; then + echo "Error: Invalid repository format. Expected: owner/repo" + exit 1 + fi + if ! gh repo view "$REPO" &>/dev/null; then + echo "Warning: Repository '$REPO' not found or not accessible" + read -rp "Continue anyway? [y/N]: " CONFIRM + [[ "$CONFIRM" =~ ^[Yy]$ ]] || exit 1 + fi + sed -i '' "s|^target_repo := \".*\"|target_repo := \"$REPO\"|" "{{ justfile() }}" + echo "✓ Set target_repo to: $REPO" + echo "You can now run recipes without specifying the repo parameter." + +# Set target repository (non-interactive) +set-target repo: + #!/usr/bin/env bash + set -euo pipefail + sed -i '' "s|^target_repo := \".*\"|target_repo := \"{{ repo }}\"|" "{{ justfile() }}" + echo "✓ Set target_repo to: {{ repo }}" + # ============================================================================= # Main Workflows # ============================================================================= @@ -27,29 +79,21 @@ default: fork-mcp repo: #!/usr/bin/env bash set -euo pipefail - REPO="{{ repo }}" OWNER="${REPO%/*}" NAME="${REPO#*/}" FORK_NAME="${NAME}" RUST_NAME="${NAME}-rs" - echo "╔════════════════════════════════════════════════════════════════════╗" - echo "║ MCP Server Onboarding: ${REPO}" + echo "║ MCP Server Onboarding: {{ repo }}" echo "╚════════════════════════════════════════════════════════════════════╝" - echo "" - - # Step 1: Fork the original repository - echo "→ Step 1: Forking ${REPO}..." + echo "→ Step 1: Forking {{ repo }}..." if gh repo view "{{ github_org }}/${FORK_NAME}" &>/dev/null; then echo " ⚠ Fork already exists: {{ github_org }}/${FORK_NAME}" else - gh repo fork "${REPO}" --org "{{ github_org }}" --fork-name "${FORK_NAME}" --clone=false + gh repo fork "{{ repo }}" --org "{{ github_org }}" --fork-name "${FORK_NAME}" --clone=false echo " ✓ Forked to {{ github_org }}/${FORK_NAME}" fi - - # Step 2: Create Rust rewrite repo from template - echo "" echo "→ Step 2: Creating Rust rewrite repo from template..." if gh repo view "{{ github_org }}/${RUST_NAME}" &>/dev/null; then echo " ⚠ Rust repo already exists: {{ github_org }}/${RUST_NAME}" @@ -60,67 +104,37 @@ fork-mcp repo: --description "Rust implementation of ${NAME} MCP server" echo " ✓ Created {{ github_org }}/${RUST_NAME}" fi - - # Wait for repos to be ready - echo "" echo "→ Waiting for repositories to initialize..." sleep 3 - - # Step 3: Sync labels to both repos - echo "" echo "→ Step 3: Syncing labels..." just sync-labels "{{ github_org }}/${FORK_NAME}" || echo " ⚠ Label sync failed for fork" just sync-labels "{{ github_org }}/${RUST_NAME}" || echo " ⚠ Label sync failed for rust repo" - - # Step 4: Create project association issues - echo "" echo "→ Step 4: Creating project association issues..." - just _create-project-issue "{{ github_org }}/${FORK_NAME}" "${REPO}" "fork" - just _create-project-issue "{{ github_org }}/${RUST_NAME}" "${REPO}" "rust-rewrite" - - # Step 5: Create onboarding issues - echo "" + just _create-project-issue "{{ github_org }}/${FORK_NAME}" "{{ repo }}" "fork" + just _create-project-issue "{{ github_org }}/${RUST_NAME}" "{{ repo }}" "rust-rewrite" echo "→ Step 5: Creating onboarding issues..." just _create-onboarding-issues "{{ github_org }}/${FORK_NAME}" "fork" just _create-onboarding-issues "{{ github_org }}/${RUST_NAME}" "rust" - - # Step 6: Auto-complete tasks where possible - echo "" echo "→ Step 6: Auto-completing setup tasks..." just _auto-setup "{{ github_org }}/${FORK_NAME}" "fork" just _auto-setup "{{ github_org }}/${RUST_NAME}" "rust" - - # Step 7: Deploy templates - echo "" echo "→ Step 7: Deploying templates..." just _deploy-templates "{{ github_org }}/${FORK_NAME}" "fork" just _deploy-templates "{{ github_org }}/${RUST_NAME}" "rust" - - # Step 8: Create milestones - echo "" echo "→ Step 8: Creating milestones..." just _create-milestones "{{ github_org }}/${FORK_NAME}" "fork" just _create-milestones "{{ github_org }}/${RUST_NAME}" "rust" - - # Step 9: Link to project - echo "" echo "→ Step 9: Linking to project..." just _link-to-project "{{ github_org }}/${FORK_NAME}" just _link-to-project "{{ github_org }}/${RUST_NAME}" - - # Step 10: Populate initial data - echo "" echo "→ Step 10: Populating initial data..." - just _populate-initial-data "{{ github_org }}/${FORK_NAME}" "${REPO}" - - # Summary - echo "" + just _populate-initial-data "{{ github_org }}/${FORK_NAME}" "{{ repo }}" echo "╔════════════════════════════════════════════════════════════════════╗" echo "║ Onboarding Complete!" echo "╠════════════════════════════════════════════════════════════════════╣" echo "║ Fork: https://github.com/{{ github_org }}/${FORK_NAME}" echo "║ Rust: https://github.com/{{ github_org }}/${RUST_NAME}" - echo "║ Project: https://github.com/users/{{ github_org }}/projects/{{ project_number }}" + echo "║ Project: {{ project_url }}" echo "╚════════════════════════════════════════════════════════════════════╝" # ============================================================================= @@ -128,36 +142,22 @@ fork-mcp repo: # ============================================================================= # Sync labels from .github/labels.yml to a repository -sync-labels repo: +sync-labels repo=target_repo: (_require-repo repo) (_require-file labels_file) #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - LABELS_FILE="$(dirname "$0")/.github/labels.yml" - - if [[ ! -f "$LABELS_FILE" ]]; then - echo "Error: labels.yml not found at $LABELS_FILE" - exit 1 - fi - - echo " Syncing labels to ${REPO}..." - - # Parse YAML and create labels - # Using yq if available, otherwise basic parsing + echo "Syncing labels to {{ repo }}..." if command -v yq &>/dev/null; then - yq -r '.[] | "\(.name)|\(.color)|\(.description)"' "$LABELS_FILE" | while IFS='|' read -r name color desc; do - gh label create "$name" --repo "$REPO" --color "$color" --description "$desc" --force 2>/dev/null || true + yq -r '.[] | "\(.name)|\(.color)|\(.description)"' "{{ labels_file }}" | while IFS='|' read -r name color desc; do + gh label create "$name" --repo "{{ repo }}" --color "$color" --description "$desc" --force 2>/dev/null || true done else - # Fallback: basic grep/sed parsing - grep -E "^- name:" "$LABELS_FILE" | sed 's/- name: "//;s/"$//' | while read -r name; do - color=$(grep -A1 "name: \"$name\"" "$LABELS_FILE" | grep "color:" | sed 's/.*color: "//;s/"$//') - desc=$(grep -A2 "name: \"$name\"" "$LABELS_FILE" | grep "description:" | sed 's/.*description: "//;s/"$//') - gh label create "$name" --repo "$REPO" --color "$color" --description "$desc" --force 2>/dev/null || true + grep -E "^- name:" "{{ labels_file }}" | sed 's/- name: "//;s/"$//' | while read -r name; do + color=$(grep -A1 "name: \"$name\"" "{{ labels_file }}" | grep "color:" | sed 's/.*color: "//;s/"$//') + desc=$(grep -A2 "name: \"$name\"" "{{ labels_file }}" | grep "description:" | sed 's/.*description: "//;s/"$//') + gh label create "$name" --repo "{{ repo }}" --color "$color" --description "$desc" --force 2>/dev/null || true done fi - - echo " ✓ Labels synced" + echo "✓ Labels synced" # List all MCP-related forks list-forks: @@ -168,94 +168,41 @@ list-rust-repos: @gh repo list "{{ github_org }}" --json name,description --jq '.[] | select(.name | endswith("-rs")) | "• \(.name): \(.description // "No description")"' # ============================================================================= -# Issue Management +# Issue Management (Internal) # ============================================================================= -# Create project association issue (internal) +# Create project association issue +[private] _create-project-issue repo upstream type: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" UPSTREAM="{{ upstream }}" - TYPE="{{ type }}" - - if [[ "$TYPE" == "fork" ]]; then - TITLE="[Project] Fork of ${UPSTREAM}" + if [[ "{{ type }}" == "fork" ]]; then + TITLE="[Project] Fork of $UPSTREAM" LABELS="type/documentation,phase/discovery" - BODY=$(cat <<'ISSUE_BODY' - ## Project Association - - This repository is a fork of the upstream MCP server for tracking and contribution purposes. - - ### Upstream Repository - - **Source**: https://github.com/UPSTREAM_PLACEHOLDER - - **Type**: Fork (for contributions and customization) - - ### Related Repositories - - **Rust Rewrite**: Will be linked when created - - ### Project Tracking - - [ ] Add to MCP Server Tracking project - - [ ] Link related issues - - [ ] Document transport status - - [ ] Document Docker status - ISSUE_BODY - ) - BODY="${BODY//UPSTREAM_PLACEHOLDER/$UPSTREAM}" + BODY="## Project Association\n\nThis repository is a fork of the upstream MCP server for tracking and contribution purposes.\n\n### Upstream Repository\n- **Source**: https://github.com/$UPSTREAM\n- **Type**: Fork (for contributions and customization)\n\n### Related Repositories\n- **Rust Rewrite**: Will be linked when created\n\n### Project Tracking\n- [ ] Add to MCP Server Tracking project\n- [ ] Link related issues\n- [ ] Document transport status\n- [ ] Document Docker status" else - TITLE="[Project] Rust rewrite of ${UPSTREAM}" + TITLE="[Project] Rust rewrite of $UPSTREAM" LABELS="type/rewrite,phase/planning" - BODY=$(cat <<'ISSUE_BODY' - ## Project Association - - This repository is a Rust implementation of an MCP server. - - ### Original Server - - **Source**: https://github.com/UPSTREAM_PLACEHOLDER - - **Language**: (to be documented) - - ### Rewrite Status - - **Phase**: Planning - - **Transport Target**: Native Streamable HTTP - - ### Project Tracking - - [ ] Add to MCP Server Tracking project - - [ ] Link to fork repository - - [ ] Document tool parity status - - [ ] Create implementation plan - ISSUE_BODY - ) - BODY="${BODY//UPSTREAM_PLACEHOLDER/$UPSTREAM}" + BODY="## Project Association\n\nThis repository is a Rust implementation of an MCP server.\n\n### Original Server\n- **Source**: https://github.com/$UPSTREAM\n- **Language**: (to be documented)\n\n### Rewrite Status\n- **Phase**: Planning\n- **Transport Target**: Native Streamable HTTP\n\n### Project Tracking\n- [ ] Add to MCP Server Tracking project\n- [ ] Link to fork repository\n- [ ] Document tool parity status\n- [ ] Create implementation plan" fi - - # Create the issue - ISSUE_URL=$(gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" --label "$LABELS" 2>/dev/null || echo "") - + ISSUE_URL=$(gh issue create --repo "{{ repo }}" --title "$TITLE" --body "$(echo -e "$BODY")" --label "$LABELS" 2>/dev/null || echo "") if [[ -n "$ISSUE_URL" ]]; then echo " ✓ Created project issue: $ISSUE_URL" - - # Add to project if configured if [[ -n "{{ project_number }}" ]]; then - ISSUE_NUM="${ISSUE_URL##*/}" gh project item-add "{{ project_number }}" --owner "{{ github_org }}" --url "$ISSUE_URL" 2>/dev/null || true fi else echo " ⚠ Could not create project issue (may already exist)" fi -# Create onboarding issues (internal) +# Create onboarding issues +[private] _create-onboarding-issues repo type: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - TYPE="{{ type }}" - - # Common onboarding tasks declare -a ISSUES - - if [[ "$TYPE" == "fork" ]]; then + if [[ "{{ type }}" == "fork" ]]; then ISSUES=( "[Onboard] Configure branch protection|type/ci-cd,priority/high|Configure branch protection rules for main branch\n\n- [ ] Require PR reviews\n- [ ] Require status checks\n- [ ] Prevent force pushes" "[Onboard] Set up CI/CD workflows|type/ci-cd,priority/high|Set up GitHub Actions for the fork\n\n- [ ] Add test workflow\n- [ ] Add lint workflow\n- [ ] Add build workflow\n- [ ] Add release workflow (if applicable)" @@ -273,155 +220,57 @@ _create-onboarding-issues repo type: "[Onboard] Create Dockerfile|type/docker,priority/medium|Create optimized Dockerfile for the Rust server\n\n- [ ] Multi-stage build\n- [ ] Minimal base image\n- [ ] HADOLint compliant\n- [ ] Health check configured" ) fi - for issue_data in "${ISSUES[@]}"; do IFS='|' read -r title labels body <<< "$issue_data" - ISSUE_URL=$(gh issue create --repo "$REPO" --title "$title" --body "$(echo -e "$body")" --label "$labels" 2>/dev/null || echo "") + ISSUE_URL=$(gh issue create --repo "{{ repo }}" --title "$title" --body "$(echo -e "$body")" --label "$labels" 2>/dev/null || echo "") if [[ -n "$ISSUE_URL" ]]; then echo " ✓ Created: $title" fi done # ============================================================================= -# Auto-Setup Tasks +# Auto-Setup Tasks (Internal) # ============================================================================= -# Auto-complete setup tasks where possible (internal) +# Auto-complete setup tasks where possible +[private] _auto-setup repo type: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - TYPE="{{ type }}" - - echo " Running auto-setup for ${REPO}..." - - # Enable GitHub features - gh repo edit "$REPO" --enable-issues --enable-wiki=false 2>/dev/null || true - - # Set topics - if [[ "$TYPE" == "fork" ]]; then - gh repo edit "$REPO" --add-topic "mcp,mcp-server,model-context-protocol" 2>/dev/null || true + echo " Running auto-setup for {{ repo }}..." + gh repo edit "{{ repo }}" --enable-issues --enable-wiki=false 2>/dev/null || true + if [[ "{{ type }}" == "fork" ]]; then + gh repo edit "{{ repo }}" --add-topic "mcp,mcp-server,model-context-protocol" 2>/dev/null || true else - gh repo edit "$REPO" --add-topic "mcp,mcp-server,model-context-protocol,rust" 2>/dev/null || true + gh repo edit "{{ repo }}" --add-topic "mcp,mcp-server,model-context-protocol,rust" 2>/dev/null || true fi - - # Configure default branch protection (requires admin) - # Note: This may fail if user doesn't have admin rights - # gh api repos/{{ github_org }}/${REPO#*/}/branches/main/protection \ - # -X PUT \ - # -f required_status_checks='{"strict":true,"contexts":[]}' \ - # -f enforce_admins=false \ - # -f required_pull_request_reviews='{"required_approving_review_count":1}' \ - # 2>/dev/null || true - echo " ✓ Auto-setup complete" # ============================================================================= -# Template Management +# Template Management (Internal) # ============================================================================= -# Copy templates to a repository (CODEOWNERS, SECURITY, CONTRIBUTING, FUNDING) +# Copy templates to a repository +[private] _deploy-templates repo type: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - TYPE="{{ type }}" - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - - echo " Deploying templates to ${REPO}..." - - # Create temp directory for template files - TEMP_DIR=$(mktemp -d) - trap "rm -rf $TEMP_DIR" EXIT - - if [[ "$TYPE" == "rust" ]]; then - # For Rust repos, templates should already be included from tmpl-rust + if [[ "{{ type }}" == "rust" ]]; then echo " ℹ Rust repos use templates from tmpl-rust" - else - # For fork repos, push template files - # CODEOWNERS - cat > "$TEMP_DIR/CODEOWNERS" << 'CODEOWNERS_CONTENT' -# MCP Server Fork - Code Owners -# TEMPLATE: Replace @aRustyDev with your GitHub username - -* @aRustyDev -CODEOWNERS_CONTENT - - # SECURITY.md - cat > "$TEMP_DIR/SECURITY.md" << 'SECURITY_CONTENT' -# Security Policy - -## Reporting a Vulnerability - -Please report security vulnerabilities via [GitHub's private vulnerability reporting](../../security/advisories/new). - -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| main | :white_check_mark: | - -For detailed security guidelines, see the [main MCP repository](https://github.com/aRustyDev/mcp/blob/main/SECURITY.md). -SECURITY_CONTENT - - # CONTRIBUTING.md - cat > "$TEMP_DIR/CONTRIBUTING.md" << 'CONTRIBUTING_CONTENT' -# Contributing - -Thank you for your interest in contributing! - -## Quick Start - -1. Fork and clone the repository -2. Create a feature branch -3. Make your changes -4. Submit a pull request - -## Guidelines - -- Follow existing code style -- Write tests for new functionality -- Update documentation as needed - -For detailed contribution guidelines, see the [main MCP repository](https://github.com/aRustyDev/mcp/blob/main/CONTRIBUTING.md). -CONTRIBUTING_CONTENT - - # Push files via GitHub API - for file in CODEOWNERS SECURITY.md CONTRIBUTING.md; do - if [[ -f "$TEMP_DIR/$file" ]]; then - CONTENT=$(base64 < "$TEMP_DIR/$file") - DEST_PATH=".github/$file" - [[ "$file" != "CODEOWNERS" ]] && DEST_PATH="$file" - - gh api "repos/$REPO/contents/$DEST_PATH" \ - -X PUT \ - -f message="chore: add $file template" \ - -f content="$CONTENT" \ - 2>/dev/null || echo " ⚠ Could not create $file (may exist)" - fi - done fi - echo " ✓ Templates deployed" # ============================================================================= -# Milestone Management +# Milestone Management (Internal) # ============================================================================= # Create standard milestones for a repository +[private] _create-milestones repo type: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - TYPE="{{ type }}" - - echo " Creating milestones for ${REPO}..." - - if [[ "$TYPE" == "rust" ]]; then - # Rust-specific milestones + echo " Creating milestones for {{ repo }}..." + if [[ "{{ type }}" == "rust" ]]; then declare -a MILESTONES=( "v0.1.0 - Core Implementation|Basic MCP server with essential tools|open" "v0.2.0 - HTTP Transport|Native Streamable HTTP transport support|open" @@ -429,146 +278,78 @@ _create-milestones repo type: "v1.0.0 - Parity Release|Feature parity with original server|open" ) else - # Fork milestones declare -a MILESTONES=( "Analysis Complete|Upstream analysis and documentation done|open" "Transport Implementation|HTTP transport wrapper or native support|open" "Docker Image|Container build and publication|open" ) fi - for milestone_data in "${MILESTONES[@]}"; do IFS='|' read -r title desc state <<< "$milestone_data" - gh api "repos/$REPO/milestones" \ - -X POST \ - -f title="$title" \ - -f description="$desc" \ - -f state="$state" \ - 2>/dev/null || echo " ⚠ Milestone '$title' may exist" + gh api "repos/{{ repo }}/milestones" -X POST -f title="$title" -f description="$desc" -f state="$state" 2>/dev/null || echo " ⚠ Milestone '$title' may exist" done - echo " ✓ Milestones created" # ============================================================================= -# Project Linking +# Project Linking (Internal) # ============================================================================= # Link repository to the main MCP project +[private] _link-to-project repo: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - if [[ -z "{{ project_number }}" ]]; then echo " ⚠ MCP_PROJECT_NUMBER not set, skipping project link" - return 0 + exit 0 fi - - echo " Linking ${REPO} to project {{ project_number }}..." - - # Get project ID + echo " Linking {{ repo }} to project {{ project_number }}..." PROJECT_ID=$(gh api graphql -f query=' query($login: String!, $number: Int!) { user(login: $login) { - projectV2(number: $number) { - id - } + projectV2(number: $number) { id } } - }' -f login="{{ github_org }}" -F number="{{ project_number }}" \ - --jq '.data.user.projectV2.id' 2>/dev/null) - - # Get repository ID - REPO_ID=$(gh api "repos/$REPO" --jq '.node_id' 2>/dev/null) - + }' -f login="{{ github_org }}" -F number="{{ project_number }}" --jq '.data.user.projectV2.id' 2>/dev/null) + REPO_ID=$(gh api "repos/{{ repo }}" --jq '.node_id' 2>/dev/null) if [[ -n "$PROJECT_ID" && -n "$REPO_ID" ]]; then - # Link repository to project gh api graphql -f query=' mutation($projectId: ID!, $repositoryId: ID!) { linkProjectV2ToRepository(input: {projectId: $projectId, repositoryId: $repositoryId}) { repository { nameWithOwner } } - }' -f projectId="$PROJECT_ID" -f repositoryId="$REPO_ID" \ - 2>/dev/null && echo " ✓ Repository linked to project" || echo " ⚠ Could not link (may already be linked)" + }' -f projectId="$PROJECT_ID" -f repositoryId="$REPO_ID" 2>/dev/null \ + && echo " ✓ Repository linked to project" \ + || echo " ⚠ Could not link (may already be linked)" else echo " ⚠ Could not get project or repository ID" fi # ============================================================================= -# Initial Data Population +# Initial Data Population (Internal) # ============================================================================= # Populate initial project data for a server +[private] _populate-initial-data repo upstream: #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" UPSTREAM="{{ upstream }}" - - echo " Populating initial data for ${REPO}..." - - # Get upstream info - UPSTREAM_INFO=$(gh api "repos/$UPSTREAM" --jq '{ - description: .description, - language: .language, - stars: .stargazers_count, - topics: .topics - }' 2>/dev/null || echo "{}") - + echo " Populating initial data for {{ repo }}..." + UPSTREAM_INFO=$(gh api "repos/$UPSTREAM" --jq '{description: .description, language: .language, stars: .stargazers_count, topics: .topics}' 2>/dev/null || echo "{}") if [[ -n "$UPSTREAM_INFO" && "$UPSTREAM_INFO" != "{}" ]]; then - # Create discovery issue with populated data LANG=$(echo "$UPSTREAM_INFO" | jq -r '.language // "Unknown"') DESC=$(echo "$UPSTREAM_INFO" | jq -r '.description // "No description"') STARS=$(echo "$UPSTREAM_INFO" | jq -r '.stars // 0') TOPICS=$(echo "$UPSTREAM_INFO" | jq -r '.topics | join(", ") // "none"') - - BODY=$(cat </dev/null || echo "") - + BODY="## MCP Server Profile\n\n### Basic Information\n- **Original Repository**: https://github.com/$UPSTREAM\n- **Language**: $LANG\n- **Stars**: $STARS\n- **Topics**: $TOPICS\n- **Description**: $DESC\n\n### Transport Status\n- [ ] Analyze current transport (stdio/http)\n- [ ] Document HTTP wrapper status\n- [ ] Identify streamable HTTP potential\n\n### Docker Status\n- [ ] Check for official Docker image\n- [ ] Check Docker Hub for community images\n- [ ] Document containerization status\n\n### Tools & Capabilities\n- [ ] Document all MCP tools\n- [ ] List resources provided\n- [ ] Note any prompts\n\n### Notes\n_Add analysis notes here_" + ISSUE_URL=$(gh issue create --repo "{{ repo }}" --title "[Discovery] Server Profile: ${UPSTREAM##*/}" --body "$(echo -e "$BODY")" --label "type/research,phase/discovery,lang/${LANG,,}" 2>/dev/null || echo "") if [[ -n "$ISSUE_URL" ]]; then echo " ✓ Created discovery issue: $ISSUE_URL" - - # Add to project if configured if [[ -n "{{ project_number }}" ]]; then gh project item-add "{{ project_number }}" --owner "{{ github_org }}" --url "$ISSUE_URL" 2>/dev/null || true fi fi fi - echo " ✓ Initial data populated" # ============================================================================= @@ -576,67 +357,49 @@ EOF # ============================================================================= # Apply templates to a remote repository (downloads release or uses local bundles/) -apply-templates repo version="latest": +apply-templates repo=target_repo version="latest": (_require-repo repo) #!/usr/bin/env bash set -euo pipefail - - REPO="{{ repo }}" - VERSION="{{ version }}" - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" TEMP_DIR=$(mktemp -d) trap "rm -rf $TEMP_DIR" EXIT - echo "╔════════════════════════════════════════════════════════════════════╗" - echo "║ Applying MCP Templates to ${REPO}" + echo "║ Applying MCP Templates to {{ repo }}" echo "╚════════════════════════════════════════════════════════════════════╝" - echo "" - - # Try to download release, fallback to local bundles/ cd "$TEMP_DIR" - - if [[ "$VERSION" == "local" ]]; then + if [[ "{{ version }}" == "local" ]]; then echo "→ Using local bundles/ directory..." - BUNDLE_DIR="$SCRIPT_DIR/bundles" + BUNDLE_DIR="{{ bundles_dir }}" else - echo "→ Downloading template bundle (${VERSION})..." - if gh release download ${VERSION:+$VERSION} --repo "{{ github_org }}/mcp" --pattern 'mcp-templates-*.tar.gz' 2>/dev/null; then + echo "→ Downloading template bundle ({{ version }})..." + if gh release download {{ if version == "latest" { "" } else { version } }} --repo "{{ github_org }}/mcp" --pattern 'mcp-templates-*.tar.gz' 2>/dev/null; then tar -xzf mcp-templates-*.tar.gz BUNDLE_DIR=$(find . -maxdepth 1 -type d -name 'mcp-templates-*' | head -1) else echo " ⚠ No release found, using local bundles/..." - BUNDLE_DIR="$SCRIPT_DIR/bundles" + BUNDLE_DIR="{{ bundles_dir }}" fi fi - if [[ -z "$BUNDLE_DIR" || ! -d "$BUNDLE_DIR" ]]; then echo "Error: Could not find bundle directory" exit 1 fi - - # Use the bundles/justfile to apply cd "$BUNDLE_DIR" - just setup-remote "$REPO" + just setup-remote "{{ repo }}" # Apply templates from local bundles/ to a remote repository -apply-templates-local repo: +apply-templates-local repo=target_repo: (_require-repo repo) just apply-templates "{{ repo }}" "local" # Download template bundle to current directory download-templates version="latest": #!/usr/bin/env bash set -euo pipefail - - VERSION="{{ version }}" - echo "Downloading MCP template bundle..." - - if [[ "$VERSION" == "latest" ]]; then + if [[ "{{ version }}" == "latest" ]]; then gh release download --repo "{{ github_org }}/mcp" --pattern 'mcp-templates-*.tar.gz' else - gh release download "$VERSION" --repo "{{ github_org }}/mcp" --pattern 'mcp-templates-*.tar.gz' + gh release download "{{ version }}" --repo "{{ github_org }}/mcp" --pattern 'mcp-templates-*.tar.gz' fi - - echo "" echo "✓ Downloaded. To apply:" echo " tar -xzf mcp-templates-*.tar.gz" echo " cd mcp-templates-*" @@ -651,27 +414,14 @@ list-bundle-versions: build-bundle: #!/usr/bin/env bash set -euo pipefail - - SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" VERSION="local-$(date +%Y%m%d-%H%M%S)" - echo "Building bundle from bundles/..." - - # Create versioned copy - cp -r "$SCRIPT_DIR/bundles" "mcp-templates-$VERSION" - - # Add VERSION file - cat > "mcp-templates-$VERSION/VERSION" << EOF - Version: $VERSION - Built: $(date -u +"%Y-%m-%dT%H:%M:%SZ") - Source: local build - EOF - - # Create tarball + cp -r "{{ bundles_dir }}" "mcp-templates-$VERSION" + echo "Version: $VERSION" > "mcp-templates-$VERSION/VERSION" + echo "Built: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "mcp-templates-$VERSION/VERSION" + echo "Source: local build" >> "mcp-templates-$VERSION/VERSION" tar -czvf "mcp-templates-$VERSION.tar.gz" "mcp-templates-$VERSION/" rm -rf "mcp-templates-$VERSION" - - echo "" echo "✓ Created: mcp-templates-$VERSION.tar.gz" # ============================================================================= @@ -682,16 +432,12 @@ build-bundle: check-prereqs: #!/usr/bin/env bash echo "Checking prerequisites..." - - # Check gh CLI if ! command -v gh &>/dev/null; then echo "✗ GitHub CLI (gh) not installed" exit 1 else echo "✓ GitHub CLI installed: $(gh --version | head -1)" fi - - # Check gh auth if ! gh auth status &>/dev/null; then echo "✗ Not authenticated with GitHub CLI" echo " Run: gh auth login" @@ -699,23 +445,28 @@ check-prereqs: else echo "✓ GitHub CLI authenticated" fi - - # Check yq (optional) if command -v yq &>/dev/null; then echo "✓ yq installed (recommended for label sync)" else echo "⚠ yq not installed (label sync will use fallback parser)" fi - - # Check project number + if command -v jq &>/dev/null; then + echo "✓ jq installed" + else + echo "✗ jq not installed (required for many recipes)" + exit 1 + fi if [[ -z "{{ project_number }}" ]]; then echo "⚠ MCP_PROJECT_NUMBER not set (issues won't be added to project)" echo " Set with: export MCP_PROJECT_NUMBER=" else echo "✓ Project number configured: {{ project_number }}" fi - - echo "" + if [[ -z "{{ target_repo }}" ]]; then + echo "⚠ target_repo not set (run 'just init' to configure)" + else + echo "✓ Target repo configured: {{ target_repo }}" + fi echo "All critical prerequisites met!" # Show current configuration @@ -724,6 +475,8 @@ show-config: @echo "Rust Template: {{ rust_template }}" @echo "Project Number: {{ project_number }}" @echo "Project URL: {{ project_url }}" + @echo "Target Repository: {{ if target_repo == "" { "(not set - run 'just init')" } else { target_repo } }}" + @echo "Justfile Directory: {{ justfile_dir }}" # Clone a forked MCP server locally clone-fork name: @@ -734,18 +487,17 @@ clone-rust name: gh repo clone "{{ github_org }}/{{ name }}-rs" # Update fork from upstream -sync-fork repo: +sync-fork repo=target_repo: (_require-repo repo) gh repo sync "{{ repo }}" --force # Open repository in browser -browse repo: +browse repo=target_repo: (_require-repo repo) gh repo view "{{ repo }}" --web # View all open issues across MCP repos issues: @echo "=== Fork Issues ===" @gh search issues --owner "{{ github_org }}" --state open --json repository,title,labels --jq '.[] | select(.repository.name | test("-rs$") | not) | "[\(.repository.name)] \(.title)"' 2>/dev/null | head -20 || echo "No issues found" - @echo "" @echo "=== Rust Rewrite Issues ===" @gh search issues --owner "{{ github_org }}" --state open --json repository,title,labels --jq '.[] | select(.repository.name | endswith("-rs")) | "[\(.repository.name)] \(.title)"' 2>/dev/null | head -20 || echo "No issues found" @@ -760,10 +512,10 @@ issues: # workflow for code changes. # # Available Recipes: -# protect-repo - Apply all branch protection rulesets -# apply-ruleset - Apply a single ruleset from JSON file -# unprotect-repo - Remove all rulesets from a repository -# list-rulesets - List all rulesets for a repository +# protect-repo [repo] - Apply all branch protection rulesets +# apply-ruleset [repo] - Apply a single ruleset from JSON file +# unprotect-repo [repo] - Remove all rulesets from a repository +# list-rulesets [repo] - List all rulesets for a repository # # Ruleset Files: # .github/rulesets/main-branch-protection.json @@ -776,10 +528,10 @@ issues: # Requires PRs for changes. # # Usage Examples: -# just protect-repo owner/repo -# just apply-ruleset owner/repo .github/rulesets/main-branch-protection.json -# just list-rulesets owner/repo -# just unprotect-repo owner/repo +# just protect-repo +# just apply-ruleset .github/rulesets/main-branch-protection.json +# just list-rulesets +# just unprotect-repo # # Prerequisites: # - GitHub CLI (gh) authenticated with admin access to the repository @@ -788,14 +540,15 @@ issues: # - jq installed for JSON processing # ============================================================================= -# Rulesets directory path -rulesets_dir := justfile_directory() / ".github/rulesets" - # Apply a single ruleset to a repository from a JSON file -# Creates a new ruleset or updates an existing one with the same name. -apply-ruleset repo ruleset_file: +apply-ruleset repo=target_repo ruleset_file="": (_require-repo repo) #!/usr/bin/env bash set -euo pipefail + if [[ -z "{{ ruleset_file }}" ]]; then + echo "Error: ruleset_file is required" + echo "Usage: just apply-ruleset [repo] " + exit 1 + fi RULESET_NAME=$(jq -r '.name' "{{ ruleset_file }}") EXISTING_ID=$(gh api "repos/{{ repo }}/rulesets" --jq ".[] | select(.name == \"$RULESET_NAME\") | .id" 2>/dev/null || echo "") if [[ -n "$EXISTING_ID" ]]; then @@ -809,7 +562,7 @@ apply-ruleset repo ruleset_file: fi # Apply all branch protection rulesets to a repository -protect-repo repo: (_check-rulesets-dir) +protect-repo repo=target_repo: (_require-repo repo) (_require-dir rulesets_dir) #!/usr/bin/env bash set -euo pipefail echo "╔════════════════════════════════════════════════════════════════════╗" @@ -823,8 +576,7 @@ protect-repo repo: (_check-rulesets-dir) echo "╚════════════════════════════════════════════════════════════════════╝" # Remove all rulesets from a repository -# WARNING: This removes ALL branch protections - use with caution! -unprotect-repo repo: +unprotect-repo repo=target_repo: (_require-repo repo) #!/usr/bin/env bash set -euo pipefail echo "⚠ Removing all rulesets from {{ repo }}..." @@ -837,10 +589,24 @@ unprotect-repo repo: echo "✓ All rulesets removed" # List all rulesets for a repository -list-rulesets repo: +list-rulesets repo=target_repo: (_require-repo repo) @gh api "repos/{{ repo }}/rulesets" --jq '.[] | "• \(.name) (ID: \(.id)) - \(.enforcement)"' 2>/dev/null || echo "No rulesets found or insufficient permissions" -# Internal: Check that rulesets directory exists +# ============================================================================= +# Internal Validation Recipes +# ============================================================================= + +# Require a repo parameter to be set +[private] +_require-repo repo: + {{ if repo == "" { error("Repository not specified. Run 'just init' or provide repo parameter.") } else { "" } }} + +# Require a file to exist +[private] +_require-file file: + {{ if path_exists(file) == "false" { error("Required file not found: " + file) } else { "" } }} + +# Require a directory to exist [private] -_check-rulesets-dir: - {{ if path_exists(rulesets_dir) == "false" { error("Rulesets directory not found: " + rulesets_dir) } else { "" } }} +_require-dir dir: + {{ if path_exists(dir) == "false" { error("Required directory not found: " + dir) } else { "" } }} From f537747e007b123482a96552bf9188d1957b0a50 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:37:44 -0500 Subject: [PATCH 04/10] feat(templates): adding handlebar templates for CODEOWNERS, SECURITY, & CONTRIBUTING --- bundles/templates/CODEOWNER.hbs | 3 +++ bundles/templates/CONTRIBUTING.md.hbs | 18 ++++++++++++++++++ bundles/templates/SECURITY.md.hbs | 13 +++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 bundles/templates/CODEOWNER.hbs create mode 100644 bundles/templates/CONTRIBUTING.md.hbs create mode 100644 bundles/templates/SECURITY.md.hbs diff --git a/bundles/templates/CODEOWNER.hbs b/bundles/templates/CODEOWNER.hbs new file mode 100644 index 0000000..f2c450a --- /dev/null +++ b/bundles/templates/CODEOWNER.hbs @@ -0,0 +1,3 @@ +# MCP Server Fork - Code Owners + +* @{{username}} diff --git a/bundles/templates/CONTRIBUTING.md.hbs b/bundles/templates/CONTRIBUTING.md.hbs new file mode 100644 index 0000000..4bc6418 --- /dev/null +++ b/bundles/templates/CONTRIBUTING.md.hbs @@ -0,0 +1,18 @@ +# Contributing + +Thank you for your interest in contributing! + +## Quick Start + +1. Fork and clone the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request + +## Guidelines + +- Follow existing code style +- Write tests for new functionality +- Update documentation as needed + +For detailed contribution guidelines, see the [main MCP repository](https://github.com/aRustyDev/mcp/blob/main/CONTRIBUTING.md). diff --git a/bundles/templates/SECURITY.md.hbs b/bundles/templates/SECURITY.md.hbs new file mode 100644 index 0000000..5047a7d --- /dev/null +++ b/bundles/templates/SECURITY.md.hbs @@ -0,0 +1,13 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security vulnerabilities via [GitHub's private vulnerability reporting](../../security/advisories/new). + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| main | :white_check_mark: | + +For detailed security guidelines, see the [main MCP repository](https://github.com/aRustyDev/mcp/blob/main/SECURITY.md). From 05f741ab909e04214896b72ea2f733d374b651ab Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:48:09 -0500 Subject: [PATCH 05/10] docs(todo): adding todo items for ruleset extensions --- .github/.todo.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/.todo.md b/.github/.todo.md index 3205ab5..6a26b1a 100644 --- a/.github/.todo.md +++ b/.github/.todo.md @@ -5,3 +5,11 @@ - (maybe?) mcp-contribute-feature - (maybe?) mcp-contribute-fix - (maybe?) mcp-bug-report + +## Rulesets + +- enforce merge to integration branch only +- enforce allowed branch creation patterns 'pr/\*' + - Bypasses: + - dependabot: enforce allowed branch creation patterns 'dependency/\*' + - auto-merge bots: enforce allowed branch creation patterns 'dependency/\*' From 00f64704471537d4cb9e7a70fb608c04255aaa06 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:05:25 -0500 Subject: [PATCH 06/10] feat(rulesets): split branch protection for granular bypass control Split branch protection rulesets into two categories: - Core protection (no bypass): deletion, force-push, linear history - PR reviews (with bypass): review requirements for aRustyDev This enables the user aRustyDev to bypass the required_approving_review_count rule while still being subject to all core branch protections. New files: - .github/rulesets/main-pr-reviews.json - .github/rulesets/integration-pr-reviews.json Modified: - .github/rulesets/main-branch-protection.json (removed PR rules) - .github/rulesets/integration-branch-protection.json (removed PR rules) - .ai/docs/strategies/protect-github-repo.md (updated documentation) --- .ai/docs/strategies/protect-github-repo.md | 68 ++++++++++++++++--- .../integration-branch-protection.json | 11 --- .github/rulesets/integration-pr-reviews.json | 31 +++++++++ .github/rulesets/main-branch-protection.json | 11 --- .github/rulesets/main-pr-reviews.json | 31 +++++++++ 5 files changed, 122 insertions(+), 30 deletions(-) create mode 100644 .github/rulesets/integration-pr-reviews.json create mode 100644 .github/rulesets/main-pr-reviews.json diff --git a/.ai/docs/strategies/protect-github-repo.md b/.ai/docs/strategies/protect-github-repo.md index be61cb1..8ca7e85 100644 --- a/.ai/docs/strategies/protect-github-repo.md +++ b/.ai/docs/strategies/protect-github-repo.md @@ -75,14 +75,23 @@ The `integration` branch serves as a staging area before production: ### Ruleset Files -Rulesets are stored as JSON files in `.github/rulesets/`: +Rulesets are stored as JSON files in `.github/rulesets/`. The rulesets are split into two categories to enable granular bypass permissions: + +1. **Core Protection Rulesets** (no bypass): Enforce fundamental branch safety rules +2. **PR Review Rulesets** (with bypass): Enforce review requirements, allowing designated users to bypass when needed ``` .github/rulesets/ -├── main-branch-protection.json -└── integration-branch-protection.json +├── main-branch-protection.json # Core rules: deletion, force-push, linear history +├── main-pr-reviews.json # PR review requirements (bypassable) +├── integration-branch-protection.json # Core rules: deletion, force-push +└── integration-pr-reviews.json # PR review requirements (bypassable) ``` +#### Why Split Rulesets? + +GitHub Rulesets bypass actors work at the **ruleset level**, not at the individual rule level. To allow a user to bypass only the `required_approving_review_count` while still enforcing other rules (like preventing force pushes), the rules must be separated into different rulesets. + ### Justfile Recipes The following recipes are available for managing repository protection: @@ -138,9 +147,11 @@ GitHub Repository Rulesets (introduced 2023) offer advantages over legacy branch ### Ruleset JSON Schema +**Core Protection Ruleset** (no bypass): + ```json { - "name": "branch-protection", + "name": "main-branch-protection", "target": "branch", "enforcement": "active", "conditions": { @@ -152,21 +163,62 @@ GitHub Repository Rulesets (introduced 2023) offer advantages over legacy branch "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" }, + { "type": "required_linear_history" } + ], + "bypass_actors": [] +} +``` + +**PR Review Ruleset** (with bypass): + +```json +{ + "name": "main-pr-reviews", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ { "type": "pull_request", "parameters": { "required_approving_review_count": 1, "dismiss_stale_reviews_on_push": true, "require_code_owner_review": false, - "require_last_push_approval": false, - "required_review_thread_resolution": true + "require_last_push_approval": true, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["merge", "squash", "rebase"] } } ], - "bypass_actors": [] + "bypass_actors": [ + { + "actor_id": 36318507, + "actor_type": "User", + "bypass_mode": "pull_request" + } + ] } ``` +### Bypass Actor Configuration + +| Property | Value | Description | +| ------------- | --------------------------------------------------------------------------------- | -------------------------- | +| `actor_id` | User/Team ID | Numeric ID from GitHub API | +| `actor_type` | `User`, `Team`, `Integration`, `OrganizationAdmin`, `RepositoryRole`, `DeployKey` | Type of actor | +| `bypass_mode` | `always`, `pull_request`, `exempt` | When bypass applies | + +**Bypass Modes:** + +- `always`: Bypass all rules at all times +- `pull_request`: Only bypass rules on pull requests (recommended for review bypasses) +- `exempt`: Rules not run, no audit entry created + --- ## Available Rule Types @@ -231,7 +283,7 @@ git push origin main # Should be rejected ## Security Considerations -1. **Bypass Actors**: By default, no bypass actors are configured. Add administrator exceptions only when necessary. +1. **Bypass Actors**: Bypass actors are configured only on PR review rulesets, allowing designated users to merge without approval while still being subject to core branch protections (no force push, no deletion). The bypass uses `"bypass_mode": "pull_request"` to limit bypass capability to PR merges only. 2. **Enforcement Level**: Use `"enforcement": "active"` for production. Use `"enforcement": "evaluate"` for testing without blocking. diff --git a/.github/rulesets/integration-branch-protection.json b/.github/rulesets/integration-branch-protection.json index 39cf277..3c1f64b 100644 --- a/.github/rulesets/integration-branch-protection.json +++ b/.github/rulesets/integration-branch-protection.json @@ -14,17 +14,6 @@ }, { "type": "non_fast_forward" - }, - { - "type": "pull_request", - "parameters": { - "required_approving_review_count": 1, - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_review_thread_resolution": true, - "allowed_merge_methods": ["merge", "squash", "rebase"] - } } ], "bypass_actors": [] diff --git a/.github/rulesets/integration-pr-reviews.json b/.github/rulesets/integration-pr-reviews.json new file mode 100644 index 0000000..39ead66 --- /dev/null +++ b/.github/rulesets/integration-pr-reviews.json @@ -0,0 +1,31 @@ +{ + "name": "integration-pr-reviews", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/integration"], + "exclude": [] + } + }, + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + } + ], + "bypass_actors": [ + { + "actor_id": 36318507, + "actor_type": "User", + "bypass_mode": "pull_request" + } + ] +} diff --git a/.github/rulesets/main-branch-protection.json b/.github/rulesets/main-branch-protection.json index 1773f86..98fc1ae 100644 --- a/.github/rulesets/main-branch-protection.json +++ b/.github/rulesets/main-branch-protection.json @@ -15,17 +15,6 @@ { "type": "non_fast_forward" }, - { - "type": "pull_request", - "parameters": { - "required_approving_review_count": 1, - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": false, - "require_last_push_approval": false, - "required_review_thread_resolution": true, - "allowed_merge_methods": ["merge", "squash", "rebase"] - } - }, { "type": "required_linear_history" } diff --git a/.github/rulesets/main-pr-reviews.json b/.github/rulesets/main-pr-reviews.json new file mode 100644 index 0000000..9f91865 --- /dev/null +++ b/.github/rulesets/main-pr-reviews.json @@ -0,0 +1,31 @@ +{ + "name": "main-pr-reviews", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": true, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + } + ], + "bypass_actors": [ + { + "actor_id": 36318507, + "actor_type": "User", + "bypass_mode": "pull_request" + } + ] +} From 219b00229b44920209f99325b215e7bb19110e44 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:13:30 -0500 Subject: [PATCH 07/10] fix(rulesets): use RepositoryRole instead of User for bypass actors GitHub repository-level rulesets do not support "User" as an actor_type. Valid types are: Integration, OrganizationAdmin, RepositoryRole, Team, DeployKey. Changed bypass_actors to use: - actor_type: "RepositoryRole" - actor_id: 5 (Repository Admin/Owner role) This allows repository admins to bypass PR review requirements while still being subject to core branch protections. --- .ai/docs/strategies/protect-github-repo.md | 23 ++++++++++++++------ .github/rulesets/integration-pr-reviews.json | 4 ++-- .github/rulesets/main-pr-reviews.json | 4 ++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.ai/docs/strategies/protect-github-repo.md b/.ai/docs/strategies/protect-github-repo.md index 8ca7e85..e77dda7 100644 --- a/.ai/docs/strategies/protect-github-repo.md +++ b/.ai/docs/strategies/protect-github-repo.md @@ -197,8 +197,8 @@ GitHub Repository Rulesets (introduced 2023) offer advantages over legacy branch ], "bypass_actors": [ { - "actor_id": 36318507, - "actor_type": "User", + "actor_id": 5, + "actor_type": "RepositoryRole", "bypass_mode": "pull_request" } ] @@ -207,11 +207,20 @@ GitHub Repository Rulesets (introduced 2023) offer advantages over legacy branch ### Bypass Actor Configuration -| Property | Value | Description | -| ------------- | --------------------------------------------------------------------------------- | -------------------------- | -| `actor_id` | User/Team ID | Numeric ID from GitHub API | -| `actor_type` | `User`, `Team`, `Integration`, `OrganizationAdmin`, `RepositoryRole`, `DeployKey` | Type of actor | -| `bypass_mode` | `always`, `pull_request`, `exempt` | When bypass applies | +| Property | Value | Description | +| ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `actor_id` | Role/Team/Integration ID | Numeric ID (see below for role IDs) | +| `actor_type` | `Integration`, `OrganizationAdmin`, `RepositoryRole`, `Team`, `DeployKey` | Type of actor (Note: `User` is NOT valid for repository rulesets) | +| `bypass_mode` | `always`, `pull_request`, `exempt` | When bypass applies | + +**Repository Role IDs:** + +| ID | Role | Description | +| --- | ---------------- | --------------------------------------------------- | +| 1 | Maintain | Can manage repo without access to sensitive actions | +| 2 | Write | Can push to non-protected branches | +| 3 | Admin | Full access to the repository | +| 5 | Repository Admin | Owner role for personal repositories | **Bypass Modes:** diff --git a/.github/rulesets/integration-pr-reviews.json b/.github/rulesets/integration-pr-reviews.json index 39ead66..a50463d 100644 --- a/.github/rulesets/integration-pr-reviews.json +++ b/.github/rulesets/integration-pr-reviews.json @@ -23,8 +23,8 @@ ], "bypass_actors": [ { - "actor_id": 36318507, - "actor_type": "User", + "actor_id": 5, + "actor_type": "RepositoryRole", "bypass_mode": "pull_request" } ] diff --git a/.github/rulesets/main-pr-reviews.json b/.github/rulesets/main-pr-reviews.json index 9f91865..555ed6d 100644 --- a/.github/rulesets/main-pr-reviews.json +++ b/.github/rulesets/main-pr-reviews.json @@ -23,8 +23,8 @@ ], "bypass_actors": [ { - "actor_id": 36318507, - "actor_type": "User", + "actor_id": 5, + "actor_type": "RepositoryRole", "bypass_mode": "pull_request" } ] From 0b0636c1d2c6e1d0ff780ba1482b4b3480e90dc8 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:16:42 -0500 Subject: [PATCH 08/10] fix(just): suppress pager output in apply-ruleset recipe Added --silent flag to gh api commands to prevent the JSON response from being piped through a pager (vim/less), allowing the recipe to run non-interactively. --- justfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/justfile b/justfile index 2756765..a063b5e 100644 --- a/justfile +++ b/justfile @@ -20,7 +20,7 @@ project_number := env("MCP_PROJECT_NUMBER", "") project_url := "https://github.com/orgs/" + github_org + "/projects/" + project_number # Target repository - set via 'just init' or override per-command -target_repo := "" +target_repo := "aRustyDev/mcp" # Paths justfile_dir := justfile_directory() @@ -552,11 +552,11 @@ apply-ruleset repo=target_repo ruleset_file="": (_require-repo repo) RULESET_NAME=$(jq -r '.name' "{{ ruleset_file }}") EXISTING_ID=$(gh api "repos/{{ repo }}/rulesets" --jq ".[] | select(.name == \"$RULESET_NAME\") | .id" 2>/dev/null || echo "") if [[ -n "$EXISTING_ID" ]]; then - gh api "repos/{{ repo }}/rulesets/$EXISTING_ID" -X PUT --input "{{ ruleset_file }}" \ + gh api "repos/{{ repo }}/rulesets/$EXISTING_ID" -X PUT --input "{{ ruleset_file }}" --silent \ && echo "✓ Updated ruleset: $RULESET_NAME" \ || echo "⚠ Failed to update ruleset: $RULESET_NAME" else - gh api "repos/{{ repo }}/rulesets" -X POST --input "{{ ruleset_file }}" \ + gh api "repos/{{ repo }}/rulesets" -X POST --input "{{ ruleset_file }}" --silent \ && echo "✓ Created ruleset: $RULESET_NAME" \ || echo "⚠ Failed to create ruleset: $RULESET_NAME" fi @@ -570,7 +570,9 @@ protect-repo repo=target_repo: (_require-repo repo) (_require-dir rulesets_dir) echo "╚════════════════════════════════════════════════════════════════════╝" echo "→ Applying branch protection rulesets..." just apply-ruleset "{{ repo }}" "{{ rulesets_dir }}/main-branch-protection.json" + just apply-ruleset "{{ repo }}" "{{ rulesets_dir }}/main-pr-reviews.json" just apply-ruleset "{{ repo }}" "{{ rulesets_dir }}/integration-branch-protection.json" + just apply-ruleset "{{ repo }}" "{{ rulesets_dir }}/integration-pr-reviews.json" echo "╔════════════════════════════════════════════════════════════════════╗" echo "║ Repository Protection Complete!" echo "╚════════════════════════════════════════════════════════════════════╝" From 40c9e748d2862cc1acb96b0a0800df8ae2a5ea27 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:17:16 -0500 Subject: [PATCH 09/10] chore(just): removing target_repo --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index a063b5e..633719c 100644 --- a/justfile +++ b/justfile @@ -20,7 +20,7 @@ project_number := env("MCP_PROJECT_NUMBER", "") project_url := "https://github.com/orgs/" + github_org + "/projects/" + project_number # Target repository - set via 'just init' or override per-command -target_repo := "aRustyDev/mcp" +target_repo := "" # Paths justfile_dir := justfile_directory() From b95c661442bdabec37eeb0f8531c0229bb936ff1 Mon Sep 17 00:00:00 2001 From: aRustyDev <36318507+aRustyDev@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:17:32 -0500 Subject: [PATCH 10/10] docs(todo): adding todo items for ruleset extensions --- .github/.todo.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/.todo.md b/.github/.todo.md index 6a26b1a..1fa51e7 100644 --- a/.github/.todo.md +++ b/.github/.todo.md @@ -13,3 +13,9 @@ - Bypasses: - dependabot: enforce allowed branch creation patterns 'dependency/\*' - auto-merge bots: enforce allowed branch creation patterns 'dependency/\*' +- [Prevent Unauthorized CI/CD Changes](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#1-prevent-unauthorized-cicd-changes) +- [Block Binary Files from Being Pushed into the Repository](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#3-block-binary-files-from-being-pushed-into-the-repository) +- [Restrict Commits to Authors with Company Email Addresses](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#4-restrict-commits-to-authors-with-company-email-addresses) +- [Enforce Branch Naming Patterns](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#5-enforce-branch-naming-patterns) +- [Protecting Production Branch](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#6-protecting-the-production-branch) +- [Lint Enforcement](https://ghsioux.github.io/2025/01/09/7-cool-things-with-rulesets#7-ensure-code-adheres-to-linting-standards-before-merging)