From a68f7ad433a83f18f0b1adad1b4786ec33302cbb Mon Sep 17 00:00:00 2001 From: Thibaud Desodt Date: Wed, 13 Jan 2021 23:01:00 +0100 Subject: [PATCH] feat(author): return user who triggered the workflow by retrieving the commit's author --- src/api/controllers/BuildInfoController.ts | 13 ++++- src/composition-root.ts | 6 +- src/domain/IWorkflowRunRepository.ts | 5 ++ src/infra/github/CommitAuthorRepository.ts | 42 ++++++++++++++ src/infra/github/WorkflowRunRepository.ts | 27 ++++++++- .../__tests__/WorkflowRunRepository.spec.ts | 56 +++++++++++++++++-- 6 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 src/infra/github/CommitAuthorRepository.ts diff --git a/src/api/controllers/BuildInfoController.ts b/src/api/controllers/BuildInfoController.ts index bc10ef0..539ef8a 100644 --- a/src/api/controllers/BuildInfoController.ts +++ b/src/api/controllers/BuildInfoController.ts @@ -149,15 +149,26 @@ export class BuildInfoController extends Controller { } private mapToBuild(run: WorkflowRun): catlightCore.Build { - return { + let result: catlightCore.Build = { id: run.id, startTime: run.startTime, status: run.status, finishTime: run.finishTime, name: run.name, webUrl: run.webUrl, + // TODO: contributors, triggeredByUser }; + if (run.mainAuthor) { + result = { + ...result, + triggeredByUser: { + id: run.mainAuthor.login, + name: run.mainAuthor.name, + }, + }; + } + return result; } private mapToBuildDefinitionMetadata( diff --git a/src/composition-root.ts b/src/composition-root.ts index ce7a4d3..83009ec 100644 --- a/src/composition-root.ts +++ b/src/composition-root.ts @@ -3,6 +3,7 @@ import { MetaInfo, meta as metaFromPackageJson } from './meta'; import { BearerAuthenticationProvider } from './api/auth/BearerAuthenticationProvider'; import { BuildInfoController } from './api/controllers/BuildInfoController'; import { CachedRepoRepository } from './infra/caching/CachedRepoRepository'; +import { CommitAuthorRepository } from './infra/github/CommitAuthorRepository'; import { Controller } from '@tsoa/runtime'; import { DiagnosticsController } from './api/controllers/DiagnosticsController'; import { DynamicBuildInfoController } from './api/controllers/DynamicBuildInfoController'; @@ -52,7 +53,10 @@ export class CompositionRoot implements IControllerFactory { return new CompositionRoot(settings, metaFromPackageJson, { userRepo: new UserRepository(octokitFactory), repoRepo: new RepoRepository(octokitFactory), - workflowRunRepo: new WorkflowRunRepository(octokitFactory), + workflowRunRepo: new WorkflowRunRepository( + octokitFactory, + new CommitAuthorRepository(octokitFactory) + ), }); } diff --git a/src/domain/IWorkflowRunRepository.ts b/src/domain/IWorkflowRunRepository.ts index fddbd0b..3d91261 100644 --- a/src/domain/IWorkflowRunRepository.ts +++ b/src/domain/IWorkflowRunRepository.ts @@ -8,6 +8,10 @@ export type WorkflowRunStatus = | 'Failed' | 'Canceled'; +export interface WorkflowRunAuthor { + readonly login: string; + readonly name: string; +} export interface WorkflowRun { readonly id: string; readonly name?: string; @@ -16,6 +20,7 @@ export interface WorkflowRun { readonly event: string; readonly startTime: Date; readonly finishTime?: Date; + readonly mainAuthor?: WorkflowRunAuthor; } export interface WorflowRunFilter { diff --git a/src/infra/github/CommitAuthorRepository.ts b/src/infra/github/CommitAuthorRepository.ts new file mode 100644 index 0000000..caa9a96 --- /dev/null +++ b/src/infra/github/CommitAuthorRepository.ts @@ -0,0 +1,42 @@ +import { OctokitFactory } from './OctokitFactory'; +import { RepoName } from '../../domain/IRepoRepository'; + +export interface CommitAuthor { + readonly login: string; + readonly name?: string; +} +export interface ICommitAuthorRepository { + getAuthorForCommit( + token: string, + repoName: RepoName, + commitId: string + ): Promise; +} + +export class CommitAuthorRepository implements ICommitAuthorRepository { + public constructor(private readonly octokitFactory: OctokitFactory) {} + + public async getAuthorForCommit( + token: string, + repoName: RepoName, + commitId: string + ): Promise { + const octokit = this.octokitFactory(token); + + const response = await octokit.repos.getCommit({ + owner: repoName.owner, + repo: repoName.name, + ref: commitId, + }); + + if (!response.data.author) { + return null; + } + + const result = { + login: response.data.author.login, + name: response.data.commit.author?.name, + }; + return result; + } +} diff --git a/src/infra/github/WorkflowRunRepository.ts b/src/infra/github/WorkflowRunRepository.ts index 6032ae5..75a5f78 100644 --- a/src/infra/github/WorkflowRunRepository.ts +++ b/src/infra/github/WorkflowRunRepository.ts @@ -3,15 +3,21 @@ import { IWorkflowRunRepository, WorflowRunFilter, WorkflowRun, + WorkflowRunAuthor, WorkflowRunStatus, WorkflowRunsPerBranch, } from '../../domain/IWorkflowRunRepository'; +import { ICommitAuthorRepository } from './CommitAuthorRepository'; +import { Octokit } from '@octokit/rest'; import { OctokitFactory } from './OctokitFactory'; import { RepoName } from '../../domain/IRepoRepository'; import { parseISO } from 'date-fns'; export class WorkflowRunRepository implements IWorkflowRunRepository { - public constructor(private readonly octokitFactory: OctokitFactory) {} + public constructor( + private readonly octokitFactory: OctokitFactory, + private readonly commitAuthorRepo: ICommitAuthorRepository + ) {} public async getLatestRunsForWorkflow( token: string, @@ -46,6 +52,8 @@ export class WorkflowRunRepository implements IWorkflowRunRepository { const currentForBranch = result.get(branchKey) || []; + const isLatestRunInThisBranch = currentForBranch.length === 0; + if (currentForBranch.length >= filter.maxRunsPerBranch) { // skipping this run because we already have enough builds for this branch // eslint-disable-next-line no-continue @@ -53,6 +61,22 @@ export class WorkflowRunRepository implements IWorkflowRunRepository { } const status = this.parseWorkflowRunStatus(run.status, run.conclusion); + let author: WorkflowRunAuthor | undefined; + if (isLatestRunInThisBranch) { + // eslint-disable-next-line no-await-in-loop + const commitAuthor = await this.commitAuthorRepo.getAuthorForCommit( + token, + repoName, + run.head_commit.id + ); + + if (commitAuthor) { + author = { + login: commitAuthor.login, + name: commitAuthor.name ?? commitAuthor.login, + }; + } + } const workflowRun: WorkflowRun = { id: run.id.toString(), webUrl: run.html_url, @@ -61,6 +85,7 @@ export class WorkflowRunRepository implements IWorkflowRunRepository { status, finishTime: parseISO(run.updated_at), event: run.event, + mainAuthor: author, }; result.set(branchKey, [workflowRun, ...currentForBranch]); diff --git a/src/infra/github/__tests__/WorkflowRunRepository.spec.ts b/src/infra/github/__tests__/WorkflowRunRepository.spec.ts index 67a9c9c..d40e9aa 100644 --- a/src/infra/github/__tests__/WorkflowRunRepository.spec.ts +++ b/src/infra/github/__tests__/WorkflowRunRepository.spec.ts @@ -1,18 +1,33 @@ +import { + CommitAuthor, + CommitAuthorRepository, + ICommitAuthorRepository, +} from '../CommitAuthorRepository'; import { THIS_REPO_MAIN_WORKFLOW, THIS_REPO_NAME } from '../__testTools__/TestConstants'; +import { WorkflowRun, WorkflowRunAuthor } from '../../../domain/IWorkflowRunRepository'; import { RepoName } from '../../../domain/IRepoRepository'; -import { WorkflowRun } from '../../../domain/IWorkflowRunRepository'; import { WorkflowRunRepository } from '../WorkflowRunRepository'; import { getOctokitFactory } from '../OctokitFactory'; import { testCredentials } from '../__testTools__/TestCredentials'; +class EmptyCommitAuthorRepo implements ICommitAuthorRepository { + public async getAuthorForCommit( + token: string, + repoName: RepoName, + commitId: string + ): Promise { + return null; + } +} describe('WorkflowRunRepository', () => { const octokitFactory = getOctokitFactory({ version: 'v0-tests', buildInfo: {}, }); + const emptyCommitAutorRepo = new EmptyCommitAuthorRepo(); describe('getLatestRunsForWorkflow', () => { test('should retrieve runs of public repo', async () => { - const sut = new WorkflowRunRepository(octokitFactory); + const sut = new WorkflowRunRepository(octokitFactory, emptyCommitAutorRepo); const actual = await sut.getLatestRunsForWorkflow( testCredentials.PAT_NO_SCOPE, @@ -42,8 +57,37 @@ describe('WorkflowRunRepository', () => { }); }); + test('should retrieve authors of commits on public repo', async () => { + const sut = new WorkflowRunRepository( + octokitFactory, + new CommitAuthorRepository(octokitFactory) + ); + + const actual = await sut.getLatestRunsForWorkflow( + testCredentials.PAT_NO_SCOPE, + THIS_REPO_NAME, + THIS_REPO_MAIN_WORKFLOW.id, + { + maxAgeInDays: 10, + maxRunsPerBranch: 1, + } + ); + + expect([...actual.entries()]).not.toHaveLength(0); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const runsOfBranchMain = actual.get('main')!; + expect(runsOfBranchMain).toBeDefined(); + expect(runsOfBranchMain).not.toHaveLength(0); + + const actualRun = runsOfBranchMain[0]; + expect(actualRun.mainAuthor).toEqual({ + login: expect.stringContaining(''), + name: expect.stringContaining(''), + }); + }, 20000); + test('should name branches according to triggering event', async () => { - const sut = new WorkflowRunRepository(octokitFactory); + const sut = new WorkflowRunRepository(octokitFactory, emptyCommitAutorRepo); const actual = await sut.getLatestRunsForWorkflow( testCredentials.PAT_NO_SCOPE, @@ -74,7 +118,7 @@ describe('WorkflowRunRepository', () => { }); test('should sort builds from older to newer', async () => { - const sut = new WorkflowRunRepository(octokitFactory); + const sut = new WorkflowRunRepository(octokitFactory, emptyCommitAutorRepo); const actual = await sut.getLatestRunsForWorkflow( testCredentials.PAT_NO_SCOPE, @@ -97,7 +141,7 @@ describe('WorkflowRunRepository', () => { test('should apply maxAgeInDays', async () => { const maxAgeInDays = 3; - const sut = new WorkflowRunRepository(octokitFactory); + const sut = new WorkflowRunRepository(octokitFactory, emptyCommitAutorRepo); const actual = await sut.getLatestRunsForWorkflow( testCredentials.PAT_NO_SCOPE, @@ -119,7 +163,7 @@ describe('WorkflowRunRepository', () => { test('should apply maxRunsPerBranch in repo with lots of activity', async () => { const maxRunsPerBranch = 2; - const sut = new WorkflowRunRepository(octokitFactory); + const sut = new WorkflowRunRepository(octokitFactory, emptyCommitAutorRepo); const actual = await sut.getLatestRunsForWorkflow( testCredentials.PAT_NO_SCOPE,