Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:00:26 +08:00
commit 11bee70e53
13 changed files with 2402 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env node
/**
* Find Buildkite builds for a specific commit
*
* Searches across pipelines for builds matching a commit SHA.
* Useful for post-push workflows to find which builds are running.
*
* Usage:
* find-commit-builds.js <org> <commit-sha> [options]
*
* Options:
* --pipeline <slug> Limit search to specific pipeline
* --branch <name> Limit to specific branch
* --format <json|plain> Output format (default: plain)
*
* Exit codes:
* 0 - Builds found
* 1 - No builds found
* 2 - Error occurred
*/
const { execSync } = require('child_process');
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length < 2 || args[0] === '--help' || args[0] === '-h') {
console.error('Usage: find-commit-builds.js <org> <commit-sha> [options]');
console.error('Options:');
console.error(' --pipeline <slug> Limit search to specific pipeline');
console.error(' --branch <name> Limit to specific branch');
console.error(' --format <json|plain> Output format (default: plain)');
process.exit(2);
}
const org = args[0];
const commit = args[1];
// Parse options
let pipelineSlug = null;
let branch = null;
let format = 'plain';
for (let i = 2; i < args.length; i++) {
if (args[i] === '--pipeline' && i + 1 < args.length) {
pipelineSlug = args[++i];
} else if (args[i] === '--branch' && i + 1 < args.length) {
branch = args[++i];
} else if (args[i] === '--format' && i + 1 < args.length) {
format = args[++i];
}
}
function getPipelines() {
try {
const cmd = `npx bktide pipelines --format json ${org}`;
const output = execSync(cmd, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
return JSON.parse(output);
} catch (error) {
throw new Error(`Failed to get pipelines: ${error.message}`);
}
}
function getBuildsForPipeline(pipeline, commit, branch) {
try {
let cmd = `npx bktide builds --format json ${org}/${pipeline}`;
if (commit) {
// Note: bktide might not support commit filtering directly,
// so we'll fetch recent builds and filter
cmd += ` --state running --state scheduled`;
}
const output = execSync(cmd, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const builds = JSON.parse(output);
// Filter by commit
return builds.filter((build) => {
const commitMatch = !commit || build.commit?.startsWith(commit);
const branchMatch = !branch || build.branch === branch;
return commitMatch && branchMatch;
});
} catch (error) {
// Pipeline might not exist or be accessible, return empty array
return [];
}
}
function main() {
try {
const allBuilds = [];
if (pipelineSlug) {
// Search single pipeline
const builds = getBuildsForPipeline(pipelineSlug, commit, branch);
allBuilds.push(...builds);
} else {
// Search all pipelines
const pipelines = getPipelines();
for (const pipeline of pipelines) {
const builds = getBuildsForPipeline(pipeline.slug, commit, branch);
if (builds.length > 0) {
allBuilds.push(...builds);
}
}
}
if (format === 'json') {
console.log(JSON.stringify(allBuilds, null, 2));
} else {
if (allBuilds.length === 0) {
console.log(`No builds found for commit ${commit}`);
process.exit(1);
}
console.log(`Found ${allBuilds.length} build(s) for commit ${commit}:\n`);
for (const build of allBuilds) {
console.log(
` ${build.pipeline.slug}#${build.number} - ${build.state}`
);
console.log(` Branch: ${build.branch}`);
console.log(` URL: ${build.web_url}`);
console.log();
}
}
process.exit(allBuilds.length > 0 ? 0 : 1);
} catch (error) {
console.error('Error:', error.message);
process.exit(2);
}
}
main();

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env node
/**
* Get build logs for a specific job
*
* Usage:
* get-build-logs.js <org> <pipeline> <build> <job-label-or-uuid>
*
* Examples:
* get-build-logs.js gusto payroll-building-blocks 29627 "ste rspec"
* get-build-logs.js gusto payroll-building-blocks 29627 019a5f20-2d30-4c67-9edd-87fb92e1f487
*
* Features:
* - Accepts job label or UUID
* - Automatically resolves label to UUID if needed
* - Handles step ID vs job UUID confusion
* - Outputs formatted logs
*/
import { execSync } from 'child_process';
function usage() {
console.error(
'Usage: get-build-logs.js <org> <pipeline> <build> <job-label-or-uuid>'
);
console.error('');
console.error('Examples:');
console.error(
' get-build-logs.js gusto payroll-building-blocks 29627 "ste rspec"'
);
console.error(
' get-build-logs.js gusto payroll-building-blocks 29627 019a5f20-2d30-4c67-9edd-87fb92e1f487'
);
process.exit(1);
}
function getBuildDetails(org, pipeline, build) {
try {
const output = execSync(
`npx bktide build ${org}/${pipeline}/${build} --format json`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
);
return JSON.parse(output);
} catch (error) {
console.error(`Error getting build details: ${error.message}`);
process.exit(1);
}
}
function isUuid(str) {
// UUIDs are 36 characters with specific format
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
str
);
}
function resolveJobUuid(buildDetails, jobLabelOrUuid) {
// If it looks like a UUID, assume it's a job UUID
if (isUuid(jobLabelOrUuid)) {
return jobLabelOrUuid;
}
// Otherwise treat as label and search for matching job
// Note: bktide JSON format needs to be checked - this is a placeholder
console.error(`Note: Searching for job with label "${jobLabelOrUuid}"`);
console.error(
`Note: This script is a placeholder and needs MCP tool integration`
);
console.error(`Note: Use MCP buildkite:get_logs directly instead:`);
console.error(``);
console.error(`mcp__MCPProxy__call_tool("buildkite:get_logs", {`);
console.error(` org_slug: "${buildDetails.organization.slug}",`);
console.error(` pipeline_slug: "${buildDetails.pipeline.slug}",`);
console.error(` build_number: "${buildDetails.number}",`);
console.error(` job_id: "<job-uuid>"`);
console.error(`})`);
process.exit(1);
}
function main() {
const args = process.argv.slice(2);
if (args.length < 4 || args.includes('--help') || args.includes('-h')) {
usage();
}
const [org, pipeline, build, jobLabelOrUuid] = args;
console.error(`Fetching build details for ${org}/${pipeline}/${build}...`);
const buildDetails = getBuildDetails(org, pipeline, build);
console.error(`Resolving job identifier...`);
const jobUuid = resolveJobUuid(buildDetails, jobLabelOrUuid);
console.error(`\nNote: This script is a placeholder.`);
console.error(`For actual log retrieval, use MCP tools directly:`);
console.error(``);
console.error(`mcp__MCPProxy__call_tool("buildkite:get_logs", {`);
console.error(` org_slug: "${org}",`);
console.error(` pipeline_slug: "${pipeline}",`);
console.error(` build_number: "${build}",`);
console.error(` job_id: "${jobUuid}"`);
console.error(`})`);
}
main();

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* Parse Buildkite URL to extract components
*
* Usage:
* parse-buildkite-url.js <url>
*
* Examples:
* parse-buildkite-url.js "https://buildkite.com/gusto/payroll-building-blocks/builds/29627"
* parse-buildkite-url.js "https://buildkite.com/gusto/payroll-building-blocks/builds/29627/steps/canvas?sid=019a5f23..."
*
* Output:
* JSON object with: org, pipeline, buildNumber, stepId (if present)
*/
function usage() {
console.error('Usage: parse-buildkite-url.js <url>');
console.error('');
console.error('Examples:');
console.error(
' parse-buildkite-url.js "https://buildkite.com/gusto/payroll-building-blocks/builds/29627"'
);
console.error(
' parse-buildkite-url.js "https://buildkite.com/gusto/payroll-building-blocks/builds/29627/steps/canvas?sid=019a5f..."'
);
process.exit(1);
}
function parseBuildkiteUrl(url) {
// Match build URL pattern
const buildMatch = url.match(
/buildkite\.com\/([^/]+)\/([^/]+)\/builds\/(\d+)/
);
if (!buildMatch) {
throw new Error(
'Invalid Buildkite URL - expected format: https://buildkite.com/{org}/{pipeline}/builds/{number}'
);
}
const result = {
org: buildMatch[1],
pipeline: buildMatch[2],
buildNumber: buildMatch[3],
};
// Check for step ID query parameter
const sidMatch = url.match(/[?&]sid=([^&]+)/);
if (sidMatch) {
result.stepId = sidMatch[1];
result.note =
'stepId is for UI routing only - use API to get job UUID for log retrieval';
}
// Check for job UUID in path
const jobMatch = url.match(/\/jobs\/([0-9a-f-]+)/i);
if (jobMatch) {
result.jobUuid = jobMatch[1];
result.note = 'jobUuid can be used directly for log retrieval';
}
return result;
}
function main() {
const args = process.argv.slice(2);
if (args.length !== 1 || args.includes('--help') || args.includes('-h')) {
usage();
}
const url = args[0];
try {
const parsed = parseBuildkiteUrl(url);
console.log(JSON.stringify(parsed, null, 2));
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env node
/**
* Background build monitor for Buildkite
*
* Polls a build until it reaches a terminal state (passed, failed, canceled)
* with configurable timeout and polling interval.
*
* Usage:
* wait-for-build.js <org> <pipeline> <build-number> [options]
*
* Options:
* --timeout <seconds> Maximum time to wait (default: 1800 = 30 minutes)
* --interval <seconds> Polling interval (default: 30)
* --quiet Suppress progress updates
*
* Exit codes:
* 0 - Build passed
* 1 - Build failed
* 2 - Build canceled
* 3 - Timeout reached
* 4 - Error occurred
*/
const { execSync } = require('child_process');
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length < 3 || args[0] === '--help' || args[0] === '-h') {
console.error(
'Usage: wait-for-build.js <org> <pipeline> <build-number> [options]'
);
console.error('Options:');
console.error(
' --timeout <seconds> Maximum time to wait (default: 1800)'
);
console.error(' --interval <seconds> Polling interval (default: 30)');
console.error(' --quiet Suppress progress updates');
process.exit(4);
}
const org = args[0];
const pipeline = args[1];
const buildNumber = args[2];
// Parse options
let timeout = 1800; // 30 minutes default
let interval = 30; // 30 seconds default
let quiet = false;
for (let i = 3; i < args.length; i++) {
if (args[i] === '--timeout' && i + 1 < args.length) {
timeout = parseInt(args[++i], 10);
} else if (args[i] === '--interval' && i + 1 < args.length) {
interval = parseInt(args[++i], 10);
} else if (args[i] === '--quiet') {
quiet = true;
}
}
const startTime = Date.now();
const timeoutMs = timeout * 1000;
function log(message) {
if (!quiet) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
function getBuildStatus() {
try {
const cmd = `npx bktide build --format json gusto/${pipeline}#${buildNumber}`;
const output = execSync(cmd, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
return JSON.parse(output);
} catch (error) {
if (error.stdout) {
try {
return JSON.parse(error.stdout);
} catch {}
}
throw new Error(`Failed to get build status: ${error.message}`);
}
}
function isTerminalState(state) {
return ['passed', 'failed', 'canceled', 'blocked', 'skipped'].includes(state);
}
function getExitCode(state) {
switch (state) {
case 'passed':
return 0;
case 'failed':
return 1;
case 'canceled':
return 2;
default:
return 4;
}
}
log(`Monitoring build: ${org}/${pipeline}#${buildNumber}`);
log(`Timeout: ${timeout}s, Polling interval: ${interval}s`);
async function main() {
while (true) {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
// Check timeout
if (elapsed >= timeout) {
log(`Timeout reached after ${elapsed}s`);
process.exit(3);
}
try {
const build = getBuildStatus();
const state = build.state;
log(`Build state: ${state} (elapsed: ${elapsed}s)`);
if (isTerminalState(state)) {
log(`Build finished with state: ${state}`);
log(`Build URL: ${build.web_url}`);
// Show job summary if available
if (build.job_summary) {
const summary = build.job_summary;
log(`Jobs: ${summary.total} total`);
if (summary.by_state) {
const states = Object.entries(summary.by_state)
.map(([s, count]) => `${count} ${s}`)
.join(', ');
log(` ${states}`);
}
}
process.exit(getExitCode(state));
}
// Wait before next poll
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
} catch (error) {
log(`Error checking build status: ${error.message}`);
log('Retrying...');
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
}
}
}
main().catch((error) => {
console.error('Fatal error:', error.message);
process.exit(4);
});