-
-
Notifications
You must be signed in to change notification settings - Fork 261
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 1 commit
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,104 @@ | ||
| 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 | ||
|
|
||
| steps: | ||
| - name: Extract linked issues from PR | ||
| id: extract_issues | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| pr_number="${{ github.event.pull_request.number }}" | ||
| pr_body="${{ github.event.pull_request.body }}" | ||
| 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 "") | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| # Note: Replace with actual BLT production URL | ||
| # Add BLT_API_TOKEN to repository secrets for authentication | ||
| 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\"}" \ | ||
| https://blt.owasp.org/api/bounty_payout/ \ | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| -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 |
|---|---|---|
| @@ -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 | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3742,3 +3742,212 @@ | |||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||
| logger.error(f"Error extracting payment info from comments for issue #{issue_number}: {str(e)}") | ||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @require_POST | ||||||||||||||||||||||||||
| def process_bounty_payout(request): | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| API endpoint to process bounty payouts for merged PRs with attached issues. | ||||||||||||||||||||||||||
| Called by GitHub Action when a PR is merged. | ||||||||||||||||||||||||||
| Expected POST data: | ||||||||||||||||||||||||||
| - issue_url: URL of the GitHub issue with bounty | ||||||||||||||||||||||||||
| - pr_url: URL of the merged PR (optional, for logging) | ||||||||||||||||||||||||||
| Expected Headers: | ||||||||||||||||||||||||||
| - X-BLT-API-TOKEN: API token for authentication (optional but recommended) | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| # Optional: Validate API token if provided | ||||||||||||||||||||||||||
| api_token = request.headers.get("X-BLT-API-TOKEN") | ||||||||||||||||||||||||||
| expected_token = getattr(settings, "BLT_API_TOKEN", None) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if expected_token and api_token != expected_token: | ||||||||||||||||||||||||||
| logger.warning(f"Invalid API token provided for bounty payout") | ||||||||||||||||||||||||||
| return JsonResponse( | ||||||||||||||||||||||||||
| {"success": False, "error": "Invalid API token"}, | ||||||||||||||||||||||||||
| status=401 | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Parse request data | ||||||||||||||||||||||||||
| data = json.loads(request.body) if request.body else {} | ||||||||||||||||||||||||||
| issue_url = data.get("issue_url") | ||||||||||||||||||||||||||
| pr_url = data.get("pr_url", "") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if not issue_url: | ||||||||||||||||||||||||||
| return JsonResponse( | ||||||||||||||||||||||||||
| {"success": False, "error": "issue_url is required"}, | ||||||||||||||||||||||||||
| status=400 | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| logger.info(f"Processing bounty payout for issue: {issue_url}, PR: {pr_url}") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Extract issue details from URL | ||||||||||||||||||||||||||
| # URL format: https://github.com/owner/repo/issues/number | ||||||||||||||||||||||||||
| parts = issue_url.rstrip("/").split("/") | ||||||||||||||||||||||||||
| if len(parts) < 7 or parts[5] != "issues": | ||||||||||||||||||||||||||
| return JsonResponse( | ||||||||||||||||||||||||||
| {"success": False, "error": "Invalid issue URL format"}, | ||||||||||||||||||||||||||
| status=400 | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| owner = parts[3] | ||||||||||||||||||||||||||
| repo_name = parts[4] | ||||||||||||||||||||||||||
| issue_number = parts[6] | ||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Fetch issue from GitHub API | ||||||||||||||||||||||||||
| headers = {} | ||||||||||||||||||||||||||
| if settings.GITHUB_TOKEN: | ||||||||||||||||||||||||||
| headers["Authorization"] = f"token {settings.GITHUB_TOKEN}" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| api_url = f"https://api.github.com/repos/{owner}/{repo_name}/issues/{issue_number}" | ||||||||||||||||||||||||||
| response = requests.get(api_url, headers=headers, timeout=10) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if response.status_code != 200: | ||||||||||||||||||||||||||
| return JsonResponse( | ||||||||||||||||||||||||||
| {"success": False, "error": f"Failed to fetch issue from GitHub: {response.status_code}"}, | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| return JsonResponse( | |
| {"success": False, "error": f"Failed to fetch issue from GitHub: {response.status_code}"}, | |
| if response.status_code == 404: | |
| error_msg = "GitHub issue not found." | |
| elif response.status_code in (401, 403): | |
| error_msg = "Authentication with GitHub failed or access denied." | |
| elif response.status_code == 429: | |
| error_msg = "GitHub API rate limit exceeded. Please try again later." | |
| else: | |
| error_msg = "Failed to fetch issue from GitHub." | |
| return JsonResponse( | |
| {"success": False, "error": error_msg}, |
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import statements should be placed at the top of the file rather than inside functions for better readability and performance.
Outdated
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import statements should be placed at the top of the file rather than inside functions for better readability and performance.
| from website.models import Repo | |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add the logic to process the payment
Outdated
Copilot
AI
Oct 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using timestamp as a placeholder transaction ID could create conflicts. Consider using UUIDs or a more robust identifier format.
Uh oh!
There was an error while loading. Please reload this page.