Skip to content

Commit

Permalink
Job summary service (#307)
Browse files Browse the repository at this point in the history
* pre-post services return their execution result

* added getEventUrl and getDefinitionFileUrl and updated test cases

* added job summary service

* added test cases for job summary

* fix in config service test cases

* added requested changes
  • Loading branch information
shubhbapna committed Aug 23, 2022
1 parent b14ad18 commit 7a37530
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 18 deletions.
10 changes: 9 additions & 1 deletion src/service/config/configuration-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class ConfigurationService {
logAndThrow("Invalid entrypoint. Please contact with the administrator or report and issue to build-chain tool repository");
}
this._nodeChain = [];
this._definitionFile = {version: "2.1"};
this._definitionFile = { version: "2.1" };
}

get nodeChain(): Node[] {
Expand Down Expand Up @@ -200,4 +200,12 @@ export class ConfigurationService {
getPost(): Post | undefined {
return this.definitionFile.post;
}

getDefinitionFileUrl(): string {
return this.configuration.parsedInputs.definitionFile;
}

getEventUrl(): string {
return this.getFlowType() === FlowType.BRANCH ? "" : this.configuration.gitEventData.html_url;
}
}
150 changes: 150 additions & 0 deletions src/service/job-summary/job-summary-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import Container, { Service } from "typedi";
import * as core from "@actions/core";
import { ConfigurationService } from "@bc/service/config/configuration-service";
import { FlowType } from "@bc/domain/inputs";
import { GitCLIService } from "@bc/service/git/git-cli";
import { ExecuteNodeResult } from "@bc/domain/execute-node-result";
import { CheckedOutNode } from "@bc/domain/checkout";
import { ExecuteCommandResult, ExecutionResult } from "@bc/domain/execute-command-result";
import { FlowResult } from "@bc/domain/flow";
import { ExecutionPhase } from "@bc/domain/execution-phase";

@Service()
export class JobSummaryService {
private configService: ConfigurationService;
private gitService: GitCLIService;

constructor() {
this.configService = Container.get(ConfigurationService);
this.gitService = Container.get(GitCLIService);
}

async generateSummary(flowResult: FlowResult, preResult: ExecuteCommandResult[], postResult: ExecuteCommandResult[]) {
const flowType = this.configService.getFlowType();
if (flowType === FlowType.BRANCH) {
return;
}
const localExecution = core.summary
.emptyBuffer()
.addRaw("You can copy paste the following commands to locally execute build chain tool.", true)
.addCodeBlock(
`npm i ${process.env.npm_package_name}@${
process.env.npm_package_version
} -g build-chain-action -f ${this.configService.getDefinitionFileUrl()} build ${flowType} -u ${this.configService.getEventUrl()}`
)
.addEOL()
.addRaw(`**Git Version**: \`${await this.gitService.version()}\``, true)
.addRaw("> **_Notice_**: The `GITHUB_TOKEN` should be set in the environment.", true)
.stringify();

const before = this.constructExecutionResult(flowResult.executionResult.before, flowResult.checkoutInfo);
const current = this.constructExecutionResult(flowResult.executionResult.commands, flowResult.checkoutInfo);
const after = this.constructExecutionResult(flowResult.executionResult.after, flowResult.checkoutInfo);
const pre = this.constructPrePostResult(preResult);
const post = this.constructPrePostResult(postResult);

await core.summary
.emptyBuffer()
.addHeading("Build Chain Execution Summary")
.addEOL()
.addRaw(
`**Project Starting the Job:** [${this.configService.getStarterProjectName()}](https://github.com/${this.configService.getStarterProjectName()})`,
true
)
.addRaw(`<details open><summary><h2>Pre</h2></summary>${pre}</details>`, true)
.addRaw(`<details open><summary><h2>Execution phase: ${ExecutionPhase.BEFORE}</h2></summary>${before}</details>`, true)
.addRaw(`<details open><summary><h2>Execution phase: ${ExecutionPhase.CURRENT}</h2></summary>${current}</details>`, true)
.addRaw(`<details open><summary><h2>Execution phase: ${ExecutionPhase.AFTER}</h2></summary>${after}</details>`, true)
.addRaw(`<details open><summary><h2>Post</h2></summary>${post}</details>`, true)
.addRaw(`<details open><summary><h2>Local Execution</h2></summary>${localExecution}</details>`, true)
.write();
}

private constructPrePostResult(result: ExecuteCommandResult[]): string {
const prePostTableHeaders = [
{ data: "Command", header: true },
{ data: "Execution Result", header: true },
{ data: "Execution Time", header: true },
];
const data = result.map((res) => [res.command, this.getExecutionResultString(res.result), `${res.time}`]);
return core.summary
.emptyBuffer()
.addTable([prePostTableHeaders, ...data])
.stringify();
}

private constructExecutionResult(executionNodeResult: ExecuteNodeResult[], checkoutInfo: CheckedOutNode[]): string {
const tableHeaders = [
{ data: "Project", header: true },
{ data: "Source", header: true },
{ data: "Target", header: true },
{ data: "Merged", header: true },
{ data: "Execution Result", header: true },
{ data: "Avg Execution Time", header: true },
];
return core.summary
.emptyBuffer()
.addTable([tableHeaders, ...this.getExecutionResultData(executionNodeResult, checkoutInfo)])
.addEOL()
.addRaw("```mermaid", true)
.addRaw(this.constructGraph(executionNodeResult), true)
.addRaw("```", true)
.stringify();
}

private getExecutionResult(executeCommandResults: ExecuteCommandResult[]): ExecutionResult {
return executeCommandResults.find((res) => res.result !== ExecutionResult.OK)?.result ?? ExecutionResult.OK;
}

private getExecutionResultString(result: ExecutionResult): string {
switch (result) {
case ExecutionResult.NOT_OK:
return "\u274C";
case ExecutionResult.SKIP:
return "&#9940;";
default:
return "\u2705";
}
}

private getExecutionResultData(executionResult: ExecuteNodeResult[], checkoutInfo: CheckedOutNode[]): string[][] {
return executionResult.map((res) => {
const nodeCheckoutInfo = checkoutInfo.find((info) => info.node.project === res.node.project)!.checkoutInfo;
const result = this.getExecutionResultString(this.getExecutionResult(res.executeCommandResults));

return [
res.node.project,
nodeCheckoutInfo ? `${nodeCheckoutInfo.targetGroup}/${nodeCheckoutInfo.targetName}:${nodeCheckoutInfo.targetBranch}` : "checkout skipped",
nodeCheckoutInfo ? `${nodeCheckoutInfo.sourceGroup}/${nodeCheckoutInfo.sourceName}:${nodeCheckoutInfo.sourceBranch}` : "checkout skipped",
nodeCheckoutInfo?.merge ? "\u2705" : "\u274C",
result,
res.executeCommandResults.length > 0
? `${res.executeCommandResults.reduce((prev, curr) => prev + curr.time, 0) / res.executeCommandResults.length}`
: "0",
];
});
}

private constructGraph(executionResult: ExecuteNodeResult[]) {
return `flowchart LR;
${executionResult
.map((res) => {
const result = this.getExecutionResult(res.executeCommandResults);
let className = "okClass";
switch (result) {
case ExecutionResult.NOT_OK:
className = "errorClass";
break;
case ExecutionResult.SKIP:
className = "noEntry";
break;
}
return `${res.node.project}:::${className}`;
})
.join("==>")}
${executionResult.map((res) => `click ${res.node.project} 'https://github.com/${res.node.project}'`).join("\n\t\t\t\t")}
classDef okClass fill:#218838,stroke:#1e7e34,color: #fff,border-radius: 4px
classDef errorClass fill:#dc3545,stroke:#dc3545,color: #fff,border-radius: 4px
classDef noEntry fill:#6c757d,stroke:#6c757d,color: #fff,border-radius: 4px`;
}
}
11 changes: 7 additions & 4 deletions src/service/pre-post/post.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ExecuteCommandResult } from "@bc/domain/execute-command-result";
import { PrePostExecutor } from "@bc/service/pre-post/pre-post";
import { Inject, Service } from "typedi";

Expand All @@ -10,25 +11,27 @@ export class PostExecutor extends PrePostExecutor {
this.executionSuccess = executionSuccess;
}

async run() {
async run(): Promise<ExecuteCommandResult[]> {
const post = this.configService.getPost();
let result: ExecuteCommandResult[] = [];
if (post) {
this.logger.startGroup("[POST] Executing post section");
if (this.executionSuccess) {
this.logger.info("[POST] execution result is OK, so 'success' and 'always' sections will be executed");
if (post.success) {
await this.execute(post.success);
result = await this.execute(post.success);
}
} else {
this.logger.info("[POST] execution result is NOT OK, so 'failure' and 'always' sections will be executed");
if (post.failure) {
await this.execute(post.failure);
result = await this.execute(post.failure);
}
}
if (post.always) {
await this.execute(post.always);
result = [...result, ...await this.execute(post.always)];
}
this.logger.endGroup();
}
return result;
}
}
9 changes: 6 additions & 3 deletions src/service/pre-post/pre-post.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ExecuteCommandResult } from "@bc/domain/execute-command-result";
import { ExecuteCommandService } from "@bc/service/command/execute-command-service";
import { ConfigurationService } from "@bc/service/config/configuration-service";
import { LoggerService } from "@bc/service/logger/logger-service";
Expand All @@ -15,13 +16,15 @@ export abstract class PrePostExecutor {
this.executeService = Container.get(ExecuteCommandService);
}

protected async execute(cmds: string | string[]) {
protected async execute(cmds: string | string[]): Promise<ExecuteCommandResult[]> {
const result: ExecuteCommandResult[] = [];
if (Array.isArray(cmds)) {
for (const cmd of cmds) {
await this.executeService.executeCommand(cmd, process.cwd());
result.push(await this.executeService.executeCommand(cmd, process.cwd()));
}
} else {
await this.executeService.executeCommand(cmds, process.cwd());
result.push(await this.executeService.executeCommand(cmds, process.cwd()));
}
return result;
}
}
7 changes: 5 additions & 2 deletions src/service/pre-post/pre.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { ExecuteCommandResult } from "@bc/domain/execute-command-result";
import { PrePostExecutor } from "@bc/service/pre-post/pre-post";
import { Service } from "typedi";

@Service()
export class PreExecutor extends PrePostExecutor {
async run() {
async run(): Promise<ExecuteCommandResult[]> {
const pre = this.configService.getPre();
let result: ExecuteCommandResult[] = [];
if (pre) {
this.logger.startGroup("[PRE] Executing pre section");
await this.execute(pre);
result = await this.execute(pre);
this.logger.endGroup();
}
return result;
}
}
2 changes: 2 additions & 0 deletions test/unitary/service/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{
"code": 200,
"data": {
"html_url": "https://github.com/pulls/109",
"head": {
"ref": "feature",
"repo": {
Expand Down Expand Up @@ -55,6 +56,7 @@
"eventPayloadFileName": "event.json",
"eventPayload": {
"pull_request": {
"html_url": "https://github.com/pulls/109",
"head": {
"ref": "feature",
"repo": {
Expand Down
28 changes: 20 additions & 8 deletions test/unitary/service/config/configuration-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const mockGithub = new MockGithub(path.join(__dirname, "config.json"), "event");

beforeEach(async () => {
await mockGithub.setup();
jest.spyOn(DefinitionFileReader.prototype, "generateNodeChain").mockImplementation(async () => []);
jest.spyOn(DefinitionFileReader.prototype, "getDefinitionFile").mockImplementation(async () => {
return { version: "2.1" };
});
});

afterEach(() => {
Expand All @@ -40,8 +44,6 @@ describe("cli", () => {
beforeEach(async () => {
currentInput = { ...defaultInputValues, startProject, url: "https://github.com/owner/project/pull/270" };
jest.spyOn(BaseConfiguration.prototype, "parsedInputs", "get").mockImplementation(() => currentInput);
jest.spyOn(DefinitionFileReader.prototype, "generateNodeChain").mockImplementation(async () => []);
jest.spyOn(DefinitionFileReader.prototype, "getDefinitionFile").mockImplementation(async () => {return {version: "2.1"};});
config = new ConfigurationService();
await config.init();
});
Expand Down Expand Up @@ -192,7 +194,8 @@ describe("cli", () => {
describe("action", () => {
let config: ConfigurationService;
let currentInput: InputValues;
const env = JSON.parse(fs.readFileSync(path.join(__dirname, "config.json"), "utf8")).env;
const data = JSON.parse(fs.readFileSync(path.join(__dirname, "config.json"), "utf8"));

beforeAll(() => {
Container.set(constants.CONTAINER.ENTRY_POINT, EntryPoint.GITHUB_EVENT);
});
Expand All @@ -209,7 +212,7 @@ describe("action", () => {
});

test("getStarterProjectName: success", () => {
expect(config.getStarterProjectName()).toBe(env.repository);
expect(config.getStarterProjectName()).toBe(data.env.repository);
});

test("getStarterProjectName: failure", () => {
Expand All @@ -218,15 +221,15 @@ describe("action", () => {
});

test.each([
[true, env.repository],
[true, data.env.repository],
[false, "falsename"],
])("isNodeStarter %p", (isNodeStarter: boolean, project: string) => {
expect(config.isNodeStarter({ project: project })).toBe(isNodeStarter);
});

test("getStarterNode: success", () => {
const chain: Node[] = [{ project: "abc" }, { project: env.repository }, { project: "def" }];
const nodeFound: Node = { project: env.repository };
const chain: Node[] = [{ project: "abc" }, { project: data.env.repository }, { project: "def" }];
const nodeFound: Node = { project: data.env.repository };
jest.spyOn(ConfigurationService.prototype, "nodeChain", "get").mockImplementation(() => chain);

expect(config.getStarterNode()).toStrictEqual(nodeFound);
Expand All @@ -244,7 +247,7 @@ describe("action", () => {
["current", 1, NodeExecutionLevel.CURRENT],
["downstream", 2, NodeExecutionLevel.DOWNSTREAM],
])("getNodeExecutionLevel: %p", (title: string, currNodeIndex: number, executionLevel: NodeExecutionLevel) => {
const chain: Node[] = [{ project: "abc" }, { project: env.repository }, { project: "def" }];
const chain: Node[] = [{ project: "abc" }, { project: data.env.repository }, { project: "def" }];
jest.spyOn(ConfigurationService.prototype, "nodeChain", "get").mockImplementation(() => chain);

expect(config.getNodeExecutionLevel(chain[currNodeIndex])).toBe(executionLevel);
Expand Down Expand Up @@ -342,4 +345,13 @@ describe("action", () => {
});
expect(config.getPost()).toStrictEqual({ success: "hello" });
});

test.each([
["branch flow", FlowType.BRANCH, ""],
["non-branch flow", FlowType.CROSS_PULL_REQUEST, data.action.eventPayload.pull_request.html_url],
])("getEventUrl: %p", (_title: string, flowType: FlowType, result: string) => {
jest.spyOn(ConfigurationService.prototype, "getFlowType").mockImplementationOnce(() => flowType);

expect(config.getEventUrl()).toBe(result);
});
});

0 comments on commit 7a37530

Please sign in to comment.