name: Bounty detector on: issues: types: [labeled] permissions: issues: write pull-requests: read jobs: notify: runs-on: ubuntu-latest if: startsWith(github.event.label.name, 'diff:') steps: - name: Comment bounty info uses: actions/github-script@v7 env: FORUM_URL: "https://hub.jmonkeyengine.org/t/bounty-program-trial-starts-today/49394/" RESERVE_HOURS: "48" TIMER_SVG_BASE: "https://jme-bounty-reservation-indicator.rblb.workers.dev/timer.svg" with: script: | const issue = context.payload.issue; const actor = context.actor; const issueOwner = issue.user?.login; if (!issueOwner) return; const forumUrl = process.env.FORUM_URL || "TBD"; const reserveHours = Number(process.env.RESERVE_HOURS || "48"); const svgBase = process.env.TIMER_SVG_BASE || ""; // "previous contributor" = has at least one merged PR authored in this repo const repoFull = `${context.repo.owner}/${context.repo.repo}`; const q = `repo:${repoFull} type:pr author:${issueOwner} is:merged`; let isPreviousContributor = false; try { const search = await github.rest.search.issuesAndPullRequests({ q, per_page: 1 }); isPreviousContributor = (search.data.total_count ?? 0) > 0; } catch (e) { isPreviousContributor = false; } // Reserve only if previous contributor AND labeler is NOT the issue owner const shouldReserve = isPreviousContributor && (actor !== issueOwner); const lines = []; lines.push(`## 💰 This issue has a bounty`); lines.push(`Resolve it to receive a reward.`); lines.push(`For details (amount, rules, eligibility), see: ${forumUrl}`); lines.push(""); lines.push(`If you want to start working on this, **comment on this issue** with your intent.`); lines.push(`If accepted by a maintainer, the issue will be **assigned** to you.`); lines.push(""); if (shouldReserve && svgBase) { const reservedUntil = new Date(Date.now() + reserveHours * 60 * 60 * 1000); const reservedUntilIso = reservedUntil.toISOString(); const svgUrl = `${svgBase}` + `?until=${encodeURIComponent(reservedUntilIso)}` + `&user=${encodeURIComponent(issueOwner)}` + `&theme=dark`; lines.push(`![bounty reservation](${svgUrl})`); lines.push(""); } // Avoid duplicate comments for the same label const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, per_page: 100, }); const already = comments.data.some(c => c.user?.login === "github-actions[bot]" && typeof c.body === "string" && c.body.includes("This issue has a bounty") ); if (already) return; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, body: lines.join("\n"), });