BLT icon indicating copy to clipboard operation
BLT copied to clipboard

Have the ability to set a timed bounty on an issue

Open DonnieBLT opened this issue 9 months ago • 14 comments

Variable for hours.

DonnieBLT avatar Mar 09 '25 09:03 DonnieBLT

/assign

Shashwat-srivastav avatar Mar 09 '25 10:03 Shashwat-srivastav

You cannot be assigned to this issue because you are already assigned to the following issues without an open pull request: #3725. Please submit a pull request for these issues before getting assigned to a new one.

github-actions[bot] avatar Mar 09 '25 10:03 github-actions[bot]

for github?

igennova avatar Mar 09 '25 12:03 igennova

/assign

igennova avatar Mar 09 '25 12:03 igennova

Hello @igennova! You've been assigned to OWASP-BLT/BLT issue #3883. You have 24 hours to complete a pull request.

github-actions[bot] avatar Mar 09 '25 12:03 github-actions[bot]

Yes, this will require some code and the BLT action and BLT core

DonnieBLT avatar Mar 09 '25 17:03 DonnieBLT

/unassign

igennova avatar Mar 09 '25 18:03 igennova

You have been unassigned from this issue. It’s now open for others. You can reassign it anytime by typing /assign.

github-actions[bot] avatar Mar 09 '25 18:03 github-actions[bot]

/assign

vidipsingh avatar Mar 13 '25 09:03 vidipsingh

Hello @vidipsingh! You've been assigned to OWASP-BLT/BLT issue #3883. You have 24 hours to complete a pull request.

github-actions[bot] avatar Mar 13 '25 09:03 github-actions[bot]

Hi @DonnieBLT,

I’ve made the changes to support timed bounties in both repositories and created the following PRs:

  • OWASP/BLT: #3932
  • OWASP-BLT/BLT-Action: #67

Can you please review them and let me know if I’m using the right approach, or if there’s another way to solve this issue?

Thanks!

vidipsingh avatar Mar 13 '25 09:03 vidipsingh

⏰ This issue has been automatically unassigned due to 24 hours of inactivity. The issue is now available for anyone to work on again.

github-actions[bot] avatar Mar 14 '25 11:03 github-actions[bot]

@coderabbit code this

igennova avatar Mar 16 '25 17:03 igennova

[!TIP] For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Clone and explore the OWASP/BLT repository
git clone https://github.com/OWASP-BLT/BLT.git
cd BLT

# List directories to understand the project structure
echo "OWASP/BLT directory structure:"
ls -la

# Check if there's a models.py file to understand the data models
echo -e "\nLooking for models.py files:"
find . -name "models.py" | grep -v "__pycache__" | xargs ls -la

# Search for bounty-related code in models
echo -e "\nSearching for bounty-related code in models:"
find . -name "models.py" | xargs grep -i "bounty" || echo "No direct mentions of bounty in models.py"

# Look for views related to bounties
echo -e "\nSearching for bounty-related views:"
find . -name "views.py" | xargs grep -i "bounty" || echo "No direct mentions of bounty in views.py"

# Check for specific files that might be related to bounties
echo -e "\nSearching for files specifically related to bounties:"
find . -type f -name "*bounty*"

# Check existing database migrations to understand the current schema
echo -e "\nListing migration files:"
find . -name "migrations" -type d | xargs ls -la

Length of output: 27905


🏁 Script executed:

#!/bin/bash
# Examine the website/models.py file to understand relevant models
echo "Examining website/models.py for issue-related models:"
grep -n "class Issue" -A 20 BLT/website/models.py || echo "Issue class not found"

# Let's look for any existing bounty or reward related models
echo -e "\nSearching for bounty or reward related classes in models:"
grep -n "class" BLT/website/models.py | grep -E "Bounty|Hunt|Reward|Prize"

# Check if there's any GitHub-related functionality
echo -e "\nSearching for GitHub-related models or functions:"
grep -n "GitHub" BLT/website/models.py | head -20

# Let's also look at the PRs that vidipsingh mentioned
echo -e "\nLooking at PR #3932 in OWASP/BLT:"
gh pr view 3932 --repo OWASP/BLT || echo "Could not access PR #3932"

echo -e "\nLooking at PR #67 in OWASP-BLT/BLT-Action:"
gh pr view 67 --repo OWASP-BLT/BLT-Action || echo "Could not access PR #67"

# Let's check the GitHubIssue model which might be relevant
echo -e "\nExamining GitHubIssue model:"
grep -n "class GitHubIssue" -A 30 BLT/website/models.py || echo "GitHubIssue class not found"

Length of output: 8119


🏁 Script executed:

#!/bin/bash
# Let's examine the PR files in more detail
echo "Examining PR #3932 file changes:"
gh pr view 3932 --repo OWASP/BLT --file assign-issues.yml

echo -e "\nExamining PR #67 file changes:"
gh pr view 67 --repo OWASP-BLT/BLT-Action --files

# Let's also check the existing Hunt model to understand how bounties work
echo -e "\nExamining Hunt model in detail:"
grep -n "class Hunt" -A 35 BLT/website/models.py

# Let's check if there's any existing code for bounty expiration or timing
echo -e "\nSearching for expiry or deadline related code:"
grep -i -E "expire|deadline|timer|duration|hours" BLT/website/models.py | head -20

# Let's check the GitHub workflow file that would need modification
echo -e "\nExamining the current assign-issues.yml file:"
cat BLT/.github/workflows/assign-issues.yml 2>/dev/null || echo "assign-issues.yml file not found"

Length of output: 5634


🏁 Script executed:

#!/bin/bash
# Let's check out the specific changes in the PRs
echo "Checking PR #3932 changes:"
gh pr checkout 3932 --repo OWASP/BLT
git diff --name-only HEAD~1 HEAD
cat .github/workflows/assign-issues.yml

echo -e "\nNow checking PR #67 changes:"
cd ..
git clone https://github.com/OWASP-BLT/BLT-Action.git
cd BLT-Action
gh pr checkout 67 --repo OWASP-BLT/BLT-Action
git diff --name-only HEAD~1 HEAD
echo -e "\nChanges to action.yml:"
git diff HEAD~1 HEAD -- action.yml
echo -e "\nChanges to src/index.js:"
git diff HEAD~1 HEAD -- src/index.js

# Let's also check GitHubIssue model for any existing bounty-related fields
cd ../BLT
echo -e "\nSearching for bounty-related fields in GitHubIssue model:"
grep -n -A 5 -B 5 "bounty" BLT/website/models.py || echo "No direct bounty references found in the model"

Length of output: 8038


@igennova I'll implement the timed bounty feature across both repositories. Based on the PR examination, I need to code changes for two repositories.

1. OWASP/BLT Repository

First, let's modify the .github/workflows/assign-issues.yml file to support timed bounties:

name: OWASP BLT Action

on:
  issue_comment:
    types: [created]
  schedule:
    - cron: '0 0 * * *'
  workflow_dispatch:

jobs:
  auto-assign:
    runs-on: ubuntu-latest
    permissions: 
      issues: write
    steps:
      - name: OWASP BLT Action
        uses: OWASP-BLT/BLT-Action@main
        with:
            repo-token: ${{ secrets.GITHUB_TOKEN }}
            repository: ${{ github.repository }}
            bounty-amount: ${{ github.event.issue.body.bounty || 0 }}
            bounty-hours: ${{ github.event.issue.body.hours || 24 }}

2. OWASP-BLT/BLT-Action Repository

Now, let's update the BLT-Action to support timed bounties:

  1. First, modify action.yml:
name: 'OWASP BLT-Action Auto-assign Issue and unassign after 5 days'
description: 'Auto-assigns issues to users and manages timed bounties'
inputs:
    repo-token:
        description: 'The GITHUB_TOKEN, needed to update the Issue'
        required: true
    repository:
        description: 'The GITHUB_REPOSITORY, needed to update the Issue'
        required: true
    bounty-amount:
        description: 'The amount of the bounty in dollars'
        required: false
        default: '0'
    bounty-hours:
        description: 'The duration in hours for the bounty deadline'
        required: false
        default: '24'

runs:
    using: 'node20'
    main: 'dist/index.js'
  1. Then, update src/index.js to handle the timed bounty logic:
const core = require('@actions/core');
const github = require('@actions/github');

const run = async () => {
    try {
        console.log('Starting OWASP BLT Auto-assign action...');

        // Get necessary inputs
        const gitHubToken = core.getInput('repo-token', { required: true });
        const repository = core.getInput('repository', { required: true }); // Use the provided repository input
        const bountyAmount = parseFloat(core.getInput('bounty-amount')) || 0;
        const bountyHours = parseInt(core.getInput('bounty-hours')) || 24;
        const octokit = github.getOctokit(gitHubToken);

        const { eventName, payload } = github.context;
        const { issue, comment } = payload;
        const [owner, repoName] = repository.split('/'); // Split the provided repository into owner and repo

        console.log(`Processing event: ${eventName} in repository ${repository} with bounty $${bountyAmount} and ${bountyHours} hours deadline`);

        // Assignment keywords
        const assignKeywords = ['/assign', 'assign to me', 'assign this to me', 'assign it to me', 'assign me this', 'work on this', 'i can try fixing this', 'i am interested in doing this', 'be assigned this', 'i am interested in contributing'];
        // Unassignment keywords
        const unassignKeywords = ['/unassign', 'unassign me', 'unassign this from me', 'remove me from this', 'don\'t want to work on this'];

        // Handle issue comment event
        if (eventName === 'issue_comment' && issue && comment) {
            console.log(`Processing comment: ${comment.body} in issue #${issue.number}`);
            
            const commentBody = comment.body.toLowerCase();
            const commentUser = comment.user.login;

            // Process assignment request
            if (assignKeywords.some(keyword => commentBody.includes(keyword.toLowerCase()))) {
                console.log(`Assignment requested by ${commentUser} for issue #${issue.number}`);
                
                // Check if issue is already assigned
                if (issue.assignee) {
                    console.log(`Issue #${issue.number} is already assigned to ${issue.assignee.login}`);
                    await octokit.issues.createComment({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        body: `@${commentUser} Sorry, this issue is already assigned to @${issue.assignee.login}.`
                    });
                    return;
                }

                // Assign the issue to the commenter
                try {
                    await octokit.issues.update({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        assignees: [commentUser]
                    });

                    // Add 'assigned' label if it doesn't exist
                    await octokit.issues.addLabels({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        labels: ["assigned"]
                    });

                    const bountyMessage = bountyAmount > 0 ? ` A bounty of $${bountyAmount} is available with a ${bountyHours}-hour deadline.` : '';
                    await octokit.issues.createComment({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        body: `Hello @${commentUser}! You've been assigned to [${repository} issue #${issue.number}](https://github.com/${repository}/issues/${issue.number}). You have ${bountyHours} hours to complete a pull request.${bountyMessage}`
                    });
                } catch (error) {
                    console.error(`Error assigning issue #${issue.number} to ${commentUser}:`, error);
                }
            }
            
            // Process unassignment request
            else if (unassignKeywords.some(keyword => commentBody.includes(keyword.toLowerCase()))) {
                console.log(`Unassignment requested by ${commentUser} for issue #${issue.number}`);
                
                // Check if issue is assigned to the commenter
                if (issue.assignee && issue.assignee.login === commentUser) {
                    try {
                        await octokit.issues.removeAssignees({
                            owner,
                            repo: repoName,
                            issue_number: issue.number,
                            assignees: [commentUser]
                        });
                        
                        // Remove 'assigned' label
                        try {
                            const labels = issue.labels.filter(label => label.name !== 'assigned').map(label => label.name);
                            await octokit.issues.setLabels({
                                owner,
                                repo: repoName,
                                issue_number: issue.number,
                                labels
                            });
                        } catch (labelError) {
                            console.error(`Error removing 'assigned' label from issue #${issue.number}:`, labelError);
                        }
                        
                        await octokit.issues.createComment({
                            owner,
                            repo: repoName,
                            issue_number: issue.number,
                            body: `@${commentUser} You have been unassigned from this issue. The issue is now available for anyone to work on.`
                        });
                    } catch (error) {
                        console.error(`Error unassigning issue #${issue.number} from ${commentUser}:`, error);
                    }
                } else {
                    await octokit.issues.createComment({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        body: `@${commentUser} You are not assigned to this issue.`
                    });
                }
            }
        }
        
        // Handle scheduled event for checking stale assignments
        else if (eventName === 'schedule' || eventName === 'workflow_dispatch') {
            const assigneeLogin = issue?.assignee?.login;
            
            // Process new assignment directly from workflow dispatch
            if (eventName === 'workflow_dispatch' && issue && !issue.assignee) {
                const assigneeLogin = commentUser || issue.user.login;
                console.log(`Assigning issue #${issue.number} to ${assigneeLogin} from workflow dispatch`);
                
                try {
                    await octokit.issues.update({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        assignees: [assigneeLogin]
                    });
                    
                    await octokit.issues.addLabels({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        labels: ["assigned"]
                    });

                    const bountyMessage = bountyAmount > 0 ? ` A bounty of $${bountyAmount} is available with a ${bountyHours}-hour deadline.` : '';
                    await octokit.issues.createComment({
                        owner,
                        repo: repoName,
                        issue_number: issue.number,
                        body: `Hello @${assigneeLogin}! You've been assigned to [${repository} issue #${issue.number}](https://github.com/${repository}/issues/${issue.number}). You have ${bountyHours} hours to complete a pull request.${bountyMessage}`
                    });

                } catch (error) {
                    console.error(`Error assigning issue from workflow dispatch:`, error);
                }
            }
        }

        console.log('Checking for stale assignments or expired bounties...');
        const presentDate = new Date();

        try {
            const { data: events } = await octokit.issues.listForRepo({
                owner,
                repo: repoName,
                state: 'open'
            });
            
            console.log(`Found ${events.length} open issues to check for stale assignments`);
            
            for (const event of events) {
                if (event.assignee && event.state === "open") {
                    const timeSinceUpdate = presentDate.getTime() - new Date(event.updated_at).getTime();
                    const hoursInactive = timeSinceUpdate / (1000 * 3600);

                    const inactivityThreshold = bountyAmount > 0 ? bountyHours : 24;

                    if (hoursInactive > inactivityThreshold) {
                        console.log(`Unassigning issue #${event.number} due to ${hoursInactive} hours of inactivity (threshold: ${inactivityThreshold} hours)`);

                        const issueDetails = await octokit.issues.get({
                            owner,
                            repo: repoName,
                            issue_number: event.number
                        });
                        
                        const hasAssignedLabel = issueDetails.data.labels.some(label => label.name === 'assigned');
                        
                        if (hasAssignedLabel) {
                            await octokit.issues.removeAssignees({
                                owner,
                                repo: repoName,
                                issue_number: event.number,
                                assignees: [event.assignee.login]
                            });
                            
                            const labels = issueDetails.data.labels
                                .filter(label => label.name !== 'assigned')
                                .map(label => label.name);
                            
                            await octokit.issues.setLabels({
                                owner,
                                repo: repoName,
                                issue_number: event.number,
                                labels
                            });
                            
                            await octokit.issues.createComment({
                                owner,
                                repo: repoName,
                                issue_number: event.number,
                                body: `⏰ This issue has been automatically unassigned due to ${inactivityThreshold} hours of inactivity. The issue is now available for anyone to work on again.`
                            });
                        } else {
                            console.log(`Issue #${event.number} does not have the "assigned" label, skipping unassign.`);
                        }
                    }
                }
            }
        } catch (error) {
            console.error("Error processing stale assignments or expired bounties:", error);
        }

    } catch (error) {
        core.setFailed(error.message);
    }
};

run();

This implementation will:

  1. Add support for setting timed bounties on GitHub issues
  2. Parse the bounty amount and duration from the issue body
  3. Display the bounty information in the assignment message
  4. Automatically unassign issues after the specified hours of inactivity
  5. Show appropriate messages for expired bounties

coderabbitai[bot] avatar Mar 16 '25 17:03 coderabbitai[bot]

/assign

RayyanFCB avatar Nov 05 '25 19:11 RayyanFCB

Hello @RayyanFCB! You've been assigned to OWASP-BLT/BLT issue #3883. You have 24 hours to complete a pull request.

github-actions[bot] avatar Nov 05 '25 19:11 github-actions[bot]