Skip to content

Instantly share code, notes, and snippets.

@t1m0thyj
Last active January 8, 2024 21:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save t1m0thyj/3812605364813c5f2ac7213333bcc9b4 to your computer and use it in GitHub Desktop.
Save t1m0thyj/3812605364813c5f2ac7213333bcc9b4 to your computer and use it in GitHub Desktop.
Imports issues and pull requests into GitHub v2 project
/**
* Imports issues and pull requests into GitHub v2 project
*
* To use this script:
* 1. Install the dependencies:
* npm install -D dayjs @octokit/core @octokit/plugin-paginate-graphql @octokit/plugin-paginate-rest
* 2. Define GitHub token in an environment variable GITHUB_TOKEN
* 3. Run the script with a project ID passed on the command line:
* node importIssues.js <projectId>
*/
const dayjs = require("dayjs");
const { Octokit } = require("@octokit/core");
const { paginateGraphql } = require("@octokit/plugin-paginate-graphql");
const { paginateRest } = require("@octokit/plugin-paginate-rest");
const DELETE_OLD_ISSUES = false; // Specify true to remove issues closed >90 days ago from board
const PROJECT_ORG = "zowe";
const PROJECT_REPOS = {
15: [ // Zowe Explorer
"zowe/vscode-extension-for-zowe",
"zowe/cics-for-zowe-client",
"zowe/vscode-extension-for-cics",
"zowe/zowe-cli-secrets-for-kubernetes",
["zowe/docs-site", "area: zowe-explorer"]
],
21: [ // Zowe CLI
"zowe/zowe-cli",
"zowe/zowe-cli-cics-plugin",
"zowe/zowe-cli-db2-plugin",
"zowe/zowe-cli-ftp-plugin",
"zowe/zowe-cli-ims-plugin",
"zowe/zowe-cli-mq-plugin",
"zowe/zowe-cli-sample-plugin",
"zowe/zowe-cli-scs-plugin",
"zowe/zowe-cli-standalone-package",
"zowe/zowe-cli-web-help-generator",
"zowe/zowe-client-python-sdk",
["zowe/docs-site", "area: cli"]
]
}
const PROJECT_RULES = [
{
column: "Epics",
kind: "issue",
state: "open",
label: "Epic"
},
{
column: "High Priority",
kind: "issue",
state: "open",
label: "priority-high"
},
{
column: "Medium Priority",
kind: "issue",
state: "open",
label: "priority-medium"
},
{
column: "Low Priority",
kind: "issue",
state: "open",
label: "priority-low"
},
{
column: "New Issues",
kind: "issue",
state: "open"
},
{
column: "In Progress",
kind: "pull_request",
state: "open"
},
{
column: "Closed",
state: "closed"
}
];
async function importIssues(projectNumber) {
const MyOctokit = Octokit.plugin(paginateGraphql, paginateRest);
const octokit = new MyOctokit({ auth: process.env.GITHUB_TOKEN });
const projectData = (await octokit.graphql(`query {
organization(login: "${PROJECT_ORG}") {
projectV2(number: ${projectNumber}) {
id
title
}
}
}`)).organization.projectV2;
console.log(`Found GitHub project "${projectData.title}" (${projectData.id})`);
const statusField = (await octokit.graphql(`query {
node(id: "${projectData.id}") {
... on ProjectV2 {
items(first: 1) {
nodes {
fieldValues(first: 10) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
field {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}
}
}
}
}`)).node.items.nodes[0].fieldValues.nodes.find((z) => z.field?.name === "Status").field;
console.log(`Found status fields: ${statusField.options.map((x) => x.name).join(", ")}`);
const issueIdMap = (await octokit.graphql.paginate(`query paginate($cursor: String) {
node(id: "${projectData.id}") {
... on ProjectV2 {
items(first: 100, after: $cursor) {
nodes {
id
fieldValues(first: 10) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field {
... on ProjectV2SingleSelectField {
name
}
}
}
}
}
content {
... on Issue {
id
}
... on PullRequest {
id
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}`)).node.items.nodes.reduce((x, y) => ({
...x, [y.content.id]: {
itemId: y.id,
status: y.fieldValues.nodes.find((z) => z.field?.name === "Status").name
}
}), {});
console.log(`Found ${Object.keys(issueIdMap).length} issues in project`);
for (const repoData of PROJECT_REPOS[projectNumber]) {
const [repoName, labelFilter] = typeof repoData === "string" ? [repoData] : repoData;
const [owner, repo] = repoName.split("/", 2);
for (const issue of await octokit.paginate("GET /repos/{owner}/{repo}/issues", { owner, repo, state: "all", labels: labelFilter })) {
const shouldExist = issue.state === "open" || dayjs(issue.closed_at).isAfter(dayjs().subtract(90, "day"));
const doesExist = Object.keys(issueIdMap).includes(issue.node_id);
if (shouldExist && !doesExist) {
const itemId = (await octokit.graphql(`mutation {
addProjectV2ItemById(input: { projectId: "${projectData.id}" contentId: "${issue.node_id}" }) {
item {
id
}
}
}`)).addProjectV2ItemById.item.id;
console.log(`Added issue ${repoName}#${issue.number} to project`);
issueIdMap[issue.node_id] = { itemId, status: statusField.options[0].name };
} else if (!shouldExist && doesExist && DELETE_OLD_ISSUES) {
const itemId = (await octokit.graphql(`mutation {
deleteProjectV2Item(input: { projectId: "${projectData.id}" itemId: "${issueIdMap[issue.node_id].itemId}" }) {
deletedItemId
}
}`)).deleteProjectV2Item.deletedItemId;
console.log(`Deleted issue ${repoName}#${issue.number} from project`);
delete issueIdMap[issue.node_id];
}
if (issueIdMap[issue.node_id]?.status != null) {
const columnNames = statusField.options.map((x) => x.name);
const oldColumnName = issueIdMap[issue.node_id].status;
let newColumnName;
for (const { column, kind, state, label } of PROJECT_RULES) {
if ((kind == null || (kind === "issue" && issue.pull_request == null) || (kind === "pull_request" && issue.pull_request != null)) &&
(state == null || state === issue.state) &&
(label == null || issue.labels.find((x) => label === x.name) != null)) {
newColumnName = column;
break;
}
}
if (newColumnName == null) {
console.warn(`Could not find column matching issue ${repoName}#${issue.number}`);
} else if (columnNames.indexOf(newColumnName) > columnNames.indexOf(oldColumnName) && !oldColumnName.includes("Backlog")) {
const itemId = (await octokit.graphql(`mutation {
updateProjectV2ItemFieldValue(
input: {
projectId: "${projectData.id}",
itemId: "${issueIdMap[issue.node_id].itemId}",
fieldId: "${statusField.id}",
value: {
singleSelectOptionId: "${statusField.options.find((x) => newColumnName === x.name).id}"
}
}
) {
projectV2Item {
id
}
}
}`)).updateProjectV2ItemFieldValue.projectV2Item.id;
console.log(`Moved issue ${repoName}#${issue.number} from "${oldColumnName}" to "${newColumnName}"`);
}
}
}
}
}
const projectNumber = parseInt(process.argv[2]);
if (isNaN(projectNumber) || PROJECT_REPOS[projectNumber] == null) {
throw new Error(`Unsupported project ID ${projectNumber}, expected one of [${Object.keys(PROJECT_REPOS).map((x) => x.toString()).join(", ")}]`);
}
importIssues(projectNumber).catch((err) => {
console.error(err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment