feat: 补齐 Gitea PR 来源摘要
This commit is contained in:
@@ -3,7 +3,12 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error';
|
||||
import { redactSensitive } from '../../common/security/redact-sensitive';
|
||||
import { EnvConfig } from '../../config/env.schema';
|
||||
import { GiteaBranch, GiteaCommitSummary, GiteaTag } from './gitea.types';
|
||||
import {
|
||||
GiteaBranch,
|
||||
GiteaCommitSummary,
|
||||
GiteaPullRequestSummary,
|
||||
GiteaTag,
|
||||
} from './gitea.types';
|
||||
|
||||
@Injectable()
|
||||
export class GiteaClient {
|
||||
@@ -62,6 +67,66 @@ export class GiteaClient {
|
||||
};
|
||||
}
|
||||
|
||||
async getCommitPullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
sha: string,
|
||||
): Promise<GiteaPullRequestSummary | undefined> {
|
||||
const payload = await this.optionalRequest<{
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
merged?: boolean;
|
||||
merged_at?: string;
|
||||
merge_commit_sha?: string;
|
||||
head?: {
|
||||
ref?: string;
|
||||
sha?: string;
|
||||
};
|
||||
base?: {
|
||||
ref?: string;
|
||||
};
|
||||
user?: {
|
||||
login?: string;
|
||||
};
|
||||
}>(`/api/v1/repos/${owner}/${repo}/commits/${encodeURIComponent(sha)}/pull`);
|
||||
|
||||
if (!payload) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
number: payload.number,
|
||||
title: payload.title,
|
||||
state: payload.state,
|
||||
merged: payload.merged === true,
|
||||
mergedAt: payload.merged_at,
|
||||
mergeCommitSha: payload.merge_commit_sha,
|
||||
headRef: payload.head?.ref,
|
||||
headSha: payload.head?.sha,
|
||||
baseRef: payload.base?.ref,
|
||||
authorName: payload.user?.login,
|
||||
};
|
||||
}
|
||||
|
||||
private async optionalRequest<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
return await this.request<T>(path, init);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof IntegrationRequestError &&
|
||||
error.details?.status === 404
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
this.assertConfigured();
|
||||
const baseUrl = this.config.get('GITEA_BASE_URL', { infer: true }) ?? '';
|
||||
|
||||
@@ -14,3 +14,16 @@ export type GiteaCommitSummary = {
|
||||
authorName?: string;
|
||||
authoredAt?: string;
|
||||
};
|
||||
|
||||
export type GiteaPullRequestSummary = {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
merged: boolean;
|
||||
mergedAt?: string;
|
||||
mergeCommitSha?: string;
|
||||
headRef?: string;
|
||||
headSha?: string;
|
||||
baseRef?: string;
|
||||
authorName?: string;
|
||||
};
|
||||
|
||||
@@ -16,26 +16,34 @@ export type ProjectSummary = {
|
||||
environments: ProjectEnvironmentSummary[];
|
||||
};
|
||||
|
||||
export type ProjectPullRequestSummary = {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
merged: boolean;
|
||||
mergedAt?: string;
|
||||
mergeCommitSha?: string;
|
||||
headRef?: string;
|
||||
headSha?: string;
|
||||
baseRef?: string;
|
||||
authorName?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type ProjectRefItemSummary = {
|
||||
name: string;
|
||||
commitSha?: string;
|
||||
commit?: {
|
||||
sha: string;
|
||||
message: string;
|
||||
authorName?: string;
|
||||
authoredAt?: string;
|
||||
};
|
||||
pullRequest?: ProjectPullRequestSummary;
|
||||
};
|
||||
|
||||
export type ProjectRefSummary = {
|
||||
source: 'gitea' | 'fallback';
|
||||
branches: Array<{
|
||||
name: string;
|
||||
commitSha?: string;
|
||||
commit?: {
|
||||
sha: string;
|
||||
message: string;
|
||||
authorName?: string;
|
||||
authoredAt?: string;
|
||||
};
|
||||
}>;
|
||||
tags: Array<{
|
||||
name: string;
|
||||
commitSha?: string;
|
||||
commit?: {
|
||||
sha: string;
|
||||
message: string;
|
||||
authorName?: string;
|
||||
authoredAt?: string;
|
||||
};
|
||||
}>;
|
||||
branches: ProjectRefItemSummary[];
|
||||
tags: ProjectRefItemSummary[];
|
||||
};
|
||||
|
||||
@@ -21,6 +21,15 @@ describe('ProjectsService', () => {
|
||||
branches?: Array<{ name: string; commitSha: string }>;
|
||||
tags?: Array<{ name: string; commitSha: string }>;
|
||||
commits?: Record<string, { sha: string; message: string }>;
|
||||
pullRequests?: Record<
|
||||
string,
|
||||
{
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
merged: boolean;
|
||||
}
|
||||
>;
|
||||
}) {
|
||||
const repository = {
|
||||
findMany: jest.fn(() => Promise.resolve([project])),
|
||||
@@ -46,9 +55,16 @@ describe('ProjectsService', () => {
|
||||
},
|
||||
),
|
||||
),
|
||||
getCommitPullRequest: jest.fn((owner: string, repo: string, sha: string) =>
|
||||
Promise.resolve(options?.pullRequests?.[sha]),
|
||||
),
|
||||
} satisfies Pick<
|
||||
GiteaClient,
|
||||
'healthSummary' | 'listBranches' | 'listTags' | 'getCommit'
|
||||
| 'healthSummary'
|
||||
| 'listBranches'
|
||||
| 'listTags'
|
||||
| 'getCommit'
|
||||
| 'getCommitPullRequest'
|
||||
>;
|
||||
const auth = {
|
||||
listAllowedProjectKeys: jest.fn(() => Promise.resolve([project.key])),
|
||||
@@ -90,6 +106,14 @@ describe('ProjectsService', () => {
|
||||
abc123: { sha: 'abc123', message: 'feat: add platform refs' },
|
||||
def456: { sha: 'def456', message: 'chore: tag release' },
|
||||
},
|
||||
pullRequests: {
|
||||
abc123: {
|
||||
number: 2,
|
||||
title: 'feat: add platform refs',
|
||||
state: 'closed',
|
||||
merged: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const refs = await service.listProjectRefs(project.key);
|
||||
@@ -101,6 +125,13 @@ describe('ProjectsService', () => {
|
||||
name: 'feature/devops-platform',
|
||||
commitSha: 'abc123',
|
||||
commit: { sha: 'abc123', message: 'feat: add platform refs' },
|
||||
pullRequest: {
|
||||
number: 2,
|
||||
title: 'feat: add platform refs',
|
||||
state: 'closed',
|
||||
merged: true,
|
||||
url: 'https://gitea.ops.mrzhan.top/my-project/access-manage/pulls/2',
|
||||
},
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
|
||||
@@ -3,10 +3,19 @@ import { AuthService } from '../auth/auth.service';
|
||||
import { AuthenticatedUser } from '../auth/auth.types';
|
||||
import { AppError } from '../common/errors/app-error';
|
||||
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
||||
import { GiteaCommitSummary } from '../integrations/gitea/gitea.types';
|
||||
import {
|
||||
GiteaCommitSummary,
|
||||
GiteaPullRequestSummary,
|
||||
} from '../integrations/gitea/gitea.types';
|
||||
import { ProjectRepository } from './project.repository';
|
||||
import { ProjectRefSummary, ProjectSummary } from './project.types';
|
||||
|
||||
type ParsedRepository = {
|
||||
owner: string;
|
||||
name: string;
|
||||
webBaseUrl: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ProjectsService {
|
||||
constructor(
|
||||
@@ -83,9 +92,16 @@ export class ProjectsService {
|
||||
* Commit 摘要只作为发布前辅助信息;单个摘要读取失败不能影响 refs 主流程。
|
||||
*/
|
||||
private async enrichRefsWithCommit<T extends { name: string; commitSha?: string }>(
|
||||
repo: { owner: string; name: string },
|
||||
repo: ParsedRepository,
|
||||
refs: T[],
|
||||
): Promise<Array<T & { commit?: GiteaCommitSummary }>> {
|
||||
): Promise<
|
||||
Array<
|
||||
T & {
|
||||
commit?: GiteaCommitSummary;
|
||||
pullRequest?: GiteaPullRequestSummary & { url?: string };
|
||||
}
|
||||
>
|
||||
> {
|
||||
return Promise.all(
|
||||
refs.map(async (ref) => {
|
||||
if (!ref.commitSha) {
|
||||
@@ -93,13 +109,24 @@ export class ProjectsService {
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
...ref,
|
||||
commit: await this.giteaClient.getCommit(
|
||||
const [commit, pullRequest] = await Promise.all([
|
||||
this.giteaClient.getCommit(repo.owner, repo.name, ref.commitSha),
|
||||
this.giteaClient.getCommitPullRequest(
|
||||
repo.owner,
|
||||
repo.name,
|
||||
ref.commitSha,
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
...ref,
|
||||
commit,
|
||||
pullRequest: pullRequest
|
||||
? {
|
||||
...pullRequest,
|
||||
url: `${repo.webBaseUrl}/${repo.owner}/${repo.name}/pulls/${pullRequest.number}`,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return ref;
|
||||
@@ -110,14 +137,21 @@ export class ProjectsService {
|
||||
|
||||
private parseRepository(
|
||||
project: ProjectSummary,
|
||||
): { owner: string; name: string } | null {
|
||||
): ParsedRepository | null {
|
||||
try {
|
||||
const pathname = new URL(project.repositoryUrl).pathname.replace(/\.git$/, '');
|
||||
const url = new URL(project.repositoryUrl);
|
||||
const pathname = url.pathname.replace(/\.git$/, '');
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
const name = parts.at(-1);
|
||||
const owner = parts.at(-2);
|
||||
|
||||
return owner && name ? { owner, name } : null;
|
||||
return owner && name
|
||||
? {
|
||||
owner,
|
||||
name,
|
||||
webBaseUrl: url.origin,
|
||||
}
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user