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:
@@ -0,0 +1,32 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { GitGateway, GitIntegration } from '@task-stream/web/tracker/domain';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import {
|
||||
DisableGitIntegrationDocument,
|
||||
EnableGitIntegrationDocument,
|
||||
GitIntegrationDocument,
|
||||
} from '../graphql/generated';
|
||||
import { GraphqlOperations } from './graphql-operations';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlGitGateway extends GitGateway {
|
||||
private readonly gql = inject(GraphqlOperations);
|
||||
|
||||
integration(projectId: string): Observable<GitIntegration> {
|
||||
return this.gql
|
||||
.query(GitIntegrationDocument, { projectId })
|
||||
.pipe(map((d) => d.gitIntegration));
|
||||
}
|
||||
|
||||
enable(projectId: string): Observable<GitIntegration> {
|
||||
return this.gql
|
||||
.mutate(EnableGitIntegrationDocument, { projectId })
|
||||
.pipe(map((d) => d.enableGitIntegration));
|
||||
}
|
||||
|
||||
disable(projectId: string): Observable<void> {
|
||||
return this.gql
|
||||
.mutate(DisableGitIntegrationDocument, { projectId })
|
||||
.pipe(map(() => undefined));
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,16 @@ fragment IssueFields on Issue {
|
||||
text
|
||||
done
|
||||
}
|
||||
devLinks {
|
||||
kind
|
||||
url
|
||||
title
|
||||
ref
|
||||
state
|
||||
provider
|
||||
author
|
||||
at
|
||||
}
|
||||
attachments {
|
||||
id
|
||||
fileName
|
||||
|
||||
@@ -75,6 +75,10 @@ export type CreateSprintInput = {
|
||||
startDate: string;
|
||||
};
|
||||
|
||||
export type DevLinkKind = 'BRANCH' | 'COMMIT' | 'PULL_REQUEST';
|
||||
|
||||
export type DevLinkState = 'CLOSED' | 'MERGED' | 'OPEN';
|
||||
|
||||
export type IssueFilterInput = {
|
||||
areas?: Array<string> | null | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
@@ -336,6 +340,16 @@ export type IssueFieldsFragment = {
|
||||
}>;
|
||||
watchers: Array<{ id: string; name: string }>;
|
||||
checklist: Array<{ id: string; text: string; done: boolean }>;
|
||||
devLinks: Array<{
|
||||
kind: DevLinkKind;
|
||||
url: string;
|
||||
title: string;
|
||||
ref: string;
|
||||
state: DevLinkState | null;
|
||||
provider: string;
|
||||
author: string | null;
|
||||
at: string;
|
||||
}>;
|
||||
attachments: Array<{
|
||||
id: string;
|
||||
fileName: string;
|
||||
@@ -387,6 +401,42 @@ export type CommandResultFieldsFragment = {
|
||||
version: number;
|
||||
};
|
||||
|
||||
export type GitIntegrationFieldsFragment = {
|
||||
enabled: boolean;
|
||||
secret: string | null;
|
||||
webhookPath: string;
|
||||
};
|
||||
|
||||
export type GitIntegrationQueryVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type GitIntegrationQuery = {
|
||||
gitIntegration: {
|
||||
enabled: boolean;
|
||||
secret: string | null;
|
||||
webhookPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type EnableGitIntegrationMutationVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type EnableGitIntegrationMutation = {
|
||||
enableGitIntegration: {
|
||||
enabled: boolean;
|
||||
secret: string | null;
|
||||
webhookPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type DisableGitIntegrationMutationVariables = Exact<{
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
export type DisableGitIntegrationMutation = { disableGitIntegration: boolean };
|
||||
|
||||
export type BoardQueryVariables = Exact<{
|
||||
idOrKey: string;
|
||||
releaseId?: string | null | undefined;
|
||||
@@ -469,6 +519,16 @@ export type IssueQuery = {
|
||||
}>;
|
||||
watchers: Array<{ id: string; name: string }>;
|
||||
checklist: Array<{ id: string; text: string; done: boolean }>;
|
||||
devLinks: Array<{
|
||||
kind: DevLinkKind;
|
||||
url: string;
|
||||
title: string;
|
||||
ref: string;
|
||||
state: DevLinkState | null;
|
||||
provider: string;
|
||||
author: string | null;
|
||||
at: string;
|
||||
}>;
|
||||
attachments: Array<{
|
||||
id: string;
|
||||
fileName: string;
|
||||
@@ -1944,6 +2004,23 @@ export const IssueFieldsFragmentDoc = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'devLinks' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'kind' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'title' } },
|
||||
{ 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: 'author' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'attachments' },
|
||||
@@ -2219,6 +2296,27 @@ export const CommandResultFieldsFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<CommandResultFieldsFragment, unknown>;
|
||||
export const GitIntegrationFieldsFragmentDoc = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'GitIntegrationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'GitIntegration' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<GitIntegrationFieldsFragment, unknown>;
|
||||
export const NotificationFieldsFragmentDoc = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
@@ -2386,6 +2484,188 @@ export const SprintFieldsFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<SprintFieldsFragment, unknown>;
|
||||
export const GitIntegrationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'GitIntegration' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'gitIntegration' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'GitIntegrationFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'GitIntegrationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'GitIntegration' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<GitIntegrationQuery, GitIntegrationQueryVariables>;
|
||||
export const EnableGitIntegrationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'EnableGitIntegration' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'enableGitIntegration' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'GitIntegrationFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'GitIntegrationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'GitIntegration' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'enabled' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'secret' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'webhookPath' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
EnableGitIntegrationMutation,
|
||||
EnableGitIntegrationMutationVariables
|
||||
>;
|
||||
export const DisableGitIntegrationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'DisableGitIntegration' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'disableGitIntegration' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'projectId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
DisableGitIntegrationMutation,
|
||||
DisableGitIntegrationMutationVariables
|
||||
>;
|
||||
export const BoardDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
@@ -2950,6 +3230,23 @@ export const IssueDocument = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'devLinks' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'kind' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'url' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'title' } },
|
||||
{ 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: 'author' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'attachments' },
|
||||
|
||||
21
libs/web/tracker/infrastructure/src/lib/graphql/git.graphql
Normal file
21
libs/web/tracker/infrastructure/src/lib/graphql/git.graphql
Normal file
@@ -0,0 +1,21 @@
|
||||
fragment GitIntegrationFields on GitIntegration {
|
||||
enabled
|
||||
secret
|
||||
webhookPath
|
||||
}
|
||||
|
||||
query GitIntegration($projectId: ID!) {
|
||||
gitIntegration(projectId: $projectId) {
|
||||
...GitIntegrationFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation EnableGitIntegration($projectId: ID!) {
|
||||
enableGitIntegration(projectId: $projectId) {
|
||||
...GitIntegrationFields
|
||||
}
|
||||
}
|
||||
|
||||
mutation DisableGitIntegration($projectId: ID!) {
|
||||
disableGitIntegration(projectId: $projectId)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ReleasesGateway,
|
||||
SavedFiltersGateway,
|
||||
SprintsGateway,
|
||||
GitGateway,
|
||||
TrackerRealtimeGateway,
|
||||
UsersGateway,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
@@ -28,6 +29,7 @@ import { GraphqlProjectsGateway } from './adapters/graphql-projects.gateway';
|
||||
import { GraphqlRealtimeGateway } from './adapters/graphql-realtime.gateway';
|
||||
import { GraphqlSavedFiltersGateway } from './adapters/graphql-saved-filters.gateway';
|
||||
import { GraphqlSprintsGateway } from './adapters/graphql-sprints.gateway';
|
||||
import { GraphqlGitGateway } from './adapters/graphql-git.gateway';
|
||||
|
||||
/** Ports → GraphQL adapters (and REST for files). */
|
||||
export function provideTrackerInfrastructure(): EnvironmentProviders {
|
||||
@@ -43,5 +45,6 @@ export function provideTrackerInfrastructure(): EnvironmentProviders {
|
||||
{ provide: TrackerRealtimeGateway, useClass: GraphqlRealtimeGateway },
|
||||
{ provide: SavedFiltersGateway, useClass: GraphqlSavedFiltersGateway },
|
||||
{ provide: SprintsGateway, useClass: GraphqlSprintsGateway },
|
||||
{ provide: GitGateway, useClass: GraphqlGitGateway },
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user