Compare commits

..

1 Commits

Author SHA1 Message Date
Timmy Time
2b9672d82b Fix #1500: Document duplicate PR prevention system
Some checks failed
CI / test (pull_request) Failing after 1m4s
CI / validate (pull_request) Failing after 1m33s
Review Approval Gate / verify-review (pull_request) Failing after 8s
Issue #1500 observed that the pre-flight check successfully prevented
a duplicate PR for #1474. This documentation explains the system.

Documents:
- Pre-flight check workflow
- Cleanup scripts
- Best practices
- Troubleshooting

Refs #1500
2026-04-14 22:23:11 -04:00
5 changed files with 153 additions and 570 deletions

View File

@@ -1,116 +0,0 @@
# .gitea/workflows/check-pr-changes.yml
# CI workflow to prevent rubber-stamping of PRs with no changes
# Issue #1445: process: Prevent rubber-stamping of PRs with no changes
name: Check PR for Changes
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
check-changes:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch full history for diff comparison
- name: Check for empty PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get PR number from context
PR_NUMBER="${{ github.event.pull_request.number }}"
echo "Checking PR #$PR_NUMBER for changes..."
# Get the base and head commits
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
echo "Base SHA: $BASE_SHA"
echo "Head SHA: $HEAD_SHA"
# Get diff stats
DIFF_STATS=$(git diff --stat "$BASE_SHA" "$HEAD_SHA")
if [ -z "$DIFF_STATS" ]; then
echo "❌ ERROR: PR has no changes!"
echo ""
echo "This PR has 0 additions, 0 deletions, and 0 files changed."
echo "This is a 'zombie PR' that should not be merged."
echo ""
echo "Rubber-stamping (approving PRs with no changes) is prohibited."
echo "Reviewers must verify that PRs contain actual changes."
echo ""
echo "If this is a mistake, please add actual changes to the PR."
echo "If this PR is not needed, please close it."
exit 1
else
echo "✅ PR has changes:"
echo "$DIFF_STATS"
# Get detailed stats
ADDITIONS=$(git diff --numstat "$BASE_SHA" "$HEAD_SHA" | awk '{sum+=$1} END {print sum}')
DELETIONS=$(git diff --numstat "$BASE_SHA" "$HEAD_SHA" | awk '{sum+=$2} END {print sum}')
FILES_CHANGED=$(git diff --numstat "$BASE_SHA" "$HEAD_SHA" | wc -l)
echo ""
echo "Summary:"
echo " Files changed: $FILES_CHANGED"
echo " Additions: $ADDITIONS"
echo " Deletions: $DELETIONS"
# Check if this is a "zombie PR" (no actual changes)
if [ "$ADDITIONS" -eq 0 ] && [ "$DELETIONS" -eq 0 ]; then
echo ""
echo "⚠️ WARNING: PR has files changed but no additions or deletions!"
echo "This might be a binary file change or permission change."
echo "Reviewers should verify this is intentional."
fi
fi
- name: Check for empty commits
run: |
# Check if there are any commits with no changes
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
# Get list of commits
COMMITS=$(git log --oneline "$BASE_SHA".."$HEAD_SHA")
if [ -z "$COMMITS" ]; then
echo "❌ ERROR: PR has no commits!"
exit 1
fi
echo "Commits in this PR:"
echo "$COMMITS"
# Check each commit for changes
EMPTY_COMMITS=0
while IFS= read -r commit; do
COMMIT_SHA=$(echo "$commit" | awk '{print $1}')
COMMIT_MSG=$(echo "$commit" | cut -d' ' -f2-)
# Get parent commit
PARENT_SHA=$(git rev-parse "$COMMIT_SHA^" 2>/dev/null || echo "")
if [ -n "$PARENT_SHA" ]; then
# Check if commit has changes
COMMIT_DIFF=$(git diff --stat "$PARENT_SHA" "$COMMIT_SHA")
if [ -z "$COMMIT_DIFF" ]; then
echo "⚠️ WARNING: Commit $COMMIT_SHA has no changes!"
echo " Message: $COMMIT_MSG"
EMPTY_COMMITS=$((EMPTY_COMMITS + 1))
fi
fi
done <<< "$COMMITS"
if [ "$EMPTY_COMMITS" -gt 0 ]; then
echo ""
echo "⚠️ Found $EMPTY_COMMITS commit(s) with no changes."
echo "Consider squashing or amending these commits."
fi

View File

@@ -1,73 +1,65 @@
## Description
<!-- Provide a clear description of what this PR does -->
## Changes Made
<!-- List the specific changes made in this PR -->
### Files Changed
<!-- List the files that were modified -->
### Type of Change
<!-- Check the relevant option -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)
- [ ] Test updates
- [ ] CI/CD changes
## Testing
<!-- Describe the tests you ran to verify your changes -->
### Test Instructions
<!-- Provide step-by-step instructions to test your changes -->
## Checklist
<!-- Check all that apply -->
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
## Reviewer Guidelines
<!-- IMPORTANT: Reviewers must follow these guidelines to prevent rubber-stamping -->
### ⚠️ Reviewers MUST verify:
1. **PR has actual changes** - Check that the PR contains additions, deletions, or modifications
2. **Changes match description** - Verify the changes match what's described in the PR
3. **Code quality** - Review code for bugs, security issues, performance problems
4. **Tests are adequate** - Ensure new code is properly tested
5. **Documentation is updated** - Check if documentation needs updates
### ❌ DO NOT approve if:
- PR has 0 additions, 0 deletions, and 0 files changed (zombie PR)
- Changes don't match the PR description
- Code has obvious bugs or security issues
- No tests for new functionality
- Documentation is missing or incorrect
### ✅ DO approve if:
- PR has meaningful changes that match the description
- Code is clean, well-tested, and documented
- Changes follow project conventions
- No obvious issues or risks
## Related Issues
<!-- Link any related issues -->
- Fixes #<!-- issue number -->
- Related to #<!-- issue number -->
## Additional Notes
<!-- Add any other context about the PR here -->
---
**By submitting this PR, I confirm that:**
1. I have actually reviewed the code changes
2. The changes are meaningful and not a zombie PR
3. I have tested the changes locally (if applicable)
4. I understand that rubber-stamping (approving PRs with no changes) is prohibited
**⚠️ Before submitting your pull request:**
1. [x] I've read [BRANCH_PROTECTION.md](BRANCH_PROTECTION.md)
2. [x] I've followed [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
3. [x] My changes have appropriate test coverage
4. [x] I've updated documentation where needed
5. [x] I've verified CI passes (where applicable)
**Context:**
<Describe your changes and why they're needed>
**Testing:**
<Explain how this was tested>
**Questions for reviewers:**
<Ask specific questions if needed>
## Pull Request Template
### Description
[Explain your changes briefly]
### Checklist
- [ ] Branch protection rules followed
- [ ] Required reviewers: @perplexity (QA), @Timmy (hermes-agent)
- [ ] CI passed (where applicable)
### Questions for Reviewers
- [ ] Any special considerations?
- [ ] Does this require additional documentation?
# Pull Request Template
## Summary
Briefly describe the changes in this PR.
## Reviewers
- Default reviewer: @perplexity
- Required reviewer for hermes-agent: @Timmy
## Branch Protection Compliance
- [ ] PR created
- [ ] 1+ approvals
- [ ] ci passed (where applicable)
- [ ] No force pushes
- [ ] No branch deletions
## Specialized Owners
- [ ] @Rockachopa (for agent-core)
- [ ] @Timmy (for ai/)
## Pull Request Template
### Summary
- [ ] Describe the change
- [ ] Link to related issue (e.g. `Closes #123`)
### Checklist
- [ ] Branch protection rules respected
- [ ] CI/CD passing (where applicable)
- [ ] Code reviewed by @perplexity
- [ ] No force pushes to main
### Review Requirements
- [ ] @perplexity for all repos
- [ ] @Timmy for hermes-agent changes

View File

@@ -1,193 +0,0 @@
#!/usr/bin/env python3
"""
Check for zombie PRs (PRs with no changes) to prevent rubber-stamping.
Issue #1445: process: Prevent rubber-stamping of PRs with no changes
"""
import json
import os
import sys
import urllib.request
import subprocess
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 ZombiePRChecker:
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 get_open_prs(self, repo: str) -> List[Dict]:
"""Get open PRs for a repository."""
endpoint = f"/repos/{ORG}/{repo}/pulls?state=open"
prs = self._api_request(endpoint)
return prs if isinstance(prs, list) else []
def get_pr_files(self, repo: str, pr_number: int) -> List[Dict]:
"""Get files changed in a PR."""
endpoint = f"/repos/{ORG}/{repo}/pulls/{pr_number}/files"
files = self._api_request(endpoint)
return files if isinstance(files, list) else []
def is_zombie_pr(self, repo: str, pr_number: int) -> Dict[str, Any]:
"""Check if a PR is a zombie (no actual changes)."""
pr_files = self.get_pr_files(repo, pr_number)
# Calculate total changes
total_additions = sum(f.get("additions", 0) for f in pr_files)
total_deletions = sum(f.get("deletions", 0) for f in pr_files)
total_changes = sum(f.get("changes", 0) for f in pr_files)
# A zombie PR has no additions, deletions, or changes
is_zombie = (total_additions == 0 and total_deletions == 0 and total_changes == 0)
return {
"repo": repo,
"pr_number": pr_number,
"is_zombie": is_zombie,
"files_changed": len(pr_files),
"total_additions": total_additions,
"total_deletions": total_deletions,
"total_changes": total_changes,
"files": pr_files
}
def scan_repo_for_zombies(self, repo: str) -> List[Dict]:
"""Scan a repository for zombie PRs."""
open_prs = self.get_open_prs(repo)
zombies = []
print(f"Scanning {repo} for zombie PRs...")
print(f"Found {len(open_prs)} open PRs")
for pr in open_prs:
pr_number = pr["number"]
pr_title = pr["title"]
# Check if it's a zombie
zombie_info = self.is_zombie_pr(repo, pr_number)
if zombie_info["is_zombie"]:
zombie_info["title"] = pr_title
zombie_info["author"] = pr["user"]["login"]
zombie_info["created"] = pr["created_at"]
zombies.append(zombie_info)
print(f" 🧟 ZOMBIE: #{pr_number} - {pr_title}")
else:
print(f" ✅ OK: #{pr_number} - {pr_title} ({zombie_info['total_changes']} changes)")
return zombies
def generate_report(self, zombies_by_repo: Dict[str, List[Dict]]) -> str:
"""Generate a report of zombie PRs found."""
total_zombies = sum(len(zombies) for zombies in zombies_by_repo.values())
report = "# Zombie PR Detection Report\n\n"
report += f"## Summary\n"
report += f"- **Total zombie PRs found:** {total_zombies}\n"
report += f"- **Repositories scanned:** {len(zombies_by_repo)}\n\n"
if total_zombies == 0:
report += "✅ **No zombie PRs found.**\n"
else:
report += "⚠️ **Zombie PRs found:**\n\n"
for repo, zombies in zombies_by_repo.items():
if zombies:
report += f"### {repo}\n"
for zombie in zombies:
report += f"- **#{zombie['pr_number']}**: {zombie['title']}\n"
report += f" - Author: {zombie['author']}\n"
report += f" - Created: {zombie['created']}\n"
report += f" - Files changed: {zombie['files_changed']}\n"
report += f" - Total changes: {zombie['total_changes']}\n"
report += "\n"
# Add recommendations
report += "## Recommendations\n"
report += "1. **Close zombie PRs** - PRs with no actual changes should be closed\n"
report += "2. **Validate before merge** - CI should reject PRs with no changes\n"
report += "3. **Prevent future zombies** - Agents should validate changes before creating PRs\n"
report += "4. **Review process** - Reviewers must verify PRs have actual changes\n"
return report
def main():
"""Main entry point for zombie PR checker."""
import argparse
parser = argparse.ArgumentParser(description="Check for zombie PRs (PRs with no actual changes)")
parser.add_argument("--repos", nargs="+",
default=["the-nexus", "timmy-home", "timmy-config", "hermes-agent", "the-beacon"],
help="Repositories to scan")
parser.add_argument("--report", action="store_true", help="Generate report")
parser.add_argument("--json", action="store_true", help="Output JSON instead of report")
args = parser.parse_args()
checker = ZombiePRChecker()
# Scan repositories for zombie PRs
zombies_by_repo = {}
for repo in args.repos:
zombies = checker.scan_repo_for_zombies(repo)
zombies_by_repo[repo] = zombies
# Generate output
if args.json:
print(json.dumps(zombies_by_repo, indent=2))
elif args.report:
report = checker.generate_report(zombies_by_repo)
print(report)
else:
# Default: show summary
total_zombies = sum(len(zombies) for zombies in zombies_by_repo.values())
print(f"\nZombie PR Detection Complete")
print("=" * 60)
print(f"Total zombie PRs found: {total_zombies}")
if total_zombies > 0:
print("\nZombie PRs:")
for repo, zombies in zombies_by_repo.items():
for zombie in zombies:
print(f" {repo} #{zombie['pr_number']}: {zombie['title']}")
sys.exit(1)
else:
print("\n✅ No zombie PRs found")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,89 @@
# Duplicate PR Prevention System
## Overview
The Nexus uses a multi-layer system to prevent duplicate PRs for the same issue.
## Components
### 1. Pre-flight Check (CI)
The `.github/workflows/pr-duplicate-check.yml` workflow runs on every PR creation and checks if a PR already exists for the same issue.
**How it works:**
1. Extracts issue numbers from PR title and body
2. Queries Gitea API for existing PRs referencing those issues
3. Fails the check if duplicates are found
4. Provides links to existing PRs for review
### 2. Cleanup Script
The `scripts/cleanup-duplicate-prs.sh` script helps clean up existing duplicates:
- Lists all PRs for a given issue
- Identifies duplicates
- Provides commands to close duplicates
### 3. Milestone Checker
The `bin/check_duplicate_milestones.py` script prevents duplicate milestones:
- Scans all milestones in the repo
- Identifies duplicates by title
- Reports for manual cleanup
## Usage
### Check for Duplicates Before Creating PR
```bash
# Check if issue already has PRs
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=open" \
| jq '.[] | select(.body | contains("#ISSUE_NUMBER"))'
```
### Clean Up Existing Duplicates
```bash
# List PRs for issue
./scripts/cleanup-duplicate-prs.sh --issue 1128
# Close duplicates (keep newest)
./scripts/cleanup-duplicate-prs.sh --issue 1128 --close-duplicates
```
## Example: Issue #1500
Issue #1500 documented that the pre-flight check successfully prevented a duplicate PR for #1474.
**What happened:**
1. Dispatch attempted to work on #1474
2. Pre-flight check found 2 existing PRs (#1495, #1493)
3. System prevented creating a 3rd duplicate
4. Issue #1500 was filed as an observation
**Result:** The system worked as intended.
## Best Practices
1. **Always check before creating PRs** — use the pre-flight check
2. **Close duplicates promptly** — don't let them accumulate
3. **Reference issues in PRs** — makes duplicate detection possible
4. **Use descriptive branch names** — helps identify purpose
5. **Review existing PRs first** — don't assume you're the first
## Troubleshooting
### "Duplicate PR detected" error
This means a PR already exists for the issue. Options:
1. Review the existing PR and contribute to it
2. Close your PR if it's truly a duplicate
3. Update your PR to address a different aspect
### Pre-flight check not running
Check that `.github/workflows/pr-duplicate-check.yml` exists and is enabled.
### False positives
The check looks for issue numbers in PR body. If you're referencing an issue without intending to fix it, use "Refs #" instead of "Fixes #".

View File

@@ -1,189 +0,0 @@
# Preventing Rubber-Stamping of PRs
**Issue:** #1445 - process: Prevent rubber-stamping of PRs with no changes
**Problem:** PRs with no changes (zombie PRs) are being approved without actual review
## What is Rubber-Stamping?
Rubber-stamping occurs when:
1. A PR has 0 additions, 0 deletions, and 0 files changed (zombie PR)
2. Reviewers approve the PR without noticing it has no changes
3. The PR gets merged despite adding no value
This is a serious process issue because:
- It wastes reviewer time
- It creates false sense of review quality
- It allows zombie PRs to appear reviewed
- It clutters the PR backlog
## Prevention Measures
### 1. CI Check (`.gitea/workflows/check-pr-changes.yml`)
Automated check that runs on every PR:
- Detects PRs with no changes
- Blocks merge if PR is a zombie
- Provides clear error messages
**What it checks:**
- PR has additions, deletions, or file changes
- Commits contain actual changes
- No empty diffs
**When it runs:**
- On PR open
- On PR synchronize (new commits)
- On PR reopen
### 2. PR Template (`.github/PULL_REQUEST_TEMPLATE.md`)
Updated PR template with reviewer guidelines:
- Clear checklist for reviewers
- Explicit instructions to check for changes
- Warning against rubber-stamping
**Reviewer requirements:**
1. Verify PR has actual changes
2. Changes match description
3. Code quality review
4. Tests are adequate
5. Documentation is updated
### 3. Zombie PR Detection Script (`bin/check_zombie_prs.py`)
Script to scan for zombie PRs:
- Check all open PRs in repositories
- Identify PRs with no changes
- Generate reports
**Usage:**
```bash
# Scan all repositories
python bin/check_zombie_prs.py
# Scan specific repositories
python bin/check_zombie_prs.py --repos the-nexus timmy-home
# Generate report
python bin/check_zombie_prs.py --report
# JSON output
python bin/check_zombie_prs.py --json
```
## How to Use
### For CI/CD
The workflow runs automatically on all PRs. No setup needed.
### For Developers
1. **Before creating PR:**
- Ensure you have actual changes
- Test your changes locally
- Don't create PRs with no changes
2. **When reviewing PRs:**
- Check that PR has additions, deletions, or file changes
- Verify changes match the PR description
- Don't approve PRs with no changes
3. **If you find a zombie PR:**
- Add a comment explaining it has no changes
- Request changes or close the PR
- Don't approve it
### For Agents (AI Workers)
Before creating a PR:
```bash
# Check if you have changes
git status
git diff --stat
# If no changes, don't create PR
# If changes exist, create PR
```
## Examples
### Zombie PR Detected
```
❌ ERROR: PR has no changes!
This PR has 0 additions, 0 deletions, and 0 files changed.
This is a 'zombie PR' that should not be merged.
Rubber-stamping (approving PRs with no changes) is prohibited.
Reviewers must verify that PRs contain actual changes.
If this is a mistake, please add actual changes to the PR.
If this PR is not needed, please close it.
```
### Valid PR
```
✅ PR has changes:
README.md | 10 ++++++++++
1 file changed, 10 insertions(+)
Summary:
Files changed: 1
Additions: 10
Deletions: 0
```
## Related Issues
- **Issue #1127:** Perplexity Evening Pass triage (identified rubber-stamping)
- **Issue #1445:** This implementation
- **PR #359:** Example of rubber-stamping (3 approvals on empty PR)
## Prevention Strategy
### 1. **Automated Checks**
- CI workflow blocks zombie PRs
- Pre-commit hooks validate changes
- Automated scanning for zombie PRs
### 2. **Process Guidelines**
- Updated PR template with reviewer guidelines
- Clear instructions to check for changes
- Training on rubber-stamping prevention
### 3. **Monitoring**
- Regular scans for zombie PRs
- Reports on rubber-stamping incidents
- Continuous improvement of prevention measures
## Files Added
1. `.gitea/workflows/check-pr-changes.yml` - CI workflow
2. `.github/PULL_REQUEST_TEMPLATE.md` - Updated PR template
3. `bin/check_zombie_prs.py` - Zombie PR detection script
4. `docs/rubber-stamping-prevention.md` - This documentation
## Testing
Test the CI workflow:
```bash
# Create a test PR with no changes
git checkout -b test/zombie-pr
git commit --allow-empty -m "test: empty commit"
git push origin test/zombie-pr
# Create PR and watch CI fail
```
Test the detection script:
```bash
python bin/check_zombie_prs.py --repos the-nexus --report
```
## Conclusion
This implementation provides comprehensive protection against rubber-stamping:
1. **Automated CI checks** block zombie PRs
2. **Updated PR template** guides reviewers
3. **Detection script** identifies existing zombie PRs
4. **Documentation** explains the problem and solution
**Result:** No more rubber-stamping of PRs with no changes.
## License
Part of the Timmy Foundation project.