Initial commit
This commit is contained in:
50
skills/git.createpr/README.md
Normal file
50
skills/git.createpr/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# git.createpr
|
||||
|
||||
Create GitHub pull requests with auto-generated titles and descriptions based on commit analysis. Analyzes commit history, identifies related issues, and creates well-formatted PRs with proper linking and metadata. - git-commits - Commit history between base and feature branch - git-repository - Local git repository with commit information - github-credentials - GitHub token for API access (from gh CLI or environment) - pull-request - Created GitHub pull request with metadata - pr-report - Summary of PR creation including URL, number, and status - base_branch (string): Base branch for PR (default: main) - title (string): PR title (optional, auto-generated from commits if not provided) - draft (boolean): Create as draft PR (default: false) - auto_merge (boolean): Enable auto-merge if checks pass (default: false) - reviewers (array): List of GitHub usernames to request reviews from - labels (array): Labels to apply to PR (optional, auto-detected from commits) - body (string): PR description (optional, auto-generated if not provided) - git command line tool - GitHub CLI (gh) or GitHub API access with token - Access to git repository - GitHub repository with permissions to create PRs 1. Validate we're in a git repository 2. Get current branch name 3. Validate base branch exists 4. Fetch latest changes from remote 5. Get commit history between base and current branch 6. Analyze commits to extract: - Commit messages - Conventional commit types (feat, fix, docs, etc.) - Issue references (#123) - Breaking changes 7. Generate PR title (if not provided): - Use most recent commit message - Or summarize multiple commits - Format: "type(scope): description" 8. Generate PR description (if not provided): - Summary of changes - List of commits with links - Related issues section - Breaking changes warning (if any) 9. Detect labels from commit types: - feat → enhancement - fix → bug - docs → documentation - etc. 10. Create PR using GitHub CLI (gh pr create): - Set title and body - Set base and head branches - Apply labels - Request reviewers - Set draft status 11. Parse PR URL and number from output 12. Return structured result with PR metadata ```bash python3 skills/git.createpr/git_createpr.py python3 skills/git.createpr/git_createpr.py --draft python3 skills/git.createpr/git_createpr.py --reviewers alice bob python3 skills/git.createpr/git_createpr.py --base develop python3 skills/git.createpr/git_createpr.py --title "feat: add user authentication" python3 skills/git.createpr/git_createpr.py --labels enhancement breaking-change ``` ```json { "ok": true, "status": "success", "pr_number": 123, "pr_url": "https://github.com/owner/repo/pull/123", "title": "feat: add user authentication", "base_branch": "main", "head_branch": "feature/auth", "commits_analyzed": 5, "issues_linked": ["#45", "#67"], "labels_applied": ["enhancement", "feature"], "reviewers_requested": ["alice", "bob"], "is_draft": false } ``` - git - github - pull-request - automation - workflow - pr This skill requires SKILL_AND_COMMAND pattern due to: - 12 steps (exceeds threshold) - High autonomy (auto-generates PR content intelligently) - Highly reusable for release automation and CI/CD - Complex GitHub API interaction - Commit analysis and pattern detection - Multiple execution contexts (CLI, agents, workflows) This implementation uses GitHub CLI (gh) for simplicity and authentication: - Leverages existing gh authentication - Simpler than managing GitHub API tokens - Better error messages - Handles pagination automatically If gh CLI is not available, falls back to REST API with token from: - GITHUB_TOKEN environment variable - GH_TOKEN environment variable
|
||||
|
||||
## Overview
|
||||
|
||||
**Purpose:** Create GitHub pull requests with auto-generated titles and descriptions based on commit analysis. Analyzes commit history, identifies related issues, and creates well-formatted PRs with proper linking and metadata. - git-commits - Commit history between base and feature branch - git-repository - Local git repository with commit information - github-credentials - GitHub token for API access (from gh CLI or environment) - pull-request - Created GitHub pull request with metadata - pr-report - Summary of PR creation including URL, number, and status - base_branch (string): Base branch for PR (default: main) - title (string): PR title (optional, auto-generated from commits if not provided) - draft (boolean): Create as draft PR (default: false) - auto_merge (boolean): Enable auto-merge if checks pass (default: false) - reviewers (array): List of GitHub usernames to request reviews from - labels (array): Labels to apply to PR (optional, auto-detected from commits) - body (string): PR description (optional, auto-generated if not provided) - git command line tool - GitHub CLI (gh) or GitHub API access with token - Access to git repository - GitHub repository with permissions to create PRs 1. Validate we're in a git repository 2. Get current branch name 3. Validate base branch exists 4. Fetch latest changes from remote 5. Get commit history between base and current branch 6. Analyze commits to extract: - Commit messages - Conventional commit types (feat, fix, docs, etc.) - Issue references (#123) - Breaking changes 7. Generate PR title (if not provided): - Use most recent commit message - Or summarize multiple commits - Format: "type(scope): description" 8. Generate PR description (if not provided): - Summary of changes - List of commits with links - Related issues section - Breaking changes warning (if any) 9. Detect labels from commit types: - feat → enhancement - fix → bug - docs → documentation - etc. 10. Create PR using GitHub CLI (gh pr create): - Set title and body - Set base and head branches - Apply labels - Request reviewers - Set draft status 11. Parse PR URL and number from output 12. Return structured result with PR metadata ```bash python3 skills/git.createpr/git_createpr.py python3 skills/git.createpr/git_createpr.py --draft python3 skills/git.createpr/git_createpr.py --reviewers alice bob python3 skills/git.createpr/git_createpr.py --base develop python3 skills/git.createpr/git_createpr.py --title "feat: add user authentication" python3 skills/git.createpr/git_createpr.py --labels enhancement breaking-change ``` ```json { "ok": true, "status": "success", "pr_number": 123, "pr_url": "https://github.com/owner/repo/pull/123", "title": "feat: add user authentication", "base_branch": "main", "head_branch": "feature/auth", "commits_analyzed": 5, "issues_linked": ["#45", "#67"], "labels_applied": ["enhancement", "feature"], "reviewers_requested": ["alice", "bob"], "is_draft": false } ``` - git - github - pull-request - automation - workflow - pr This skill requires SKILL_AND_COMMAND pattern due to: - 12 steps (exceeds threshold) - High autonomy (auto-generates PR content intelligently) - Highly reusable for release automation and CI/CD - Complex GitHub API interaction - Commit analysis and pattern detection - Multiple execution contexts (CLI, agents, workflows) This implementation uses GitHub CLI (gh) for simplicity and authentication: - Leverages existing gh authentication - Simpler than managing GitHub API tokens - Better error messages - Handles pagination automatically If gh CLI is not available, falls back to REST API with token from: - GITHUB_TOKEN environment variable - GH_TOKEN environment variable
|
||||
|
||||
**Command:** `/git/createpr`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
python3 skills/git/createpr/git_createpr.py
|
||||
```
|
||||
|
||||
### With Arguments
|
||||
|
||||
```bash
|
||||
python3 skills/git/createpr/git_createpr.py \
|
||||
--output-format json
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
This skill can be used in agents by including it in `skills_available`:
|
||||
|
||||
```yaml
|
||||
name: my.agent
|
||||
skills_available:
|
||||
- git.createpr
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with:
|
||||
|
||||
```bash
|
||||
pytest skills/git/createpr/test_git_createpr.py -v
|
||||
```
|
||||
|
||||
## Created By
|
||||
|
||||
This skill was generated by **meta.skill**, the skill creator meta-agent.
|
||||
|
||||
---
|
||||
|
||||
*Part of the Betty Framework*
|
||||
1
skills/git.createpr/__init__.py
Normal file
1
skills/git.createpr/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
534
skills/git.createpr/git_createpr.py
Executable file
534
skills/git.createpr/git_createpr.py
Executable file
@@ -0,0 +1,534 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
git.createpr - Create GitHub pull requests with auto-generated content
|
||||
|
||||
Analyzes commit history, identifies related issues, and creates well-formatted PRs
|
||||
with proper linking and metadata.
|
||||
|
||||
Generated by meta.skill with Betty Framework certification
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
import subprocess
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
|
||||
from betty.config import BASE_DIR
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.certification import certified_skill
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
class GitCreatepr:
|
||||
"""
|
||||
Create GitHub pull requests with auto-generated titles and descriptions.
|
||||
"""
|
||||
|
||||
# Conventional commit types and their labels
|
||||
COMMIT_TYPE_LABELS = {
|
||||
"feat": "enhancement",
|
||||
"fix": "bug",
|
||||
"docs": "documentation",
|
||||
"style": "style",
|
||||
"refactor": "refactor",
|
||||
"test": "testing",
|
||||
"chore": "maintenance",
|
||||
"perf": "performance",
|
||||
"ci": "ci/cd",
|
||||
"build": "build"
|
||||
}
|
||||
|
||||
def __init__(self, base_dir: str = "."):
|
||||
"""Initialize skill"""
|
||||
self.base_dir = Path(base_dir)
|
||||
|
||||
def run_command(self, command: List[str]) -> Tuple[bool, str]:
|
||||
"""
|
||||
Run a command and return success status and output
|
||||
|
||||
Args:
|
||||
command: Command as list of arguments
|
||||
|
||||
Returns:
|
||||
Tuple of (success, output)
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=self.base_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return True, result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
return False, e.stderr.strip()
|
||||
|
||||
def is_git_repository(self) -> bool:
|
||||
"""Check if current directory is a git repository"""
|
||||
success, _ = self.run_command(["git", "rev-parse", "--is-inside-work-tree"])
|
||||
return success
|
||||
|
||||
def get_current_branch(self) -> Optional[str]:
|
||||
"""Get the current branch name"""
|
||||
success, output = self.run_command(["git", "branch", "--show-current"])
|
||||
return output if success else None
|
||||
|
||||
def branch_exists(self, branch: str) -> bool:
|
||||
"""Check if a branch exists"""
|
||||
success, _ = self.run_command(["git", "rev-parse", "--verify", branch])
|
||||
return success
|
||||
|
||||
def get_commits_between(self, base: str, head: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Get commits between two branches
|
||||
|
||||
Args:
|
||||
base: Base branch
|
||||
head: Head branch
|
||||
|
||||
Returns:
|
||||
List of commit dicts with hash, message, author
|
||||
"""
|
||||
success, output = self.run_command([
|
||||
"git", "log", f"{base}..{head}",
|
||||
"--format=%H|%s|%an|%ae"
|
||||
])
|
||||
|
||||
if not success or not output:
|
||||
return []
|
||||
|
||||
commits = []
|
||||
for line in output.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split('|')
|
||||
if len(parts) >= 4:
|
||||
commits.append({
|
||||
"hash": parts[0],
|
||||
"message": parts[1],
|
||||
"author": parts[2],
|
||||
"email": parts[3]
|
||||
})
|
||||
|
||||
return commits
|
||||
|
||||
def parse_conventional_commit(self, message: str) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Parse conventional commit message
|
||||
|
||||
Args:
|
||||
message: Commit message
|
||||
|
||||
Returns:
|
||||
Dict with type, scope, description, breaking
|
||||
"""
|
||||
# Pattern: type(scope): description
|
||||
pattern = r'^(\w+)(\([^)]+\))?:\s*(.+)$'
|
||||
match = re.match(pattern, message)
|
||||
|
||||
if match:
|
||||
commit_type = match.group(1)
|
||||
scope = match.group(2)[1:-1] if match.group(2) else None
|
||||
description = match.group(3)
|
||||
breaking = "BREAKING CHANGE" in message or message.startswith(f"{commit_type}!")
|
||||
return {
|
||||
"type": commit_type,
|
||||
"scope": scope,
|
||||
"description": description,
|
||||
"breaking": breaking
|
||||
}
|
||||
|
||||
return {
|
||||
"type": None,
|
||||
"scope": None,
|
||||
"description": message,
|
||||
"breaking": False
|
||||
}
|
||||
|
||||
def extract_issue_references(self, message: str) -> List[str]:
|
||||
"""
|
||||
Extract GitHub issue references from commit message
|
||||
|
||||
Args:
|
||||
message: Commit message
|
||||
|
||||
Returns:
|
||||
List of issue references (e.g., ["#123", "#456"])
|
||||
"""
|
||||
pattern = r'#(\d+)'
|
||||
matches = re.findall(pattern, message)
|
||||
return [f"#{num}" for num in matches]
|
||||
|
||||
def generate_pr_title(self, commits: List[Dict[str, str]]) -> str:
|
||||
"""
|
||||
Generate PR title from commits
|
||||
|
||||
Args:
|
||||
commits: List of commits
|
||||
|
||||
Returns:
|
||||
Generated PR title
|
||||
"""
|
||||
if not commits:
|
||||
return "Update"
|
||||
|
||||
# Use the most recent commit message as base
|
||||
first_commit = commits[0]
|
||||
parsed = self.parse_conventional_commit(first_commit["message"])
|
||||
|
||||
if parsed["type"]:
|
||||
# Use conventional commit format
|
||||
if parsed["scope"]:
|
||||
return f"{parsed['type']}({parsed['scope']}): {parsed['description']}"
|
||||
else:
|
||||
return f"{parsed['type']}: {parsed['description']}"
|
||||
else:
|
||||
# Use first commit message as-is
|
||||
return first_commit["message"]
|
||||
|
||||
def generate_pr_body(self, commits: List[Dict[str, str]], issues: List[str]) -> str:
|
||||
"""
|
||||
Generate PR description from commits
|
||||
|
||||
Args:
|
||||
commits: List of commits
|
||||
issues: List of related issues
|
||||
|
||||
Returns:
|
||||
Generated PR body in markdown
|
||||
"""
|
||||
body = []
|
||||
|
||||
# Summary section
|
||||
body.append("## Summary\n")
|
||||
if len(commits) == 1:
|
||||
body.append(f"{commits[0]['message']}\n")
|
||||
else:
|
||||
body.append(f"This PR includes {len(commits)} commits:\n")
|
||||
for commit in commits[:5]: # Show first 5
|
||||
parsed = self.parse_conventional_commit(commit["message"])
|
||||
body.append(f"- {commit['message']}")
|
||||
if len(commits) > 5:
|
||||
body.append(f"- ... and {len(commits) - 5} more commits")
|
||||
body.append("")
|
||||
|
||||
# Related issues
|
||||
if issues:
|
||||
body.append("## Related Issues\n")
|
||||
for issue in issues:
|
||||
body.append(f"- Closes {issue}")
|
||||
body.append("")
|
||||
|
||||
# Commits section
|
||||
body.append("## Commits\n")
|
||||
for commit in commits:
|
||||
short_hash = commit['hash'][:7]
|
||||
body.append(f"- {short_hash} {commit['message']}")
|
||||
body.append("")
|
||||
|
||||
# Check for breaking changes
|
||||
breaking = [c for c in commits if self.parse_conventional_commit(c["message"])["breaking"]]
|
||||
if breaking:
|
||||
body.append("## ⚠️ Breaking Changes\n")
|
||||
for commit in breaking:
|
||||
body.append(f"- {commit['message']}")
|
||||
body.append("")
|
||||
|
||||
return "\n".join(body)
|
||||
|
||||
def detect_labels(self, commits: List[Dict[str, str]]) -> List[str]:
|
||||
"""
|
||||
Detect labels from commit types
|
||||
|
||||
Args:
|
||||
commits: List of commits
|
||||
|
||||
Returns:
|
||||
List of label names
|
||||
"""
|
||||
labels = set()
|
||||
|
||||
for commit in commits:
|
||||
parsed = self.parse_conventional_commit(commit["message"])
|
||||
if parsed["type"] and parsed["type"] in self.COMMIT_TYPE_LABELS:
|
||||
labels.add(self.COMMIT_TYPE_LABELS[parsed["type"]])
|
||||
|
||||
if parsed["breaking"]:
|
||||
labels.add("breaking-change")
|
||||
|
||||
return list(labels)
|
||||
|
||||
def check_gh_cli(self) -> bool:
|
||||
"""Check if GitHub CLI is available"""
|
||||
success, _ = self.run_command(["gh", "--version"])
|
||||
return success
|
||||
|
||||
@certified_skill("git.createpr")
|
||||
def execute(
|
||||
self,
|
||||
base_branch: str = "main",
|
||||
title: Optional[str] = None,
|
||||
draft: bool = False,
|
||||
auto_merge: bool = False,
|
||||
reviewers: Optional[List[str]] = None,
|
||||
labels: Optional[List[str]] = None,
|
||||
body: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute the skill
|
||||
|
||||
Args:
|
||||
base_branch: Base branch for PR
|
||||
title: PR title (auto-generated if not provided)
|
||||
draft: Create as draft PR
|
||||
auto_merge: Enable auto-merge
|
||||
reviewers: List of reviewer usernames
|
||||
labels: Labels to apply
|
||||
body: PR description (auto-generated if not provided)
|
||||
|
||||
Returns:
|
||||
Dict with execution results
|
||||
"""
|
||||
try:
|
||||
logger.info("Executing git.createpr...")
|
||||
|
||||
# Validate git repository
|
||||
if not self.is_git_repository():
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": "Not in a git repository"
|
||||
}
|
||||
|
||||
# Check gh CLI
|
||||
if not self.check_gh_cli():
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": "GitHub CLI (gh) not found. Install: https://cli.github.com/"
|
||||
}
|
||||
|
||||
# Get current branch
|
||||
head_branch = self.get_current_branch()
|
||||
if not head_branch:
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": "Could not determine current branch"
|
||||
}
|
||||
|
||||
# Validate base branch exists
|
||||
if not self.branch_exists(base_branch):
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": f"Base branch '{base_branch}' does not exist"
|
||||
}
|
||||
|
||||
# Get commits
|
||||
logger.info(f"Analyzing commits between {base_branch} and {head_branch}")
|
||||
commits = self.get_commits_between(base_branch, head_branch)
|
||||
|
||||
if not commits:
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": f"No commits found between {base_branch} and {head_branch}"
|
||||
}
|
||||
|
||||
logger.info(f"Found {len(commits)} commits")
|
||||
|
||||
# Generate title if not provided
|
||||
if not title:
|
||||
title = self.generate_pr_title(commits)
|
||||
logger.info(f"Generated title: {title}")
|
||||
|
||||
# Extract issue references
|
||||
all_issues = set()
|
||||
for commit in commits:
|
||||
issues = self.extract_issue_references(commit["message"])
|
||||
all_issues.update(issues)
|
||||
|
||||
# Generate body if not provided
|
||||
if not body:
|
||||
body = self.generate_pr_body(commits, list(all_issues))
|
||||
logger.info("Generated PR description")
|
||||
|
||||
# Detect labels if not provided
|
||||
if not labels:
|
||||
labels = self.detect_labels(commits)
|
||||
logger.info(f"Detected labels: {labels}")
|
||||
|
||||
# Build gh command
|
||||
gh_cmd = ["gh", "pr", "create", "--base", base_branch, "--title", title, "--body", body]
|
||||
|
||||
if draft:
|
||||
gh_cmd.append("--draft")
|
||||
|
||||
if reviewers:
|
||||
gh_cmd.extend(["--reviewer", ",".join(reviewers)])
|
||||
|
||||
if labels:
|
||||
gh_cmd.extend(["--label", ",".join(labels)])
|
||||
|
||||
# Create PR
|
||||
logger.info("Creating pull request...")
|
||||
success, output = self.run_command(gh_cmd)
|
||||
|
||||
if not success:
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": f"Failed to create PR: {output}"
|
||||
}
|
||||
|
||||
# Parse PR URL from output
|
||||
pr_url = output.strip()
|
||||
logger.info(f"PR created: {pr_url}")
|
||||
|
||||
# Extract PR number from URL
|
||||
pr_number = None
|
||||
match = re.search(r'/pull/(\d+)', pr_url)
|
||||
if match:
|
||||
pr_number = int(match.group(1))
|
||||
|
||||
# Build result
|
||||
result = {
|
||||
"ok": True,
|
||||
"status": "success",
|
||||
"pr_url": pr_url,
|
||||
"pr_number": pr_number,
|
||||
"title": title,
|
||||
"base_branch": base_branch,
|
||||
"head_branch": head_branch,
|
||||
"commits_analyzed": len(commits),
|
||||
"issues_linked": list(all_issues),
|
||||
"labels_applied": labels or [],
|
||||
"reviewers_requested": reviewers or [],
|
||||
"is_draft": draft
|
||||
}
|
||||
|
||||
logger.info("Skill completed successfully")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing skill: {e}")
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create GitHub pull request with auto-generated content"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--base",
|
||||
dest="base_branch",
|
||||
default="main",
|
||||
help="Base branch for PR (default: main)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--title",
|
||||
help="PR title (auto-generated if not provided)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--draft",
|
||||
action="store_true",
|
||||
help="Create as draft PR"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--auto-merge",
|
||||
dest="auto_merge",
|
||||
action="store_true",
|
||||
help="Enable auto-merge if checks pass"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--reviewers",
|
||||
nargs="+",
|
||||
help="GitHub usernames to request reviews from"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--labels",
|
||||
nargs="+",
|
||||
help="Labels to apply to PR"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--body",
|
||||
help="PR description (auto-generated if not provided)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--output-format",
|
||||
choices=["json", "yaml", "human"],
|
||||
default="human",
|
||||
help="Output format"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create skill instance
|
||||
skill = GitCreatepr()
|
||||
|
||||
# Execute skill
|
||||
result = skill.execute(
|
||||
base_branch=args.base_branch,
|
||||
title=args.title,
|
||||
draft=args.draft,
|
||||
auto_merge=args.auto_merge,
|
||||
reviewers=args.reviewers,
|
||||
labels=args.labels,
|
||||
body=args.body
|
||||
)
|
||||
|
||||
# Output result
|
||||
if args.output_format == "json":
|
||||
print(json.dumps(result, indent=2))
|
||||
elif args.output_format == "yaml":
|
||||
print(yaml.dump(result, default_flow_style=False))
|
||||
else:
|
||||
# Human-readable output
|
||||
if result.get("ok"):
|
||||
print(f"\n✓ Pull Request Created!")
|
||||
print(f" URL: {result.get('pr_url')}")
|
||||
if result.get('pr_number'):
|
||||
print(f" Number: #{result.get('pr_number')}")
|
||||
print(f" Title: {result.get('title')}")
|
||||
print(f" Base: {result.get('base_branch')} ← {result.get('head_branch')}")
|
||||
print(f" Commits: {result.get('commits_analyzed')}")
|
||||
if result.get('issues_linked'):
|
||||
print(f" Issues: {', '.join(result.get('issues_linked', []))}")
|
||||
if result.get('labels_applied'):
|
||||
print(f" Labels: {', '.join(result.get('labels_applied', []))}")
|
||||
if result.get('reviewers_requested'):
|
||||
print(f" Reviewers: {', '.join(result.get('reviewers_requested', []))}")
|
||||
if result.get('is_draft'):
|
||||
print(f" Status: Draft")
|
||||
else:
|
||||
print(f"\n✗ Error: {result.get('error', 'Unknown error')}")
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if result.get("ok") else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
58
skills/git.createpr/skill.yaml
Normal file
58
skills/git.createpr/skill.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
name: git.createpr
|
||||
version: 0.1.0
|
||||
description: "Create GitHub pull requests with auto-generated titles and descriptions\
|
||||
\ based on commit analysis. Analyzes commit history, identifies related issues,\
|
||||
\ and creates well-formatted PRs with proper linking and metadata. - git-commits\
|
||||
\ - Commit history between base and feature branch - git-repository - Local git\
|
||||
\ repository with commit information - github-credentials - GitHub token for API\
|
||||
\ access (from gh CLI or environment) - pull-request - Created GitHub pull request\
|
||||
\ with metadata - pr-report - Summary of PR creation including URL, number, and\
|
||||
\ status - base_branch (string): Base branch for PR (default: main) - title (string):\
|
||||
\ PR title (optional, auto-generated from commits if not provided) - draft (boolean):\
|
||||
\ Create as draft PR (default: false) - auto_merge (boolean): Enable auto-merge\
|
||||
\ if checks pass (default: false) - reviewers (array): List of GitHub usernames\
|
||||
\ to request reviews from - labels (array): Labels to apply to PR (optional, auto-detected\
|
||||
\ from commits) - body (string): PR description (optional, auto-generated if not\
|
||||
\ provided) - git command line tool - GitHub CLI (gh) or GitHub API access with\
|
||||
\ token - Access to git repository - GitHub repository with permissions to create\
|
||||
\ PRs 1. Validate we're in a git repository 2. Get current branch name 3. Validate\
|
||||
\ base branch exists 4. Fetch latest changes from remote 5. Get commit history between\
|
||||
\ base and current branch 6. Analyze commits to extract: - Commit messages - Conventional\
|
||||
\ commit types (feat, fix, docs, etc.) - Issue references (#123) - Breaking changes\
|
||||
\ 7. Generate PR title (if not provided): - Use most recent commit message - Or\
|
||||
\ summarize multiple commits - Format: \"type(scope): description\" 8. Generate\
|
||||
\ PR description (if not provided): - Summary of changes - List of commits with\
|
||||
\ links - Related issues section - Breaking changes warning (if any) 9. Detect labels\
|
||||
\ from commit types: - feat \u2192 enhancement - fix \u2192 bug - docs \u2192 documentation\
|
||||
\ - etc. 10. Create PR using GitHub CLI (gh pr create): - Set title and body - Set\
|
||||
\ base and head branches - Apply labels - Request reviewers - Set draft status 11.\
|
||||
\ Parse PR URL and number from output 12. Return structured result with PR metadata\
|
||||
\ ```bash python3 skills/git.createpr/git_createpr.py python3 skills/git.createpr/git_createpr.py\
|
||||
\ --draft python3 skills/git.createpr/git_createpr.py --reviewers alice bob python3\
|
||||
\ skills/git.createpr/git_createpr.py --base develop python3 skills/git.createpr/git_createpr.py\
|
||||
\ --title \"feat: add user authentication\" python3 skills/git.createpr/git_createpr.py\
|
||||
\ --labels enhancement breaking-change ``` ```json { \"ok\": true, \"status\": \"\
|
||||
success\", \"pr_number\": 123, \"pr_url\": \"https://github.com/owner/repo/pull/123\"\
|
||||
, \"title\": \"feat: add user authentication\", \"base_branch\": \"main\", \"head_branch\"\
|
||||
: \"feature/auth\", \"commits_analyzed\": 5, \"issues_linked\": [\"#45\", \"#67\"\
|
||||
], \"labels_applied\": [\"enhancement\", \"feature\"], \"reviewers_requested\":\
|
||||
\ [\"alice\", \"bob\"], \"is_draft\": false } ``` - git - github - pull-request\
|
||||
\ - automation - workflow - pr This skill requires SKILL_AND_COMMAND pattern due\
|
||||
\ to: - 12 steps (exceeds threshold) - High autonomy (auto-generates PR content\
|
||||
\ intelligently) - Highly reusable for release automation and CI/CD - Complex GitHub\
|
||||
\ API interaction - Commit analysis and pattern detection - Multiple execution contexts\
|
||||
\ (CLI, agents, workflows) This implementation uses GitHub CLI (gh) for simplicity\
|
||||
\ and authentication: - Leverages existing gh authentication - Simpler than managing\
|
||||
\ GitHub API tokens - Better error messages - Handles pagination automatically If\
|
||||
\ gh CLI is not available, falls back to REST API with token from: - GITHUB_TOKEN\
|
||||
\ environment variable - GH_TOKEN environment variable"
|
||||
inputs: []
|
||||
outputs: []
|
||||
status: active
|
||||
permissions: []
|
||||
entrypoints:
|
||||
- command: /git/createpr
|
||||
handler: git_createpr.py
|
||||
runtime: python
|
||||
description: Create GitHub pull requests with auto-generated titles and descriptions
|
||||
based on commit analysis. An
|
||||
62
skills/git.createpr/test_git_createpr.py
Normal file
62
skills/git.createpr/test_git_createpr.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for git.createpr
|
||||
|
||||
Generated by meta.skill
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||
|
||||
from skills.git_createpr import git_createpr
|
||||
|
||||
|
||||
class TestGitCreatepr:
|
||||
"""Tests for GitCreatepr"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test fixtures"""
|
||||
self.skill = git_createpr.GitCreatepr()
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test skill initializes correctly"""
|
||||
assert self.skill is not None
|
||||
assert self.skill.base_dir is not None
|
||||
|
||||
def test_execute_basic(self):
|
||||
"""Test basic execution"""
|
||||
result = self.skill.execute()
|
||||
|
||||
assert result is not None
|
||||
assert "ok" in result
|
||||
assert "status" in result
|
||||
|
||||
def test_execute_success(self):
|
||||
"""Test successful execution"""
|
||||
result = self.skill.execute()
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["status"] == "success"
|
||||
|
||||
# TODO: Add more specific tests based on skill functionality
|
||||
|
||||
|
||||
def test_cli_help(capsys):
|
||||
"""Test CLI help message"""
|
||||
sys.argv = ["git_createpr.py", "--help"]
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
git_createpr.main()
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "Create GitHub pull requests with auto-generated ti" in captured.out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user