- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 243
feat: Implement auto bounty payout for merged PRs (fixes #3941) #4633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 11 commits
6c8600a
              b227de1
              670b74a
              4c07b18
              763aabc
              f01ff13
              79e07fc
              0e55e13
              bc50cff
              c0f0f90
              89c091a
              7d32de3
              c592174
              a9dd3b6
              535c6d9
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| name: Auto Bounty Payout | ||
|  | ||
| on: | ||
| pull_request: | ||
| types: [closed] | ||
|  | ||
| jobs: | ||
| process-bounty: | ||
| # Only run if PR was merged (not just closed) | ||
| if: github.event.pull_request.merged == true | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| issues: write | ||
| pull-requests: read | ||
| contents: read | ||
|  | ||
| steps: | ||
| - name: Extract linked issues from PR | ||
| id: extract_issues | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| PR_BODY: ${{ github.event.pull_request.body }} | ||
| run: | | ||
| pr_number="${{ github.event.pull_request.number }}" | ||
|  | ||
| echo "PR #$pr_number was merged" | ||
| echo "PR Body: $PR_BODY" | ||
|  | ||
| # Extract issue numbers from PR body | ||
| # Looks for patterns like "Fixes #123", "Closes #456", "Resolves #789" | ||
| issue_numbers=$(echo "$PR_BODY" | grep -oiE "(close[sd]?|fix(e[sd])?|resolve[sd]?) #[0-9]+" | grep -oE "#[0-9]+" | sed 's/#//' || echo "") | ||
|  | ||
| if [ -z "$issue_numbers" ]; then | ||
| echo "No linked issues found in PR body" | ||
| echo "has_issues=false" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "Found linked issues: $issue_numbers" | ||
| echo "has_issues=true" >> $GITHUB_OUTPUT | ||
| # Convert to JSON array | ||
| issue_array=$(echo "$issue_numbers" | jq -R -s -c 'split("\n") | map(select(length > 0))') | ||
| echo "issue_numbers=$issue_array" >> $GITHUB_OUTPUT | ||
| fi | ||
|  | ||
| - name: Check for bounty labels and process payment | ||
| if: steps.extract_issues.outputs.has_issues == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| ISSUE_NUMBERS: ${{ steps.extract_issues.outputs.issue_numbers }} | ||
| run: | | ||
| echo "Processing issues: $ISSUE_NUMBERS" | ||
|  | ||
| # Parse JSON array | ||
| for issue_num in $(echo "$ISSUE_NUMBERS" | jq -r '.[]'); do | ||
| echo "Checking issue #$issue_num for bounty labels..." | ||
|  | ||
| # Get issue details | ||
| issue_data=$(gh api repos/${{ github.repository }}/issues/$issue_num) | ||
|  | ||
| # Check if issue has a dollar label (bounty) | ||
| has_bounty=$(echo "$issue_data" | jq -r '.labels[] | select(.name | startswith("$")) | .name' | head -n 1) | ||
|  | ||
| if [ -n "$has_bounty" ]; then | ||
| echo "Issue #$issue_num has bounty label: $has_bounty" | ||
|  | ||
| # Construct issue URL | ||
| issue_url="https://github.com/${{ github.repository }}/issues/$issue_num" | ||
| pr_url="${{ github.event.pull_request.html_url }}" | ||
|  | ||
| echo "Sending bounty payout request to BLT API..." | ||
|  | ||
| # Call the BLT bounty payout API | ||
| # BLT_API_URL must be set in repository secrets/variables | ||
| # This prevents accidental production calls during testing | ||
| # Add BLT_API_TOKEN to repository secrets for authentication | ||
| if [ -z "${{ secrets.BLT_API_URL }}" ]; then | ||
| echo "❌ BLT_API_URL is not set. Refusing to call production API by default." | ||
| echo "Please set BLT_API_URL in repository secrets/variables." | ||
| echo "For production: https://blt.owasp.org" | ||
| echo "For testing: your test instance URL" | ||
| exit 1 | ||
| fi | ||
| api_url="${{ secrets.BLT_API_URL }}" | ||
|  | ||
| echo "Using API URL: $api_url" | ||
|  | ||
| response=$(curl -X POST \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "X-BLT-API-TOKEN: ${{ secrets.BLT_API_TOKEN }}" \ | ||
| -d "{\"issue_url\": \"$issue_url\", \"pr_url\": \"$pr_url\"}" \ | ||
| "${api_url}/api/bounty_payout/" \ | ||
| -w "\n%{http_code}" -s) | ||
|  | ||
| http_code=$(echo "$response" | tail -n 1) | ||
| response_body=$(echo "$response" | head -n -1) | ||
|  | ||
| echo "API Response (HTTP $http_code): $response_body" | ||
|  | ||
| if [ "$http_code" -eq 200 ]; then | ||
| echo "✅ Successfully initiated bounty payout for issue #$issue_num" | ||
|  | ||
| # Add a comment to the issue | ||
| gh issue comment $issue_num --body "🎉 Bounty payout has been initiated for this issue! The payment will be processed via GitHub Sponsors." | ||
| else | ||
| echo "⚠️ Failed to process bounty payout for issue #$issue_num" | ||
| echo "Response: $response_body" | ||
|  | ||
| # Add a comment about the failure | ||
| gh issue comment $issue_num --body "⚠️ Automated bounty payout failed. Please contact an administrator. Error: $response_body" | ||
| fi | ||
| else | ||
| echo "Issue #$issue_num does not have a bounty label, skipping..." | ||
| fi | ||
| done | ||
|  | ||
| - name: Summary | ||
| if: always() | ||
| run: | | ||
| echo "Auto bounty payout workflow completed" | ||
| echo "PR: ${{ github.event.pull_request.html_url }}" | ||
| echo "Merged by: ${{ github.event.pull_request.merged_by.login }}" | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -631,3 +631,19 @@ | |
| } | ||
| THROTTLE_WINDOW = 60 # 60 seconds (1 minute) | ||
| THROTTLE_EXEMPT_PATHS = ["/admin/", "/static/", "/media/"] | ||
|  | ||
| # Bounty Payout Configuration | ||
| # API token for authenticating bounty payout requests | ||
| BLT_API_TOKEN = os.environ.get("BLT_API_TOKEN") | ||
| # GitHub username that sponsors will be created from (default: DonnieBLT) | ||
| GITHUB_SPONSOR_USERNAME = os.environ.get("GITHUB_SPONSOR_USERNAME", "DonnieBLT") | ||
| # Allowlist of repositories eligible for automated bounty payouts (prevents budget drain attacks) | ||
| # Format: {"owner/repo", "another-org/another-repo"} | ||
| BLT_ALLOWED_BOUNTY_REPOS = { | ||
| os.environ.get("BLT_ALLOWED_BOUNTY_REPO_1", "OWASP-BLT/BLT"), | ||
| } | ||
| # Add additional repos from environment if configured | ||
| if os.environ.get("BLT_ALLOWED_BOUNTY_REPO_2"): | ||
| BLT_ALLOWED_BOUNTY_REPOS.add(os.environ.get("BLT_ALLOWED_BOUNTY_REPO_2")) | ||
| if os.environ.get("BLT_ALLOWED_BOUNTY_REPO_3"): | ||
| BLT_ALLOWED_BOUNTY_REPOS.add(os.environ.get("BLT_ALLOWED_BOUNTY_REPO_3")) | ||
| 
      Comment on lines
    
      +651
     to 
      +666
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainDocument these settings for production deployment. The bounty payout configuration requires environment variables to function properly. Ensure deployment documentation includes: 
 Consider verifying these are documented in the deployment guide or README: 🏁 Script executed: #!/bin/bash
# Check if deployment documentation mentions these new environment variables
rg -i "BLT_API_TOKEN|GITHUB_SPONSOR_USERNAME|BLT_ALLOWED_BOUNTY_REPO" --type=mdLength of output: 0 The markdown search returned no results. Let me conduct a broader search to verify whether these environment variables are documented anywhere in the codebase, including non-markdown formats and environment configuration files. 🏁 Script executed: #!/bin/bash
# Search more broadly for documentation of these variables
# Check all file types, not just markdown
echo "=== Searching entire codebase for these variables ==="
rg -i "BLT_API_TOKEN|GITHUB_SPONSOR_USERNAME|BLT_ALLOWED_BOUNTY_REPO" -C2
echo ""
echo "=== Checking for .env files or documentation templates ==="
fd -e env -e sample -e example -e txt -e rst | head -20
echo ""
echo "=== Looking for docs or documentation directories ==="
fd -t d "doc|guide|config" | head -20Length of output: 2678 Let me verify what deployment documentation exists: 🏁 Script executed: #!/bin/bash
# Check deployment and README documentation
echo "=== Files in docs/ directory ==="
find docs -type f | head -30
echo ""
echo "=== Root README files ==="
ls -la | grep -i readme
echo ""
echo "=== Checking for deployment/setup docs ==="
rg -i "deployment|setup|environment|configure" docs/ --type=md 2>/dev/null | head -20Length of output: 1571 Let me check the specific content of the setup documentation to see if these bounty variables are mentioned: 🏁 Script executed: #!/bin/bash
# Check Setup.md for bounty variables
echo "=== Searching Setup.md for bounty-related variables ==="
rg "BLT_API_TOKEN|GITHUB_SPONSOR|ALLOWED_BOUNTY" docs/Setup.md
echo ""
echo "=== Searching bot-setup.md for bounty-related variables ==="
rg "BLT_API_TOKEN|GITHUB_SPONSOR|ALLOWED_BOUNTY" docs/bot-setup.md
echo ""
echo "=== Check if .env.example exists ==="
find . -name ".env.example" -o -name "*.env*"
echo ""
echo "=== View Setup.md environment variables section ==="
sed -n '/environment variables/,/^## /Ip' docs/Setup.md | head -40Length of output: 679 🏁 Script executed: #!/bin/bash
# Check .env.example for bounty configuration
echo "=== Full content of .env.example ==="
cat .env.exampleLength of output: 1412 Add bounty payout environment variables to  The bounty payout configuration is missing from  
 Add these to  🤖 Prompt for AI Agents | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Generated migration for preferred_payment_method field | ||
|  | ||
| from django.db import migrations, models | ||
|  | ||
|  | ||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("website", "0246_add_user_progress_models"), | ||
| ] | ||
|  | ||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="userprofile", | ||
| name="preferred_payment_method", | ||
| field=models.CharField( | ||
| blank=True, | ||
| choices=[("sponsors", "GitHub Sponsors"), ("bch", "Bitcoin Cash")], | ||
| default="sponsors", | ||
| help_text="Preferred payment method for bounty payouts", | ||
| max_length=20, | ||
| null=True, | ||
| ), | ||
| ), | ||
| ] | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| from django.db import migrations, models | ||
|  | ||
|  | ||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("website", "0247_userprofile_preferred_payment_method"), | ||
| ] | ||
|  | ||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="githubissue", | ||
| name="sponsors_cancellation_failed", | ||
| field=models.BooleanField(default=False), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="githubissue", | ||
| name="sponsors_cancellation_attempts", | ||
| field=models.IntegerField(default=0), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="githubissue", | ||
| name="sponsors_cancellation_last_attempt", | ||
| field=models.DateTimeField(null=True, blank=True), | ||
| ), | ||
| ] | 
Uh oh!
There was an error while loading. Please reload this page.