add woodpecker dashboard proxy
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-06 18:21:27 +09:00
parent 84c7889857
commit 3aa90d2eee
5 changed files with 142 additions and 4 deletions
+13
View File
@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { WoodpeckerService } from './woodpecker.service';
@Controller('woodpecker')
export class WoodpeckerController {
constructor(private readonly woodpeckerService: WoodpeckerService) {}
@Get('repos')
listRepos() {
return this.woodpeckerService.listRepos();
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { WoodpeckerController } from './woodpecker.controller';
import { WoodpeckerService } from './woodpecker.service';
@Module({
controllers: [WoodpeckerController],
providers: [WoodpeckerService],
})
export class WoodpeckerModule {}
+109
View File
@@ -0,0 +1,109 @@
import { BadGatewayException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
type WoodpeckerRepo = Record<string, unknown>;
type WoodpeckerPipeline = Record<string, unknown>;
@Injectable()
export class WoodpeckerService {
private readonly baseUrl: string;
private readonly token?: string;
constructor(configService: ConfigService) {
this.baseUrl = configService
.get<string>('WOODPECKER_BASE_URL', 'https://woodpecker.autonomystudio.net')
.replace(/\/$/, '');
this.token =
configService.get<string>('WOODPECKER_TOKEN') ||
configService.get<string>('WOODPECKER_ADMIN_TOKEN') ||
undefined;
}
async listRepos() {
this.assertToken();
const body = await this.request<unknown>('/api/repos?latest=true');
const repos = Array.isArray(body)
? body
: isRecord(body) && Array.isArray(body.repos)
? body.repos
: [];
return {
repos: repos.map((repo) => normalizeRepo(repo)).sort((a, b) => b.createdAt - a.createdAt),
};
}
private async request<T>(path: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${this.token}`,
},
});
if (!response.ok) {
const detail = await response.text().catch(() => '');
throw new BadGatewayException({
message: 'Woodpecker API 요청이 실패했습니다.',
status: response.status,
detail,
});
}
return (await response.json()) as T;
}
private assertToken() {
if (!this.token) {
throw new UnauthorizedException('WOODPECKER_TOKEN이 설정되어 있지 않습니다.');
}
}
}
function normalizeRepo(repo: WoodpeckerRepo) {
const pipeline = getPipeline(repo);
const createdAt = getPipelineTime(pipeline);
const repository = getString(repo.full_name) || getString(repo.fullName) || joinRepoName(repo) || getString(repo.name) || '-';
return {
id: `${getString(repo.id) || repository}-${getString(pipeline.id) || getString(pipeline.number) || createdAt}`,
repository,
message: getString(pipeline.message) || getString(pipeline.title) || getString(pipeline.event) || '-',
branch: getString(pipeline.branch) || '-',
status: getString(pipeline.status) || '-',
createdAt,
url: getString(repo.link_url) || getString(repo.forge_url) || getString(repo.url) || null,
};
}
function getPipeline(repo: WoodpeckerRepo): WoodpeckerPipeline {
const pipeline = repo.last_pipeline ?? repo.pipeline;
return isRecord(pipeline) ? pipeline : {};
}
function getPipelineTime(pipeline: WoodpeckerPipeline) {
const raw = pipeline.created ?? pipeline.created_at ?? pipeline.started ?? pipeline.started_at ?? pipeline.finished ?? pipeline.updated_at;
if (typeof raw === 'number') return raw > 10_000_000_000 ? raw : raw * 1000;
if (typeof raw === 'string') {
const numeric = Number(raw);
if (Number.isFinite(numeric) && numeric > 0) return numeric > 10_000_000_000 ? numeric : numeric * 1000;
const parsed = Date.parse(raw);
return Number.isFinite(parsed) ? parsed : 0;
}
return 0;
}
function joinRepoName(repo: WoodpeckerRepo) {
const owner = getString(repo.owner);
const name = getString(repo.name);
return owner && name ? `${owner} / ${name}` : '';
}
function getString(value: unknown) {
return typeof value === 'string' || typeof value === 'number' ? String(value) : '';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}