Просмотр исходного кода

Merge pull request #5085 from GameServerManagers/ci/milestone-workflows-on-master

ci: add milestone, release issue workflows and PR template to master
Daniel Gibbs 2 дней назад
Родитель
Сommit
76131ec275

+ 11 - 13
.github/pull_request_template.md

@@ -1,20 +1,23 @@
 # Description
 
-Please include a summary of the change and which issues are fixed.
+<!-- Summarise the change and why it is needed. -->
 
 Fixes #[issue]
 
+Affected servers: <!-- shortnames, e.g. ck, rust, or "all" -->
+
 ## Type of change
 
 - [ ] Bug fix (a change which fixes an issue).
 - [ ] New feature (a change which adds functionality).
 - [ ] New Server (new server added).
 - [ ] Refactor (restructures existing code).
+- [ ] Chore (CI, workflows, tooling or other maintenance).
 - [ ] Comment update (typo, spelling, explanation, examples, etc).
 
 ## Testing
 
-Please list the exact validation you performed and the outcome.
+<!-- List the exact validation you performed and the outcome. -->
 
 - Commands/tests run:
 - Result:
@@ -30,28 +33,23 @@ Please list the exact validation you performed and the outcome.
 - [ ] No breaking changes.
 - [ ] Breaking changes included (describe below).
 
-## Documentation impact
+## Documentation
+
+<!-- If documentation needs updating, open a PR on the docs repo (preferred) or request an update. -->
 
 - [ ] No documentation update required.
-- [ ] User documentation update required.
-- [ ] Developer documentation update required.
+- [ ] User documentation update required ([LinuxGSM-Docs](https://github.com/GameServerManagers/LinuxGSM-Docs)).
+- [ ] Developer documentation update required ([LinuxGSM-Dev-Docs](https://github.com/GameServerManagers/LinuxGSM-Dev-Docs)).
 
 ## Checklist
 
 PR will not be merged until all steps are complete.
 
-- [ ] This pull request links to an issue.
+- [ ] This pull request links to an issue ("Fixes", or "Related to" for a partial fix).
 - [ ] This pull request uses the develop branch as its base.
 - [ ] This pull request subject follows the Conventional Commits standard.
 - [ ] This code follows the style guidelines of this project.
 - [ ] I have performed a self-review of my code.
 - [ ] I have provided a detailed enough description of this PR.
 
-## Documentation
-
-If documentation does need updating either update it by creating a PR (preferred) or request a documentation update.
-
-- User docs: <https://github.com/GameServerManagers/LinuxGSM-Docs>
-- Dev docs: <https://github.com/GameServerManagers/LinuxGSM-Dev-Docs>
-
 **Thank you for your Pull Request!**

+ 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}.`);
+            }