Compare commits

...

1 Commits

Author SHA1 Message Date
Alexander Whitestone
59784e620c fix: #1445
Some checks failed
Check PR for Changes / check-changes (pull_request) Successful in 21s
Review Approval Gate / verify-review (pull_request) Successful in 9s
CI / test (pull_request) Failing after 1m46s
CI / validate (pull_request) Failing after 1m47s
- Add CI workflow to check for empty PRs
- Update PR template with reviewer guidelines
- Add zombie PR detection script
- Add documentation for rubber-stamping prevention

Prevents rubber-stamping of PRs with no changes by:
1. Automated CI checks that block zombie PRs
2. Clear reviewer guidelines in PR template
3. Detection script for existing zombie PRs
4. Comprehensive documentation

Addresses issue #1445: process: Prevent rubber-stamping of PRs with no changes
2026-04-14 23:36:34 -04:00
4 changed files with 570 additions and 64 deletions

View File

@@ -0,0 +1,116 @@
# .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,65 +1,73 @@
## 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 -->
---
**⚠️ 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
**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

193
bin/check_zombie_prs.py Executable file
View File

@@ -0,0 +1,193 @@
#!/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,189 @@
# 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.