fix: stabilize backend prisma and lint
This commit is contained in:
@@ -3,7 +3,12 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
import { GiteaService } from '../gitea/gitea.service';
|
||||
|
||||
type QaStatus = 'passed' | 'failed' | 'unknown';
|
||||
type ProjectPhase = 'PLANNING' | 'IMPLEMENT' | 'QA' | 'READY FOR DEPLOY' | 'DEPLOYED';
|
||||
type ProjectPhase =
|
||||
| 'PLANNING'
|
||||
| 'IMPLEMENT'
|
||||
| 'QA'
|
||||
| 'READY FOR DEPLOY'
|
||||
| 'DEPLOYED';
|
||||
|
||||
export interface RepoDocumentMeta {
|
||||
path: string;
|
||||
@@ -36,7 +41,10 @@ function normalizeRepoName(repoUrl: string): string {
|
||||
return repoUrl.replace(/\/+$/, '').split('/').pop() ?? '';
|
||||
}
|
||||
|
||||
function extractDocOrder(path: string, kind: 'sprint' | 'hotfix' | 'qa'): number {
|
||||
function extractDocOrder(
|
||||
path: string,
|
||||
kind: 'sprint' | 'hotfix' | 'qa',
|
||||
): number {
|
||||
const name = path.split('/').pop() ?? '';
|
||||
if (kind === 'hotfix') {
|
||||
const hotfix = name.match(/HOTFIX-(\d+)/i);
|
||||
@@ -81,7 +89,8 @@ function parseTaskSummary(content: string): string | null {
|
||||
function parseQaStatus(content: string): QaStatus {
|
||||
if (/passed\s*[:=]\s*true/i.test(content)) return 'passed';
|
||||
if (/passed\s*[:=]\s*false/i.test(content)) return 'failed';
|
||||
if (/\bPASSED\b/i.test(content) && !/\bFAILED\b/i.test(content)) return 'passed';
|
||||
if (/\bPASSED\b/i.test(content) && !/\bFAILED\b/i.test(content))
|
||||
return 'passed';
|
||||
if (/\bFAILED\b/i.test(content)) return 'failed';
|
||||
return 'unknown';
|
||||
}
|
||||
@@ -89,19 +98,27 @@ function parseQaStatus(content: string): QaStatus {
|
||||
function parseBlockerCount(content: string): number {
|
||||
const jsonMatch = content.match(/"errors"\s*:\s*\[(.*?)\]/is);
|
||||
if (jsonMatch) {
|
||||
const items = jsonMatch[1].split(',').map((item) => item.trim()).filter(Boolean);
|
||||
const items = jsonMatch[1]
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
if (items.length > 0) return items.length;
|
||||
}
|
||||
|
||||
const errorLines = content
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => /^[-*]\s+/.test(line) && /(error|blocker|실패|문제)/i.test(line));
|
||||
.filter(
|
||||
(line) =>
|
||||
/^[-*]\s+/.test(line) && /(error|blocker|실패|문제)/i.test(line),
|
||||
);
|
||||
|
||||
return errorLines.length;
|
||||
}
|
||||
|
||||
function isRepoDocumentMeta(value: RepoDocumentMeta | null): value is RepoDocumentMeta {
|
||||
function isRepoDocumentMeta(
|
||||
value: RepoDocumentMeta | null,
|
||||
): value is RepoDocumentMeta {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
@@ -141,11 +158,12 @@ function buildProjectMeta(params: {
|
||||
phase = 'IMPLEMENT';
|
||||
}
|
||||
|
||||
const deployStatus = phase === 'DEPLOYED'
|
||||
? 'DEPLOYED ON MAIN'
|
||||
: phase === 'READY FOR DEPLOY'
|
||||
? 'READY FOR DEPLOY'
|
||||
: 'REDEPLOY REQUIRED';
|
||||
const deployStatus =
|
||||
phase === 'DEPLOYED'
|
||||
? 'DEPLOYED ON MAIN'
|
||||
: phase === 'READY FOR DEPLOY'
|
||||
? 'READY FOR DEPLOY'
|
||||
: 'REDEPLOY REQUIRED';
|
||||
|
||||
return {
|
||||
phase,
|
||||
@@ -177,17 +195,30 @@ export class ProjectsService {
|
||||
|
||||
const [repoMetaMap, repoDocsMap] = await Promise.all([
|
||||
this.getRepoMetaMap(),
|
||||
this.getRepoDocumentMap(dbProjects.map((project) => normalizeRepoName(project.repoUrl))),
|
||||
this.getRepoDocumentMap(
|
||||
dbProjects.map((project) => normalizeRepoName(project.repoUrl)),
|
||||
),
|
||||
]);
|
||||
|
||||
return dbProjects.map((project) => {
|
||||
const totalSprints = project.sprints.length;
|
||||
const doneSprints = project.sprints.filter((sprint) => sprint.status === 'done').length;
|
||||
const inProgressSprint = project.sprints.find((sprint) => sprint.status === 'in_progress');
|
||||
const ownerSister = project.sprints.flatMap((sprint) => sprint.tasks).find((task) => task.assignee)?.assignee ?? 'narang';
|
||||
const progress = totalSprints > 0 ? Math.round((doneSprints / totalSprints) * 100) : 0;
|
||||
const doneSprints = project.sprints.filter(
|
||||
(sprint) => sprint.status === 'done',
|
||||
).length;
|
||||
const inProgressSprint = project.sprints.find(
|
||||
(sprint) => sprint.status === 'in_progress',
|
||||
);
|
||||
const ownerSister =
|
||||
project.sprints
|
||||
.flatMap((sprint) => sprint.tasks)
|
||||
.find((task) => task.assignee)?.assignee ?? 'narang';
|
||||
const progress =
|
||||
totalSprints > 0 ? Math.round((doneSprints / totalSprints) * 100) : 0;
|
||||
const repoMeta = repoMetaMap[project.giteaId];
|
||||
const docs = repoDocsMap.get(normalizeRepoName(project.repoUrl)) ?? { hotfixes: [], qas: [] };
|
||||
const docs = repoDocsMap.get(normalizeRepoName(project.repoUrl)) ?? {
|
||||
hotfixes: [],
|
||||
qas: [],
|
||||
};
|
||||
const latestHotfix = docs.hotfixes.at(-1) ?? null;
|
||||
const latestQa = docs.qas.at(-1) ?? null;
|
||||
const meta = buildProjectMeta({
|
||||
@@ -213,7 +244,11 @@ export class ProjectsService {
|
||||
totalSprints,
|
||||
doneSprints,
|
||||
sprintCount: totalSprints,
|
||||
currentSprint: inProgressSprint?.name ?? (doneSprints === totalSprints && totalSprints > 0 ? 'COMPLETED' : null),
|
||||
currentSprint:
|
||||
inProgressSprint?.name ??
|
||||
(doneSprints === totalSprints && totalSprints > 0
|
||||
? 'COMPLETED'
|
||||
: null),
|
||||
ownerSister,
|
||||
openPRs: repoMeta?.openPRs ?? 0,
|
||||
updatedAt: repoMeta?.updatedAt ?? project.updatedAt.toISOString(),
|
||||
@@ -253,8 +288,12 @@ export class ProjectsService {
|
||||
]);
|
||||
|
||||
const totalSprints = project.sprints.length;
|
||||
const doneSprints = project.sprints.filter((sprint) => sprint.status === 'done').length;
|
||||
const activeSprint = project.sprints.find((sprint) => sprint.status === 'in_progress');
|
||||
const doneSprints = project.sprints.filter(
|
||||
(sprint) => sprint.status === 'done',
|
||||
).length;
|
||||
const activeSprint = project.sprints.find(
|
||||
(sprint) => sprint.status === 'in_progress',
|
||||
);
|
||||
const meta = buildProjectMeta({
|
||||
totalSprints,
|
||||
doneSprints,
|
||||
@@ -276,7 +315,8 @@ export class ProjectsService {
|
||||
label: docs.qas.at(-1)?.label ?? null,
|
||||
status: docs.qas.at(-1)?.qaStatus ?? 'unknown',
|
||||
blockerCount: docs.qas.at(-1)?.blockerCount ?? 0,
|
||||
summary: docs.qas.at(-1)?.summary ?? docs.qas.at(-1)?.description ?? null,
|
||||
summary:
|
||||
docs.qas.at(-1)?.summary ?? docs.qas.at(-1)?.description ?? null,
|
||||
}
|
||||
: null,
|
||||
deploy: {
|
||||
@@ -310,17 +350,23 @@ export class ProjectsService {
|
||||
const repoName = normalizeRepoName(project.repoUrl);
|
||||
const docs = await this.collectRepoDocuments(repoName);
|
||||
|
||||
return [...docs.sprints, ...docs.hotfixes].filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order);
|
||||
return [...docs.sprints, ...docs.hotfixes]
|
||||
.filter(isRepoDocumentMeta)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
private async getProjectMeta(id: number) {
|
||||
const project = await this.prisma.project.findUnique({ where: { id }, select: { id: true, repoUrl: true } });
|
||||
const project = await this.prisma.project.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, repoUrl: true },
|
||||
});
|
||||
if (!project) throw new NotFoundException(`Project ${id} not found`);
|
||||
return project;
|
||||
}
|
||||
|
||||
private async getRepoMetaMap() {
|
||||
const repoMetaMap: Record<number, { openPRs: number; updatedAt: string }> = {};
|
||||
const repoMetaMap: Record<number, { openPRs: number; updatedAt: string }> =
|
||||
{};
|
||||
try {
|
||||
const repos = await this.gitea.getOrgRepos();
|
||||
for (const repo of repos) {
|
||||
@@ -337,35 +383,80 @@ export class ProjectsService {
|
||||
|
||||
private async getRepoDocumentMap(repoNames: string[]) {
|
||||
const uniqueRepoNames = Array.from(new Set(repoNames.filter(Boolean)));
|
||||
const entries = await Promise.all(uniqueRepoNames.map(async (repoName) => [repoName, await this.collectRepoDocuments(repoName)] as const));
|
||||
const entries = await Promise.all(
|
||||
uniqueRepoNames.map(
|
||||
async (repoName) =>
|
||||
[repoName, await this.collectRepoDocuments(repoName)] as const,
|
||||
),
|
||||
);
|
||||
return new Map(entries);
|
||||
}
|
||||
|
||||
private async collectRepoDocuments(repoName: string) {
|
||||
const [sprintPaths, hotfixPathGroups, qaPathGroups] = await Promise.all([
|
||||
this.gitea.getRepoTree(repoName, SPRINT_PATH),
|
||||
Promise.all(HOTFIX_PATH_CANDIDATES.map((path) => this.gitea.getRepoTree(repoName, path))),
|
||||
Promise.all(QA_PATH_CANDIDATES.map((path) => this.gitea.getRepoTree(repoName, path))),
|
||||
Promise.all(
|
||||
HOTFIX_PATH_CANDIDATES.map((path) =>
|
||||
this.gitea.getRepoTree(repoName, path),
|
||||
),
|
||||
),
|
||||
Promise.all(
|
||||
QA_PATH_CANDIDATES.map((path) =>
|
||||
this.gitea.getRepoTree(repoName, path),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const sprintFiles = sprintPaths.filter((path) => /SPRINT-\d+\.md$/i.test(path));
|
||||
const hotfixFiles = Array.from(new Set(hotfixPathGroups.flat().filter((path) => /HOTFIX-\d+\.md$/i.test(path))));
|
||||
const qaFiles = Array.from(new Set(qaPathGroups.flat().filter((path) => /(?:SPRINT|HOTFIX)-\d+.*(?:review|qa).*\.md$/i.test(path))));
|
||||
const sprintFiles = sprintPaths.filter((path) =>
|
||||
/SPRINT-\d+\.md$/i.test(path),
|
||||
);
|
||||
const hotfixFiles = Array.from(
|
||||
new Set(
|
||||
hotfixPathGroups.flat().filter((path) => /HOTFIX-\d+\.md$/i.test(path)),
|
||||
),
|
||||
);
|
||||
const qaFiles = Array.from(
|
||||
new Set(
|
||||
qaPathGroups
|
||||
.flat()
|
||||
.filter((path) =>
|
||||
/(?:SPRINT|HOTFIX)-\d+.*(?:review|qa).*\.md$/i.test(path),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const [sprints, hotfixes, qas] = await Promise.all([
|
||||
Promise.all(sprintFiles.map((path) => this.readRepoDocument(repoName, path, 'sprint'))),
|
||||
Promise.all(hotfixFiles.map((path) => this.readRepoDocument(repoName, path, 'hotfix'))),
|
||||
Promise.all(qaFiles.map((path) => this.readRepoDocument(repoName, path, 'qa'))),
|
||||
Promise.all(
|
||||
sprintFiles.map((path) =>
|
||||
this.readRepoDocument(repoName, path, 'sprint'),
|
||||
),
|
||||
),
|
||||
Promise.all(
|
||||
hotfixFiles.map((path) =>
|
||||
this.readRepoDocument(repoName, path, 'hotfix'),
|
||||
),
|
||||
),
|
||||
Promise.all(
|
||||
qaFiles.map((path) => this.readRepoDocument(repoName, path, 'qa')),
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
sprints: sprints.filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order),
|
||||
hotfixes: hotfixes.filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order),
|
||||
sprints: sprints
|
||||
.filter(isRepoDocumentMeta)
|
||||
.sort((a, b) => a.order - b.order),
|
||||
hotfixes: hotfixes
|
||||
.filter(isRepoDocumentMeta)
|
||||
.sort((a, b) => a.order - b.order),
|
||||
qas: qas.filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order),
|
||||
};
|
||||
}
|
||||
|
||||
private async readRepoDocument(repoName: string, path: string, kind: 'sprint' | 'hotfix' | 'qa'): Promise<RepoDocumentMeta | null> {
|
||||
private async readRepoDocument(
|
||||
repoName: string,
|
||||
path: string,
|
||||
kind: 'sprint' | 'hotfix' | 'qa',
|
||||
): Promise<RepoDocumentMeta | null> {
|
||||
const content = await this.gitea.getRawFile(repoName, path);
|
||||
if (!content) return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user