-
-
Notifications
You must be signed in to change notification settings - Fork 272
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 4 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,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' }}" | ||||||||||||||
|
||||||||||||||
| 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 }}" |
|
| 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. |
| 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 | ||
| ), | ||
| ), | ||
| ] |
Uh oh!
There was an error while loading. Please reload this page.