فهرست منبع

ci: add milestone sync and release issue workflows to master

Daniel Gibbs 2 روز پیش
والد
کامیت
c4a5c502e2
2فایلهای تغییر یافته به همراه159 افزوده شده و 0 حذف شده
  1. 94 0
      .github/workflows/action-milestone-sync.yml
  2. 65 0
      .github/workflows/action-release-issues.yml

+ 94 - 0
.github/workflows/action-milestone-sync.yml

@@ -0,0 +1,94 @@
+name: Milestone Sync
+on:
+  pull_request_target:
+    types:
+      - opened
+      - reopened
+      - ready_for_review
+      - edited
+      - milestoned
+      - closed
+    branches:
+      - develop
+
+permissions:
+  issues: write
+  pull-requests: write
+
+concurrency:
+  group: milestone-sync-${{ github.event.pull_request.number }}
+
+jobs:
+  milestone-sync:
+    if: github.repository_owner == 'GameServerManagers'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Set PR milestone and update linked issues
+        uses: actions/github-script@v9
+        with:
+          script: |
+            const { owner, repo } = context.repo;
+            const pr = context.payload.pull_request;
+            const action = context.payload.action;
+            const fixedLabel = 'status: fixed in develop';
+
+            if (action === 'closed' && !pr.merged) {
+              return;
+            }
+
+            let milestone = pr.milestone;
+            if (!milestone && ['opened', 'reopened', 'ready_for_review'].includes(action)) {
+              const open = await github.paginate(github.rest.issues.listMilestones, { owner, repo, state: 'open', per_page: 100 });
+              open.sort((a, b) => {
+                if (a.due_on && b.due_on) return a.due_on.localeCompare(b.due_on);
+                if (a.due_on) return -1;
+                if (b.due_on) return 1;
+                return a.number - b.number;
+              });
+              if (open.length === 0) {
+                core.warning('No open milestone to assign.');
+              } else {
+                milestone = open[0];
+                await github.rest.issues.update({ owner, repo, issue_number: pr.number, milestone: milestone.number });
+                core.info(`Set PR #${pr.number} milestone to ${milestone.title}.`);
+              }
+            }
+
+            const body = (pr.body || '').replace(/<!--[\s\S]*?-->/g, '');
+            const issueRef = /\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?|related to):?\s+(?:([\w.-]+)\/([\w.-]+))?#(\d+)/gi;
+            const issues = new Map();
+            for (const m of body.matchAll(issueRef)) {
+              if (m[2] && (m[2].toLowerCase() !== owner.toLowerCase() || m[3].toLowerCase() !== repo.toLowerCase())) {
+                continue;
+              }
+              const number = Number(m[4]);
+              const closing = !/^related to$/i.test(m[1]);
+              issues.set(number, issues.get(number) || closing);
+            }
+
+            for (const [number, closing] of issues) {
+              let issue;
+              try {
+                ({ data: issue } = await github.rest.issues.get({ owner, repo, issue_number: number }));
+              } catch (error) {
+                core.warning(`Could not read #${number}: ${error.message}`);
+                continue;
+              }
+              if (issue.pull_request) {
+                continue;
+              }
+
+              if (milestone && issue.milestone?.number !== milestone.number) {
+                if (issue.milestone && !pr.merged) {
+                  core.info(`#${number} is in ${issue.milestone.title}; it will move to ${milestone.title} when this PR merges.`);
+                } else {
+                  await github.rest.issues.update({ owner, repo, issue_number: number, milestone: milestone.number });
+                  core.info(`Set #${number} milestone to ${milestone.title}.`);
+                }
+              }
+
+              if (pr.merged && closing && issue.state === 'open' && !issue.labels.some((l) => (l.name || l) === fixedLabel)) {
+                await github.rest.issues.addLabels({ owner, repo, issue_number: number, labels: [fixedLabel] });
+                core.info(`Labelled #${number} "${fixedLabel}".`);
+              }
+            }

+ 65 - 0
.github/workflows/action-release-issues.yml

@@ -0,0 +1,65 @@
+name: Release Issues
+on:
+  pull_request_target:
+    types:
+      - opened
+      - reopened
+      - synchronize
+      - ready_for_review
+      - closed
+    branches:
+      - master
+
+permissions:
+  issues: write
+  pull-requests: write
+
+concurrency:
+  group: release-issues-${{ github.event.pull_request.number }}
+
+jobs:
+  release-issues:
+    if: github.repository_owner == 'GameServerManagers' && startsWith(github.event.pull_request.head.ref, 'release/') && github.event.pull_request.head.repo.full_name == github.repository
+    runs-on: ubuntu-latest
+    steps:
+      - name: List fixed issues in the release PR and close them on merge
+        uses: actions/github-script@v9
+        with:
+          script: |
+            const { owner, repo } = context.repo;
+            const pr = context.payload.pull_request;
+            const fixedLabel = 'status: fixed in develop';
+            const start = '<!-- release-issues:start -->';
+            const end = '<!-- release-issues:end -->';
+
+            const labelled = (await github.paginate(github.rest.issues.listForRepo, { owner, repo, state: 'all', labels: fixedLabel, per_page: 100 }))
+              .filter((issue) => !issue.pull_request)
+              .sort((a, b) => a.number - b.number);
+
+            if (context.payload.action === 'closed') {
+              if (!pr.merged) {
+                return;
+              }
+              const version = pr.head.ref.replace(/^release\//, '');
+              for (const issue of labelled) {
+                if (issue.state === 'open') {
+                  await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `Released in ${version} (#${pr.number}).` });
+                  await github.rest.issues.update({ owner, repo, issue_number: issue.number, state: 'closed', state_reason: 'completed' });
+                  core.info(`Closed #${issue.number}.`);
+                }
+                await github.rest.issues.removeLabel({ owner, repo, issue_number: issue.number, name: fixedLabel }).catch(() => {});
+              }
+              return;
+            }
+
+            const open = labelled.filter((issue) => issue.state === 'open');
+            const lines = open.length > 0 ? open.map((issue) => `Fixes #${issue.number}`) : [`No open issues are labelled "${fixedLabel}".`];
+            const block = [start, '## Issues fixed in this release', '', ...lines, end].join('\n');
+
+            const current = pr.body || '';
+            const pattern = new RegExp(`${start}[\\s\\S]*?${end}`);
+            const updated = pattern.test(current) ? current.replace(pattern, block) : `${current.trim() ? `${current.trimEnd()}\n\n` : ''}${block}\n`;
+            if (updated !== current) {
+              await github.rest.pulls.update({ owner, repo, pull_number: pr.number, body: updated });
+              core.info(`Listed ${open.length} issue(s) in #${pr.number}.`);
+            }