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,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { GitIntegrationsRepository } from '@task-stream/api/tracker/application';
|
||||
import { Model } from 'mongoose';
|
||||
import { GitIntegrationDoc, ReadModels } from './read-model.schemas';
|
||||
|
||||
@Injectable()
|
||||
export class MongoGitIntegrationsRepository extends GitIntegrationsRepository {
|
||||
constructor(
|
||||
@InjectModel(ReadModels.GitIntegration)
|
||||
private readonly integrations: Model<GitIntegrationDoc>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async secretOf(projectId: string): Promise<string | null> {
|
||||
const doc = await this.integrations
|
||||
.findById(projectId)
|
||||
.lean<GitIntegrationDoc>();
|
||||
return doc?.secret ?? null;
|
||||
}
|
||||
|
||||
async save(projectId: string, secret: string): Promise<void> {
|
||||
await this.integrations.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { secret, createdAt: new Date().toISOString() } },
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
async remove(projectId: string): Promise<void> {
|
||||
await this.integrations.deleteOne({ _id: projectId });
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,11 @@ export type ActivityDoc = Doc<ActivityReadModel>;
|
||||
export type TimelineDoc = Doc<IssueTimelineEntry>;
|
||||
export type NotificationDoc = Doc<NotificationReadModel>;
|
||||
export type SavedFilterDoc = Doc<SavedFilterReadModel>;
|
||||
export interface GitIntegrationDoc {
|
||||
_id: string;
|
||||
secret: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface NotificationSettingsDoc {
|
||||
_id: string;
|
||||
email: boolean;
|
||||
@@ -46,6 +51,7 @@ export const ReadModels = {
|
||||
NotificationSettings: 'NotificationSettings',
|
||||
SavedFilter: 'SavedFilter',
|
||||
Sprint: 'SprintReadModel',
|
||||
GitIntegration: 'GitIntegration',
|
||||
} as const;
|
||||
|
||||
const userRef = { _id: false, id: String, name: String };
|
||||
@@ -192,12 +198,18 @@ export const SavedFilterSchema = new Schema<SavedFilterDoc>(
|
||||
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 },
|
||||
{ ...loose, collection: 'git_integrations' },
|
||||
);
|
||||
|
||||
export const READ_MODEL_SCHEMAS = [
|
||||
{ name: ReadModels.Project, schema: ProjectSchema },
|
||||
{ name: ReadModels.Issue, schema: IssueSchema },
|
||||
{ name: ReadModels.Comment, schema: CommentSchema },
|
||||
{ name: ReadModels.Release, schema: ReleaseSchema },
|
||||
{ name: ReadModels.Sprint, schema: SprintSchema },
|
||||
{ name: ReadModels.GitIntegration, schema: GitIntegrationSchema },
|
||||
{ name: ReadModels.Activity, schema: ActivitySchema },
|
||||
{ name: ReadModels.Timeline, schema: TimelineSchema },
|
||||
{ name: ReadModels.Notification, schema: NotificationSchema },
|
||||
@@ -225,6 +237,7 @@ 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 ?? [],
|
||||
checklist: rest.checklist ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
ActivityReadModel,
|
||||
AttachmentReadModel,
|
||||
ChecklistItemReadModel,
|
||||
DevLink,
|
||||
MAX_DEV_LINKS,
|
||||
CommentReadModel,
|
||||
IssueLinkReadModel,
|
||||
IssueReadModel,
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
ReadModels,
|
||||
ReleaseDoc,
|
||||
SavedFilterDoc,
|
||||
GitIntegrationDoc,
|
||||
TimelineDoc,
|
||||
} from './read-model.schemas';
|
||||
import { upsertIfNewer } from './upsert-if-newer';
|
||||
@@ -47,6 +50,8 @@ export class MongoUpsertMaterializedProjectRepository extends UpsertMaterialized
|
||||
private readonly timeline: Model<TimelineDoc>,
|
||||
@InjectModel(ReadModels.SavedFilter)
|
||||
private readonly savedFilters: Model<SavedFilterDoc>,
|
||||
@InjectModel(ReadModels.GitIntegration)
|
||||
private readonly gitIntegrations: Model<GitIntegrationDoc>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -58,6 +63,7 @@ export class MongoUpsertMaterializedProjectRepository extends UpsertMaterialized
|
||||
this.timeline.deleteMany({ projectId }),
|
||||
// Views of the board of a project that is gone.
|
||||
this.savedFilters.deleteMany({ projectId }),
|
||||
this.gitIntegrations.deleteOne({ _id: projectId }),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -168,6 +174,18 @@ export class MongoUpsertMaterializedIssueRepository extends UpsertMaterializedIs
|
||||
);
|
||||
}
|
||||
|
||||
async putDevLink(issueId: string, link: DevLink) {
|
||||
// Same URL: the newer report replaces the older one.
|
||||
await this.issues.updateOne(
|
||||
{ _id: issueId },
|
||||
{ $pull: { devLinks: { url: link.url } } },
|
||||
);
|
||||
await this.issues.updateOne(
|
||||
{ _id: issueId },
|
||||
{ $push: { devLinks: { $each: [link], $slice: -MAX_DEV_LINKS } } },
|
||||
);
|
||||
}
|
||||
|
||||
async renameParent(parentId: string, title: string) {
|
||||
await this.issues.updateMany(
|
||||
{ 'parent.id': parentId },
|
||||
|
||||
Reference in New Issue
Block a user