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:
2026-10-09 10:39:59 +02:00
parent e6ac02d328
commit c2928d2385
31 changed files with 1451 additions and 301 deletions

View File

@@ -41,11 +41,22 @@ export interface AutomationRunDoc {
_id: string;
at: Date;
}
export interface GitIntegrationDoc {
_id: string;
export interface GitRepositoryDoc {
id: string;
name: string;
secret: string;
createdAt: string;
}
/**
* One per project. `secret` alone is how the single webhook of before was
* kept: it reads as a repository named "principal" until the list is saved.
*/
export interface GitIntegrationDoc {
_id: string;
repositories?: GitRepositoryDoc[];
secret?: string;
createdAt?: string;
}
export interface NotificationSettingsDoc {
_id: string;
email: boolean;
@@ -214,7 +225,7 @@ SavedFilterSchema.index({ projectId: 1, 'owner.id': 1 });
SavedFilterSchema.index({ projectId: 1, shared: 1 });
export const GitIntegrationSchema = new Schema<GitIntegrationDoc>(
{ _id: String, secret: String, createdAt: String },
{ _id: String, repositories: [Object], secret: String, createdAt: String },
{ ...loose, collection: 'git_integrations' },
);
@@ -285,7 +296,11 @@ export function issueFromDoc(doc: IssueDoc): IssueReadModel {
rank: rest.rank ?? implicitRank(rest.number),
startDate: rest.startDate ?? null,
sprint: rest.sprint?.id ? rest.sprint : null,
devLinks: rest.devLinks ?? [],
// Links recorded before projects had several repositories name none.
devLinks: (rest.devLinks ?? []).map((link) => ({
...link,
repository: link.repository ?? null,
})),
trashedAt: rest.trashedAt ?? null,
checklist: rest.checklist ?? [],
};