bounty.yml 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. name: Bounty detector
  2. on:
  3. issues:
  4. types: [labeled]
  5. permissions:
  6. issues: write
  7. pull-requests: read
  8. jobs:
  9. notify:
  10. runs-on: ubuntu-latest
  11. if: startsWith(github.event.label.name, 'diff:')
  12. steps:
  13. - name: Comment bounty info
  14. uses: actions/github-script@v7
  15. env:
  16. FORUM_URL: "https://hub.jmonkeyengine.org/t/bounty-program-trial-starts-today/49394/"
  17. RESERVE_HOURS: "48"
  18. TIMER_SVG_BASE: "https://jme-bounty-reservation-indicator.rblb.workers.dev/timer.svg"
  19. with:
  20. script: |
  21. const issue = context.payload.issue;
  22. const actor = context.actor;
  23. const issueOwner = issue.user?.login;
  24. if (!issueOwner) return;
  25. const forumUrl = process.env.FORUM_URL || "TBD";
  26. const reserveHours = Number(process.env.RESERVE_HOURS || "48");
  27. const svgBase = process.env.TIMER_SVG_BASE || "";
  28. // "previous contributor" = has at least one merged PR authored in this repo
  29. const repoFull = `${context.repo.owner}/${context.repo.repo}`;
  30. const q = `repo:${repoFull} type:pr author:${issueOwner} is:merged`;
  31. let isPreviousContributor = false;
  32. try {
  33. const search = await github.rest.search.issuesAndPullRequests({ q, per_page: 1 });
  34. isPreviousContributor = (search.data.total_count ?? 0) > 0;
  35. } catch (e) {
  36. isPreviousContributor = false;
  37. }
  38. // Reserve only if previous contributor AND labeler is NOT the issue owner
  39. const shouldReserve = isPreviousContributor && (actor !== issueOwner);
  40. const lines = [];
  41. lines.push(`## 💰 This issue has a bounty`);
  42. lines.push(`Resolve it to receive a reward.`);
  43. lines.push(`For details (amount, rules, eligibility), see: ${forumUrl}`);
  44. lines.push("");
  45. lines.push(`If you want to start working on this, **comment on this issue** with your intent.`);
  46. lines.push(`If accepted by a maintainer, the issue will be **assigned** to you.`);
  47. lines.push("");
  48. if (shouldReserve && svgBase) {
  49. const reservedUntil = new Date(Date.now() + reserveHours * 60 * 60 * 1000);
  50. const reservedUntilIso = reservedUntil.toISOString();
  51. const svgUrl =
  52. `${svgBase}` +
  53. `?until=${encodeURIComponent(reservedUntilIso)}` +
  54. `&user=${encodeURIComponent(issueOwner)}` +
  55. `&theme=dark`;
  56. lines.push(`![bounty reservation](${svgUrl})`);
  57. lines.push("");
  58. }
  59. // Avoid duplicate comments for the same label
  60. const comments = await github.rest.issues.listComments({
  61. owner: context.repo.owner,
  62. repo: context.repo.repo,
  63. issue_number: issue.number,
  64. per_page: 100,
  65. });
  66. const already = comments.data.some(c =>
  67. c.user?.login === "github-actions[bot]" &&
  68. typeof c.body === "string" &&
  69. c.body.includes("This issue has a bounty")
  70. );
  71. if (already) return;
  72. await github.rest.issues.createComment({
  73. owner: context.repo.owner,
  74. repo: context.repo.repo,
  75. issue_number: issue.number,
  76. body: lines.join("\n"),
  77. });