diff --git a/.ai/docs/strategies/protect-github-repo.md b/.ai/docs/strategies/protect-github-repo.md new file mode 100644 index 0000000..e77dda7 --- /dev/null +++ b/.ai/docs/strategies/protect-github-repo.md @@ -0,0 +1,309 @@ +--- +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/`. 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 # 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: + +| 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 + +**Core Protection Ruleset** (no bypass): + +```json +{ + "name": "main-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "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": true, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + } + ], + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ] +} +``` + +### Bypass Actor Configuration + +| 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:** + +- `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 + +| 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**: 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. + +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/.github/.todo.md b/.github/.todo.md index 3205ab5..1fa51e7 100644 --- a/.github/.todo.md +++ b/.github/.todo.md @@ -5,3 +5,17 @@ - (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/\*' +- [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) diff --git a/.github/rulesets/integration-branch-protection.json b/.github/rulesets/integration-branch-protection.json new file mode 100644 index 0000000..3c1f64b --- /dev/null +++ b/.github/rulesets/integration-branch-protection.json @@ -0,0 +1,20 @@ +{ + "name": "integration-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/integration"], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + } + ], + "bypass_actors": [] +} diff --git a/.github/rulesets/integration-pr-reviews.json b/.github/rulesets/integration-pr-reviews.json new file mode 100644 index 0000000..a50463d --- /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": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ] +} diff --git a/.github/rulesets/main-branch-protection.json b/.github/rulesets/main-branch-protection.json new file mode 100644 index 0000000..98fc1ae --- /dev/null +++ b/.github/rulesets/main-branch-protection.json @@ -0,0 +1,23 @@ +{ + "name": "main-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "required_linear_history" + } + ], + "bypass_actors": [] +} diff --git a/.github/rulesets/main-pr-reviews.json b/.github/rulesets/main-pr-reviews.json new file mode 100644 index 0000000..555ed6d --- /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": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ] +} 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). diff --git a/justfile b/justfile index c971fc5..633719c 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,17 +487,128 @@ 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" + +# ============================================================================= +# 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 [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 +# .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 +# 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 +# - Repository must exist on GitHub +# - Target branches must already exist +# - jq installed for JSON processing +# ============================================================================= + +# Apply a single ruleset to a repository from a JSON 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 + 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 }}" --silent \ + && echo "✓ Created ruleset: $RULESET_NAME" \ + || echo "⚠ Failed to create ruleset: $RULESET_NAME" + fi + +# Apply all branch protection rulesets to a repository +protect-repo repo=target_repo: (_require-repo repo) (_require-dir rulesets_dir) + #!/usr/bin/env bash + set -euo pipefail + 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 }}/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 "╚════════════════════════════════════════════════════════════════════╝" + +# Remove all rulesets from a repository +unprotect-repo repo=target_repo: (_require-repo repo) + #!/usr/bin/env bash + set -euo pipefail + 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 "✓ All rulesets removed" + +# List all rulesets for a repository +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 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] +_require-dir dir: + {{ if path_exists(dir) == "false" { error("Required directory not found: " + dir) } else { "" } }}