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 { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error';
|
||||||
import { redactSensitive } from '../../common/security/redact-sensitive';
|
import { redactSensitive } from '../../common/security/redact-sensitive';
|
||||||
import { EnvConfig } from '../../config/env.schema';
|
import { EnvConfig } from '../../config/env.schema';
|
||||||
import { GiteaBranch, GiteaCommitSummary, GiteaTag } from './gitea.types';
|
import {
|
||||||
|
GiteaBranch,
|
||||||
|
GiteaCommitSummary,
|
||||||
|
GiteaPullRequestSummary,
|
||||||
|
GiteaTag,
|
||||||
|
} from './gitea.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GiteaClient {
|
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> {
|
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
this.assertConfigured();
|
this.assertConfigured();
|
||||||
const baseUrl = this.config.get('GITEA_BASE_URL', { infer: true }) ?? '';
|
const baseUrl = this.config.get('GITEA_BASE_URL', { infer: true }) ?? '';
|
||||||
|
|||||||
@@ -14,3 +14,16 @@ export type GiteaCommitSummary = {
|
|||||||
authorName?: string;
|
authorName?: string;
|
||||||
authoredAt?: 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[];
|
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 = {
|
export type ProjectRefSummary = {
|
||||||
source: 'gitea' | 'fallback';
|
source: 'gitea' | 'fallback';
|
||||||
branches: Array<{
|
branches: ProjectRefItemSummary[];
|
||||||
name: string;
|
tags: ProjectRefItemSummary[];
|
||||||
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;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ describe('ProjectsService', () => {
|
|||||||
branches?: Array<{ name: string; commitSha: string }>;
|
branches?: Array<{ name: string; commitSha: string }>;
|
||||||
tags?: Array<{ name: string; commitSha: string }>;
|
tags?: Array<{ name: string; commitSha: string }>;
|
||||||
commits?: Record<string, { sha: string; message: string }>;
|
commits?: Record<string, { sha: string; message: string }>;
|
||||||
|
pullRequests?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
merged: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
}) {
|
}) {
|
||||||
const repository = {
|
const repository = {
|
||||||
findMany: jest.fn(() => Promise.resolve([project])),
|
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<
|
} satisfies Pick<
|
||||||
GiteaClient,
|
GiteaClient,
|
||||||
'healthSummary' | 'listBranches' | 'listTags' | 'getCommit'
|
| 'healthSummary'
|
||||||
|
| 'listBranches'
|
||||||
|
| 'listTags'
|
||||||
|
| 'getCommit'
|
||||||
|
| 'getCommitPullRequest'
|
||||||
>;
|
>;
|
||||||
const auth = {
|
const auth = {
|
||||||
listAllowedProjectKeys: jest.fn(() => Promise.resolve([project.key])),
|
listAllowedProjectKeys: jest.fn(() => Promise.resolve([project.key])),
|
||||||
@@ -90,6 +106,14 @@ describe('ProjectsService', () => {
|
|||||||
abc123: { sha: 'abc123', message: 'feat: add platform refs' },
|
abc123: { sha: 'abc123', message: 'feat: add platform refs' },
|
||||||
def456: { sha: 'def456', message: 'chore: tag release' },
|
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);
|
const refs = await service.listProjectRefs(project.key);
|
||||||
@@ -101,6 +125,13 @@ describe('ProjectsService', () => {
|
|||||||
name: 'feature/devops-platform',
|
name: 'feature/devops-platform',
|
||||||
commitSha: 'abc123',
|
commitSha: 'abc123',
|
||||||
commit: { sha: 'abc123', message: 'feat: add platform refs' },
|
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: [
|
tags: [
|
||||||
|
|||||||
@@ -3,10 +3,19 @@ import { AuthService } from '../auth/auth.service';
|
|||||||
import { AuthenticatedUser } from '../auth/auth.types';
|
import { AuthenticatedUser } from '../auth/auth.types';
|
||||||
import { AppError } from '../common/errors/app-error';
|
import { AppError } from '../common/errors/app-error';
|
||||||
import { GiteaClient } from '../integrations/gitea/gitea.client';
|
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 { ProjectRepository } from './project.repository';
|
||||||
import { ProjectRefSummary, ProjectSummary } from './project.types';
|
import { ProjectRefSummary, ProjectSummary } from './project.types';
|
||||||
|
|
||||||
|
type ParsedRepository = {
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
webBaseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProjectsService {
|
export class ProjectsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -83,9 +92,16 @@ export class ProjectsService {
|
|||||||
* Commit 摘要只作为发布前辅助信息;单个摘要读取失败不能影响 refs 主流程。
|
* Commit 摘要只作为发布前辅助信息;单个摘要读取失败不能影响 refs 主流程。
|
||||||
*/
|
*/
|
||||||
private async enrichRefsWithCommit<T extends { name: string; commitSha?: string }>(
|
private async enrichRefsWithCommit<T extends { name: string; commitSha?: string }>(
|
||||||
repo: { owner: string; name: string },
|
repo: ParsedRepository,
|
||||||
refs: T[],
|
refs: T[],
|
||||||
): Promise<Array<T & { commit?: GiteaCommitSummary }>> {
|
): Promise<
|
||||||
|
Array<
|
||||||
|
T & {
|
||||||
|
commit?: GiteaCommitSummary;
|
||||||
|
pullRequest?: GiteaPullRequestSummary & { url?: string };
|
||||||
|
}
|
||||||
|
>
|
||||||
|
> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
refs.map(async (ref) => {
|
refs.map(async (ref) => {
|
||||||
if (!ref.commitSha) {
|
if (!ref.commitSha) {
|
||||||
@@ -93,13 +109,24 @@ export class ProjectsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return {
|
const [commit, pullRequest] = await Promise.all([
|
||||||
...ref,
|
this.giteaClient.getCommit(repo.owner, repo.name, ref.commitSha),
|
||||||
commit: await this.giteaClient.getCommit(
|
this.giteaClient.getCommitPullRequest(
|
||||||
repo.owner,
|
repo.owner,
|
||||||
repo.name,
|
repo.name,
|
||||||
ref.commitSha,
|
ref.commitSha,
|
||||||
),
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ref,
|
||||||
|
commit,
|
||||||
|
pullRequest: pullRequest
|
||||||
|
? {
|
||||||
|
...pullRequest,
|
||||||
|
url: `${repo.webBaseUrl}/${repo.owner}/${repo.name}/pulls/${pullRequest.number}`,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return ref;
|
return ref;
|
||||||
@@ -110,14 +137,21 @@ export class ProjectsService {
|
|||||||
|
|
||||||
private parseRepository(
|
private parseRepository(
|
||||||
project: ProjectSummary,
|
project: ProjectSummary,
|
||||||
): { owner: string; name: string } | null {
|
): ParsedRepository | null {
|
||||||
try {
|
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 parts = pathname.split('/').filter(Boolean);
|
||||||
const name = parts.at(-1);
|
const name = parts.at(-1);
|
||||||
const owner = parts.at(-2);
|
const owner = parts.at(-2);
|
||||||
|
|
||||||
return owner && name ? { owner, name } : null;
|
return owner && name
|
||||||
|
? {
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
webBaseUrl: url.origin,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user