Initial commit
This commit is contained in:
137
skills/buildkite-status/scripts/find-commit-builds.js
Executable file
137
skills/buildkite-status/scripts/find-commit-builds.js
Executable 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();
|
||||
107
skills/buildkite-status/scripts/get-build-logs.js
Executable file
107
skills/buildkite-status/scripts/get-build-logs.js
Executable 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();
|
||||
84
skills/buildkite-status/scripts/parse-buildkite-url.js
Executable file
84
skills/buildkite-status/scripts/parse-buildkite-url.js
Executable 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();
|
||||
155
skills/buildkite-status/scripts/wait-for-build.js
Executable file
155
skills/buildkite-status/scripts/wait-for-build.js
Executable 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);
|
||||
});
|
||||
Reference in New Issue
Block a user