Skip to main content
Glama
build-config-manager.ts12.5 kB
/** * Build Configuration Manager for TeamCity * * Manages listing, filtering, and paginating build configurations * from TeamCity projects. */ import type { Logger } from 'winston'; import type { BuildType, BuildTypes } from '@/teamcity-client'; import type { TeamCityUnifiedClient } from './types/client'; /** * Build configuration with normalized fields */ export interface ManagedBuildConfiguration { id: string; name: string; projectId: string; projectName: string; description?: string; webUrl?: string; paused: boolean; templateFlag: boolean; templateId?: string; parameters?: Record<string, string>; vcsRootIds?: string[]; buildSteps?: number; triggers?: number; dependencies?: { snapshot: string[]; artifact: string[]; }; } /** * Filtering options for build configurations */ export interface BuildConfigurationFilters { projectId?: string; projectIds?: string[]; namePattern?: string; templateFlag?: boolean; paused?: boolean; tags?: string[]; hasVcsRoot?: boolean; hasTriggers?: boolean; } /** * Sorting options */ export interface BuildConfigurationSort { by?: 'name' | 'projectName' | 'id' | 'created' | 'modified'; order?: 'asc' | 'desc'; } /** * Pagination options */ export interface BuildConfigurationPagination { page?: number; pageSize?: number; } /** * Response with pagination metadata */ export interface PaginatedBuildConfigurations { configurations: ManagedBuildConfiguration[]; pagination: { page: number; pageSize: number; totalCount: number; totalPages: number; hasNext: boolean; hasPrevious: boolean; }; } /** * Manager for build configurations */ export class BuildConfigManager { constructor( private readonly client: TeamCityUnifiedClient, private readonly logger: Logger ) {} /** * List build configurations with filtering and pagination */ async listConfigurations( options: { filters?: BuildConfigurationFilters; sort?: BuildConfigurationSort; pagination?: BuildConfigurationPagination; includeDetails?: boolean; } = {} ): Promise<PaginatedBuildConfigurations> { const { filters = {}, sort = { by: 'name', order: 'asc' }, pagination = { page: 1, pageSize: 50 }, } = options; try { // Construct locator string for TeamCity API const locator = this.buildLocator(filters); // Fetch build types from TeamCity const response = await this.client.modules.buildTypes.getAllBuildTypes( locator, this.buildFieldsSpec(options.includeDetails) ); const allConfigs = this.normalizeBuildTypes(response.data); // Apply additional filtering let filteredConfigs = this.applyFilters(allConfigs, filters); // Sort configurations filteredConfigs = this.sortConfigurations(filteredConfigs, sort); // Apply pagination const paginatedResult = this.paginate(filteredConfigs, pagination); return paginatedResult; } catch (error) { this.logger.error('Failed to list build configurations', { error, options }); throw error; } } /** * Get configurations by project with hierarchy */ async getProjectConfigurations( projectId: string, includeSubprojects: boolean = false ): Promise<ManagedBuildConfiguration[]> { try { const filters: BuildConfigurationFilters = { projectId }; if (includeSubprojects) { // Get all subprojects const subprojectIds = await this.getSubprojectIds(projectId); filters.projectIds = [projectId, ...subprojectIds]; delete filters.projectId; } const result = await this.listConfigurations({ filters, pagination: { pageSize: 1000 }, // Get all }); return result.configurations; } catch (error) { this.logger.error('Failed to get project configurations', { error, projectId, includeSubprojects, }); throw error; } } /** * Get template hierarchy for configurations */ async getTemplateHierarchy(templateId: string): Promise<{ template: ManagedBuildConfiguration; inheritors: ManagedBuildConfiguration[]; }> { try { // Get the template itself const templateResponse = await this.client.modules.buildTypes.getBuildType( templateId, this.buildFieldsSpec(true) ); const template = this.normalizeBuildType(templateResponse.data); // Find all configurations using this template const allConfigs = await this.listConfigurations({ pagination: { pageSize: 1000 }, }); const inheritors = allConfigs.configurations.filter( (config) => config.templateId === templateId ); return { template, inheritors }; } catch (error) { this.logger.error('Failed to get template hierarchy', { error, templateId }); throw error; } } /** * Build locator string for TeamCity API */ private buildLocator(filters: BuildConfigurationFilters): string | undefined { const parts: string[] = []; if (filters.projectId) { parts.push(`affectedProject:(id:${filters.projectId})`); } else if (filters.projectIds && filters.projectIds.length > 0) { const projectLocator = filters.projectIds.map((id) => `id:${id}`).join(','); parts.push(`affectedProject:(${projectLocator})`); } if (filters.templateFlag !== undefined) { parts.push(`templateFlag:${filters.templateFlag}`); } if (filters.paused !== undefined) { parts.push(`paused:${filters.paused}`); } if (filters.tags && filters.tags.length > 0) { const tagLocator = filters.tags.join(','); parts.push(`tag:(${tagLocator})`); } return parts.length > 0 ? parts.join(',') : undefined; } /** * Build fields specification for API request */ private buildFieldsSpec(includeDetails?: boolean): string { const baseFields = [ 'id', 'name', 'projectId', 'projectName', 'description', 'webUrl', 'paused', 'templateFlag', 'template(id)', ]; if (includeDetails) { baseFields.push( 'parameters(property(name,value))', 'vcs-root-entries(vcs-root-entry(id))', 'steps(count)', 'triggers(count)', 'snapshot-dependencies(count)', 'artifact-dependencies(count)' ); } return `buildType(${baseFields.join(',')})`; } /** * Normalize build types from API response */ private normalizeBuildTypes(response: BuildTypes): ManagedBuildConfiguration[] { const buildTypes = response.buildType ?? []; return buildTypes.map((bt) => this.normalizeBuildType(bt)); } /** * Normalize a single build type */ private normalizeBuildType(buildType: Partial<BuildType>): ManagedBuildConfiguration { const config: ManagedBuildConfiguration = { id: buildType.id ?? '', name: buildType.name ?? '', projectId: buildType.projectId ?? '', projectName: buildType.projectName ?? '', description: buildType.description, webUrl: buildType.webUrl, paused: buildType.paused ?? false, templateFlag: buildType.templateFlag ?? false, }; // Add template ID if present if (buildType.template?.id) { config.templateId = buildType.template.id; } // Add parameters if present if (buildType.parameters?.property) { config.parameters = {}; for (const param of buildType.parameters.property) { if (param.name && param.value) { config.parameters[param.name] = param.value; } } } // Add VCS root IDs if present if (buildType['vcs-root-entries']?.['vcs-root-entry']) { config.vcsRootIds = buildType['vcs-root-entries']['vcs-root-entry'] .map((entry: { id?: string }) => entry.id) .filter((id): id is string => Boolean(id)); } // Add counts if present if (buildType.steps?.count !== undefined) { config.buildSteps = buildType.steps.count; } if (buildType.triggers?.count !== undefined) { config.triggers = buildType.triggers.count; } // Add dependencies if present if (buildType['snapshot-dependencies'] ?? buildType['artifact-dependencies']) { config.dependencies = { snapshot: [], artifact: [], }; if (buildType['snapshot-dependencies']?.['snapshot-dependency']) { config.dependencies.snapshot = buildType['snapshot-dependencies']['snapshot-dependency'] .map((dep: { id?: string }) => dep.id) .filter((id): id is string => Boolean(id)); } if (buildType['artifact-dependencies']?.['artifact-dependency']) { config.dependencies.artifact = buildType['artifact-dependencies']['artifact-dependency'] .map((dep: { id?: string }) => dep.id) .filter((id): id is string => Boolean(id)); } } return config; } /** * Apply additional filters not supported by locator */ private applyFilters( configurations: ManagedBuildConfiguration[], filters: BuildConfigurationFilters ): ManagedBuildConfiguration[] { let filtered = [...configurations]; // Filter by name pattern if (filters.namePattern) { const pattern = filters.namePattern.toLowerCase(); if (pattern.includes('*')) { // Wildcard pattern const regex = new RegExp(`^${pattern.replace(/\*/g, '.*').replace(/\?/g, '.')}$`, 'i'); filtered = filtered.filter((config) => regex.test(config.name)); } else { // Simple contains filtered = filtered.filter((config) => config.name.toLowerCase().includes(pattern)); } } // Filter by VCS root presence if (filters.hasVcsRoot !== undefined) { filtered = filtered.filter((config) => { const hasVcs = config.vcsRootIds && config.vcsRootIds.length > 0; return filters.hasVcsRoot ? hasVcs : !hasVcs; }); } // Filter by trigger presence if (filters.hasTriggers !== undefined) { filtered = filtered.filter((config) => { const hasTriggers = config.triggers != null && config.triggers > 0; return filters.hasTriggers === true ? hasTriggers : !hasTriggers; }); } return filtered; } /** * Sort configurations */ private sortConfigurations( configurations: ManagedBuildConfiguration[], sort: BuildConfigurationSort ): ManagedBuildConfiguration[] { const sorted = [...configurations]; const { by = 'name', order = 'asc' } = sort; sorted.sort((a, b) => { let comparison = 0; switch (by) { case 'name': comparison = a.name.localeCompare(b.name); break; case 'projectName': comparison = a.projectName.localeCompare(b.projectName); break; case 'id': comparison = a.id.localeCompare(b.id); break; default: comparison = 0; } return order === 'asc' ? comparison : -comparison; }); return sorted; } /** * Apply pagination */ private paginate( configurations: ManagedBuildConfiguration[], pagination: BuildConfigurationPagination ): PaginatedBuildConfigurations { const { page = 1, pageSize = 50 } = pagination; const totalCount = configurations.length; const totalPages = Math.ceil(totalCount / pageSize); const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedConfigs = configurations.slice(startIndex, endIndex); return { configurations: paginatedConfigs, pagination: { page, pageSize, totalCount, totalPages, hasNext: page < totalPages, hasPrevious: page > 1, }, }; } /** * Get subproject IDs recursively */ private async getSubprojectIds(projectId: string): Promise<string[]> { try { const response = await this.client.modules.projects.getAllSubprojectsOrdered( projectId, 'id,parentProjectId' ); const subprojects = response.data.project ?? []; return subprojects .map((p: { id?: string }) => p.id) .filter((id): id is string => Boolean(id)); } catch (error) { this.logger.warn('Failed to get subprojects', { error, projectId }); return []; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Daghis/teamcity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server