Skip to content

Commit 58b010e

Browse files
committed
Add builds running command to show in-progress builds
- Add `builds running` CLI command with --product and --all-profiles flags - Add "Running Builds" option to interactive mode top-level menu - Scans all products/workflows for builds currently in progress - Multi-profile support shows running builds across all configured accounts
1 parent 4d410b1 commit 58b010e

3 files changed

Lines changed: 353 additions & 0 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ xcodecloud
3030
├── builds
3131
│ ├── list --workflow <id> → List build runs for a workflow
3232
│ ├── find <commit-sha> → Find a build by commit SHA
33+
│ ├── running → Show all running builds
3334
│ ├── get <id> → Get details for a build run
3435
│ ├── start <workflow-id> → Start a new build run
3536
│ ├── watch <build-id> → Watch a build until completion
@@ -309,6 +310,15 @@ xcodecloud builds find abc1234
309310
# Narrow the search to a specific product
310311
xcodecloud builds find abc1234 --product <product-id>
311312

313+
# Show all running builds across all products
314+
xcodecloud builds running
315+
316+
# Show running builds for a specific product
317+
xcodecloud builds running --product <product-id>
318+
319+
# Show running builds across all configured profiles
320+
xcodecloud builds running --all-profiles
321+
312322
# Get build details
313323
xcodecloud builds get <build-id>
314324

@@ -442,6 +452,8 @@ List commands support client-side filtering. Filters are applied after fetching
442452
| `builds list` | `--status <status>` | Filter by completion status: `SUCCEEDED`, `FAILED`, `ERRORED`, `CANCELED`, `SKIPPED` |
443453
| `builds list` | `--running` | Show only builds currently in progress |
444454
| `builds list` | `--commit <sha>` | Filter by commit SHA prefix |
455+
| `builds running` | `--product <id>` | Narrow to a specific product |
456+
| `builds running` | `--all-profiles` | Check all configured profiles |
445457

446458
Filters can be combined:
447459

Sources/XcodeCloudCLI/Commands/Builds/BuildsCommand.swift

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ struct BuildsCommand: ParsableCommand {
3333
Find a build by commit SHA:
3434
$ xcodecloud builds find abc1234
3535
36+
Show all running builds:
37+
$ xcodecloud builds running
38+
3639
Show build errors:
3740
$ xcodecloud builds errors <build-id>
3841
@@ -42,6 +45,7 @@ struct BuildsCommand: ParsableCommand {
4245
subcommands: [
4346
BuildsListCommand.self,
4447
BuildsFindCommand.self,
48+
BuildsRunningCommand.self,
4549
BuildsGetCommand.self,
4650
BuildsStartCommand.self,
4751
BuildsWatchCommand.self,
@@ -443,6 +447,236 @@ struct BuildsFindCommand: ParsableCommand {
443447
}
444448
}
445449

450+
struct BuildsRunningCommand: ParsableCommand {
451+
static let configuration = CommandConfiguration(
452+
commandName: "running",
453+
abstract: "Show all running builds",
454+
discussion: """
455+
Shows all builds currently in progress across all products and workflows.
456+
457+
Use --product to narrow the search to a specific product.
458+
Use --all-profiles to check all configured profiles.
459+
460+
EXAMPLES
461+
Show all running builds:
462+
$ xcodecloud builds running
463+
464+
Narrow to a specific product:
465+
$ xcodecloud builds running --product <product-id>
466+
467+
Check all profiles:
468+
$ xcodecloud builds running --all-profiles
469+
470+
JSON output for scripting:
471+
$ xcodecloud builds running -o json
472+
"""
473+
)
474+
475+
@OptionGroup var options: GlobalOptions
476+
477+
@Option(name: .long, help: "Narrow search to a specific product ID")
478+
var product: String?
479+
480+
@Flag(name: .customLong("all-profiles"), help: "Check all configured profiles")
481+
var allProfiles: Bool = false
482+
483+
struct RunningBuild: Codable {
484+
let profile: String?
485+
let productId: String
486+
let productName: String
487+
let workflowId: String
488+
let workflowName: String
489+
let buildId: String
490+
let buildNumber: Int?
491+
let status: String
492+
let commit: String?
493+
let startedAt: String?
494+
}
495+
496+
mutating func run() throws {
497+
let verbose = options.verbose
498+
let quiet = options.quiet
499+
500+
var allRunningBuilds: [RunningBuild] = []
501+
502+
// Determine which profiles to check
503+
let profilesToCheck: [(name: String?, credentials: Credentials)]
504+
505+
if allProfiles {
506+
let resolver = CredentialResolver()
507+
var profiles: [(String?, Credentials)] = []
508+
509+
// Load from global config
510+
if let config = try resolver.loadConfig(from: CredentialResolver.globalConfigPath) {
511+
for (name, profile) in config.profiles {
512+
if let creds = try? profile.toCredentials() {
513+
profiles.append((name, creds))
514+
}
515+
}
516+
}
517+
518+
// Load from local config (may override)
519+
if let config = try resolver.loadConfig(from: CredentialResolver.localConfigPath) {
520+
for (name, profile) in config.profiles {
521+
if let creds = try? profile.toCredentials() {
522+
// Add if not already present
523+
if !profiles.contains(where: { $0.0 == name }) {
524+
profiles.append((name, creds))
525+
}
526+
}
527+
}
528+
}
529+
530+
if profiles.isEmpty {
531+
printError("No profiles found in config")
532+
throw ExitCode.failure
533+
}
534+
535+
profilesToCheck = profiles
536+
} else {
537+
// Just use the current profile
538+
let resolver = CredentialResolver()
539+
let credOptions = CredentialOptions(
540+
keyId: options.keyId,
541+
issuerId: options.issuerId,
542+
privateKeyPath: options.privateKeyPath,
543+
privateKey: options.privateKey,
544+
profile: options.profile
545+
)
546+
let creds: Credentials
547+
do {
548+
creds = try resolver.resolve(options: credOptions)
549+
} catch let error as CLIError {
550+
printError(error.localizedDescription)
551+
throw ExitCode(rawValue: error.exitCode)
552+
}
553+
profilesToCheck = [(options.profile, creds)]
554+
}
555+
556+
for (profileName, credentials) in profilesToCheck {
557+
let client = APIClient(credentials: credentials)
558+
let profileLabel = profileName ?? "default"
559+
560+
if allProfiles && !quiet {
561+
printVerbose("Checking profile '\(profileLabel)'...", verbose: verbose)
562+
}
563+
564+
do {
565+
let products: [CiProduct]
566+
if let productId = product {
567+
printVerbose("Fetching product \(productId)...", verbose: verbose)
568+
let response = try runAsync {
569+
try await client.getProduct(id: productId)
570+
}
571+
products = [response.data]
572+
} else {
573+
printVerbose("Fetching all products...", verbose: verbose)
574+
let response = try runAsync {
575+
try await client.listAllProducts()
576+
}
577+
products = response.data
578+
}
579+
580+
for product in products {
581+
let productName = product.attributes?.name ?? product.id
582+
printVerbose("Checking workflows for \(productName)...", verbose: verbose)
583+
584+
let workflowsResponse = try runAsync {
585+
try await client.listAllWorkflows(productId: product.id)
586+
}
587+
588+
for workflow in workflowsResponse.data {
589+
let workflowName = workflow.attributes?.name ?? workflow.id
590+
591+
let buildsResponse = try runAsync {
592+
try await client.listBuildRuns(workflowId: workflow.id, limit: 10)
593+
}
594+
595+
let running = buildsResponse.data.filter {
596+
$0.attributes?.executionProgress != "COMPLETE"
597+
}
598+
599+
for build in running {
600+
let attrs = build.attributes
601+
allRunningBuilds.append(RunningBuild(
602+
profile: allProfiles ? profileLabel : nil,
603+
productId: product.id,
604+
productName: productName,
605+
workflowId: workflow.id,
606+
workflowName: workflowName,
607+
buildId: build.id,
608+
buildNumber: attrs?.number,
609+
status: attrs?.executionProgress ?? "UNKNOWN",
610+
commit: attrs?.sourceCommit?.commitSha.map { String($0.prefix(7)) },
611+
startedAt: attrs?.startedDate
612+
))
613+
}
614+
}
615+
}
616+
} catch let error as CLIError {
617+
if allProfiles {
618+
// Continue with other profiles on error
619+
printError("Profile '\(profileLabel)': \(error.localizedDescription)")
620+
} else {
621+
printError(error.localizedDescription)
622+
throw ExitCode(rawValue: error.exitCode)
623+
}
624+
}
625+
}
626+
627+
let formatter = options.outputFormatter()
628+
629+
if options.output == .json {
630+
let output = try formatter.formatRawJSON(allRunningBuilds)
631+
print(output)
632+
} else {
633+
if allRunningBuilds.isEmpty {
634+
if !quiet {
635+
print("No running builds")
636+
}
637+
} else {
638+
// Table output
639+
if allProfiles {
640+
print(String(format: "%-12s %-20s %-20s %-8s %-10s %-8s %-20s",
641+
"PROFILE", "PRODUCT", "WORKFLOW", "BUILD", "STATUS", "COMMIT", "STARTED"))
642+
} else {
643+
print(String(format: "%-20s %-20s %-8s %-10s %-8s %-20s",
644+
"PRODUCT", "WORKFLOW", "BUILD", "STATUS", "COMMIT", "STARTED"))
645+
}
646+
647+
for build in allRunningBuilds {
648+
let buildNum = build.buildNumber.map { "#\($0)" } ?? build.buildId
649+
let started = build.startedAt.flatMap { formatDate($0) } ?? "-"
650+
651+
if allProfiles {
652+
print(String(format: "%-12s %-20s %-20s %-8s %-10s %-8s %-20s",
653+
build.profile ?? "-",
654+
String(build.productName.prefix(20)),
655+
String(build.workflowName.prefix(20)),
656+
String(buildNum.prefix(8)),
657+
build.status,
658+
build.commit ?? "-",
659+
started))
660+
} else {
661+
print(String(format: "%-20s %-20s %-8s %-10s %-8s %-20s",
662+
String(build.productName.prefix(20)),
663+
String(build.workflowName.prefix(20)),
664+
String(buildNum.prefix(8)),
665+
build.status,
666+
build.commit ?? "-",
667+
started))
668+
}
669+
}
670+
671+
if !quiet {
672+
print("")
673+
print("\(allRunningBuilds.count) running build(s)")
674+
}
675+
}
676+
}
677+
}
678+
}
679+
446680
struct BuildsGetCommand: ParsableCommand {
447681
static let configuration = CommandConfiguration(
448682
commandName: "get",

0 commit comments

Comments
 (0)