feat: several Git repositories per project
A project keeps a list of repositories (api, web, admin…), each with its own secret, instead of a single webhook secret. They all call the same URL; the secret that verifies the signature tells which one it was, and every commit, branch or pull request is linked with its repository's name (shown on the ticket and in the MCP). Rotating or removing one leaves the others alone, and what it linked stays. A project that had turned the single webhook on keeps its secret, as a repository named "principal". API: the enable/disable endpoints and mutations give way to adding a repository, rotating its secret and removing it (REST and GraphQL). The web settings card lists the repositories. Docs, Word and PDF updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,11 +2,13 @@ import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||
import { ForbiddenError } from '@task-stream/api/shared/domain';
|
||||
import {
|
||||
DisableGitIntegrationCommand,
|
||||
EnableGitIntegrationCommand,
|
||||
AddGitRepositoryCommand,
|
||||
GetGitIntegrationQuery,
|
||||
GitIntegration,
|
||||
GitRepository,
|
||||
RecordGitMentionsCommand,
|
||||
RemoveGitRepositoryCommand,
|
||||
RotateGitRepositorySecretCommand,
|
||||
} from './git/git';
|
||||
import {
|
||||
gitMentionsOf,
|
||||
@@ -40,41 +42,59 @@ export class GitService {
|
||||
);
|
||||
}
|
||||
|
||||
enable(actorId: string, projectId: string): Promise<GitIntegration> {
|
||||
addRepository(
|
||||
actorId: string,
|
||||
projectId: string,
|
||||
name: string,
|
||||
): Promise<GitRepository> {
|
||||
return this.commandBus.execute(
|
||||
new EnableGitIntegrationCommand(actorId, projectId),
|
||||
new AddGitRepositoryCommand(actorId, projectId, name),
|
||||
);
|
||||
}
|
||||
|
||||
disable(actorId: string, projectId: string): Promise<void> {
|
||||
rotateSecret(
|
||||
actorId: string,
|
||||
projectId: string,
|
||||
repositoryId: string,
|
||||
): Promise<GitRepository> {
|
||||
return this.commandBus.execute(
|
||||
new DisableGitIntegrationCommand(actorId, projectId),
|
||||
new RotateGitRepositorySecretCommand(actorId, projectId, repositoryId),
|
||||
);
|
||||
}
|
||||
|
||||
removeRepository(
|
||||
actorId: string,
|
||||
projectId: string,
|
||||
repositoryId: string,
|
||||
): Promise<void> {
|
||||
return this.commandBus.execute(
|
||||
new RemoveGitRepositoryCommand(actorId, projectId, repositoryId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A webhook delivery. Unknown project, integration off or a bad signature
|
||||
* all look the same from outside (403): nothing to learn by probing.
|
||||
* A webhook delivery. Every repository of the project shares the URL; the
|
||||
* secret that verifies the signature says which one it came from. 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,
|
||||
)
|
||||
)
|
||||
const repository =
|
||||
project && source
|
||||
? (await this.integrations.repositoriesOf(project.id)).find((r) =>
|
||||
verifyGitSignature(
|
||||
source.provider,
|
||||
delivery.headers,
|
||||
delivery.rawBody,
|
||||
r.secret,
|
||||
),
|
||||
)
|
||||
: undefined;
|
||||
if (!project || !source || !repository)
|
||||
throw new ForbiddenError(
|
||||
'Unknown project, integration off or bad signature',
|
||||
);
|
||||
@@ -85,7 +105,7 @@ export class GitService {
|
||||
);
|
||||
if (!mentions.length) return { recorded: 0 };
|
||||
const recorded: number = await this.commandBus.execute(
|
||||
new RecordGitMentionsCommand(project.id, mentions),
|
||||
new RecordGitMentionsCommand(project.id, repository.name, mentions),
|
||||
);
|
||||
return { recorded };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user