Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
120 changes: 120 additions & 0 deletions .github/workflows/auto-bounty-payout.yml
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 }}"
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
6 changes: 6 additions & 0 deletions website/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,11 +582,15 @@ class GitHubIssueAdmin(admin.ModelAdmin):
"merged_at",
"updated_at",
"url",
"sponsors_tx_id",
"p2p_amount_usd",
"p2p_amount_bch",
"sent_by_user",
"p2p_payment_created_at",
"bch_tx_id",
"sponsors_cancellation_failed",
"sponsors_cancellation_attempts",
"sponsors_cancellation_last_attempt",
)
list_filter = [
"type",
Expand All @@ -596,12 +600,14 @@ class GitHubIssueAdmin(admin.ModelAdmin):
"contributor",
"sent_by_user",
"repo",
"sponsors_cancellation_failed",
]
search_fields = [
"title",
"url",
"user_profile__user__username",
"contributor__name",
"sponsors_tx_id",
"bch_tx_id",
]
date_hierarchy = "created_at"
Expand Down
24 changes: 24 additions & 0 deletions website/migrations/0247_userprofile_preferred_payment_method.py
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,
),
),
]
25 changes: 25 additions & 0 deletions website/migrations/0248_githubissue_sponsors_cancel_flags.py
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),
),
]
12 changes: 12 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 Expand Up @@ -1697,6 +1705,10 @@ class GitHubIssue(models.Model):
related_name="github_issue_p2p_payments",
)
bch_tx_id = models.CharField(max_length=255, null=True, blank=True)
# Sponsors cancellation tracking (observability for immediate-cancel flow)
sponsors_cancellation_failed = models.BooleanField(default=False)
sponsors_cancellation_attempts = models.IntegerField(default=0)
sponsors_cancellation_last_attempt = models.DateTimeField(null=True, blank=True)
# Related pull requests
linked_pull_requests = models.ManyToManyField(
"self",
Expand Down
Loading