|
| 1 | +/* |
| 2 | + * Copyright OpenSearch Contributors |
| 3 | + * SPDX-License-Identifier: Apache-2.0 |
| 4 | + * |
| 5 | + * The OpenSearch Contributors require contributions made to |
| 6 | + * this file be licensed under the Apache-2.0 license or a |
| 7 | + * compatible open source license. |
| 8 | + */ |
| 9 | + |
| 10 | +// Name : addIssueToGitHubProjectV2 |
| 11 | +// Description : add issue to github project v2 based on labels |
| 12 | +// Arguments : |
| 13 | +// - itemId : (string) the item Node Id when it gets added to a project. |
| 14 | +// - method : (string) the method to update an item field in a given project, the item must have been already added to a project. |
| 15 | +// : - label: Adding `Roadmap:Releases/Project Health` label will update the `Roadmap` field value of an item to `Releases, Project Health`. |
| 16 | +// : Field name and value separated by `:` and `/` replaced with `, `. |
| 17 | +// - project : (string) the `<Organization Name>/<Project Number> where the item field belongs to. |
| 18 | +// : Ex: `opensearch-project/206` which is the OpenSearch Roadmap Project |
| 19 | +// Return : (string) `Item Node Id` if success, else `null` |
| 20 | +// Requirements : ADDITIONAL_RESOURCE_CONTEXT=true |
| 21 | + |
| 22 | +import { randomBytes } from 'crypto'; |
| 23 | +import { Probot } from 'probot'; |
| 24 | +import { Resource } from '../service/resource/resource'; |
| 25 | +import { validateResourceConfig } from '../utility/verification/verify-resource'; |
| 26 | + |
| 27 | +export interface UpdateGithubProjectV2ItemFieldParams { |
| 28 | + itemId: string; |
| 29 | + method: string; |
| 30 | + project: string; |
| 31 | +} |
| 32 | + |
| 33 | +export async function validateProject(app: Probot, resource: Resource, project: string): Promise<Boolean> { |
| 34 | + const projOrg = project.split('/')[0]; |
| 35 | + const projNum = Number(project.split('/')[1]); |
| 36 | + const projRes = resource.organizations.get(projOrg)?.projects.get(projNum); |
| 37 | + |
| 38 | + if (!projRes) { |
| 39 | + app.log.error(`Project ${projNum} in organization ${projOrg} is not defined in resource config!`); |
| 40 | + return false; |
| 41 | + } |
| 42 | + |
| 43 | + return true; |
| 44 | +} |
| 45 | + |
| 46 | +export default async function updateGithubProjectV2ItemField( |
| 47 | + app: Probot, |
| 48 | + context: any, |
| 49 | + resource: Resource, |
| 50 | + { itemId, method, project }: UpdateGithubProjectV2ItemFieldParams, |
| 51 | +): Promise<string | null> { |
| 52 | + if (!(await validateResourceConfig(app, context, resource))) return null; |
| 53 | + if (!(await validateProject(app, resource, project))) return null; |
| 54 | + |
| 55 | + // Verify triggered event |
| 56 | + if (!context.payload.label) { |
| 57 | + app.log.error("Only 'issues.labeled' event is supported on this call."); |
| 58 | + return null; |
| 59 | + } |
| 60 | + |
| 61 | + // Verify itemId present |
| 62 | + if (!itemId) { |
| 63 | + app.log.error('No Item Node Id provided in parameter.'); |
| 64 | + return null; |
| 65 | + } |
| 66 | + |
| 67 | + // Verify update method |
| 68 | + if (method !== 'label') { |
| 69 | + app.log.error("Only 'label' method is supported in this call at the moment."); |
| 70 | + return null; |
| 71 | + } |
| 72 | + |
| 73 | + const projectSplit = project.split('/'); |
| 74 | + const projectNode = resource.organizations.get(projectSplit[0])?.projects.get(Number(projectSplit[1])); |
| 75 | + const projectNodeId = projectNode?.nodeId; |
| 76 | + const labelName = context.payload.label.name; |
| 77 | + const labelSplit = labelName.split(':'); |
| 78 | + |
| 79 | + // At the moment only labels has `:` as separator will be assigned to a field or update values |
| 80 | + if (!labelSplit[1]) { |
| 81 | + app.log.error(`Label '${labelName}' is invalid. Please make sure your label is formatted as '<FieldName>:<FieldValue>'.`); |
| 82 | + return null; |
| 83 | + } |
| 84 | + |
| 85 | + const fieldName = labelSplit[0]; |
| 86 | + const fieldValue = labelSplit[1].replaceAll('/', ', '); |
| 87 | + const fieldNode = projectNode?.fields.get(fieldName); |
| 88 | + |
| 89 | + // Update item field |
| 90 | + try { |
| 91 | + app.log.info(`Attempt to update field '${fieldName}' with value '${fieldValue}' for item '${itemId}' in project ${project} ...`); |
| 92 | + const mutationId = await randomBytes(20).toString('hex'); |
| 93 | + if (projectNode && fieldNode && fieldNode?.fieldType === 'SINGLE_SELECT') { |
| 94 | + const matchingFieldOption = fieldNode.context.options.find((fieldOption: any) => fieldOption.name === fieldValue); |
| 95 | + if (matchingFieldOption) { |
| 96 | + const updateItemFieldMutation = ` |
| 97 | + mutation { |
| 98 | + updateProjectV2ItemFieldValue( |
| 99 | + input: { |
| 100 | + clientMutationId: "${mutationId}", |
| 101 | + projectId: "${projectNodeId}", |
| 102 | + itemId: "${itemId}", |
| 103 | + fieldId: "${fieldNode?.nodeId}", |
| 104 | + value: { |
| 105 | + singleSelectOptionId: "${matchingFieldOption.id}" |
| 106 | + } |
| 107 | + } |
| 108 | + ) { |
| 109 | + projectV2Item { |
| 110 | + id |
| 111 | + } |
| 112 | + } |
| 113 | + } |
| 114 | + `; |
| 115 | + const responseUpdateItemField = await context.octokit.graphql(updateItemFieldMutation); |
| 116 | + app.log.info(responseUpdateItemField); |
| 117 | + return responseUpdateItemField.updateProjectV2ItemFieldValue.projectV2Item.id; |
| 118 | + } |
| 119 | + } |
| 120 | + app.log.error(`Either '${project}' / '${fieldName}' not exist, or '${fieldName}' has an unsupported field type (currently support: SINGLE_SELECT)`); |
| 121 | + } catch (e) { |
| 122 | + app.log.error(`ERROR: ${e}`); |
| 123 | + return null; |
| 124 | + } |
| 125 | + |
| 126 | + return null; |
| 127 | +} |
0 commit comments