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:
2026-10-08 15:28:12 +02:00
parent 24b37b6628
commit 046d77c813
48 changed files with 2638 additions and 12 deletions

View File

@@ -0,0 +1,60 @@
import { assertThat } from '@task-stream/api/shared/domain';
export enum DevLinkKind {
COMMIT = 'COMMIT',
BRANCH = 'BRANCH',
PULL_REQUEST = 'PULL_REQUEST',
}
export enum DevLinkState {
OPEN = 'OPEN',
MERGED = 'MERGED',
CLOSED = 'CLOSED',
}
/**
* Code that mentions a ticket: a commit, a branch or a pull/merge request,
* as the Git host reported it. The URL identifies it.
*/
export interface DevLink {
kind: DevLinkKind;
url: string;
/** Commit message's first line, branch name or PR title. */
title: string;
/** Short SHA, branch or PR number. */
ref: string;
/** Pull requests only. */
state: DevLinkState | null;
/** github, gitea, gitlab. */
provider: string;
author: string | null;
at: string;
}
/** A ticket keeps the last this-many pieces of code. */
export const MAX_DEV_LINKS = 100;
/** Ticket keys of a project mentioned in a text (WEB-12, web-12), once each. */
export function issueKeysIn(text: string, projectKey: string): string[] {
const pattern = new RegExp(
`(?<![A-Za-z0-9])${projectKey}-(\\d+)(?![A-Za-z0-9])`,
'gi',
);
return [
...new Set(
[...text.matchAll(pattern)].map(
(m) => `${projectKey.toUpperCase()}-${m[1]}`,
),
),
];
}
export function normalizeDevLink(link: DevLink): DevLink {
assertThat(/^https?:\/\//.test(link.url), 'A dev link needs an http(s) URL');
return {
...link,
title: link.title.split('\n')[0].trim().slice(0, 200) || link.ref,
ref: link.ref.slice(0, 120),
author: link.author?.slice(0, 120) ?? null,
};
}