Skip to content

Commit a2031fe

Browse files
committed
releaser: Trigger and list app builds
1 parent 810e221 commit a2031fe

File tree

4 files changed

+394
-10
lines changed

4 files changed

+394
-10
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import chalk from 'chalk';
2+
import inquirer from 'inquirer';
3+
import { triggerBuildWorkflows } from '../utils/github.js';
4+
5+
interface BuildOptions {
6+
platform?: string;
7+
force?: boolean;
8+
}
9+
10+
const VALID_PLATFORMS = ['all', 'windows', 'mac', 'linux'];
11+
12+
export async function buildArtifacts(gitRef: string, options: BuildOptions): Promise<void> {
13+
const platform = options.platform || 'all';
14+
15+
// Validate platform
16+
if (!VALID_PLATFORMS.includes(platform)) {
17+
console.error(chalk.red(`Error: Invalid platform "${platform}". Valid options are: ${VALID_PLATFORMS.join(', ')}`));
18+
process.exit(1);
19+
}
20+
21+
console.log(chalk.blue(`Triggering build artifacts for platform(s): ${platform}`));
22+
console.log(chalk.blue(`Using git ref: ${gitRef}`));
23+
24+
try {
25+
// Confirm unless --force is used
26+
if (!options.force) {
27+
const { confirmed } = await inquirer.prompt([
28+
{
29+
type: 'confirm',
30+
name: 'confirmed',
31+
message: chalk.yellow(`Are you sure you want to trigger build workflows for ${platform} using ref "${gitRef}"?`),
32+
default: false
33+
}
34+
]);
35+
36+
if (!confirmed) {
37+
console.log(chalk.yellow('Build trigger cancelled'));
38+
return;
39+
}
40+
}
41+
42+
// Trigger the workflows
43+
const runs = await triggerBuildWorkflows(gitRef, platform);
44+
45+
console.log(chalk.green(`\n✅ Successfully triggered build workflow(s) for ${platform}`));
46+
47+
if (runs.length > 0) {
48+
console.log(chalk.blue('\nTriggered workflow runs:'));
49+
runs.forEach(run => {
50+
console.log(chalk.cyan(` • ${run.name}: ${run.url}`));
51+
});
52+
}
53+
54+
console.log(chalk.blue('\nYou can monitor all workflows at:'));
55+
console.log(chalk.cyan('https://github.com/kubernetes-sigs/headlamp/actions'));
56+
} catch (error) {
57+
console.error(chalk.red('Error triggering build workflows:'));
58+
console.error(error);
59+
process.exit(1);
60+
}
61+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import chalk from 'chalk';
2+
import { getLatestAppRuns } from '../utils/github.js';
3+
4+
interface GetAppRunsOptions {
5+
latest?: number;
6+
platform?: string;
7+
output?: string;
8+
}
9+
10+
const VALID_PLATFORMS = ['all', 'windows', 'mac', 'linux'];
11+
const VALID_OUTPUT_FORMATS = ['simple', 'json'];
12+
13+
export async function getAppRuns(options: GetAppRunsOptions): Promise<void> {
14+
const limit = options.latest || 1;
15+
const platform = options.platform || 'all';
16+
const output = options.output;
17+
18+
// Validate platform
19+
if (!VALID_PLATFORMS.includes(platform)) {
20+
console.error(chalk.red(`Error: Invalid platform "${platform}". Valid options are: ${VALID_PLATFORMS.join(', ')}`));
21+
process.exit(1);
22+
}
23+
24+
// Validate output format
25+
if (output && !VALID_OUTPUT_FORMATS.includes(output)) {
26+
console.error(chalk.red(`Error: Invalid output format "${output}". Valid options are: ${VALID_OUTPUT_FORMATS.join(', ')}`));
27+
process.exit(1);
28+
}
29+
30+
if (output !== 'json') {
31+
const platformDesc = platform === 'all' ? 'each platform' : platform;
32+
console.log(chalk.blue(`Fetching latest ${limit} app build run${limit > 1 ? 's' : ''} for ${platformDesc}...\n`));
33+
}
34+
35+
try {
36+
const runs = await getLatestAppRuns(limit, platform);
37+
38+
if (runs.length === 0) {
39+
if (output === 'json') {
40+
console.log(JSON.stringify([], null, 2));
41+
} else {
42+
console.log(chalk.yellow('No workflow runs found'));
43+
}
44+
return;
45+
}
46+
47+
// JSON output
48+
if (output === 'json') {
49+
console.log(JSON.stringify(runs, null, 2));
50+
return;
51+
}
52+
53+
// Simple output - just platform name and run URL
54+
if (output === 'simple') {
55+
runs.forEach((workflowRuns) => {
56+
workflowRuns.runs.forEach((run) => {
57+
console.log(`${workflowRuns.workflowName}: ${run.url}`);
58+
});
59+
});
60+
return;
61+
}
62+
63+
// Default detailed output
64+
runs.forEach((workflowRuns, index) => {
65+
if (index > 0) {
66+
console.log(''); // Add spacing between workflows
67+
}
68+
69+
console.log(chalk.bold.cyan(`${workflowRuns.workflowName}:`));
70+
console.log(chalk.dim('─'.repeat(60)));
71+
72+
workflowRuns.runs.forEach((run, runIndex) => {
73+
const statusIcon = run.status === 'completed'
74+
? (run.conclusion === 'success' ? '✅' : run.conclusion === 'failure' ? '❌' : '⚠️')
75+
: '🔄';
76+
77+
const statusColor = run.status === 'completed'
78+
? (run.conclusion === 'success' ? chalk.green : run.conclusion === 'failure' ? chalk.red : chalk.yellow)
79+
: chalk.blue;
80+
81+
console.log(`\n${runIndex + 1}. ${statusIcon} ${statusColor(run.status.toUpperCase())}${run.conclusion ? ` (${run.conclusion})` : ''}`);
82+
console.log(chalk.dim(` Run ID: ${run.id}`));
83+
console.log(chalk.dim(` Branch: ${run.headBranch}`));
84+
console.log(chalk.dim(` Commit: ${run.headSha.substring(0, 7)}`));
85+
console.log(chalk.dim(` Created: ${new Date(run.createdAt).toLocaleString()}`));
86+
console.log(chalk.cyan(` URL: ${run.url}`));
87+
88+
if (run.artifacts.length > 0) {
89+
console.log(chalk.green(` Artifacts (${run.artifacts.length}):`));
90+
run.artifacts.forEach(artifact => {
91+
console.log(chalk.dim(` • ${artifact.name} (${formatBytes(artifact.size)})`));
92+
console.log(chalk.dim(` Download: ${artifact.downloadUrl}`));
93+
});
94+
} else if (run.status === 'completed' && run.conclusion === 'success') {
95+
console.log(chalk.yellow(` No artifacts available`));
96+
}
97+
});
98+
});
99+
100+
console.log('\n' + chalk.dim('─'.repeat(60)));
101+
console.log(chalk.blue('\nView all runs at:'));
102+
console.log(chalk.cyan('https://github.com/kubernetes-sigs/headlamp/actions'));
103+
} catch (error) {
104+
if (output === 'json') {
105+
console.error(JSON.stringify({ error: String(error) }, null, 2));
106+
} else {
107+
console.error(chalk.red('Error fetching app runs:'));
108+
console.error(error);
109+
}
110+
process.exit(1);
111+
}
112+
}
113+
114+
function formatBytes(bytes: number): string {
115+
if (bytes === 0) return '0 Bytes';
116+
const k = 1024;
117+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
118+
const i = Math.floor(Math.log(bytes) / Math.log(k));
119+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
120+
}

tools/releaser/src/index.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { startRelease } from './commands/start.js';
99
import { tagRelease } from './commands/tag.js';
1010
import { publishRelease } from './commands/publish.js';
1111
import { buildArtifacts } from './commands/build.js';
12+
import { getAppRuns } from './commands/get-app-runs.js';
1213

1314
const __filename = fileURLToPath(import.meta.url);
1415
const __dirname = dirname(__filename);
@@ -45,11 +46,56 @@ program.command('publish')
4546
.option('--force', 'Skip confirmation prompt')
4647
.action(publishRelease);
4748

48-
program.command('ci-build-app')
49-
.description('Trigger build artifact workflows for Windows, Mac, and/or Linux')
50-
.argument('<git-ref>', 'Git ref/branch/tag to build from (e.g., main, v0.30.0)')
51-
.option('-p, --platform <platform>', 'Platform to build: all, windows, mac, or linux', 'all')
52-
.option('--force', 'Skip confirmation prompt')
53-
.action(buildArtifacts);
49+
// CI command with subcommands
50+
const ci = program.command('ci')
51+
.description('CI-related commands');
52+
53+
ci.command('app')
54+
.description('Manage app build workflows')
55+
.option('--build <git-ref>', 'Trigger build artifact workflows for the specified git ref/branch/tag (e.g., main, v0.30.0)')
56+
.option('--list', 'List the latest app build workflow runs')
57+
.option('-p, --platform <platform>', 'Platform filter: all, windows, mac, or linux', 'all')
58+
.option('--latest <number>', 'Number of recent runs to fetch when listing', '1')
59+
.option('-o, --output <format>', 'Output format when listing: simple or json')
60+
.option('--force', 'Skip confirmation prompt when building')
61+
.action(async (options) => {
62+
// Check if both --build and --list are provided
63+
if (options.build && options.list) {
64+
console.error('Error: Cannot use both --build and --list options together');
65+
process.exit(1);
66+
}
67+
68+
// Check if neither --build nor --list are provided
69+
if (!options.build && !options.list) {
70+
console.error('Error: Must specify either --build <git-ref> or --list');
71+
process.exit(1);
72+
}
73+
74+
if (options.build) {
75+
// Build artifacts
76+
await buildArtifacts(options.build, {
77+
platform: options.platform,
78+
force: options.force,
79+
});
80+
} else if (options.list) {
81+
// List app runs
82+
const latestNum = parseInt(options.latest, 10);
83+
if (
84+
isNaN(latestNum) ||
85+
!Number.isInteger(latestNum) ||
86+
latestNum <= 0
87+
) {
88+
console.error(
89+
`Error: --latest must be a valid positive integer (got "${options.latest}")`
90+
);
91+
process.exit(1);
92+
}
93+
await getAppRuns({
94+
platform: options.platform,
95+
latest: latestNum,
96+
output: options.output,
97+
});
98+
}
99+
});
54100

55101
program.parse();

0 commit comments

Comments
 (0)