Răsfoiți Sursa

Merge pull request #5227 from GameServerManagers/ci/project-board-status-master

feat(ci): keep the LinuxGSM Backlog board in sync with pull requests (master)
Daniel Gibbs 1 zi în urmă
părinte
comite
98be8a9808

+ 1 - 1
.github/workflows/action-add-to-project.yml

@@ -15,6 +15,6 @@ jobs:
       - name: Add to Project
         uses: actions/add-to-project@v2.0.0
         with:
-          project-url: https://github.com/orgs/GameServerManagers/projects/11
+          project-url: https://github.com/orgs/GameServerManagers/projects/8
           github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
           labeled: "type: game server request"

+ 140 - 0
.github/workflows/action-project-board-status.yml

@@ -0,0 +1,140 @@
+name: Project Board Status
+on:
+  pull_request_target:
+    types:
+      - opened
+      - reopened
+      - ready_for_review
+      - edited
+      - closed
+    branches:
+      - develop
+
+permissions: {}
+
+concurrency:
+  group: project-board-status-${{ github.event.pull_request.number }}
+
+jobs:
+  project-board-status:
+    if: github.repository_owner == 'GameServerManagers'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Move linked issues on the project boards
+        uses: actions/github-script@v9
+        with:
+          github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
+          script: |
+            const { owner, repo } = context.repo;
+            const pr = context.payload.pull_request;
+            const org = 'GameServerManagers';
+            const boardNumber = 8;
+
+            if (context.payload.action === 'closed' && !pr.merged) {
+              return;
+            }
+            const targets = pr.merged ? ['Awaiting Release', 'In Review'] : ['In Progress'];
+            const movableFrom = pr.merged
+              ? ['New Issues', 'Sentenced Issues', 'The Fridge', 'On Hold', 'In Progress']
+              : ['New Issues', 'Sentenced Issues', 'The Fridge'];
+            const columnOf = (name) => (name || '').replace(/^[^A-Za-z]+/, '').trim();
+
+            const body = (pr.body || '').replace(/<!--[\s\S]*?-->/g, '');
+            const closingRef = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+(?:([\w.-]+)\/([\w.-]+))?#(\d+)/gi;
+            const numbers = new Set();
+            for (const m of body.matchAll(closingRef)) {
+              if (m[1] && (m[1].toLowerCase() !== owner.toLowerCase() || m[2].toLowerCase() !== repo.toLowerCase())) {
+                continue;
+              }
+              numbers.add(Number(m[3]));
+            }
+            if (numbers.size === 0) {
+              core.info('No closing issue references in the PR body.');
+              return;
+            }
+
+            const projectCache = new Map();
+            async function project(number) {
+              if (!projectCache.has(number)) {
+                const data = await github.graphql(
+                  `query($org:String!,$number:Int!){
+                    organization(login:$org){
+                      projectV2(number:$number){
+                        id title
+                        field(name:"Status"){ ... on ProjectV2SingleSelectField { id options { id name } } }
+                      }
+                    }
+                  }`,
+                  { org, number }
+                );
+                projectCache.set(number, data.organization.projectV2);
+              }
+              return projectCache.get(number);
+            }
+
+            async function setStatus(proj, itemId) {
+              const option = targets.map((t) => proj.field.options.find((o) => columnOf(o.name) === t)).find(Boolean);
+              if (!option) {
+                throw new Error(`${proj.title} has no "${targets.join('" or "')}" column`);
+              }
+              await github.graphql(
+                `mutation($project:ID!,$item:ID!,$field:ID!,$option:String!){
+                  updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } }
+                }`,
+                { project: proj.id, item: itemId, field: proj.field.id, option: option.id }
+              );
+              return columnOf(option.name);
+            }
+
+            for (const number of numbers) {
+              try {
+                const data = await github.graphql(
+                  `query($owner:String!,$repo:String!,$number:Int!){
+                    repository(owner:$owner,name:$repo){
+                      issueOrPullRequest(number:$number){
+                        __typename
+                        ... on Issue {
+                          id state
+                          projectItems(first:20){
+                            nodes {
+                              id
+                              project { number owner { ... on Organization { login } } }
+                              fieldValueByName(name:"Status"){ ... on ProjectV2ItemFieldSingleSelectValue { name } }
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }`,
+                  { owner, repo, number }
+                );
+                const issue = data.repository.issueOrPullRequest;
+                if (!issue || issue.__typename !== 'Issue' || issue.state !== 'OPEN') {
+                  continue;
+                }
+
+                let items = issue.projectItems.nodes.filter((i) => i.project.owner?.login === org && i.project.number === boardNumber);
+                if (items.length === 0) {
+                  const proj = await project(boardNumber);
+                  const added = await github.graphql(
+                    `mutation($project:ID!,$content:ID!){ addProjectV2ItemById(input:{projectId:$project,contentId:$content}){ item { id } } }`,
+                    { project: proj.id, content: issue.id }
+                  );
+                  items = [{ id: added.addProjectV2ItemById.item.id, project: { number: boardNumber }, fieldValueByName: null }];
+                  core.info(`Added #${number} to ${proj.title}.`);
+                }
+
+                for (const item of items) {
+                  const proj = await project(item.project.number);
+                  const current = columnOf(item.fieldValueByName?.name);
+                  if (current && !movableFrom.includes(current)) {
+                    core.info(`#${number} stays in ${current} on ${proj.title}.`);
+                    continue;
+                  }
+                  const moved = await setStatus(proj, item.id);
+                  core.info(`Moved #${number} from ${current || 'no status'} to ${moved} on ${proj.title}.`);
+                }
+              } catch (error) {
+                core.warning(`#${number}: ${error.message}`);
+              }
+            }