From 76cc88737125c269776eb112ba092b7d6d2dcc25 Mon Sep 17 00:00:00 2001 From: chyoo Date: Thu, 9 Jul 2026 10:53:53 +0900 Subject: [PATCH] paginate common app pipeline history by repository --- src/woodpecker/woodpecker.controller.ts | 14 +++++- src/woodpecker/woodpecker.service.ts | 63 +++++++++++++++++++++---- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/woodpecker/woodpecker.controller.ts b/src/woodpecker/woodpecker.controller.ts index 49d8397..bcbcc09 100644 --- a/src/woodpecker/woodpecker.controller.ts +++ b/src/woodpecker/woodpecker.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Param, Query } from '@nestjs/common'; import { WoodpeckerService } from './woodpecker.service'; @@ -10,4 +10,16 @@ export class WoodpeckerController { listRepos() { return this.woodpeckerService.listRepos(); } + + @Get('repos/:repoId/pipelines') + listRepoPipelines( + @Param('repoId') repoId: string, + @Query('page') page?: string, + @Query('limit') limit?: string, + ) { + return this.woodpeckerService.listRepoPipelines(repoId, { + page: Number(page) || 1, + limit: Number(limit) || 5, + }); + } } diff --git a/src/woodpecker/woodpecker.service.ts b/src/woodpecker/woodpecker.service.ts index e83478d..2616101 100644 --- a/src/woodpecker/woodpecker.service.ts +++ b/src/woodpecker/woodpecker.service.ts @@ -41,6 +41,32 @@ export class WoodpeckerService { }; } + async listRepoPipelines(repoId: string, input: { page?: number; limit?: number }) { + this.assertToken(); + + const page = Math.max(1, input.page || 1); + const limit = Math.min(50, Math.max(1, input.limit || 5)); + const body = await this.request( + `/api/repos/${encodeURIComponent(repoId)}/pipelines?page=${page}&limit=${limit + 1}`, + ); + const rows = Array.isArray(body) + ? body + : isRecord(body) && Array.isArray(body.pipelines) + ? body.pipelines + : []; + const pipelines = rows + .filter(isRecord) + .slice(0, limit) + .map((pipeline) => normalizePipeline(pipeline)); + + return { + page, + pageSize: limit, + hasNext: rows.length > limit, + pipelines, + }; + } + private async getLatestPipeline(repo: WoodpeckerRepo): Promise { const embeddedPipeline = getPipeline(repo); if (getPipelineTime(embeddedPipeline) > 0) return embeddedPipeline; @@ -91,23 +117,39 @@ export class WoodpeckerService { function normalizeRepo(repo: WoodpeckerRepo, pipeline = getPipeline(repo), baseUrl = '') { const createdAt = getPipelineTime(pipeline); const repository = getString(repo.full_name) || getString(repo.fullName) || joinRepoName(repo) || getString(repo.name) || '-'; - const commitSha = getCommitSha(pipeline); - const durationSeconds = getPipelineDurationSeconds(pipeline); + const normalizedPipeline = normalizePipeline(pipeline); return { - id: `${getString(repo.id) || repository}-${getString(pipeline.id) || getString(pipeline.number) || createdAt}`, + id: `${getString(repo.id) || repository}-${normalizedPipeline.id || normalizedPipeline.number || createdAt}`, + repoId: getString(repo.id) || null, repository, - message: getString(pipeline.message) || getString(pipeline.title) || getString(pipeline.event) || '-', - branch: getString(pipeline.branch) || '-', - commitSha, - durationSeconds, - status: getString(pipeline.status) || '-', + number: normalizedPipeline.number, + message: normalizedPipeline.message, + branch: normalizedPipeline.branch, + commitSha: normalizedPipeline.commitSha, + durationSeconds: normalizedPipeline.durationSeconds, + status: normalizedPipeline.status, createdAt, ciUrl: baseUrl || null, url: getString(repo.link_url) || getString(repo.forge_url) || getString(repo.url) || null, }; } +function normalizePipeline(pipeline: WoodpeckerPipeline) { + const createdAt = getPipelineTime(pipeline); + + return { + id: getString(pipeline.id) || getString(pipeline.number) || String(createdAt || ''), + number: getNumber(pipeline.number), + message: getString(pipeline.message) || getString(pipeline.title) || getString(pipeline.event) || '-', + branch: getString(pipeline.branch) || '-', + commitSha: getCommitSha(pipeline), + durationSeconds: getPipelineDurationSeconds(pipeline), + status: getString(pipeline.status) || '-', + createdAt, + }; +} + function getPipeline(repo: WoodpeckerRepo): WoodpeckerPipeline { const pipeline = repo.last_pipeline ?? repo.pipeline; return isRecord(pipeline) ? pipeline : {}; @@ -161,6 +203,11 @@ function getString(value: unknown) { return typeof value === 'string' || typeof value === 'number' ? String(value) : ''; } +function getNumber(value: unknown) { + const number = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN; + return Number.isFinite(number) ? number : null; +} + function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }