Skip to content

Commit

Permalink
Passing task outputs from one to another based on name identifier (#39
Browse files Browse the repository at this point in the history
)

* 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>
  • Loading branch information
peterzhuamazon authored Dec 9, 2024
1 parent 4a90494 commit ec8d7e5
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 21 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opensearch-automation-app",
"version": "0.1.18",
"version": "0.2.0",
"description": "An Automation App that handles all your GitHub Repository Activities",
"author": "Peter Zhu",
"homepage": "https://github.com/opensearch-project/automation-app",
Expand Down
15 changes: 10 additions & 5 deletions src/call/add-issue-to-github-project-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ export default async function addIssueToGitHubProjectV2(
context: any,
resource: Resource,
{ labels, projects }: AddIssueToGitHubProjectV2Params,
): Promise<Map<string, [string, string]> | null> {
): Promise<string | null> {
if (!(await validateResourceConfig(app, context, resource))) return null;
if (!(await validateProjects(app, resource, projects))) return null;

// Verify triggered event
if (!context.payload.label) {
app.log.error("Only 'issues.labeled' event is supported on this call.");
return null;
}

// Verify triggered label
const label = context.payload.label.name.trim();
if (!labels.includes(label)) {
Expand All @@ -60,7 +66,7 @@ export default async function addIssueToGitHubProjectV2(
const repoName = context.payload.repository.name;
const issueNumber = context.payload.issue.number;
const issueNodeId = context.payload.issue.node_id;
const itemIdMap = new Map<string, [string, string]>();
let itemId = null;

// Add to project
try {
Expand All @@ -85,14 +91,13 @@ export default async function addIssueToGitHubProjectV2(
`;
const responseAddToProject = await context.octokit.graphql(addToProjectMutation);
app.log.info(responseAddToProject);
const itemId = responseAddToProject.addProjectV2ItemById.item.id;
itemIdMap.set(project, [itemId, label]);
itemId = responseAddToProject.addProjectV2ItemById.item.id;
}),
);
} catch (e) {
app.log.error(`ERROR: ${e}`);
return null;
}

return itemIdMap;
return itemId;
}
5 changes: 4 additions & 1 deletion src/config/operation-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@ export class OperationConfig extends Config {
}

private static async _initTasks(taskDataArray: TaskData[]): Promise<Task[]> {
let taskCounter: number = 0;
const taskObjArray = taskDataArray.map((taskData) => {
taskCounter += 1;
const taskObj = new Task(taskData.call, taskData.args, taskData.name);
console.log(`Setup Task: ${taskObj.name}`);
taskObj.name += `#${taskCounter}`;
console.log(`Setup Task ${taskCounter}: ${taskObj.name}`);
return taskObj;
});
return taskObjArray;
Expand Down
13 changes: 9 additions & 4 deletions src/service/operation/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
* compatible open source license.
*/

import randomstring from 'randomstring';
import { TaskArgData } from '../../config/types';

export class Task {
private readonly _name: string; // uid
private _name: string; // uid

private readonly _callName: string;

Expand All @@ -23,8 +22,14 @@ export class Task {
const callArray = call.trim().split('@');
[this._callName, this._callFunc] = callArray;
this._callArgs = callArgs;
const namePostfix = randomstring.generate(8);
this._name = name ? `${name}#${namePostfix}` : `${this.callName}#${namePostfix}`;
this._name = name || this.callName;
}

public set name(taskName: string) {
if (!taskName || taskName.trim() === '') {
throw new Error('Task Name input cannot be empty');
}
this._name = taskName;
}

public get name(): string {
Expand Down
60 changes: 53 additions & 7 deletions src/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Operation } from './operation/operation';
import { Task } from './operation/task';
import { ResourceConfig } from '../config/resource-config';
import { OperationConfig } from '../config/operation-config';
import { TaskArgData } from '../config/types';
import { octokitAuth } from '../utility/probot/octokit';

export class Service {
Expand All @@ -26,6 +27,11 @@ export class Service {

private _app: Probot;

// Map<eventName, Map<taskName, returnValue>>
private _outputs: Map<string, Map<string, any>>;

private readonly subPattern = /\$\{\{\s*(.*?)\s*\}\}/;

constructor(name: string) {
this._name = name;
}
Expand Down Expand Up @@ -55,6 +61,7 @@ export class Service {
this._resource = await resConfigObj.initResource();
const opConfigObj = new OperationConfig(operationConfigPath);
this._operation = await opConfigObj.initOperation();
this._outputs = new Map<string, any>();
this._registerEvents();
}

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

const callPath = await realpath(`./bin/call/${task.callName}.js`);
const { callFunc } = task;
const { callArgs } = task;
const { name, callFunc, callArgs } = task;

console.log(`[${event}]: Verify call lib: ${callPath}`);
console.log(`[${event}]: Start call now: ${name}`);
console.log(`[${event}]: Check call lib: ${callPath}`);
try {
await access(callPath);
} catch (e) {
console.error(`ERROR: ${e}`);
}

const callStack = await import(callPath);
const callArgsSub = await this._outputsSubstitution({ ...callArgs }, event);

if (callFunc === 'default') {
console.log(`[${event}]: Call default function: [${callStack.default.name}]`);
await callStack.default(this.app, context, this.resource, { ...callArgs });
const resultDefault = await callStack.default(this.app, context, this.resource, { ...callArgsSub });
this._outputs.get(event)?.set(name, resultDefault);
console.log(this._outputs.get(event));
} else {
console.log(callStack);
const callFuncCustom = callStack[callFunc];
console.log(`[${event}]: Call custom function: [${callFuncCustom.name}]`);
if (!(typeof callFuncCustom === 'function')) {
throw new Error(`[${event}]: ${callFuncCustom} is not a function, please verify in ${callPath}`);
}
await callFuncCustom(this.app, context, this.resource, { ...callArgs });
this._outputs.get(event)?.set(name, await callFuncCustom(this.app, context, this.resource, { ...callArgsSub }));
}
}, Promise.resolve());
}

private async _outputsSubstitution(callArgs: TaskArgData, event: string): Promise<TaskArgData> {
console.log(`[${event}]: Call with args:`);

const callArgsTemp = callArgs;
const argEntries = Object.entries(callArgsTemp);

await Promise.all(
argEntries.map(async ([argName, argValue]) => {
console.log(`[${event}]: args: ${argName}: ${argValue}`);

// Overwrite callArgsTemp if user choose to substitute value with outputs from previous task ${{ outputs.<TaskName>#<TaskOrder> }}
if (Array.isArray(argValue)) {
// string[]
callArgsTemp[argName] = await Promise.all(argValue.map(async (argValueItem) => this._matchSubPattern(argValueItem as string, event)));
} else {
// string
callArgsTemp[argName] = await this._matchSubPattern(argValue as string, event);
}
}),
);
return callArgsTemp;
}

private async _matchSubPattern(callArgsValue: string, event: string): Promise<string> {
const match = callArgsValue.match(this.subPattern);
if (match) {
// If user substitution pattern ${{ outputs.<TaskName>#<TaskOrder> }} found in operation config
// Return substituion value based on return value saved in outputs
const outputMatch = this._outputs.get(event)?.get(match[1].replace('outputs.', ''));
console.log(`StrSub: ${callArgsValue}, Match: ${match[1]}, Output: ${outputMatch}`);
return outputMatch;
}
return callArgsValue;
}

private async _registerEvents(): Promise<void> {
const { events } = this.operation;
const { tasks } = this.operation;
const { events, tasks } = this.operation;
console.log(`Evaluate events: [${events}]`);
if (!events) {
throw new Error('No events defined in the operation!');
Expand All @@ -102,6 +147,7 @@ export class Service {

events.forEach((event) => {
console.log(`Register event: "${event}"`);
this._outputs.set(event, new Map<string, any>());
if (event === 'all') {
console.warn('WARNING! All events will be listened based on the config!');
this._app.onAny(async (context) => {
Expand Down
11 changes: 10 additions & 1 deletion test/call/add-issue-to-github-project-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ describe('addIssueToGitHubProjectV2Functions', () => {
});

describe('addIssueToGitHubProjectV2', () => {
it('should print error and return null if it is not a issues.labeled event', async () => {
context.payload.label = undefined;

const result = await addIssueToGitHubProjectV2(app, context, resource, params);

expect(app.log.error).toHaveBeenCalledWith("Only 'issues.labeled' event is supported on this call.");
expect(result).toBe(null);
});

it('should print error if context label does not match the ones in resource config', async () => {
context.payload.label.name = 'enhancement';

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

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

Expand Down

0 comments on commit ec8d7e5

Please sign in to comment.