Skip to content

Commit ec8d7e5

Browse files
Passing task outputs from one to another based on name identifier (#39)
* Add support for task outputs save and passing Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> * Switch name postfix to task order Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> * Add initial outputs passing from one to another task Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> * Allow outputs to substitute without changing initial values Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> * Tweak formats Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> * Add test cases Signed-off-by: Peter Zhu <zhujiaxi@amazon.com> --------- Signed-off-by: Peter Zhu <zhujiaxi@amazon.com>
1 parent 4a90494 commit ec8d7e5

7 files changed

+89
-21
lines changed

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.1.18",
3+
"version": "0.2.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

+10-5
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ export default async function addIssueToGitHubProjectV2(
4545
context: any,
4646
resource: Resource,
4747
{ labels, projects }: AddIssueToGitHubProjectV2Params,
48-
): Promise<Map<string, [string, string]> | null> {
48+
): Promise<string | null> {
4949
if (!(await validateResourceConfig(app, context, resource))) return null;
5050
if (!(await validateProjects(app, resource, projects))) return null;
5151

52+
// Verify triggered event
53+
if (!context.payload.label) {
54+
app.log.error("Only 'issues.labeled' event is supported on this call.");
55+
return null;
56+
}
57+
5258
// Verify triggered label
5359
const label = context.payload.label.name.trim();
5460
if (!labels.includes(label)) {
@@ -60,7 +66,7 @@ export default async function addIssueToGitHubProjectV2(
6066
const repoName = context.payload.repository.name;
6167
const issueNumber = context.payload.issue.number;
6268
const issueNodeId = context.payload.issue.node_id;
63-
const itemIdMap = new Map<string, [string, string]>();
69+
let itemId = null;
6470

6571
// Add to project
6672
try {
@@ -85,14 +91,13 @@ export default async function addIssueToGitHubProjectV2(
8591
`;
8692
const responseAddToProject = await context.octokit.graphql(addToProjectMutation);
8793
app.log.info(responseAddToProject);
88-
const itemId = responseAddToProject.addProjectV2ItemById.item.id;
89-
itemIdMap.set(project, [itemId, label]);
94+
itemId = responseAddToProject.addProjectV2ItemById.item.id;
9095
}),
9196
);
9297
} catch (e) {
9398
app.log.error(`ERROR: ${e}`);
9499
return null;
95100
}
96101

97-
return itemIdMap;
102+
return itemId;
98103
}

src/config/operation-config.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ export class OperationConfig extends Config {
6767
}
6868

6969
private static async _initTasks(taskDataArray: TaskData[]): Promise<Task[]> {
70+
let taskCounter: number = 0;
7071
const taskObjArray = taskDataArray.map((taskData) => {
72+
taskCounter += 1;
7173
const taskObj = new Task(taskData.call, taskData.args, taskData.name);
72-
console.log(`Setup Task: ${taskObj.name}`);
74+
taskObj.name += `#${taskCounter}`;
75+
console.log(`Setup Task ${taskCounter}: ${taskObj.name}`);
7376
return taskObj;
7477
});
7578
return taskObjArray;

src/service/operation/task.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
* compatible open source license.
88
*/
99

10-
import randomstring from 'randomstring';
1110
import { TaskArgData } from '../../config/types';
1211

1312
export class Task {
14-
private readonly _name: string; // uid
13+
private _name: string; // uid
1514

1615
private readonly _callName: string;
1716

@@ -23,8 +22,14 @@ export class Task {
2322
const callArray = call.trim().split('@');
2423
[this._callName, this._callFunc] = callArray;
2524
this._callArgs = callArgs;
26-
const namePostfix = randomstring.generate(8);
27-
this._name = name ? `${name}#${namePostfix}` : `${this.callName}#${namePostfix}`;
25+
this._name = name || this.callName;
26+
}
27+
28+
public set name(taskName: string) {
29+
if (!taskName || taskName.trim() === '') {
30+
throw new Error('Task Name input cannot be empty');
31+
}
32+
this._name = taskName;
2833
}
2934

3035
public get name(): string {

src/service/service.ts

+53-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Operation } from './operation/operation';
1515
import { Task } from './operation/task';
1616
import { ResourceConfig } from '../config/resource-config';
1717
import { OperationConfig } from '../config/operation-config';
18+
import { TaskArgData } from '../config/types';
1819
import { octokitAuth } from '../utility/probot/octokit';
1920

2021
export class Service {
@@ -26,6 +27,11 @@ export class Service {
2627

2728
private _app: Probot;
2829

30+
// Map<eventName, Map<taskName, returnValue>>
31+
private _outputs: Map<string, Map<string, any>>;
32+
33+
private readonly subPattern = /\$\{\{\s*(.*?)\s*\}\}/;
34+
2935
constructor(name: string) {
3036
this._name = name;
3137
}
@@ -55,6 +61,7 @@ export class Service {
5561
this._resource = await resConfigObj.initResource();
5662
const opConfigObj = new OperationConfig(operationConfigPath);
5763
this._operation = await opConfigObj.initOperation();
64+
this._outputs = new Map<string, any>();
5865
this._registerEvents();
5966
}
6067

@@ -63,35 +70,73 @@ export class Service {
6370
await promise; // Make sure tasks are completed in sequential orders
6471

6572
const callPath = await realpath(`./bin/call/${task.callName}.js`);
66-
const { callFunc } = task;
67-
const { callArgs } = task;
73+
const { name, callFunc, callArgs } = task;
6874

69-
console.log(`[${event}]: Verify call lib: ${callPath}`);
75+
console.log(`[${event}]: Start call now: ${name}`);
76+
console.log(`[${event}]: Check call lib: ${callPath}`);
7077
try {
7178
await access(callPath);
7279
} catch (e) {
7380
console.error(`ERROR: ${e}`);
7481
}
7582

7683
const callStack = await import(callPath);
84+
const callArgsSub = await this._outputsSubstitution({ ...callArgs }, event);
85+
7786
if (callFunc === 'default') {
7887
console.log(`[${event}]: Call default function: [${callStack.default.name}]`);
79-
await callStack.default(this.app, context, this.resource, { ...callArgs });
88+
const resultDefault = await callStack.default(this.app, context, this.resource, { ...callArgsSub });
89+
this._outputs.get(event)?.set(name, resultDefault);
90+
console.log(this._outputs.get(event));
8091
} else {
8192
console.log(callStack);
8293
const callFuncCustom = callStack[callFunc];
8394
console.log(`[${event}]: Call custom function: [${callFuncCustom.name}]`);
8495
if (!(typeof callFuncCustom === 'function')) {
8596
throw new Error(`[${event}]: ${callFuncCustom} is not a function, please verify in ${callPath}`);
8697
}
87-
await callFuncCustom(this.app, context, this.resource, { ...callArgs });
98+
this._outputs.get(event)?.set(name, await callFuncCustom(this.app, context, this.resource, { ...callArgsSub }));
8899
}
89100
}, Promise.resolve());
90101
}
91102

103+
private async _outputsSubstitution(callArgs: TaskArgData, event: string): Promise<TaskArgData> {
104+
console.log(`[${event}]: Call with args:`);
105+
106+
const callArgsTemp = callArgs;
107+
const argEntries = Object.entries(callArgsTemp);
108+
109+
await Promise.all(
110+
argEntries.map(async ([argName, argValue]) => {
111+
console.log(`[${event}]: args: ${argName}: ${argValue}`);
112+
113+
// Overwrite callArgsTemp if user choose to substitute value with outputs from previous task ${{ outputs.<TaskName>#<TaskOrder> }}
114+
if (Array.isArray(argValue)) {
115+
// string[]
116+
callArgsTemp[argName] = await Promise.all(argValue.map(async (argValueItem) => this._matchSubPattern(argValueItem as string, event)));
117+
} else {
118+
// string
119+
callArgsTemp[argName] = await this._matchSubPattern(argValue as string, event);
120+
}
121+
}),
122+
);
123+
return callArgsTemp;
124+
}
125+
126+
private async _matchSubPattern(callArgsValue: string, event: string): Promise<string> {
127+
const match = callArgsValue.match(this.subPattern);
128+
if (match) {
129+
// If user substitution pattern ${{ outputs.<TaskName>#<TaskOrder> }} found in operation config
130+
// Return substituion value based on return value saved in outputs
131+
const outputMatch = this._outputs.get(event)?.get(match[1].replace('outputs.', ''));
132+
console.log(`StrSub: ${callArgsValue}, Match: ${match[1]}, Output: ${outputMatch}`);
133+
return outputMatch;
134+
}
135+
return callArgsValue;
136+
}
137+
92138
private async _registerEvents(): Promise<void> {
93-
const { events } = this.operation;
94-
const { tasks } = this.operation;
139+
const { events, tasks } = this.operation;
95140
console.log(`Evaluate events: [${events}]`);
96141
if (!events) {
97142
throw new Error('No events defined in the operation!');
@@ -102,6 +147,7 @@ export class Service {
102147

103148
events.forEach((event) => {
104149
console.log(`Register event: "${event}"`);
150+
this._outputs.set(event, new Map<string, any>());
105151
if (event === 'all') {
106152
console.warn('WARNING! All events will be listened based on the config!');
107153
this._app.onAny(async (context) => {

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ describe('addIssueToGitHubProjectV2Functions', () => {
8080
});
8181

8282
describe('addIssueToGitHubProjectV2', () => {
83+
it('should print error and return null if it is not a issues.labeled event', async () => {
84+
context.payload.label = undefined;
85+
86+
const result = await addIssueToGitHubProjectV2(app, context, resource, params);
87+
88+
expect(app.log.error).toHaveBeenCalledWith("Only 'issues.labeled' event is supported on this call.");
89+
expect(result).toBe(null);
90+
});
91+
8392
it('should print error if context label does not match the ones in resource config', async () => {
8493
context.payload.label.name = 'enhancement';
8594

@@ -116,7 +125,7 @@ describe('addIssueToGitHubProjectV2Functions', () => {
116125
/* prettier-ignore-end */
117126

118127
expect(context.octokit.graphql).toHaveBeenCalledWith(graphQLCallStack);
119-
expect(JSON.stringify((result as Map<string, [string, string]>).get('test-org/222'))).toBe('["new-item-id","Meta"]');
128+
expect(result).toBe('new-item-id');
120129
expect(app.log.info).toHaveBeenCalledWith(graphQLResponse);
121130
});
122131

0 commit comments

Comments
 (0)