1
0
mirror of synced 2025-12-23 11:54:18 -05:00
Files
docs/.github/actions-scripts/projects.js

255 lines
6.8 KiB
JavaScript

import { graphql } from '@octokit/graphql'
// Shared functions for managing projects (memex)
// Pull out the node ID of a project field
export function findFieldID(fieldName, data) {
const field = data.organization.projectNext.fields.nodes.find((field) => field.name === fieldName)
if (field && field.id) {
return field.id
} else {
throw new Error(`A field called "${fieldName}" was not found. Check if the field was renamed.`)
}
}
// Pull out the node ID of a single select field value
export function findSingleSelectID(singleSelectName, fieldName, data) {
const field = data.organization.projectNext.fields.nodes.find((field) => field.name === fieldName)
if (!field) {
throw new Error(`A field called "${fieldName}" was not found. Check if the field was renamed.`)
}
const singleSelect = JSON.parse(field.settings).options.find(
(field) => field.name === singleSelectName
)
if (singleSelect && singleSelect.id) {
return singleSelect.id
} else {
throw new Error(
`A single select called "${singleSelectName}" for the field "${fieldName}" was not found. Check if the single select was renamed.`
)
}
}
// Given a list of PR/issue node IDs and a project node ID,
// adds the PRs/issues to the project
// and returns the node IDs of the project items
export async function addItemsToProject(items, project) {
console.log(`Adding ${items} to project ${project}`)
const mutations = items.map(
(item, index) => `
item_${index}: addProjectNextItem(input: {
projectId: $project
contentId: "${item}"
}) {
projectNextItem {
id
}
}
`
)
const mutation = `
mutation($project:ID!) {
${mutations.join(' ')}
}
`
const newItems = await graphql(mutation, {
project: project,
headers: {
authorization: `token ${process.env.TOKEN}`,
'GraphQL-Features': 'projects_next_graphql',
},
})
// The output of the mutation is
// {"item_0":{"projectNextItem":{"id":ID!}},...}
// Pull out the ID for each new item
const newItemIDs = Object.entries(newItems).map((item) => item[1].projectNextItem.id)
console.log(`New item IDs: ${newItemIDs}`)
return newItemIDs
}
export async function addItemToProject(item, project) {
const newItemIDs = await addItemsToProject([item], project)
const newItemID = newItemIDs[0]
return newItemID
}
// Given a GitHub login, returns a bool indicating
// whether the login is part of the docs team
export async function isDocsTeamMember(login) {
// Get all members of the docs team
const data = await graphql(
`
query {
organization(login: "github") {
team(slug: "docs") {
members {
nodes {
login
}
}
}
}
}
`,
{
headers: {
authorization: `token ${process.env.TOKEN}`,
},
}
)
const teamMembers = data.organization.team.members.nodes.map((entry) => entry.login)
return teamMembers.includes(login)
}
// Formats a date object into the required format for projects
export function formatDateForProject(date) {
return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate()
}
// Given a date object and optional turnaround time
// Calculate the date {turnaround} business days from now
// (excluding weekends; not considering holidays)
export function calculateDueDate(datePosted, turnaround = 2) {
let daysUntilDue
switch (datePosted.getDay()) {
case 4: // Thursday
daysUntilDue = turnaround + 2
break
case 5: // Friday
daysUntilDue = turnaround + 2
break
case 6: // Saturday
daysUntilDue = turnaround + 1
break
default:
daysUntilDue = turnaround
}
const millisecPerDay = 24 * 60 * 60 * 1000
const dueDate = new Date(datePosted.getTime() + millisecPerDay * daysUntilDue)
return dueDate
}
// Given a project item node ID and author login
// generates a GraphQL mutation to populate:
// - "Status" (as variable passed with the request)
// - "Date posted" (as today)
// - "Review due date" (as today + {turnaround} weekdays)
// - "Contributor type" (as variable passed with the request)
// - "Feature" (as {feature})
// - "Author" (as {author})"
export function generateUpdateProjectNextItemFieldMutation({
item,
author,
turnaround = 2,
feature = '',
}) {
const datePosted = new Date()
const dueDate = calculateDueDate(datePosted, turnaround)
// Build the mutation to update a single project field
// Specify literal=true to indicate that the value should be used as a string, not a variable
function generateMutationToUpdateField({ item, fieldID, value, literal = false }) {
const parsedValue = literal ? `value: "${value}"` : `value: ${value}`
// Strip all non-alphanumeric out of the item ID when creating the mutation ID to avoid a GraphQL parsing error
// (statistically, this should still give us a unique mutation ID)
return `
set_${fieldID.substr(1)}_item_${item.replaceAll(
/[^a-z0-9]/g,
''
)}: updateProjectNextItemField(input: {
projectId: $project
itemId: "${item}"
fieldId: ${fieldID}
${parsedValue}
}) {
projectNextItem {
id
}
}
`
}
const mutation = `
mutation(
$project: ID!
$statusID: ID!
$statusValueID: String!
$datePostedID: ID!
$reviewDueDateID: ID!
$contributorTypeID: ID!
$contributorType: String!
$sizeTypeID: ID!
$sizeType: String!
$featureID: ID!
$authorID: ID!
) {
${generateMutationToUpdateField({
item: item,
fieldID: '$statusID',
value: '$statusValueID',
})}
${generateMutationToUpdateField({
item: item,
fieldID: '$datePostedID',
value: formatDateForProject(datePosted),
literal: true,
})}
${generateMutationToUpdateField({
item: item,
fieldID: '$reviewDueDateID',
value: formatDateForProject(dueDate),
literal: true,
})}
${generateMutationToUpdateField({
item: item,
fieldID: '$contributorTypeID',
value: '$contributorType',
})}
${generateMutationToUpdateField({
item: item,
fieldID: '$sizeTypeID',
value: '$sizeType',
})}
${generateMutationToUpdateField({
item: item,
fieldID: '$featureID',
value: feature,
literal: true,
})}
${generateMutationToUpdateField({
item: item,
fieldID: '$authorID',
value: author,
literal: true,
})}
}
`
return mutation
}
export default {
addItemsToProject,
addItemToProject,
isDocsTeamMember,
findFieldID,
findSingleSelectID,
formatDateForProject,
calculateDueDate,
generateUpdateProjectNextItemFieldMutation,
}