UNPKG

bktide

Version:

Command-line interface for Buildkite CI/CD workflows with rich shell completions (Fish, Bash, Zsh) and Alfred workflow integration for macOS power users

307 lines 13.1 kB
/** * DataProfiler extracts statistical patterns from real data without storing sensitive information */ export class DataProfiler { profileBuilds(builds) { if (!builds || builds.length === 0) { return this.getEmptyBuildPatterns(); } return { states: this.getDistribution(builds.map(b => b.state)), branches: this.analyzeBranches(builds.map(b => b.branch || 'unknown')), messagePatterns: this.analyzeMessages(builds.map(b => b.message || '')), numberRange: { min: Math.min(...builds.map(b => b.number || 0)), max: Math.max(...builds.map(b => b.number || 0)) }, duration: this.analyzeDurations(builds), jobCounts: this.getDistribution(builds.map(b => b.jobs?.edges?.length || 0)), annotationCounts: this.getDistribution(builds.map(b => b.annotations?.edges?.length || 0)), creatorPatterns: this.analyzeCreators(builds) }; } profilePipelines(pipelines) { if (!pipelines || pipelines.length === 0) { return this.getEmptyPipelinePatterns(); } const names = pipelines.map(p => p.name || ''); const slugs = pipelines.map(p => p.slug || ''); return { slugFormats: this.analyzeSlugFormats(slugs), nameLength: { min: Math.min(...names.map(n => n.length)), max: Math.max(...names.map(n => n.length)), average: names.reduce((acc, n) => acc + n.length, 0) / names.length }, defaultBranches: this.getDistribution(pipelines.map(p => p.defaultBranch || 'main')), hasDescription: pipelines.filter(p => p.description).length / pipelines.length, repositoryProviders: this.getDistribution(pipelines.map(p => this.extractRepoProvider(p.repository?.url))), visibility: this.getDistribution(pipelines.map(p => p.visibility || 'PRIVATE')) }; } profileJobs(jobs) { if (!jobs || jobs.length === 0) { return this.getEmptyJobPatterns(); } return { states: this.getDistribution(jobs.map(j => j.__typename || 'JobTypeCommand')), labelPatterns: this.analyzeJobLabels(jobs), exitStatusDistribution: this.getDistribution(jobs.map(j => 'exitStatus' in j ? j.exitStatus : null)), retryRates: { automatic: jobs.filter(j => 'retriedAutomatically' in j && j.retriedAutomatically).length / jobs.length, manual: jobs.filter(j => 'retriedManually' in j && j.retriedManually).length / jobs.length }, parallelGroups: this.getDistribution(jobs.map(j => 'parallelGroupTotal' in j ? j.parallelGroupTotal || 1 : 1)), durationPatterns: this.analyzeJobDurations(jobs) }; } profileOrganizations(orgs) { if (!orgs || orgs.length === 0) { return this.getEmptyOrganizationPatterns(); } const names = orgs.map(o => o.name || ''); const slugs = orgs.map(o => o.slug || ''); return { slugFormats: this.analyzeSlugFormats(slugs), nameLength: { min: Math.min(...names.map(n => n.length)), max: Math.max(...names.map(n => n.length)), average: names.reduce((acc, n) => acc + n.length, 0) / names.length }, pipelineCount: this.getDistribution(orgs.map(o => o.pipelines?.edges?.length || 0)), memberCount: this.getDistribution(orgs.map(o => o.members?.edges?.length || 0)) }; } getDistribution(values) { const counts = new Map(); values.forEach(v => counts.set(v, (counts.get(v) || 0) + 1)); const total = values.length; const distribution = { values: Array.from(counts.entries()).map(([value, count]) => ({ value, frequency: count / total, count })).sort((a, b) => b.frequency - a.frequency), total }; return distribution; } analyzeBranches(branches) { const formats = { feature: branches.filter(b => b.startsWith('feature/')).length / branches.length, bugfix: branches.filter(b => b.startsWith('bugfix/') || b.startsWith('fix/')).length / branches.length, release: branches.filter(b => b.match(/^release\/\d+\.\d+/)).length / branches.length, main: branches.filter(b => ['main', 'master', 'develop'].includes(b)).length / branches.length, custom: 0 }; formats.custom = 1 - (formats.feature + formats.bugfix + formats.release + formats.main); return { common: this.getDistribution(this.getCommonPrefixes(branches)), formats, averageLength: branches.reduce((acc, b) => acc + b.length, 0) / branches.length }; } analyzeMessages(messages) { const nonEmpty = messages.filter(m => m.length > 0); if (nonEmpty.length === 0) { return { averageLength: 0, hasEmoji: 0, conventionalCommits: 0, commonPrefixes: { values: [], total: 0 }, githubRefs: 0, jiraRefs: 0, multiline: 0 }; } return { averageLength: nonEmpty.reduce((acc, m) => acc + m.length, 0) / nonEmpty.length, hasEmoji: nonEmpty.filter(m => /[🎉🚀✨🔧📦👷‍♂️🐛⚡️✅💚🔥]/.test(m)).length / nonEmpty.length, conventionalCommits: nonEmpty.filter(m => /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?:/.test(m)).length / nonEmpty.length, commonPrefixes: this.getDistribution(this.getCommonPrefixes(nonEmpty)), githubRefs: nonEmpty.filter(m => /#\d+/.test(m)).length / nonEmpty.length, jiraRefs: nonEmpty.filter(m => /[A-Z]{2,}-\d+/.test(m)).length / nonEmpty.length, multiline: nonEmpty.filter(m => m.includes('\n')).length / nonEmpty.length }; } analyzeDurations(builds) { const durations = builds .filter(b => b.startedAt && b.finishedAt) .map(b => new Date(b.finishedAt).getTime() - new Date(b.startedAt).getTime()); if (durations.length === 0) { return { min: 0, max: 0, median: 0, p95: 0, average: 0 }; } durations.sort((a, b) => a - b); return { min: durations[0], max: durations[durations.length - 1], median: durations[Math.floor(durations.length / 2)], p95: durations[Math.floor(durations.length * 0.95)], average: durations.reduce((acc, d) => acc + d, 0) / durations.length }; } analyzeJobDurations(jobs) { const durations = jobs .filter(j => 'startedAt' in j && 'finishedAt' in j) .map(j => { const job = j; // Type assertion for jobs with these fields if (job.startedAt && job.finishedAt) { return new Date(job.finishedAt).getTime() - new Date(job.startedAt).getTime(); } return null; }) .filter((d) => d !== null); if (durations.length === 0) { return { min: 0, max: 0, median: 0, p95: 0, average: 0 }; } durations.sort((a, b) => a - b); return { min: durations[0], max: durations[durations.length - 1], median: durations[Math.floor(durations.length / 2)], p95: durations[Math.floor(durations.length * 0.95)], average: durations.reduce((acc, d) => acc + d, 0) / durations.length }; } analyzeCreators(builds) { const creators = builds.map(b => b.createdBy); const emails = creators .map(c => c?.email || '') .filter(e => e.length > 0); const domains = emails.map(e => e.split('@')[1] || 'unknown'); const names = creators.map(c => c?.name || 'unknown'); return { nameFormats: this.getDistribution(names.map(n => this.classifyNameFormat(n))), domains: this.getDistribution(domains), botUsers: creators.filter(c => c?.name?.toLowerCase().includes('bot') || c?.email?.toLowerCase().includes('bot') || c?.name?.toLowerCase().includes('[bot]')).length / creators.length }; } analyzeSlugFormats(slugs) { const formats = slugs.map(s => { if (s.includes('-')) return 'kebab-case'; if (s.includes('_')) return 'snake_case'; if (s.match(/[A-Z]/)) return 'camelCase'; return 'lowercase'; }); return this.getDistribution(formats); } analyzeJobLabels(jobs) { const labels = jobs .map(j => 'label' in j ? j.label : null) .filter((l) => l !== null); // Extract common patterns const patterns = labels.map(l => { // Replace common variables with placeholders return l .replace(/\d+\.\d+\.\d+/g, ':version') .replace(/node-\d+/gi, 'node-:version') .replace(/python-\d+\.\d+/gi, 'python-:version') .replace(/ruby-\d+\.\d+/gi, 'ruby-:version') .replace(/(linux|ubuntu|macos|windows|darwin)/gi, ':os') .replace(/(test|tests|spec|specs)/gi, ':test') .replace(/\b[a-f0-9]{7,40}\b/g, ':sha'); }); return this.getDistribution(patterns); } getCommonPrefixes(strings, maxLength = 50) { const prefixes = []; strings.forEach(str => { const words = str.split(/[\s\-_/:]+/).filter(w => w.length > 0); if (words.length > 0) { prefixes.push(words[0].substring(0, maxLength)); } }); return prefixes; } extractRepoProvider(url) { if (!url) return 'unknown'; if (url.includes('github.com')) return 'github'; if (url.includes('gitlab.com')) return 'gitlab'; if (url.includes('bitbucket.org')) return 'bitbucket'; if (url.includes('git')) return 'git'; return 'other'; } classifyNameFormat(name) { if (name.includes(' ')) return 'full-name'; if (name.includes('.')) return 'dotted'; if (name.includes('-')) return 'hyphenated'; if (name.includes('_')) return 'underscored'; if (name.match(/^[a-z]+$/)) return 'lowercase'; if (name.match(/^[A-Z]+$/)) return 'uppercase'; return 'mixed'; } // Empty pattern generators for fallback getEmptyBuildPatterns() { return { states: { values: [], total: 0 }, branches: { common: { values: [], total: 0 }, formats: { feature: 0, bugfix: 0, release: 0, main: 0, custom: 0 }, averageLength: 0 }, messagePatterns: { averageLength: 0, hasEmoji: 0, conventionalCommits: 0, commonPrefixes: { values: [], total: 0 }, githubRefs: 0, jiraRefs: 0, multiline: 0 }, numberRange: { min: 0, max: 0 }, duration: { min: 0, max: 0, median: 0, p95: 0, average: 0 }, jobCounts: { values: [], total: 0 }, annotationCounts: { values: [], total: 0 }, creatorPatterns: { nameFormats: { values: [], total: 0 }, domains: { values: [], total: 0 }, botUsers: 0 } }; } getEmptyPipelinePatterns() { return { slugFormats: { values: [], total: 0 }, nameLength: { min: 0, max: 0, average: 0 }, defaultBranches: { values: [], total: 0 }, hasDescription: 0, repositoryProviders: { values: [], total: 0 }, visibility: { values: [], total: 0 } }; } getEmptyJobPatterns() { return { states: { values: [], total: 0 }, labelPatterns: { values: [], total: 0 }, exitStatusDistribution: { values: [], total: 0 }, retryRates: { automatic: 0, manual: 0 }, parallelGroups: { values: [], total: 0 }, durationPatterns: { min: 0, max: 0, median: 0, p95: 0, average: 0 } }; } getEmptyOrganizationPatterns() { return { slugFormats: { values: [], total: 0 }, nameLength: { min: 0, max: 0, average: 0 }, pipelineCount: { values: [], total: 0 }, memberCount: { values: [], total: 0 } }; } } //# sourceMappingURL=DataProfiler.js.map