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
JavaScript
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