4
0

action-milestone-sync.yml 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. name: Milestone Sync
  2. on:
  3. pull_request_target:
  4. types:
  5. - opened
  6. - reopened
  7. - ready_for_review
  8. - edited
  9. - milestoned
  10. - closed
  11. branches:
  12. - develop
  13. permissions:
  14. issues: write
  15. pull-requests: write
  16. concurrency:
  17. group: milestone-sync-${{ github.event.pull_request.number }}
  18. jobs:
  19. milestone-sync:
  20. if: github.repository_owner == 'GameServerManagers'
  21. runs-on: ubuntu-latest
  22. steps:
  23. - name: Set PR milestone and update linked issues
  24. uses: actions/github-script@v9
  25. with:
  26. script: |
  27. const { owner, repo } = context.repo;
  28. const pr = context.payload.pull_request;
  29. const action = context.payload.action;
  30. const fixedLabel = 'status: fixed in develop';
  31. if (action === 'closed' && !pr.merged) {
  32. return;
  33. }
  34. let milestone = pr.milestone;
  35. if (!milestone && ['opened', 'reopened', 'ready_for_review'].includes(action)) {
  36. const open = await github.paginate(github.rest.issues.listMilestones, { owner, repo, state: 'open', per_page: 100 });
  37. open.sort((a, b) => {
  38. if (a.due_on && b.due_on) return a.due_on.localeCompare(b.due_on);
  39. if (a.due_on) return -1;
  40. if (b.due_on) return 1;
  41. return a.number - b.number;
  42. });
  43. if (open.length === 0) {
  44. core.warning('No open milestone to assign.');
  45. } else {
  46. milestone = open[0];
  47. await github.rest.issues.update({ owner, repo, issue_number: pr.number, milestone: milestone.number });
  48. core.info(`Set PR #${pr.number} milestone to ${milestone.title}.`);
  49. }
  50. }
  51. const body = (pr.body || '').replace(/<!--[\s\S]*?-->/g, '');
  52. const issueRef = /\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?|related to):?\s+(?:([\w.-]+)\/([\w.-]+))?#(\d+)/gi;
  53. const issues = new Map();
  54. for (const m of body.matchAll(issueRef)) {
  55. if (m[2] && (m[2].toLowerCase() !== owner.toLowerCase() || m[3].toLowerCase() !== repo.toLowerCase())) {
  56. continue;
  57. }
  58. const number = Number(m[4]);
  59. const closing = !/^related to$/i.test(m[1]);
  60. issues.set(number, issues.get(number) || closing);
  61. }
  62. for (const [number, closing] of issues) {
  63. let issue;
  64. try {
  65. ({ data: issue } = await github.rest.issues.get({ owner, repo, issue_number: number }));
  66. } catch (error) {
  67. core.warning(`Could not read #${number}: ${error.message}`);
  68. continue;
  69. }
  70. if (issue.pull_request) {
  71. continue;
  72. }
  73. if (milestone && issue.milestone?.number !== milestone.number) {
  74. if (issue.milestone && !pr.merged) {
  75. core.info(`#${number} is in ${issue.milestone.title}; it will move to ${milestone.title} when this PR merges.`);
  76. } else {
  77. await github.rest.issues.update({ owner, repo, issue_number: number, milestone: milestone.number });
  78. core.info(`Set #${number} milestone to ${milestone.title}.`);
  79. }
  80. }
  81. if (pr.merged && closing && issue.state === 'open' && !issue.labels.some((l) => (l.name || l) === fixedLabel)) {
  82. await github.rest.issues.addLabels({ owner, repo, issue_number: number, labels: [fixedLabel] });
  83. core.info(`Labelled #${number} "${fixedLabel}".`);
  84. }
  85. }