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

144 lines 5.61 kB
export function categorizeError(error) { const message = error.message.toLowerCase(); if (message.includes('rate limit') || message.includes('429')) { return { category: 'rate_limited', message: error.message, retryable: true }; } if (message.includes('not found') || message.includes('404')) { return { category: 'not_found', message: error.message, retryable: false }; } if (message.includes('permission') || message.includes('403') || message.includes('401')) { return { category: 'permission_denied', message: error.message, retryable: false }; } if (message.includes('network') || message.includes('econnrefused') || message.includes('enotfound')) { return { category: 'network_error', message: error.message, retryable: true }; } return { category: 'unknown', message: error.message, retryable: true }; } const DEFAULT_OPTIONS = { initialInterval: 5000, maxInterval: 30000, timeout: 1800000, // 30 minutes maxConsecutiveErrors: 3, }; // Terminal states where build is complete export const TERMINAL_BUILD_STATES = ['passed', 'failed', 'canceled', 'blocked', 'not_run']; export function isTerminalState(state) { return TERMINAL_BUILD_STATES.includes(state.toLowerCase()); } export class BuildPoller { _client; _callbacks; _options; _stopped = false; _jobStates = new Map(); _signalHandler = null; constructor(client, callbacks, options) { this._client = client; this._callbacks = callbacks; this._options = { ...DEFAULT_OPTIONS, ...options }; } async watch(buildRef) { this._stopped = false; this._jobStates.clear(); this.setupSignalHandlers(); const startTime = Date.now(); try { // Initial fetch let build = await this._client.getBuild(buildRef.org, buildRef.pipeline, buildRef.buildNumber); // Process initial job states this.processJobChanges(build.jobs || []); // Check if already complete if (isTerminalState(build.state)) { this._callbacks.onBuildComplete(build); return build; } // Polling loop let currentInterval = this._options.initialInterval; let consecutiveErrors = 0; while (!this._stopped) { // Check timeout before sleep if (Date.now() - startTime >= this._options.timeout) { this._callbacks.onTimeout(); return build; } await this.sleep(currentInterval); if (this._stopped) break; // Check timeout after sleep if (Date.now() - startTime >= this._options.timeout) { this._callbacks.onTimeout(); return build; } try { build = await this._client.getBuild(buildRef.org, buildRef.pipeline, buildRef.buildNumber); // Reset on success consecutiveErrors = 0; currentInterval = this._options.initialInterval; // Process job state changes this.processJobChanges(build.jobs || []); // Check if complete if (isTerminalState(build.state)) { this._callbacks.onBuildComplete(build); return build; } } catch (error) { consecutiveErrors++; const pollError = categorizeError(error); const willRetry = pollError.retryable && consecutiveErrors < this._options.maxConsecutiveErrors; this._callbacks.onError(pollError, willRetry); if (!willRetry) { return build; } // Exponential backoff currentInterval = Math.min(currentInterval * 2, this._options.maxInterval); } } // Stopped externally return build; } finally { this.cleanupSignalHandlers(); } } setupSignalHandlers() { this._signalHandler = () => { this.stop(); }; process.on('SIGINT', this._signalHandler); } cleanupSignalHandlers() { if (this._signalHandler) { process.removeListener('SIGINT', this._signalHandler); this._signalHandler = null; } } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } processJobChanges(jobs) { for (const job of jobs) { const previousState = this._jobStates.get(job.id); const currentState = job.state; if (previousState !== currentState) { this._jobStates.set(job.id, currentState); this._callbacks.onJobStateChange({ job, previousState: previousState ?? null, timestamp: new Date(), }); } } } stop() { this._stopped = true; } // Expose for testing and internal use get client() { return this._client; } get callbacks() { return this._callbacks; } get options() { return this._options; } get stopped() { return this._stopped; } get jobStates() { return this._jobStates; } } //# sourceMappingURL=BuildPoller.js.map