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

@@ -531,6 +531,7 @@ export type IssueFieldsFragment = {
ref: string;
state: DevLinkState | null;
provider: string;
repository: string | null;
author: string | null;
at: string;
}>;
@@ -585,10 +586,11 @@ export type CommandResultFieldsFragment = {
version: number;
};
export type GitIntegrationFieldsFragment = {
enabled: boolean;
export type GitRepositoryFieldsFragment = {
id: string;
name: string;
secret: string | null;
webhookPath: string;
createdAt: string;
};
export type GitIntegrationQueryVariables = Exact<{
@@ -598,28 +600,50 @@ export type GitIntegrationQueryVariables = Exact<{
export type GitIntegrationQuery = {
gitIntegration: {
enabled: boolean;
secret: string | null;
webhookPath: string;
repositories: Array<{
id: string;
name: string;
secret: string | null;
createdAt: string;
}>;
};
};
export type EnableGitIntegrationMutationVariables = Exact<{
export type AddGitRepositoryMutationVariables = Exact<{
projectId: string;
name: string;
}>;
export type EnableGitIntegrationMutation = {
enableGitIntegration: {
enabled: boolean;
export type AddGitRepositoryMutation = {
addGitRepository: {
id: string;
name: string;
secret: string | null;
webhookPath: string;
createdAt: string;
};
};
export type DisableGitIntegrationMutationVariables = Exact<{
export type RotateGitRepositorySecretMutationVariables = Exact<{
projectId: string;
repositoryId: string;
}>;
export type DisableGitIntegrationMutation = { disableGitIntegration: boolean };
export type RotateGitRepositorySecretMutation = {
rotateGitRepositorySecret: {
id: string;
name: string;
secret: string | null;
createdAt: string;
};
};
export type RemoveGitRepositoryMutationVariables = Exact<{
projectId: string;
repositoryId: string;
}>;
export type RemoveGitRepositoryMutation = { removeGitRepository: boolean };
export type BoardQueryVariables = Exact<{
idOrKey: string;
@@ -712,6 +736,7 @@ export type IssueQuery = {
ref: string;
state: DevLinkState | null;
provider: string;
repository: string | null;
author: string | null;
at: string;
}>;
@@ -2525,6 +2550,7 @@ export const IssueFieldsFragmentDoc = {
{ kind: 'Field', name: { kind: 'Name', value: 'ref' } },
{ kind: 'Field', name: { kind: 'Name', value: 'state' } },
{ kind: 'Field', name: { kind: 'Name', value: 'provider' } },
{ kind: 'Field', name: { kind: 'Name', value: 'repository' } },
{ kind: 'Field', name: { kind: 'Name', value: 'author' } },
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
],
@@ -2806,27 +2832,28 @@ export const CommandResultFieldsFragmentDoc = {
},
],
} as unknown as DocumentNode<CommandResultFieldsFragment, unknown>;
export const GitIntegrationFieldsFragmentDoc = {
export const GitRepositoryFieldsFragmentDoc = {
kind: 'Document',
definitions: [
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'GitIntegrationFields' },
name: { kind: 'Name', value: 'GitRepositoryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'GitIntegration' },
name: { kind: 'Name', value: 'GitRepository' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
],
},
},
],
} as unknown as DocumentNode<GitIntegrationFieldsFragment, unknown>;
} as unknown as DocumentNode<GitRepositoryFieldsFragment, unknown>;
export const NotificationFieldsFragmentDoc = {
kind: 'Document',
definitions: [
@@ -3620,9 +3647,20 @@ export const GitIntegrationDocument = {
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'GitIntegrationFields' },
kind: 'Field',
name: { kind: 'Name', value: 'repositories' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'GitRepositoryFields' },
},
],
},
},
],
},
@@ -3632,29 +3670,30 @@ export const GitIntegrationDocument = {
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'GitIntegrationFields' },
name: { kind: 'Name', value: 'GitRepositoryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'GitIntegration' },
name: { kind: 'Name', value: 'GitRepository' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
],
},
},
],
} as unknown as DocumentNode<GitIntegrationQuery, GitIntegrationQueryVariables>;
export const EnableGitIntegrationDocument = {
export const AddGitRepositoryDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'EnableGitIntegration' },
name: { kind: 'Name', value: 'AddGitRepository' },
variableDefinitions: [
{
kind: 'VariableDefinition',
@@ -3667,13 +3706,24 @@ export const EnableGitIntegrationDocument = {
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
{
kind: 'VariableDefinition',
variable: { kind: 'Variable', name: { kind: 'Name', value: 'name' } },
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'String' },
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'enableGitIntegration' },
name: { kind: 'Name', value: 'addGitRepository' },
arguments: [
{
kind: 'Argument',
@@ -3683,13 +3733,21 @@ export const EnableGitIntegrationDocument = {
name: { kind: 'Name', value: 'projectId' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'name' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'name' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'GitIntegrationFields' },
name: { kind: 'Name', value: 'GitRepositoryFields' },
},
],
},
@@ -3699,32 +3757,33 @@ export const EnableGitIntegrationDocument = {
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'GitIntegrationFields' },
name: { kind: 'Name', value: 'GitRepositoryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'GitIntegration' },
name: { kind: 'Name', value: 'GitRepository' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
],
},
},
],
} as unknown as DocumentNode<
EnableGitIntegrationMutation,
EnableGitIntegrationMutationVariables
AddGitRepositoryMutation,
AddGitRepositoryMutationVariables
>;
export const DisableGitIntegrationDocument = {
export const RotateGitRepositorySecretDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'DisableGitIntegration' },
name: { kind: 'Name', value: 'RotateGitRepositorySecret' },
variableDefinitions: [
{
kind: 'VariableDefinition',
@@ -3737,13 +3796,24 @@ export const DisableGitIntegrationDocument = {
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'repositoryId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'disableGitIntegration' },
name: { kind: 'Name', value: 'rotateGitRepositorySecret' },
arguments: [
{
kind: 'Argument',
@@ -3753,6 +3823,104 @@ export const DisableGitIntegrationDocument = {
name: { kind: 'Name', value: 'projectId' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'repositoryId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'repositoryId' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'GitRepositoryFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'GitRepositoryFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'GitRepository' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
],
},
},
],
} as unknown as DocumentNode<
RotateGitRepositorySecretMutation,
RotateGitRepositorySecretMutationVariables
>;
export const RemoveGitRepositoryDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'RemoveGitRepository' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'repositoryId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'removeGitRepository' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'projectId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectId' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'repositoryId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'repositoryId' },
},
},
],
},
],
@@ -3760,8 +3928,8 @@ export const DisableGitIntegrationDocument = {
},
],
} as unknown as DocumentNode<
DisableGitIntegrationMutation,
DisableGitIntegrationMutationVariables
RemoveGitRepositoryMutation,
RemoveGitRepositoryMutationVariables
>;
export const BoardDocument = {
kind: 'Document',
@@ -4341,6 +4509,7 @@ export const IssueDocument = {
{ kind: 'Field', name: { kind: 'Name', value: 'ref' } },
{ kind: 'Field', name: { kind: 'Name', value: 'state' } },
{ kind: 'Field', name: { kind: 'Name', value: 'provider' } },
{ kind: 'Field', name: { kind: 'Name', value: 'repository' } },
{ kind: 'Field', name: { kind: 'Name', value: 'author' } },
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
],