Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Job summary service #307

Merged
merged 6 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
)
.addDetails("Pre", pre)
.addDetails(`Execution phase: ${ExecutionPhase.BEFORE}`, before)
.addDetails(`Execution phase: ${ExecutionPhase.CURRENT}`, current)
.addDetails(`Execution phase: ${ExecutionPhase.AFTER}`, after)
.addDetails("Post", post)
.addDetails("Local Execution", localExecution)
.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 "⛔";
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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map((res) => {
.map(res => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof sorry about this. I use VS code formatter for quick formatting and i think it adds these parenthesis

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);
});
});