@@ -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+
446680struct BuildsGetCommand : ParsableCommand {
447681 static let configuration = CommandConfiguration (
448682 commandName: " get " ,
0 commit comments