Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/auto-bounty-payout.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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 can be set in repository secrets/variables for testing
# Defaults to production if not set
# Add BLT_API_TOKEN to repository secrets for authentication
api_url="${{ secrets.BLT_API_URL || 'https://blt.owasp.org' }}"
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded production URL should be moved to a configuration variable or environment-specific setting to avoid accidental production calls during testing.

Suggested change
api_url="${{ secrets.BLT_API_URL || 'https://blt.owasp.org' }}"
if [ -z "${{ secrets.BLT_API_URL }}" ]; then
echo "❌ BLT_API_URL is not set. Refusing to call production API by default. Please set BLT_API_URL in repository secrets/variables."
exit 1
fi
api_url="${{ secrets.BLT_API_URL }}"

Copilot uses AI. Check for mistakes.
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 }}"
68 changes: 68 additions & 0 deletions PR_DESCRIPTION.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
Fixes #3941

## Summary

This PR implements automatic bounty payment processing when a pull request is merged that closes an issue with a bounty label. The system detects merged PRs, extracts linked issues, verifies bounty labels, and initiates payment through the GitHub Sponsors API.

## Changes

### Database
- Added `preferred_payment_method` field to UserProfile model to store user's payment preference (GitHub Sponsors or Bitcoin Cash)
- Created migration `0247_userprofile_preferred_payment_method.py`

### Backend API
- Created new endpoint `/api/bounty_payout/` in `website/views/organization.py`
- Accepts POST requests with issue URL and optional PR URL
- Validates issue has bounty label and assignee
- Checks if user exists in BLT with linked GitHub account
- Prevents duplicate payments by checking existing transaction IDs
- Returns appropriate error messages for various failure cases
- Added optional authentication via `X-BLT-API-TOKEN` header
- Added URL route in `blt/urls.py`

### GitHub Actions
- Created workflow `.github/workflows/auto-bounty-payout.yml`
- Triggers when PR is merged (not just closed)
- Extracts linked issues from PR body using regex patterns
- Checks each issue for dollar-amount labels ($5, $10, etc.)
- Calls BLT API endpoint with issue details
- Posts comment on issue confirming payment initiation or reporting errors

## Implementation Details

The workflow looks for PR descriptions containing "Fixes #123", "Closes #456", or "Resolves #789" patterns to identify linked issues. For each linked issue with a bounty label, it makes an API call to process the payment.

The API endpoint validates the request, fetches issue details from GitHub, verifies the assignee has a BLT account with connected GitHub profile, checks their payment preference, and records the transaction. Currently uses placeholder transaction IDs pending full GitHub Sponsors GraphQL API integration.

## Improvements Over Previous Attempt (PR #4236)

Based on CodeRabbit's review of the previous implementation, this version addresses several critical issues:
- Uses curl for API calls instead of github.request() which only works for GitHub's own API
- Properly reads authentication headers using request.headers.get() instead of making HTTP requests
- No duplicate class definitions
- Consistent data types throughout
- Correct migration dependencies
- Added duplicate payment prevention

## Known Limitations

- GitHub Sponsors API integration uses placeholder transaction IDs. Full GraphQL API implementation will be added in future work.
- Only GitHub Sponsors payment method is currently implemented. Bitcoin Cash support is planned.
- Requires BLT_API_TOKEN to be configured in settings and GitHub repository secrets.
- Workflow API URL needs to be updated to production URL before deployment.

## Testing

All Python files compile without syntax errors. Migration dependency has been verified against latest migration (0246). Error handling covers all expected failure cases including missing assignee, invalid issue URL, user not found, and duplicate payments.

Manual testing required after deployment to verify end-to-end workflow.

## Deployment Notes

1. Run migration: `python manage.py migrate`
2. Add BLT_API_TOKEN to Django settings
3. Add BLT_API_TOKEN to GitHub repository secrets
4. Update API URL in workflow file (line 74) to production URL
5. Restart application server

This is my first contribution to the project. I've tried to follow the existing code patterns and address feedback from previous attempts. Happy to make any changes based on review feedback.
2 changes: 2 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@
organization_dashboard_hunt_detail,
organization_dashboard_hunt_edit,
organization_hunt_results,
process_bounty_payout,
room_messages_api,
send_message_api,
sizzle,
Expand Down Expand Up @@ -642,6 +643,7 @@
re_path(r"^hunt/$", login_required(HuntCreate.as_view()), name="hunt"),
re_path(r"^bounties/$", Listbounties.as_view(), name="hunts"),
path("bounties/payouts/", BountyPayoutsView.as_view(), name="bounty_payouts"),
path("api/bounty_payout/", csrf_exempt(process_bounty_payout), name="process_bounty_payout"),
path("api/load-more-issues/", load_more_issues, name="load_more_issues"),
re_path(r"^invite/$", InviteCreate.as_view(template_name="invite.html"), name="invite"),
re_path(r"^terms/$", TemplateView.as_view(template_name="terms.html"), name="terms"),
Expand Down
25 changes: 25 additions & 0 deletions website/migrations/0247_userprofile_preferred_payment_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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
),
),
]
8 changes: 8 additions & 0 deletions website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,14 @@ class UserProfile(models.Model):
btc_address = models.CharField(max_length=100, blank=True, null=True, validators=[validate_btc_address])
bch_address = models.CharField(max_length=100, blank=True, null=True, validators=[validate_bch_address])
eth_address = models.CharField(max_length=100, blank=True, null=True)
preferred_payment_method = models.CharField(
max_length=20,
choices=[("sponsors", "GitHub Sponsors"), ("bch", "Bitcoin Cash")],
default="sponsors",
blank=True,
null=True,
help_text="Preferred payment method for bounty payouts",
)
created = models.DateTimeField(auto_now_add=True)
tags = models.ManyToManyField(Tag, blank=True)
x_username = models.CharField(max_length=50, blank=True, null=True)
Expand Down
Loading
Loading