Skip to content

Commit b50a1ac

Browse files
Add Update Field Value Call to Automation App (#48)
* Add Update Field Value Call to Automation App Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> * Add more tests Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> --------- Signed-off-by: Peter Zhu <zhujiaxi@amazon.com>
1 parent f2fe012 commit b50a1ac

7 files changed

+389
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
name: Add Meta RFC Issue to OpenSearch Project Roadmap
3+
4+
events:
5+
- issues.labeled
6+
7+
tasks:
8+
- name: Add issue to OpenSearch Project Roadmap
9+
call: add-issue-to-github-project-v2@default
10+
args:
11+
labels:
12+
- Meta
13+
- META
14+
- RFC
15+
- rfc
16+
- Roadmap:Security
17+
- Roadmap:Security Analytics
18+
- Roadmap:Modular Architecture
19+
- Roadmap:Cost/Performance/Scale
20+
- Roadmap:Stability/Availability/Resiliency
21+
- Roadmap:Releases/Project Health
22+
- Roadmap:Observability/Log Analytics
23+
- Roadmap:Vector Database/GenAI
24+
- Roadmap:Ease of Use
25+
- Roadmap:Search and ML
26+
project: opensearch-project/206
27+
28+
- name: Update item field value in OpenSearch Project Roadmap
29+
call: update-github-project-v2-item-field@default
30+
args:
31+
itemId: ${{ outputs.Add issue to OpenSearch Project Roadmap#1 }}
32+
method: label
33+
project: opensearch-project/206

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opensearch-automation-app",
3-
"version": "0.2.3",
3+
"version": "0.3.0",
44
"description": "An Automation App that handles all your GitHub Repository Activities",
55
"author": "Peter Zhu",
66
"homepage": "https://github.com/opensearch-project/automation-app",

src/call/add-issue-to-github-project-v2.ts

+24-29
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
// Description : add issue to github project v2 based on labels
1212
// Arguments :
1313
// - labels : (array) label name, add any of the listed labels on an issue will add the issue to project listed in `projects` arg
14-
// - projects : (array) the list of `<Organization Name>/<Project Number> for the issues to be added to.
14+
// - project : (string) the `<Organization Name>/<Project Number> for the issues to be added to.
1515
// : Ex: `opensearch-project/206` which is the OpenSearch Roadmap Project
16+
// Return : (string) `Item Node Id` if success, else `null`
1617
// Requirements : ADDITIONAL_RESOURCE_CONTEXT=true
1718

1819
import { randomBytes } from 'crypto';
@@ -22,32 +23,30 @@ import { validateResourceConfig } from '../utility/verification/verify-resource'
2223

2324
export interface AddIssueToGitHubProjectV2Params {
2425
labels: string[];
25-
projects: string[];
26+
project: string;
2627
}
2728

28-
export async function validateProjects(app: Probot, resource: Resource, projectsArray: string[]): Promise<Boolean> {
29-
return projectsArray.every((proj) => {
30-
const projOrg = proj.split('/')[0];
31-
const projNum = Number(proj.split('/')[1]);
32-
const project = resource.organizations.get(projOrg)?.projects.get(projNum);
29+
export async function validateProject(app: Probot, resource: Resource, project: string): Promise<Boolean> {
30+
const projOrg = project.split('/')[0];
31+
const projNum = Number(project.split('/')[1]);
32+
const projRes = resource.organizations.get(projOrg)?.projects.get(projNum);
3333

34-
if (!project) {
35-
app.log.error(`Project ${projNum} in organization ${projOrg} is not defined in resource config!`);
36-
return false;
37-
}
34+
if (!projRes) {
35+
app.log.error(`Project ${projNum} in organization ${projOrg} is not defined in resource config!`);
36+
return false;
37+
}
3838

39-
return true;
40-
});
39+
return true;
4140
}
4241

4342
export default async function addIssueToGitHubProjectV2(
4443
app: Probot,
4544
context: any,
4645
resource: Resource,
47-
{ labels, projects }: AddIssueToGitHubProjectV2Params,
46+
{ labels, project }: AddIssueToGitHubProjectV2Params,
4847
): Promise<string | null> {
4948
if (!(await validateResourceConfig(app, context, resource))) return null;
50-
if (!(await validateProjects(app, resource, projects))) return null;
49+
if (!(await validateProject(app, resource, project))) return null;
5150

5251
// Verify triggered event
5352
if (!context.payload.label) {
@@ -66,17 +65,15 @@ export default async function addIssueToGitHubProjectV2(
6665
const repoName = context.payload.repository.name;
6766
const issueNumber = context.payload.issue.number;
6867
const issueNodeId = context.payload.issue.node_id;
69-
let itemId = null;
68+
const projectSplit = project.split('/');
69+
const projectNodeId = resource.organizations.get(projectSplit[0])?.projects.get(Number(projectSplit[1]))?.nodeId;
70+
let itemNodeId = null;
7071

7172
// Add to project
7273
try {
73-
await Promise.all(
74-
projects.map(async (project) => {
75-
app.log.info(`Attempt to add ${orgName}/${repoName}/${issueNumber} to project ${project}`);
76-
const mutationId = await randomBytes(20).toString('hex');
77-
const projectSplit = project.split('/');
78-
const projectNodeId = resource.organizations.get(projectSplit[0])?.projects.get(Number(projectSplit[1]))?.nodeId;
79-
const addToProjectMutation = `
74+
app.log.info(`Attempt to add ${orgName}/${repoName}/${issueNumber} to project ${project}`);
75+
const mutationId = await randomBytes(20).toString('hex');
76+
const addToProjectMutation = `
8077
mutation {
8178
addProjectV2ItemById(input: {
8279
clientMutationId: "${mutationId}",
@@ -89,15 +86,13 @@ export default async function addIssueToGitHubProjectV2(
8986
}
9087
}
9188
`;
92-
const responseAddToProject = await context.octokit.graphql(addToProjectMutation);
93-
app.log.info(responseAddToProject);
94-
itemId = responseAddToProject.addProjectV2ItemById.item.id;
95-
}),
96-
);
89+
const responseAddToProject = await context.octokit.graphql(addToProjectMutation);
90+
app.log.info(responseAddToProject);
91+
itemNodeId = responseAddToProject.addProjectV2ItemById.item.id;
9792
} catch (e) {
9893
app.log.error(`ERROR: ${e}`);
9994
return null;
10095
}
10196

102-
return itemId;
97+
return itemNodeId;
10398
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
}

test/call/add-issue-to-github-project-v2.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import addIssueToGitHubProjectV2, { AddIssueToGitHubProjectV2Params } from '../../src/call/add-issue-to-github-project-v2';
11-
import { validateProjects } from '../../src/call/add-issue-to-github-project-v2';
11+
import { validateProject } from '../../src/call/add-issue-to-github-project-v2';
1212
import { Probot, Logger } from 'probot';
1313

1414
// Mock mutationId return
@@ -59,7 +59,7 @@ describe('addIssueToGitHubProjectV2Functions', () => {
5959

6060
params = {
6161
labels: ['Meta', 'RFC'],
62-
projects: ['test-org/222'],
62+
project: 'test-org/222',
6363
};
6464
});
6565

@@ -71,7 +71,7 @@ describe('addIssueToGitHubProjectV2Functions', () => {
7171
it('should print error if validateProjects returns false', async () => {
7272
resource.organizations.get('test-org').projects.delete(222);
7373

74-
const result = await validateProjects(app, resource, params.projects);
74+
const result = await validateProject(app, resource, params.project);
7575

7676
expect(app.log.error).toHaveBeenCalledWith('Project 222 in organization test-org is not defined in resource config!');
7777
expect(result).toBe(false);
@@ -99,7 +99,7 @@ describe('addIssueToGitHubProjectV2Functions', () => {
9999
expect(context.octokit.graphql).not.toHaveBeenCalled();
100100
});
101101

102-
it('should add context issue to project when conditions are met', async () => {
102+
it('should add issue to project when conditions are met', async () => {
103103
const graphQLResponse = {
104104
addProjectV2ItemById: { item: { id: 'new-item-id' } },
105105
};

0 commit comments

Comments
 (0)