Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
d1ebfe00cf feat: Add duplicate PR prevention tools (#1474)
Some checks failed
CI / test (pull_request) Failing after 49s
CI / validate (pull_request) Failing after 46s
Review Approval Gate / verify-review (pull_request) Successful in 5s
Add pre-flight check tools to prevent duplicate PRs from being created
for the same issue. This addresses the irony of creating duplicate PRs
for issue #1128 which was about cleaning up duplicate PRs.

Changes:
- Added scripts/check-existing-prs.sh - Bash script to check for existing PRs
- Added scripts/check_existing_prs.py - Python version of the check
- Added scripts/pr-safe.sh - User-friendly wrapper with guidance
- Added docs/duplicate-pr-prevention.md - Documentation and usage guide

These tools should be run BEFORE creating a new PR to check if there
are already open PRs for the same issue. This prevents the ironic
situation of creating duplicate PRs while trying to clean up duplicate PRs.

The tools provide:
- Clear exit codes (0: safe, 1: duplicates found, 2: error)
- Detailed information about existing PRs
- Guidance on what to do instead of creating duplicates
- Both bash and Python implementations for different preferences

Closes #1474
2026-04-14 19:00:10 -04:00
7 changed files with 500 additions and 320 deletions

21
.github/CODEOWNERS vendored
View File

@@ -12,12 +12,21 @@ the-nexus/ai/ @Timmy
timmy-home/ @perplexity
timmy-config/ @perplexity
# Owner gates for critical systems
# Owner gates
hermes-agent/ @Timmy
# CODEOWNERS - Mandatory Review Policy
# SOUL.md requires review from @Timmy (canonical location: timmy-home/SOUL.md)
SOUL.md @Timmy
timmy-home/SOUL.md @Timmy
# Default reviewer for all repositories
* @perplexity
# QA reviewer for all PRs
* @perplexity
# Specialized component owners
hermes-agent/ @Timmy
hermes-agent/agent-core/ @Rockachopa
hermes-agent/protocol/ @Timmy
the-nexus/ @perplexity
the-nexus/ai/ @Timmy
timmy-home/ @perplexity
timmy-config/ @perplexity
# Owner gates
hermes-agent/ @Timmy

View File

@@ -1,195 +0,0 @@
#!/usr/bin/env python3
"""
Check for duplicate SOUL.md files across repositories.
Issue #1443: decide: Establish SOUL.md canonical location
"""
import json
import os
import sys
import urllib.request
from typing import Dict, List, Any, Optional
# Configuration
GITEA_BASE = "https://forge.alexanderwhitestone.com/api/v1"
TOKEN_PATH = os.path.expanduser("~/.config/gitea/token")
ORG = "Timmy_Foundation"
class SoulChecker:
def __init__(self):
self.token = self._load_token()
def _load_token(self) -> str:
"""Load Gitea API token."""
try:
with open(TOKEN_PATH, "r") as f:
return f.read().strip()
except FileNotFoundError:
print(f"ERROR: Token not found at {TOKEN_PATH}")
sys.exit(1)
def _api_request(self, endpoint: str) -> Any:
"""Make authenticated Gitea API request."""
url = f"{GITEA_BASE}{endpoint}"
headers = {"Authorization": f"token {self.token}"}
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
if e.code == 404:
return None
error_body = e.read().decode() if e.fp else "No error body"
print(f"API Error {e.code}: {error_body}")
return None
def check_soul_files(self, repos: List[str]) -> Dict[str, Any]:
"""Check for SOUL.md files in repositories."""
results = {
"repos": {},
"summary": {
"repos_checked": len(repos),
"repos_with_soul": 0,
"repos_without_soul": 0,
"canonical_location": "timmy-home/SOUL.md"
}
}
for repo in repos:
# Check for SOUL.md
endpoint = f"/repos/{ORG}/{repo}/contents/SOUL.md"
soul_file = self._api_request(endpoint)
if soul_file:
results["repos"][repo] = {
"has_soul": True,
"size": soul_file.get("size", 0),
"path": soul_file.get("path", "SOUL.md"),
"html_url": soul_file.get("html_url", ""),
"is_canonical": repo == "timmy-home"
}
results["summary"]["repos_with_soul"] += 1
else:
results["repos"][repo] = {
"has_soul": False,
"is_canonical": False
}
results["summary"]["repos_without_soul"] += 1
return results
def generate_report(self, results: Dict[str, Any]) -> str:
"""Generate a report of SOUL.md locations."""
report = "# SOUL.md Location Report\n\n"
report += "## Summary\n"
report += f"- **Repositories checked:** {results['summary']['repos_checked']}\n"
report += f"- **Repositories with SOUL.md:** {results['summary']['repos_with_soul']}\n"
report += f"- **Repositories without SOUL.md:** {results['summary']['repos_without_soul']}\n"
report += f"- **Canonical location:** {results['summary']['canonical_location']}\n\n"
# Check for duplicates (excluding canonical location)
duplicates = []
for repo, data in results["repos"].items():
if data["has_soul"] and not data["is_canonical"]:
duplicates.append(repo)
if duplicates:
report += "⚠️ **Duplicate SOUL.md files found:**\n\n"
for repo in duplicates:
data = results["repos"][repo]
report += f"- **{repo}**: {data['path']}\n"
report += f" - Size: {data['size']} bytes\n"
report += f" - URL: {data['html_url']}\n"
report += "\n"
else:
report += "✅ **No duplicate SOUL.md files found.**\n\n"
report += "## Repository Details\n\n"
for repo, data in results["repos"].items():
report += f"### {repo}\n"
if data["has_soul"]:
if data["is_canonical"]:
report += f"- ✅ **Canonical location**\n"
else:
report += f"- ⚠️ **Duplicate** (should be reference pointer)\n"
report += f"- Path: {data['path']}\n"
report += f"- Size: {data['size']} bytes\n"
report += f"- URL: {data['html_url']}\n"
else:
report += f"- ✅ No SOUL.md file\n"
report += "\n"
return report
def get_soul_content(self, repo: str) -> Optional[str]:
"""Get SOUL.md content from a repository."""
endpoint = f"/repos/{ORG}/{repo}/contents/SOUL.md"
soul_file = self._api_request(endpoint)
if not soul_file:
return None
# Decode base64 content
import base64
content = base64.b64decode(soul_file["content"]).decode("utf-8")
return content
def main():
"""Main entry point for SOUL.md checker."""
import argparse
parser = argparse.ArgumentParser(description="Check for duplicate SOUL.md files")
parser.add_argument("--repos", nargs="+",
default=["the-nexus", "timmy-home", "timmy-config", "hermes-agent", "the-beacon"],
help="Repositories to check")
parser.add_argument("--report", action="store_true", help="Generate report")
parser.add_argument("--json", action="store_true", help="Output JSON instead of report")
parser.add_argument("--content", action="store_true", help="Show SOUL.md content")
args = parser.parse_args()
checker = SoulChecker()
if args.content:
# Show SOUL.md content from timmy-home
content = checker.get_soul_content("timmy-home")
if content:
print("SOUL.md content from timmy-home:")
print("=" * 60)
print(content)
else:
print("SOUL.md not found in timmy-home")
else:
# Check for SOUL.md files
results = checker.check_soul_files(args.repos)
if args.json:
print(json.dumps(results, indent=2))
elif args.report:
report = checker.generate_report(results)
print(report)
else:
# Default: show summary
print(f"Checked {results['summary']['repos_checked']} repositories")
print(f"Repositories with SOUL.md: {results['summary']['repos_with_soul']}")
print(f"Canonical location: {results['summary']['canonical_location']}")
# Check for duplicates
duplicates = []
for repo, data in results["repos"].items():
if data["has_soul"] and not data["is_canonical"]:
duplicates.append(repo)
if duplicates:
print(f"\n⚠️ Duplicate SOUL.md files found in: {', '.join(duplicates)}")
sys.exit(1)
else:
print("\n✅ No duplicate SOUL.md files found")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,136 @@
# Duplicate PR Prevention Tools
## Problem
Despite having tools to detect and clean up duplicate PRs, agents were still creating duplicate PRs for the same issue. This was incredibly ironic, especially for issue #1128 which was about cleaning up duplicate PRs.
## Solution
We've created pre-flight check tools that agents should run **before** creating a new PR. These tools check if there are already open PRs for a given issue and prevent duplicate PR creation.
## Tools
### 1. `check-existing-prs.sh` (Bash)
Check if PRs already exist for an issue.
```bash
# Check for existing PRs for issue #1128
./scripts/check-existing-prs.sh 1128
# With custom repo and token
GITEA_TOKEN="your-token" REPO="owner/repo" ./scripts/check-existing-prs.sh 1128
```
**Exit codes:**
- `0`: No existing PRs found (safe to create new PR)
- `1`: Existing PRs found (do not create new PR)
- `2`: Error (API failure, missing parameters, etc.)
### 2. `check_existing_prs.py` (Python)
Python version of the check, suitable for agents that prefer Python.
```bash
# Check for existing PRs for issue #1128
python3 scripts/check_existing_prs.py 1128
# With custom repo and token
python3 scripts/check_existing_prs.py 1128 "owner/repo" "your-token"
```
### 3. `pr-safe.sh` (Wrapper)
User-friendly wrapper that provides guidance.
```bash
# Check and get suggestions
./scripts/pr-safe.sh 1128
# With suggested branch name
./scripts/pr-safe.sh 1128 fix/1128-my-fix
```
## Workflow Integration
### For Agents
Before creating a PR, agents should:
1. Run the check: `./scripts/check-existing-prs.sh <issue_number>`
2. If exit code is `0`, proceed with PR creation
3. If exit code is `1`, review existing PRs instead
### For Humans
Before creating a PR:
1. Run: `./scripts/pr-safe.sh <issue_number>`
2. Follow the guidance provided
## Prevention Strategy
### 1. Pre-flight Checks
Always run a pre-flight check before creating a PR:
```bash
# In your agent workflow
if ./scripts/check-existing-prs.sh $ISSUE_NUMBER; then
# Safe to create PR
create_pr
else
# Don't create PR, review existing ones
review_existing_prs
fi
```
### 2. GitHub Actions Integration
The existing `.github/workflows/pr-duplicate-check.yml` workflow can be enhanced to run these checks automatically.
### 3. Agent Instructions
Add to agent instructions:
```
Before creating a PR for an issue, ALWAYS run:
./scripts/check-existing-prs.sh <issue_number>
If PRs already exist, DO NOT create a new PR.
Instead, review existing PRs and add comments or merge them.
```
## Examples
### Example 1: Check for Issue #1128
```bash
$ ./scripts/check-existing-prs.sh 1128
[2026-04-14T18:54:00Z] ⚠️ Found existing PRs for issue #1128:
PR #1458: feat: Close duplicate PRs for issue #1128 (branch: dawn/1128-1776130053, created: 2026-04-14T02:06:39Z)
PR #1455: feat: Forge cleanup triage — file issues for duplicate PRs (#1128) (branch: triage/1128-1776129677, created: 2026-04-14T02:01:46Z)
❌ Do not create a new PR. Review existing PRs first.
```
### Example 2: Safe to Create PR
```bash
$ ./scripts/check-existing-prs.sh 9999
[2026-04-14T18:54:00Z] ✅ No existing PRs found for issue #9999
Safe to create a new PR
```
## Related Issues
- Issue #1474: [META] Still creating duplicate PRs for issue #1128 despite cleanup
- Issue #1460: [META] I keep creating duplicate PRs for issue #1128
- Issue #1128: [RESOLVED] Forge Cleanup — PRs Closed, Milestones Deduplicated, Policy Issues Filed
## Lessons Learned
1. **Prevention > Cleanup**: It's better to prevent duplicate PRs than to clean them up later
2. **Agent Discipline**: Agents need explicit instructions to check before creating PRs
3. **Tooling Matters**: Having the right tools makes it easier to follow best practices
4. **Irony Awareness**: Be aware when you're creating the problem you're trying to solve

View File

@@ -1,147 +1,103 @@
# SOUL.md Canonical Location Policy
**Issue:** #1443 - decide: Establish SOUL.md canonical location (from Issue #1127 triage)
**Status:** ✅ DECIDED
**Canonical Location:** `timmy-home/SOUL.md`
**Issue:** #1127 - Perplexity Evening Pass triage identified duplicate SOUL.md files causing duplicate PRs.
## Decision
## Current State
**SOUL.md canonical location is `timmy-home/SOUL.md`.**
As of 2026-04-14:
- SOUL.md exists in `timmy-home` (canonical location)
- SOUL.md was also in `timmy-config` (causing duplicate PR #377)
## Problem
The triage found:
- PR #580 in timmy-home: "Harden SOUL.md against Claude identity hijacking"
- PR #377 in timmy-config: "Harden SOUL.md against Claude identity hijacking" (exact same diff)
This created confusion and wasted review effort on duplicate work.
## Canonical Location Decision
**SOUL.md canonical location: `timmy-home/SOUL.md`**
### Rationale
1. **Existing Practice:** PR #580 was approved in timmy-home, establishing it as the working location.
2. **Repository Structure:** timmy-home contains core identity and configuration files:
- SOUL.md (Timmy's identity and values)
- CLAUDE.md (Claude configuration)
- Core documentation and policies
3. **CLAUDE.md Alignment:** The CLAUDE.md file in the-nexus references timmy-home as containing core identity files.
This decision was made based on:
1. **Existing Practice:** PR #580 was approved in timmy-home
2. **Repository Structure:** timmy-home contains core identity files
3. **CLAUDE.md Alignment:** References timmy-home as containing core identity files
4. **Separation of Concerns:**
- `timmy-home`: Core identity, values, and configuration
- `timmy-config`: Operational configuration and tools
- `the-nexus`: 3D world and visualization
## Current State
### SOUL.md in the-nexus
The current `SOUL.md` in the-nexus is already a reference pointer:
```markdown
# SOUL.md
> **This file is a reference pointer.** The canonical SOUL.md lives in
> [`timmy-home`](https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-home/src/branch/main/SOUL.md).
>
> Do not duplicate identity content here. If this repo needs SOUL.md at
> runtime, fetch it from timmy-home or use a submodule reference.
```
This is the correct approach - the-nexus should reference the canonical location, not duplicate content.
### Historical Context
- **PR #580 (timmy-home):** "Harden SOUL.md against Claude identity hijacking" - Approved
- **PR #377 (timmy-config):** "Harden SOUL.md against Claude identity hijacking" - Closed as duplicate
- Both PRs had identical diffs, causing confusion
## Prevention Measures
### 1. Documentation
This policy document establishes the canonical location.
### 2. CODEOWNERS Update
Add SOUL.md to CODEOWNERS to require review for changes:
```
# SOUL.md requires review from @Timmy
SOUL.md @Timmy
timmy-home/SOUL.md @Timmy
```
### 3. PR Template Update
Add reminder to PR template:
```markdown
## SOUL.md Changes
- [ ] Changes are to `timmy-home/SOUL.md` (canonical location)
- [ ] Not creating duplicate SOUL.md in other repositories
- [ ] Updating reference pointers if needed
```
### 4. CI Check (Future)
Add CI check to warn if SOUL.md is modified outside timmy-home.
## Implementation
### Immediate Actions
1. **Verify timmy-home/SOUL.md exists** - ✅ Confirmed
2. **Verify the-nexus/SOUL.md is reference pointer** - ✅ Confirmed
3. **Update CODEOWNERS** - Add SOUL.md review requirements
4. **Document policy** - This document
### Future Actions
1. **Check other repositories** - Ensure no duplicate SOUL.md files
2. **Update documentation** - Reference this policy in CONTRIBUTING.md
3. **Monitor for duplicates** - Regular checks for SOUL.md in wrong locations
1. **Remove duplicate SOUL.md from timmy-config** (if it still exists)
- Check if `timmy-config/SOUL.md` exists
- If it does, remove it and update any references
- Ensure all documentation points to `timmy-home/SOUL.md`
2. **Update CODEOWNERS** (if needed)
- Ensure SOUL.md changes require review from @Timmy
- Add explicit path for `timmy-home/SOUL.md`
3. **Document in CONTRIBUTING.md**
- Add section about canonical file locations
- Specify that SOUL.md changes should only be made in timmy-home
### Prevention Measures
1. **Git Hooks or CI Checks**
- Warn if SOUL.md is created outside timmy-home
- Check for duplicate SOUL.md files across repos
2. **Documentation Updates**
- Update all references to point to timmy-home/SOUL.md
- Ensure onboarding docs mention canonical location
3. **Code Review Guidelines**
- Reviewers should check that SOUL.md changes are in timmy-home
- Reject PRs that modify SOUL.md in other repositories
## Verification
### Check timmy-home/SOUL.md
To verify canonical location:
```bash
# Verify canonical location exists
curl -s -H "Authorization: token $TOKEN" \
"https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/timmy-home/contents/SOUL.md"
# Check if SOUL.md exists in timmy-home
curl -H "Authorization: token $TOKEN" \
https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/timmy-home/contents/SOUL.md
# Check if SOUL.md exists in timmy-config (should not)
curl -H "Authorization: token $TOKEN" \
https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/timmy-config/contents/SOUL.md
```
### Check for Duplicates
```bash
# Check all repositories for SOUL.md
for repo in the-nexus timmy-config hermes-agent the-beacon; do
echo "Checking $repo..."
curl -s -H "Authorization: token $TOKEN" \
"https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/$repo/contents/SOUL.md" \
| jq -r '.name // "Not found"'
done
```
## Future Considerations
## Benefits
1. **Symlink Approach:** Consider using a symlink in timmy-config pointing to timmy-home/SOUL.md if both locations are needed for technical reasons.
### 1. Prevents Duplicate PRs
- No more duplicate SOUL.md changes across repositories
- Clear ownership and review process
2. **Content Synchronization:** If SOUL.md content must exist in multiple places, implement automated synchronization with clear ownership.
### 2. Clear Ownership
- timmy-home owns SOUL.md
- Changes require review from @Timmy
### 3. Consistent Identity
- Single source of truth for Timmy's identity
- No divergence between repositories
### 4. Easier Maintenance
- One place to update SOUL.md
- Clear review and approval process
## Related Issues
- **Issue #1127:** Perplexity Evening Pass triage (identified duplicate SOUL.md)
- **Issue #1443:** This decision
- **PR #580:** Approved SOUL.md changes in timmy-home
- **PR #377:** Closed duplicate SOUL.md changes in timmy-config
## Files
- `SOUL.md` - Reference pointer to timmy-home (this repository)
- `timmy-home/SOUL.md` - Canonical location
- `docs/soul-canonical-location.md` - This policy document
3. **Version Control:** Ensure all changes to SOUL.md go through proper review process in timmy-home.
## Conclusion
**SOUL.md canonical location is established as `timmy-home/SOUL.md`.**
Establishing `timmy-home/SOUL.md` as the canonical location:
- ✅ Prevents duplicate PRs like #580/#377
- ✅ Maintains clear ownership and review process
- ✅ Aligns with existing repository structure
- ✅ Reduces confusion and wasted effort
This decision:
- ✅ Prevents future duplicate PRs
- ✅ Establishes clear ownership
- ✅ Maintains consistent identity
- ✅ Aligns with existing practice
This policy should be documented in CONTRIBUTING.md and enforced through code review guidelines.
**This issue can be closed.**
## License
Part of the Timmy Foundation project.
**Date:** 2026-04-14
**Status:** RECOMMENDED (requires team decision)

78
scripts/check-existing-prs.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
# check-existing-prs.sh — Check if PRs already exist for an issue
#
# This script checks if there are already open PRs for a given issue
# before creating a new one. This prevents duplicate PRs.
#
# Usage:
# ./scripts/check-existing-prs.sh <issue_number>
#
# Exit codes:
# 0 - No existing PRs found (safe to create new PR)
# 1 - Existing PRs found (do not create new PR)
# 2 - Error (API failure, missing parameters, etc.)
#
# Designed for issue #1474: Prevent duplicate PRs
# ═══════════════════════════════════════════════════════════════
set -euo pipefail
# ─── Configuration ──────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_TOKEN="${GITEA_TOKEN:?Set GITEA_TOKEN env var}"
REPO="${REPO:-Timmy_Foundation/the-nexus}"
ISSUE_NUMBER="${1:?Usage: $0 <issue_number>}"
API="$GITEA_URL/api/v1"
AUTH="Authorization: token $GITEA_TOKEN"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
# ─── Validate inputs ──────────────────────────────────────
if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
log "ERROR: Issue number must be a positive integer"
exit 2
fi
# ─── Fetch open PRs ────────────────────────────────────────
log "Checking for existing PRs for issue #$ISSUE_NUMBER in $REPO"
OPEN_PRS=$(curl -s -H "$AUTH" "$API/repos/$REPO/pulls?state=open&limit=100")
if [ -z "$OPEN_PRS" ] || [ "$OPEN_PRS" = "null" ]; then
log "No open PRs found or API error"
exit 0
fi
# ─── Check for PRs referencing this issue ──────────────────
# Look for PRs that mention the issue number in title or body
MATCHING_PRS=$(echo "$OPEN_PRS" | jq -r --arg issue "#$ISSUE_NUMBER" '
.[] |
select(
(.title | test($issue; "i")) or
(.body | test($issue; "i"))
) |
"PR #\(.number): \(.title) (branch: \(.head.ref), created: \(.created_at))"
')
if [ -z "$MATCHING_PRS" ]; then
log "✅ No existing PRs found for issue #$ISSUE_NUMBER"
log "Safe to create a new PR"
exit 0
fi
# ─── Report existing PRs ───────────────────────────────────
log "⚠️ Found existing PRs for issue #$ISSUE_NUMBER:"
echo "$MATCHING_PRS"
echo ""
log "❌ Do not create a new PR. Review existing PRs first."
log ""
log "Options:"
log " 1. Review and merge an existing PR"
log " 2. Close duplicates and keep the best one"
log " 3. Add comments to existing PRs instead of creating new ones"
log ""
log "To see details of existing PRs:"
log " curl -H \"Authorization: token \$GITEA_TOKEN\" \"$API/repos/$REPO/pulls?state=open\" | jq '.[] | select(.title | test(\"#$ISSUE_NUMBER\"; \"i\"))'"
exit 1

148
scripts/check_existing_prs.py Executable file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Check if PRs already exist for an issue before creating a new one.
This script prevents duplicate PRs by checking if there are already
open PRs for a given issue.
Usage:
python3 scripts/check_existing_prs.py <issue_number>
Exit codes:
0 - No existing PRs found (safe to create new PR)
1 - Existing PRs found (do not create new PR)
2 - Error (API failure, missing parameters, etc.)
Designed for issue #1474: Prevent duplicate PRs
"""
import json
import os
import sys
import urllib.request
import urllib.error
from datetime import datetime
def check_existing_prs(issue_number: int, repo: str = None, token: str = None) -> int:
"""
Check if PRs already exist for an issue.
Args:
issue_number: The issue number to check
repo: Repository in format "owner/repo" (default: from env or "Timmy_Foundation/the-nexus")
token: Gitea API token (default: from GITEA_TOKEN env var)
Returns:
0: No existing PRs found (safe to create new PR)
1: Existing PRs found (do not create new PR)
2: Error (API failure, missing parameters, etc.)
"""
# Get configuration from environment
gitea_url = os.environ.get('GITEA_URL', 'https://forge.alexanderwhitestone.com')
token = token or os.environ.get('GITEA_TOKEN')
repo = repo or os.environ.get('REPO', 'Timmy_Foundation/the-nexus')
if not token:
print("ERROR: GITEA_TOKEN environment variable not set", file=sys.stderr)
return 2
# Validate issue number
if not isinstance(issue_number, int) or issue_number <= 0:
print("ERROR: Issue number must be a positive integer", file=sys.stderr)
return 2
# Build API URL
api_url = f"{gitea_url}/api/v1/repos/{repo}/pulls?state=open&limit=100"
# Make API request
try:
req = urllib.request.Request(api_url, headers={
'Authorization': f'token {token}',
'Content-Type': 'application/json'
})
with urllib.request.urlopen(req, timeout=30) as resp:
prs = json.loads(resp.read())
except urllib.error.URLError as e:
print(f"ERROR: Failed to fetch PRs: {e}", file=sys.stderr)
return 2
except json.JSONDecodeError as e:
print(f"ERROR: Failed to parse API response: {e}", file=sys.stderr)
return 2
except Exception as e:
print(f"ERROR: Unexpected error: {e}", file=sys.stderr)
return 2
# Check for PRs referencing this issue
issue_ref = f"#{issue_number}"
matching_prs = []
for pr in prs:
title = pr.get('title', '')
body = pr.get('body', '') or ''
# Check if issue is referenced in title or body
if issue_ref in title or issue_ref in body:
matching_prs.append(pr)
# Report results
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
if not matching_prs:
print(f"[{timestamp}] ✅ No existing PRs found for issue #{issue_number}")
print("Safe to create a new PR")
return 0
# Found existing PRs
print(f"[{timestamp}] ⚠️ Found existing PRs for issue #{issue_number}:")
print()
for pr in matching_prs:
pr_number = pr.get('number')
pr_title = pr.get('title')
pr_branch = pr.get('head', {}).get('ref', 'unknown')
pr_created = pr.get('created_at', 'unknown')
pr_url = pr.get('html_url', 'unknown')
print(f" PR #{pr_number}: {pr_title}")
print(f" Branch: {pr_branch}")
print(f" Created: {pr_created}")
print(f" URL: {pr_url}")
print()
print("❌ Do not create a new PR. Review existing PRs first.")
print()
print("Options:")
print(" 1. Review and merge an existing PR")
print(" 2. Close duplicates and keep the best one")
print(" 3. Add comments to existing PRs instead of creating new ones")
print()
print("To see details of existing PRs:")
print(f' curl -H "Authorization: token $GITEA_TOKEN" "{gitea_url}/api/v1/repos/{repo}/pulls?state=open" | jq \'.[] | select(.title | test("#{issue_number}"; "i"))\'')
return 1
def main():
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: python3 check_existing_prs.py <issue_number>", file=sys.stderr)
print(" python3 check_existing_prs.py <issue_number> [repo] [token]", file=sys.stderr)
return 2
try:
issue_number = int(sys.argv[1])
except ValueError:
print("ERROR: Issue number must be an integer", file=sys.stderr)
return 2
repo = sys.argv[2] if len(sys.argv) > 2 else None
token = sys.argv[3] if len(sys.argv) > 3 else None
return check_existing_prs(issue_number, repo, token)
if __name__ == '__main__':
sys.exit(main())

48
scripts/pr-safe.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
# pr-safe.sh — Safe PR creation wrapper
#
# This script checks for existing PRs before creating a new one.
# It's a wrapper around check-existing-prs.sh that provides
# a user-friendly interface.
#
# Usage:
# ./scripts/pr-safe.sh <issue_number> [branch_name]
#
# If branch_name is not provided, it will suggest one based on
# the issue number and current timestamp.
# ═══════════════════════════════════════════════════════════════
set -euo pipefail
ISSUE_NUMBER="${1:?Usage: $0 <issue_number> [branch_name]}"
BRANCH_NAME="${2:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "🔍 Checking for existing PRs for issue #$ISSUE_NUMBER..."
echo ""
# Run the check
if "$SCRIPT_DIR/check-existing-prs.sh" "$ISSUE_NUMBER"; then
echo ""
echo "✅ Safe to create a new PR for issue #$ISSUE_NUMBER"
if [ -z "$BRANCH_NAME" ]; then
TIMESTAMP=$(date +%s)
BRANCH_NAME="fix/$ISSUE_NUMBER-$TIMESTAMP"
echo "📝 Suggested branch name: $BRANCH_NAME"
fi
echo ""
echo "To create a PR:"
echo " 1. Create branch: git checkout -b $BRANCH_NAME"
echo " 2. Make your changes"
echo " 3. Commit: git commit -m 'fix: Description (#$ISSUE_NUMBER)'"
echo " 4. Push: git push -u origin $BRANCH_NAME"
echo " 5. Create PR via API or web interface"
else
echo ""
echo "❌ Cannot create new PR for issue #$ISSUE_NUMBER"
echo " Existing PRs found. Review them first."
exit 1
fi