From 9ffecb2fa2ccc264f2d97dde66dae1839d641d9e Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 11 Mar 2026 15:43:23 +0100 Subject: [PATCH 1/3] Clean up triage workflow automation --- .github/actions/score-triaged-defects.mjs | 249 ------------------ .github/workflows/triage-move-labelled.yml | 77 +----- .github/workflows/triage-move-unlabelled.yml | 32 --- .../workflows/x-plorers-epic-forwarding.yml | 26 -- 4 files changed, 1 insertion(+), 383 deletions(-) delete mode 100644 .github/actions/score-triaged-defects.mjs delete mode 100644 .github/workflows/triage-move-unlabelled.yml delete mode 100644 .github/workflows/x-plorers-epic-forwarding.yml diff --git a/.github/actions/score-triaged-defects.mjs b/.github/actions/score-triaged-defects.mjs deleted file mode 100644 index 5b9306b..0000000 --- a/.github/actions/score-triaged-defects.mjs +++ /dev/null @@ -1,249 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - - -// For running in an action -import { Octokit } from "@octokit/action"; -const octokit = new Octokit(); - -// For running locally -// import { Octokit } from "@octokit/core"; -// const octokit = new Octokit({ auth: process.env.GH_TOKEN }); - - -// List all the defects with their GH project "Score" field -async function listDefects(repoOwner, repoName, projectFieldName = "Score", label = "T-Defect") { - const query = ` - query ($repoOwner: String!, $repoName: String!, $label: String!, $projectFieldName: String!, $after: String) { - repository(owner: $repoOwner, name: $repoName) { - issues(labels: [$label], states: OPEN, first: 10, after: $after) { - nodes { - number - title - labels(first: 10) { - nodes { - name - } - } - projectItems(first: 10) { - nodes { - id - project { - id - } - score: fieldValueByName(name: $projectFieldName) { - ... on ProjectV2ItemFieldNumberValue { - id - number - } - } - } - } - } - pageInfo { - endCursor - hasNextPage - } - } - } - } - `; - - var issues = []; - var hasNextPage = true; - var after = null; - - while (hasNextPage) { - const parameters = { - repoOwner, - repoName, - label, - projectFieldName, - after - }; - - const result = await octokit.graphql(query, parameters); - issues = issues.concat(result.repository.issues.nodes); - hasNextPage = result.repository.issues.pageInfo.hasNextPage; - after = result.repository.issues.pageInfo.endCursor; - } - - return issues; -} - - -// Extract the score from the GraphQL response. -// scoreItem is { id, project { id }, score: { id, number }} -function getScoreItem(issue, projectId) { - var scoreItem = 0; - issue.projectItems.nodes.forEach(item => { - if (item.project.id === projectId) { - scoreItem = item; - } - }); - - if (scoreItem == null) { - console.log("No score found for issue " + issue.number); - } - - return scoreItem; -} - -// Compute the score of a defect based on the labels as per: -// https://github.com/element-hq/element-meta/wiki/triage-process#prioritisation -function computeIssueScore(issue) { - var severity = 0; - var occurence = 0; - issue.labels.nodes.forEach(label => { - switch (String(label.name)) { - case "O-Uncommon": - occurence = 1; - break; - case "O-Occasional": - occurence = 2; - break; - case "O-Frequent": - occurence = 3; - break; - case "S-Tolerable": - severity = 1; - break; - case "S-Minor": - severity = 2; - break; - case "S-Major": - severity = 3; - break; - case "S-Critical": - severity = 4; - break; - default: - break; - } - }); - - return severity * occurence; -} - -// Update a score in the GH project -async function setNewScore(scoreItem, projectFieldId, fieldValue) { - const mutation = ` - mutation($projectId: ID!, $itemId: ID!, $projectFieldId: ID!, $value: Float!) { - updateProjectV2ItemFieldValue( - input: { - projectId: $projectId - itemId: $itemId - fieldId: $projectFieldId - value: { - number: $value - } - } - ) { - projectV2Item { - id - } - } - } - `; - - const parameters = { - projectId: scoreItem.project.id, - itemId: scoreItem.id, - projectFieldId: projectFieldId, - value: fieldValue - }; - - await octokit.graphql(mutation, parameters); -} - - -(async function main() { - const repoOwner = process.env.REPO_OWNER; - const repoName = process.env.REPO_NAME; - const projectId = process.env.PROJECT_ID; - const projectFieldId = process.env.PROJECT_FIELD_ID; - const projectFieldName = process.env.PROJECT_FIELD_NAME; - - const issues = await listDefects(repoOwner, repoName, projectFieldName); - console.log("Found " + issues.length + " T-Defect issues"); - - issues.filter(issue => { - // Check if it is part of the GH project - const ok = issue.projectItems.nodes.some(item => { return item.project.id === projectId; }); - if (!ok) { - console.log("Issue " + issue.number + " is not part of the project"); - } - return ok; - }) - .filter(issue => { - // Check if it has the triaging labels, ie a label that starts with "S-" and a label that starts with "O-" - const ok = issue.labels.nodes.some(label => { return label.name.startsWith("S-"); }) && issue.labels.nodes.some(label => { return label.name.startsWith("O-"); }); - if (!ok) { - console.log("Issue " + issue.number + " is not labeled correctly. Labels: " + issue.labels.nodes.map(label => label.name)); - } - return ok; - }) - .forEach(issue => { - const scoreItem = getScoreItem(issue, projectId); - - // Ignore issues with a score manually set higher than 100. This is a way to fine control the priority of issues - if (scoreItem.score && scoreItem.score.number >= 100) { - return; - } - - // Update the score if it is different - var computedScore = computeIssueScore(issue); - if (scoreItem.score == null || scoreItem.score.number != computedScore) { - console.log(issue.number + " - " + " Updating score from " + (scoreItem.score ? scoreItem.score.number : "null") + " to " + computedScore + " - " + issue.title); - - setNewScore(scoreItem, projectFieldId, computedScore).catch(error => { - console.error("Error updating score for issue " + issue.number + ": " + error); - }); - } - }); - -})(); - - - -// The query to use in https://docs.github.com/en/graphql/overview/explorer to find ids for the GH project id and the Score field -/* -query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - issues(labels: ["T-Defect"], states: OPEN, first:3) { - nodes { - title - projectItems(first: 2) { - nodes { - project { - id - score: field(name: "Score") { - ... on ProjectV2Field { - id - } - } - } - } - } - } - } - } - } - - Variables - {"owner": "element-hq","repo": "element-x-ios"} - */ - \ No newline at end of file diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 586b477..7561feb 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -5,58 +5,6 @@ on: types: [labeled] jobs: - apply_Z-Labs_label: - name: Add Z-Labs label for features behind labs flags - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'A-Maths') || - contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || - contains(github.event.issue.labels.*.name, 'A-Threads') || - contains(github.event.issue.labels.*.name, 'A-Polls') || - contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || - contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || - contains(github.event.issue.labels.*.name, 'Z-IA') || - contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || - contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-Tags') - steps: - - uses: actions/github-script@v5 - with: - script: | - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['Z-Labs'] - }) - - add_design_issues_to_project: - name: X-Needs-Design to Design project board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'X-Needs-Design') && - (contains(github.event.issue.labels.*.name, 'S-Critical') && - (contains(github.event.issue.labels.*.name, 'O-Frequent') || - contains(github.event.issue.labels.*.name, 'O-Occasional')) || - contains(github.event.issue.labels.*.name, 'S-Major') && - contains(github.event.issue.labels.*.name, 'O-Frequent') || - contains(github.event.issue.labels.*.name, 'A11y')) - steps: - - uses: actions/add-to-project@main - with: - project-url: https://github.com/orgs/element-hq/projects/18 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - add_product_issues: - name: X-Needs-Product to Design project board - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'X-Needs-Product') - steps: - - uses: actions/add-to-project@main - with: - project-url: https://github.com/orgs/element-hq/projects/28 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} ex_platform: name: Add labelled issues to EX platform project @@ -77,28 +25,5 @@ jobs: steps: - uses: actions/add-to-project@main with: - project-url: https://github.com/orgs/element-hq/projects/41 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - qa: - name: Add labelled issues to QA project - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'Team: QA') || - contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') - steps: - - uses: actions/add-to-project@main - with: - project-url: https://github.com/orgs/element-hq/projects/69 - github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - signoff: - name: Add labelled issues to signoff project - runs-on: ubuntu-latest - if: > - contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') - steps: - - uses: actions/add-to-project@main - with: - project-url: https://github.com/orgs/element-hq/projects/89 + project-url: https://github.com/orgs/element-hq/projects/139 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-move-unlabelled.yml b/.github/workflows/triage-move-unlabelled.yml deleted file mode 100644 index 89b7b77..0000000 --- a/.github/workflows/triage-move-unlabelled.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Move unlabelled from needs info columns to triaged - -on: - issues: - types: [unlabeled] - -jobs: - remove_Z-Labs_label: - name: Remove Z-Labs label when features behind labs flags are removed - runs-on: ubuntu-latest - if: > - !(contains(github.event.issue.labels.*.name, 'A-Maths') || - contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || - contains(github.event.issue.labels.*.name, 'A-Threads') || - contains(github.event.issue.labels.*.name, 'A-Polls') || - contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || - contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || - contains(github.event.issue.labels.*.name, 'Z-IA') || - contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || - contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-Tags')) && - contains(github.event.issue.labels.*.name, 'Z-Labs') - steps: - - uses: actions/github-script@v5 - with: - script: | - github.rest.issues.removeLabel({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - name: ['Z-Labs'] - }) diff --git a/.github/workflows/x-plorers-epic-forwarding.yml b/.github/workflows/x-plorers-epic-forwarding.yml deleted file mode 100644 index 9812b6b..0000000 --- a/.github/workflows/x-plorers-epic-forwarding.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Forward epic field value into tracked issues - -on: - issues: - types: [edited] - -jobs: - forward_epic_field_value: - name: Forward epic field value into tracked issues - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 18 - - run: npm install @octokit/action - - run: node .github/actions/forward-project-field.mjs - env: - GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} - REPO_OWNER: ${{ github.event.repository.owner.login }} - REPO_NAME: ${{ github.event.repository.name }} - ISSUE_URL: ${{ github.event.issue.html_url }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" - FIELD_ID: "PVTSSF_lADOAM0swc4ALoFYzgJAimw" - FIELD_NAME: Epic From 4935684eb8127675d0e65fd13416ccacdebeabf8 Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 11 Mar 2026 15:58:58 +0100 Subject: [PATCH 2/3] Restore triaged defects scoring action --- .github/actions/score-triaged-defects.mjs | 249 ++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 .github/actions/score-triaged-defects.mjs diff --git a/.github/actions/score-triaged-defects.mjs b/.github/actions/score-triaged-defects.mjs new file mode 100644 index 0000000..5b9306b --- /dev/null +++ b/.github/actions/score-triaged-defects.mjs @@ -0,0 +1,249 @@ +/* +Copyright 2024 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +// For running in an action +import { Octokit } from "@octokit/action"; +const octokit = new Octokit(); + +// For running locally +// import { Octokit } from "@octokit/core"; +// const octokit = new Octokit({ auth: process.env.GH_TOKEN }); + + +// List all the defects with their GH project "Score" field +async function listDefects(repoOwner, repoName, projectFieldName = "Score", label = "T-Defect") { + const query = ` + query ($repoOwner: String!, $repoName: String!, $label: String!, $projectFieldName: String!, $after: String) { + repository(owner: $repoOwner, name: $repoName) { + issues(labels: [$label], states: OPEN, first: 10, after: $after) { + nodes { + number + title + labels(first: 10) { + nodes { + name + } + } + projectItems(first: 10) { + nodes { + id + project { + id + } + score: fieldValueByName(name: $projectFieldName) { + ... on ProjectV2ItemFieldNumberValue { + id + number + } + } + } + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + } + `; + + var issues = []; + var hasNextPage = true; + var after = null; + + while (hasNextPage) { + const parameters = { + repoOwner, + repoName, + label, + projectFieldName, + after + }; + + const result = await octokit.graphql(query, parameters); + issues = issues.concat(result.repository.issues.nodes); + hasNextPage = result.repository.issues.pageInfo.hasNextPage; + after = result.repository.issues.pageInfo.endCursor; + } + + return issues; +} + + +// Extract the score from the GraphQL response. +// scoreItem is { id, project { id }, score: { id, number }} +function getScoreItem(issue, projectId) { + var scoreItem = 0; + issue.projectItems.nodes.forEach(item => { + if (item.project.id === projectId) { + scoreItem = item; + } + }); + + if (scoreItem == null) { + console.log("No score found for issue " + issue.number); + } + + return scoreItem; +} + +// Compute the score of a defect based on the labels as per: +// https://github.com/element-hq/element-meta/wiki/triage-process#prioritisation +function computeIssueScore(issue) { + var severity = 0; + var occurence = 0; + issue.labels.nodes.forEach(label => { + switch (String(label.name)) { + case "O-Uncommon": + occurence = 1; + break; + case "O-Occasional": + occurence = 2; + break; + case "O-Frequent": + occurence = 3; + break; + case "S-Tolerable": + severity = 1; + break; + case "S-Minor": + severity = 2; + break; + case "S-Major": + severity = 3; + break; + case "S-Critical": + severity = 4; + break; + default: + break; + } + }); + + return severity * occurence; +} + +// Update a score in the GH project +async function setNewScore(scoreItem, projectFieldId, fieldValue) { + const mutation = ` + mutation($projectId: ID!, $itemId: ID!, $projectFieldId: ID!, $value: Float!) { + updateProjectV2ItemFieldValue( + input: { + projectId: $projectId + itemId: $itemId + fieldId: $projectFieldId + value: { + number: $value + } + } + ) { + projectV2Item { + id + } + } + } + `; + + const parameters = { + projectId: scoreItem.project.id, + itemId: scoreItem.id, + projectFieldId: projectFieldId, + value: fieldValue + }; + + await octokit.graphql(mutation, parameters); +} + + +(async function main() { + const repoOwner = process.env.REPO_OWNER; + const repoName = process.env.REPO_NAME; + const projectId = process.env.PROJECT_ID; + const projectFieldId = process.env.PROJECT_FIELD_ID; + const projectFieldName = process.env.PROJECT_FIELD_NAME; + + const issues = await listDefects(repoOwner, repoName, projectFieldName); + console.log("Found " + issues.length + " T-Defect issues"); + + issues.filter(issue => { + // Check if it is part of the GH project + const ok = issue.projectItems.nodes.some(item => { return item.project.id === projectId; }); + if (!ok) { + console.log("Issue " + issue.number + " is not part of the project"); + } + return ok; + }) + .filter(issue => { + // Check if it has the triaging labels, ie a label that starts with "S-" and a label that starts with "O-" + const ok = issue.labels.nodes.some(label => { return label.name.startsWith("S-"); }) && issue.labels.nodes.some(label => { return label.name.startsWith("O-"); }); + if (!ok) { + console.log("Issue " + issue.number + " is not labeled correctly. Labels: " + issue.labels.nodes.map(label => label.name)); + } + return ok; + }) + .forEach(issue => { + const scoreItem = getScoreItem(issue, projectId); + + // Ignore issues with a score manually set higher than 100. This is a way to fine control the priority of issues + if (scoreItem.score && scoreItem.score.number >= 100) { + return; + } + + // Update the score if it is different + var computedScore = computeIssueScore(issue); + if (scoreItem.score == null || scoreItem.score.number != computedScore) { + console.log(issue.number + " - " + " Updating score from " + (scoreItem.score ? scoreItem.score.number : "null") + " to " + computedScore + " - " + issue.title); + + setNewScore(scoreItem, projectFieldId, computedScore).catch(error => { + console.error("Error updating score for issue " + issue.number + ": " + error); + }); + } + }); + +})(); + + + +// The query to use in https://docs.github.com/en/graphql/overview/explorer to find ids for the GH project id and the Score field +/* +query($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + issues(labels: ["T-Defect"], states: OPEN, first:3) { + nodes { + title + projectItems(first: 2) { + nodes { + project { + id + score: field(name: "Score") { + ... on ProjectV2Field { + id + } + } + } + } + } + } + } + } + } + + Variables + {"owner": "element-hq","repo": "element-x-ios"} + */ + \ No newline at end of file From 8e7a29bd92beebd1a5f4997a7f32fd6f9e618fe7 Mon Sep 17 00:00:00 2001 From: manuroe Date: Wed, 11 Mar 2026 16:00:14 +0100 Subject: [PATCH 3/3] Remove unused project field forwarding action --- .github/actions/forward-project-field.mjs | 244 ---------------------- 1 file changed, 244 deletions(-) delete mode 100644 .github/actions/forward-project-field.mjs diff --git a/.github/actions/forward-project-field.mjs b/.github/actions/forward-project-field.mjs deleted file mode 100644 index 33f4cb3..0000000 --- a/.github/actions/forward-project-field.mjs +++ /dev/null @@ -1,244 +0,0 @@ -import { Octokit } from "@octokit/action"; - -const octokit = new Octokit(); - -const headers = { "GraphQL-Features": "projects_next_graphql" } - -const REPO_OWNER = process.env.REPO_OWNER; -const REPO_NAME = process.env.REPO_NAME; -const ISSUE_URL = process.env.ISSUE_URL; -const ISSUE_NUMBER = parseInt(process.env.ISSUE_NUMBER); -const PROJECT_ID = process.env.PROJECT_ID; -const FIELD_ID = process.env.FIELD_ID; -const FIELD_NAME = process.env.FIELD_NAME; - -const visitedIssueUrls = new Set(); - -async function queryFieldValue(repoOwner, repoName, issueNumber, fieldName) { - const query = `query ($owner: String!, $repo: String!, $issueNumber: Int!, $fieldName: String!) { - repository(owner: $owner, name: $repo) { - issue(number: $issueNumber) { - projectItems(first: 100) { - edges { - node { - id - fieldValueByName(name: $fieldName) { - ... on ProjectV2ItemFieldSingleSelectValue { - optionId - name - field { - ... on ProjectV2SingleSelectField { - id - } - } - } - } - project { - id - } - } - } - } - } - } - }`; - - const parameters = { - owner: repoOwner, - repo: repoName, - issueNumber: issueNumber, - fieldName: fieldName, - headers - }; - - const result = await octokit.graphql(query, parameters); - - return result.repository.issue.projectItems.edges; -} - -function determineFieldValue(projectItems, projectId, fieldId) { - for (const item of projectItems) { - if (item.node.project.id == projectId && item.node.fieldValueByName && item.node.fieldValueByName.field.id == fieldId) { - return { name: item.node.fieldValueByName.name, id: item.node.fieldValueByName.optionId }; - } - } - - return {} -} - -async function setFieldValueOnTrackedIssues(repoOwner, repoName, issueUrl, issueNumber, projectId, fieldId, fieldName, fieldValue) { - // Avoid infinite loop - - if (visitedIssueUrls.has(issueUrl)) { - return; - } - visitedIssueUrls.add(issueUrl); - - // Make sure this is actually an issue - - if (issueUrl.indexOf("/issues/") < 0) { - return; - } - - // Get tracked issues - - console.log(`Querying tracked issues of ${issueUrl}`); - const trackedIssues = await queryTrackedIssues(repoOwner, repoName, issueNumber); - - if (!trackedIssues || trackedIssues.length == 0) { - console.log("Aborting because issue has no tracked issues"); - return; - } - - console.log(trackedIssues.map(issue => issue.node.url)); - - // Set field values - - for (const issue of trackedIssues) { - const trackedIssueId = issue.node.id; - const trackedIssueUrl = issue.node.url; - const trackedIssueNumber = issue.node.number; - const trackedRepoOwner = issue.node.repository.owner.login; - const trackedRepoName = issue.node.repository.name; - - // Get project item or add one if needed - - let itemId = issue.node.projectItems.edges.find(item => item.node.project.id == projectId)?.node?.id; - - if (!itemId) { - console.log(`Adding ${trackedIssueUrl} to project`); - itemId = await addItemToProject(projectId, trackedIssueId); - } - - // Set field value - - console.log(`Setting value "${fieldValue.name}" for field "${fieldName}" of ${trackedIssueUrl}`); - mutateFieldValue(projectId, itemId, fieldId, fieldValue.id); - - // Recurse - - await setFieldValueOnTrackedIssues(trackedRepoOwner, trackedRepoName, trackedIssueUrl, trackedIssueNumber, projectId, fieldId, fieldName, fieldValue); - } -} - -async function queryTrackedIssues(repoOwner, repoName, issueNumber) { - const query = `query ($owner: String!, $repo: String!, $issueNumber: Int!) { - repository(owner: $owner, name: $repo) { - issue(number: $issueNumber) { - trackedIssues(first: 100) { - edges { - node { - projectItems(first: 100) { - edges { - node { - id - project { - id - } - } - } - } - id - url - number - repository { - name - owner { - login - } - } - } - } - } - } - } - }`; - - const parameters = { - owner: repoOwner, - repo: repoName, - issueNumber: issueNumber, - headers - }; - - const result = await octokit.graphql(query, parameters); - - return result.repository.issue.trackedIssues.edges; -} - -async function addItemToProject(projectId, contentId) { - const mutation = `mutation ($projectId: ID!, $contentId: ID!) { - addProjectV2ItemById(input: {projectId: $projectId contentId: $contentId}) { - item { - id - } - } - }`; - - const parameters = { - projectId: projectId, - contentId: contentId, - headers - }; - - const result = await octokit.graphql(mutation, parameters); - - return result.addProjectV2ItemById.item.id; -} - -async function mutateFieldValue(projectId, itemId, fieldId, fieldValueId) { - const mutation = `mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { - updateProjectV2ItemFieldValue( - input: { - projectId: $projectId - itemId: $itemId - fieldId: $fieldId - value: { - singleSelectOptionId: $optionId - } - } - ) { - projectV2Item { - id - } - } - }`; - - const parameters = { - projectId: projectId, - itemId: itemId, - fieldId: fieldId, - optionId: fieldValueId, - headers - }; - - await octokit.graphql(mutation, parameters); -} - -(async function main() { - // Get project items - - console.log(`Querying value for field "${FIELD_NAME}" of ${ISSUE_URL}`); - const projectItems = await queryFieldValue(REPO_OWNER, REPO_NAME, ISSUE_NUMBER, FIELD_NAME); - - if (!projectItems) { - console.log("Aborting because issue is not part of any projects"); - return; - } - - // Determine field value - - fieldValue = determineFieldValue(projectItems, PROJECT_ID, FIELD_ID); - - if (!fieldValue.name || !fieldValue.id) { - console.log("Aborting because issue is not part of the correct project"); - return; - } - - console.log(`Determined field value "${fieldValue.name}" (${fieldValue.id})`); - - // Set field value on tracked issues - - await setFieldValueOnTrackedIssues(REPO_OWNER, REPO_NAME, ISSUE_URL, ISSUE_NUMBER, PROJECT_ID, FIELD_ID, FIELD_NAME, fieldValue); - -})();