Procházet zdrojové kódy

feat(ci): keep the LinuxGSM Backlog board in sync with pull requests (#5226)

Pull requests target develop, so closing keywords never close or link issues and the board's own workflows only fire on close. Issues stayed in New Issues after work started and after merge.

Add a Project Board Status workflow: on PRs into develop, read the closing references in the body and move each open issue on the LinuxGSM Backlog board (project 8) to In Progress while the PR is open (only from New, Sentenced or Fridge), and to Awaiting Release (or In Review) once merged. It never moves an issue backwards and adds issues missing from the board.

Server requests moved from project 11 to project 8, which splits them out with views by issue type, so Add to Project now targets project 8.
Daniel Gibbs před 1 dnem
rodič
revize
46f8dc23ae

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