feat: link commits, branches and pull requests to tickets
A project can turn on a webhook for GitHub, Gitea or GitLab. Pushes and pull or merge requests that name a ticket key show on that ticket under Desarrollo, with the state of each pull request; the secret is shown and rotated from the project settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
92
libs/api/tracker/application/src/lib/git.service.ts
Normal file
92
libs/api/tracker/application/src/lib/git.service.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||
import { ForbiddenError } from '@task-stream/api/shared/domain';
|
||||
import {
|
||||
DisableGitIntegrationCommand,
|
||||
EnableGitIntegrationCommand,
|
||||
GetGitIntegrationQuery,
|
||||
GitIntegration,
|
||||
RecordGitMentionsCommand,
|
||||
} from './git/git';
|
||||
import {
|
||||
gitMentionsOf,
|
||||
gitProviderOf,
|
||||
verifyGitSignature,
|
||||
} from './git/git-payloads';
|
||||
import { FindProjectsRepository } from './ports/find-projects.repository';
|
||||
import { GitIntegrationsRepository } from './ports/git-integrations.repository';
|
||||
|
||||
export interface GitDelivery {
|
||||
/** Project id or key, from the webhook URL. */
|
||||
project: string;
|
||||
headers: Record<string, string | undefined>;
|
||||
rawBody: Buffer | undefined;
|
||||
body: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Links commits, branches and pull requests to the tickets they name. */
|
||||
@Injectable()
|
||||
export class GitService {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly queryBus: QueryBus,
|
||||
private readonly findProjects: FindProjectsRepository,
|
||||
private readonly integrations: GitIntegrationsRepository,
|
||||
) {}
|
||||
|
||||
integration(viewerId: string, projectId: string): Promise<GitIntegration> {
|
||||
return this.queryBus.execute(
|
||||
new GetGitIntegrationQuery(viewerId, projectId),
|
||||
);
|
||||
}
|
||||
|
||||
enable(actorId: string, projectId: string): Promise<GitIntegration> {
|
||||
return this.commandBus.execute(
|
||||
new EnableGitIntegrationCommand(actorId, projectId),
|
||||
);
|
||||
}
|
||||
|
||||
disable(actorId: string, projectId: string): Promise<void> {
|
||||
return this.commandBus.execute(
|
||||
new DisableGitIntegrationCommand(actorId, projectId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A webhook delivery. Unknown project, integration off or a bad signature
|
||||
* all look the same from outside (403): nothing to learn by probing.
|
||||
*/
|
||||
async receive(delivery: GitDelivery): Promise<{ recorded: number }> {
|
||||
const project =
|
||||
(await this.findProjects.findById(delivery.project)) ??
|
||||
(await this.findProjects.findByKey(delivery.project.toUpperCase()));
|
||||
const secret = project
|
||||
? await this.integrations.secretOf(project.id)
|
||||
: null;
|
||||
const source = gitProviderOf(delivery.headers);
|
||||
if (
|
||||
!project ||
|
||||
!secret ||
|
||||
!source ||
|
||||
!verifyGitSignature(
|
||||
source.provider,
|
||||
delivery.headers,
|
||||
delivery.rawBody,
|
||||
secret,
|
||||
)
|
||||
)
|
||||
throw new ForbiddenError(
|
||||
'Unknown project, integration off or bad signature',
|
||||
);
|
||||
const mentions = gitMentionsOf(
|
||||
source.provider,
|
||||
source.event,
|
||||
delivery.body,
|
||||
);
|
||||
if (!mentions.length) return { recorded: 0 };
|
||||
const recorded: number = await this.commandBus.execute(
|
||||
new RecordGitMentionsCommand(project.id, mentions),
|
||||
);
|
||||
return { recorded };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user