273 lines
7.4 KiB
JavaScript
273 lines
7.4 KiB
JavaScript
import 'dotenv/config'
|
|
import { Command } from 'commander'
|
|
import { Octokit } from '@octokit/core'
|
|
|
|
class ProjectImporter {
|
|
constructor(githubToken) {
|
|
this.githubToken = githubToken
|
|
this.octokit = new Octokit({auth: this.githubToken})
|
|
}
|
|
|
|
async getAllReposPage(owner, cursor = null) {
|
|
const result = await this.octokit.graphql(
|
|
`
|
|
query($owner: String!, $cursor: String) {
|
|
repositoryOwner(login: $owner) {
|
|
repositories(first: 100, after: $cursor) {
|
|
pageInfo {
|
|
endCursor
|
|
hasNextPage
|
|
}
|
|
nodes {
|
|
name
|
|
owner { login }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
owner,
|
|
cursor
|
|
}
|
|
)
|
|
return result
|
|
}
|
|
|
|
async getAllRepos(owner) {
|
|
let repos = [], cursor = null, data = null
|
|
do {
|
|
let result = await this.getAllReposPage(owner, cursor)
|
|
data = result.repositoryOwner.repositories
|
|
cursor = data.pageInfo.endCursor
|
|
repos.push(...data.nodes)
|
|
} while (data.pageInfo.hasNextPage)
|
|
return repos
|
|
}
|
|
|
|
async getRepoIssuesPage(owner, name, cursor = null) {
|
|
const result = await this.octokit.graphql(
|
|
`
|
|
query($owner: String!, $name: String!, $cursor: String) {
|
|
repository(owner: $owner, name: $name) {
|
|
issues(first: 100, after: $cursor) {
|
|
pageInfo {
|
|
endCursor
|
|
hasNextPage
|
|
}
|
|
nodes {
|
|
id
|
|
number
|
|
title
|
|
closed
|
|
assignees(first: 1) {
|
|
nodes { login }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
owner,
|
|
name,
|
|
cursor
|
|
}
|
|
)
|
|
return result
|
|
}
|
|
|
|
async getRepoIssues(owner, name) {
|
|
let issues = [], cursor = null, data = null
|
|
do {
|
|
let result = await this.getRepoIssuesPage(owner, name, cursor)
|
|
data = result.repository.issues
|
|
cursor = data.pageInfo.endCursor
|
|
issues.push(...data.nodes)
|
|
} while (data.pageInfo.hasNextPage)
|
|
return issues
|
|
}
|
|
|
|
// https://docs.github.com/en/issues/trying-out-the-new-projects-experience/using-the-api-to-manage-projects#finding-the-node-id-of-an-organization-project
|
|
// To find the project number, look at the project URL
|
|
async getProjectId(org, number) {
|
|
const result = await this.octokit.graphql(
|
|
`
|
|
query ($org: String!, $number: Int!) {
|
|
organization(login: $org) {
|
|
projectNext(number: $number) {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
org,
|
|
number: parseInt(number),
|
|
}
|
|
)
|
|
return result.organization.projectNext.id
|
|
}
|
|
|
|
// https://docs.github.com/en/issues/trying-out-the-new-projects-experience/using-the-api-to-manage-projects#finding-the-node-id-of-a-field
|
|
async getProjectFields(projectId) {
|
|
const result = await this.octokit.graphql(
|
|
`
|
|
query ($projectId: ID!) {
|
|
node(id: $projectId) {
|
|
... on ProjectNext {
|
|
fields(first: 20) {
|
|
nodes {
|
|
id
|
|
name
|
|
settings
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
projectId,
|
|
}
|
|
)
|
|
|
|
return result.node.fields.nodes
|
|
}
|
|
|
|
async getFieldOptions(fields, fieldName) {
|
|
const field = fields.find((item) => item.name === fieldName)
|
|
if (field === undefined) { return null }
|
|
const { id, settings } = field
|
|
const data = JSON.parse(settings)
|
|
/* map names to objects for easier access */
|
|
const options = Object.assign({}, ...data.options.map(s => ({[s.name]: s.id})))
|
|
return { id, options }
|
|
}
|
|
|
|
// https://docs.github.com/en/issues/trying-out-the-new-projects-experience/using-the-api-to-manage-projects#adding-an-item-to-a-project
|
|
async addProjectItem(projectId, nodeId) {
|
|
const result = await this.octokit.graphql(
|
|
`
|
|
mutation ($projectId: ID!, $nodeId: ID!) {
|
|
addProjectNextItem(
|
|
input: { projectId: $projectId, contentId: $nodeId }
|
|
) {
|
|
projectNextItem {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
projectId,
|
|
nodeId,
|
|
}
|
|
)
|
|
|
|
return {
|
|
itemId: result.addProjectNextItem.projectNextItem.id,
|
|
}
|
|
}
|
|
|
|
// https://docs.github.com/en/issues/trying-out-the-new-projects-experience/using-the-api-to-manage-projects#updating-a-single-select-field
|
|
async updateProjectItem(projectId, itemId, field) {
|
|
await this.octokit.graphql(
|
|
`
|
|
mutation ($projectId: ID!, $itemId: ID!, $fieldId: ID!, $fieldValue: String!) {
|
|
updateProjectNextItemField(
|
|
input: {projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: $fieldValue}
|
|
) {
|
|
projectNextItem {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
projectId,
|
|
itemId,
|
|
fieldId: field.id,
|
|
fieldValue: field.value,
|
|
}
|
|
)
|
|
}
|
|
|
|
async createProjectItem(projectId, nodeId, fields) {
|
|
const { itemId } = await this.addProjectItem(projectId, nodeId)
|
|
for (const field of fields) {
|
|
await this.updateProjectItem(projectId, itemId, field)
|
|
}
|
|
}
|
|
|
|
issueStatusName(issue) {
|
|
if (issue.closed) { return 'Done' }
|
|
else if (issue.assignees.nodes.length > 0) { return 'In Progress' }
|
|
else { return 'Todo' }
|
|
}
|
|
|
|
repoPlatformName(repo) {
|
|
const repoToPlatform = {
|
|
'status-react': 'Mobile',
|
|
'status-desktop': 'Desktop',
|
|
'status-web': 'Web',
|
|
}
|
|
return repoToPlatform[repo.name]
|
|
}
|
|
|
|
async importRepository(projectId, repo, fields, dryRun) {
|
|
// Get field ID and value of Status and Platform
|
|
const status = await this.getFieldOptions(fields, 'Status')
|
|
const platform = await this.getFieldOptions(fields, 'Platform')
|
|
|
|
const issues = await this.getRepoIssues(repo.owner.login, repo.name)
|
|
|
|
for (const issue of issues) {
|
|
console.log(` > ${repo.owner.login}/${repo.name}#${issue.number} - ${issue.title}`)
|
|
if (dryRun) { continue }
|
|
let fields = []
|
|
if (status) {
|
|
fields.push({ id: status.id, value: status.options[this.issueStatusName(issue)] })
|
|
}
|
|
if (platform) {
|
|
fields.push({ id: platform.id, value: platform.options[this.repoPlatformName(repo)] })
|
|
}
|
|
await this.createProjectItem(projectId, issue.id, fields)
|
|
}
|
|
}
|
|
}
|
|
|
|
const parseOpts = () => {
|
|
const program = new Command()
|
|
|
|
program
|
|
.requiredOption('-t, --github-token <token>', 'API token for GitHub', process.env.GITHUB_AUTH_TOKEN)
|
|
.requiredOption('-o, --github-org <name>', 'Name of GitHub Organization', 'status-im')
|
|
.requiredOption('-p, --project-number <number>', 'Number of GitHub Project from URL')
|
|
.option('-r, --repos-regex <regex>', 'Regex to match repos', '^status-(react|desktop|web)$')
|
|
.option('-d, --dry-run', 'Only list issues, do not import', false)
|
|
.parse()
|
|
|
|
return program.opts()
|
|
}
|
|
|
|
const main = async () => {
|
|
const opts = parseOpts()
|
|
|
|
const pi = new ProjectImporter(opts.githubToken)
|
|
const projectId = await pi.getProjectId(opts.githubOrg, opts.projectNumber)
|
|
const fields = await pi.getProjectFields(projectId)
|
|
|
|
let repos = await pi.getAllRepos(opts.githubOrg)
|
|
if (opts.reposRegex) {
|
|
repos = repos.filter(repo => repo.name.match(opts.reposRegex))
|
|
}
|
|
|
|
for (const repo of repos) {
|
|
//console.log(` * ${repo.owner.login}/${repo.name}`)
|
|
await pi.importRepository(projectId, repo, fields, opts.dryRun)
|
|
}
|
|
}
|
|
|
|
main()
|