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
109 lines • 5.42 kB
JavaScript
import path from 'path';
import { BaseCommand } from './BaseCommand.js';
import { logger } from '../services/logger.js';
import { parseBuildRef } from '../utils/parseBuildRef.js';
import { formatError, SEMANTIC_COLORS } from '../ui/theme.js';
import { Progress } from '../ui/progress.js';
import { minimatch } from 'minimatch';
import { DOWNLOADABLE_ARTIFACT_STATES } from '../types/buildkite.js';
import { parseScopeError, formatScopeError } from '../utils/scopeError.js';
export class ArtifactsDownload extends BaseCommand {
static requiresToken = true;
async execute(options) {
const outDir = options.out || './';
if (!options.id && !options.path) {
logger.console(formatError('Either --id <id> or --path <glob> is required', {
suggestions: [
'Use --id to download a specific artifact by its ID',
'Use --path "*.patch" to download artifacts matching a glob pattern',
'Run `bktide artifacts list <build-ref>` to see available artifacts',
],
}));
return 1;
}
const spinner = Progress.spinner('Fetching artifact list...', { format: options.format || 'plain' });
try {
this.token = await BaseCommand.getToken(options);
const ref = parseBuildRef(options.buildRef);
const allArtifacts = await this.restClient.listBuildArtifacts(ref.org, ref.pipeline, ref.number);
spinner.stop();
let targets;
if (options.id) {
targets = allArtifacts.filter(a => a.id === options.id);
if (targets.length === 0) {
logger.console(formatError(`No artifact found with id '${options.id}'`, {
suggestions: [`Run \`bktide artifacts list ${options.buildRef}\` to see available artifact IDs`],
}));
return 1;
}
}
else {
targets = allArtifacts.filter(a => minimatch(a.path, options.path, { matchBase: true }));
if (targets.length === 0) {
logger.console(formatError(`No artifacts match glob '${options.path}'`, {
suggestions: [`Run \`bktide artifacts list ${options.buildRef}\` to see available artifact paths`],
}));
return 1;
}
}
const downloadable = targets.filter(a => DOWNLOADABLE_ARTIFACT_STATES.has(a.state));
const skipped = targets.filter(a => !DOWNLOADABLE_ARTIFACT_STATES.has(a.state));
for (const a of skipped) {
logger.console(SEMANTIC_COLORS.warning(`⚠ Skipping ${a.path} (state: ${a.state})`));
}
if (downloadable.length === 0) {
logger.console(formatError('No downloadable artifacts found (all are expired or deleted)'));
return 1;
}
const results = [];
for (const artifact of downloadable) {
// Sanitize artifact path to prevent traversal outside the output directory
const safePath = path.normalize(artifact.path).replace(/^(\.\.(\/|\\|$))+/, '');
const destPath = path.join(outDir, safePath);
const dlSpinner = Progress.spinner(`Downloading ${artifact.filename}...`, { format: options.format || 'plain' });
try {
await this.restClient.downloadArtifact(artifact, destPath);
dlSpinner.stop();
logger.console(SEMANTIC_COLORS.success(`✓ ${artifact.path} → ${destPath}`));
results.push({ path: destPath, ok: true });
}
catch (err) {
dlSpinner.stop();
const msg = err instanceof Error ? err.message : String(err);
logger.console(SEMANTIC_COLORS.error(`✖ ${artifact.path}: ${msg}`));
results.push({ path: destPath, ok: false, error: msg });
}
}
const failed = results.filter(r => !r.ok);
if (failed.length > 0) {
logger.console(SEMANTIC_COLORS.warning(`\n${failed.length} download(s) failed`));
return 1;
}
logger.console(SEMANTIC_COLORS.muted(`\n${results.length} artifact(s) downloaded to ${outDir}`));
return 0;
}
catch (error) {
spinner.stop();
if (error instanceof Error) {
const parsed = parseScopeError(error.message);
if (parsed.matched) {
const formatted = formatScopeError(parsed.scope);
logger.console(formatError(formatted.message, { suggestions: formatted.suggestions }));
}
else {
logger.console(formatError(error.message, {
suggestions: [
'Check the build reference format',
'Run `bktide token --check` to verify token scopes',
],
}));
}
}
else {
logger.error('Unknown error occurred');
}
return 1;
}
}
}
//# sourceMappingURL=ArtifactsDownload.js.map