Skip to content

Commit

Permalink
Upload and show run detail link for long-running commands like tuist …
Browse files Browse the repository at this point in the history
…xcodebuild (tuist#7303)
  • Loading branch information
fortmarek authored Feb 12, 2025
1 parent 5fe8259 commit 86553e8
Show file tree
Hide file tree
Showing 18 changed files with 316 additions and 124 deletions.
12 changes: 9 additions & 3 deletions Sources/TuistCore/Analytics/CommandEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
public let gitBranch: String?
public let graph: RunGraph?
public let previewId: String?
public let resultBundlePath: AbsolutePath?

public enum Status: Codable, Equatable {
case success, failure(String)
Expand Down Expand Up @@ -49,6 +50,7 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
case gitBranch
case graph
case previewId
case resultBundlePath
}

public init(
Expand All @@ -69,7 +71,8 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
gitRemoteURLOrigin: String?,
gitBranch: String?,
graph: RunGraph?,
previewId: String?
previewId: String?,
resultBundlePath: AbsolutePath?
) {
self.runId = runId
self.name = name
Expand All @@ -89,6 +92,7 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
self.gitBranch = gitBranch
self.graph = graph
self.previewId = previewId
self.resultBundlePath = resultBundlePath
}
}

Expand All @@ -111,7 +115,8 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
gitRemoteURLOrigin: String? = "https://github.com/tuist/tuist",
gitBranch: String? = "main",
graph: RunGraph = RunGraph(name: "Graph", projects: []),
previewId: String? = nil
previewId: String? = nil,
resultBundlePath: AbsolutePath? = nil
) -> CommandEvent {
CommandEvent(
runId: runId,
Expand All @@ -131,7 +136,8 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent {
gitRemoteURLOrigin: gitRemoteURLOrigin,
gitBranch: gitBranch,
graph: graph,
previewId: previewId
previewId: previewId,
resultBundlePath: resultBundlePath
)
}
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/TuistCore/Analytics/RunMetadataStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public actor RunMetadataStorage {
public func update(previewId: String?) {
self.previewId = previewId
}

/// Path to the result bundle that should be uploaded when running commands like `tuist xcodebuild test`
public private(set) var resultBundlePath: AbsolutePath?
public func update(resultBundlePath: AbsolutePath?) {
self.resultBundlePath = resultBundlePath
}
}

private enum RunMetadataStorageContextKey: ServiceContextKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public final class CommandEventFactory {
gitRemoteURLOrigin: gitRemoteURLOrigin,
gitBranch: gitBranch,
graph: graph,
previewId: info.previewId
previewId: info.previewId,
resultBundlePath: info.resultBundlePath
)
return commandEvent
}
Expand Down
45 changes: 33 additions & 12 deletions Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import ArgumentParser
import Foundation
import OpenAPIRuntime
import Path
import ServiceContextModule
import TuistAnalytics
import TuistAsyncQueue
import TuistCache
import TuistCore
import TuistServer
import TuistSupport
import XcodeGraph

Expand All @@ -21,6 +23,7 @@ public struct TrackableCommandInfo {
let binaryCacheItems: [AbsolutePath: [String: CacheItem]]
let selectiveTestingCacheItems: [AbsolutePath: [String: CacheItem]]
let previewId: String?
let resultBundlePath: AbsolutePath?
}

/// A `TrackableCommand` wraps a `ParsableCommand` and reports its execution to an analytics provider
Expand All @@ -31,25 +34,28 @@ public class TrackableCommand {
private let commandEventFactory: CommandEventFactory
private let asyncQueue: AsyncQueuing
private let fileHandler: FileHandling
private let ciChecker: CIChecking

public init(
command: ParsableCommand,
commandArguments: [String],
clock: Clock = WallClock(),
commandEventFactory: CommandEventFactory = CommandEventFactory(),
asyncQueue: AsyncQueuing = AsyncQueue.sharedInstance,
fileHandler: FileHandling = FileHandler.shared
fileHandler: FileHandling = FileHandler.shared,
ciChecker: CIChecking = CIChecker()
) {
self.command = command
self.commandArguments = commandArguments
self.clock = clock
self.commandEventFactory = commandEventFactory
self.asyncQueue = asyncQueue
self.fileHandler = fileHandler
self.ciChecker = ciChecker
}

public func run(
analyticsEnabled: Bool
backend: TuistAnalyticsServerBackend?
) async throws {
let timer = clock.startTimer()
let pathIndex = commandArguments.firstIndex(of: "--path")
Expand All @@ -69,23 +75,25 @@ public class TrackableCommand {
} else {
try command.run()
}
if analyticsEnabled {
if let backend {
try await dispatchCommandEvent(
timer: timer,
status: .success,
runId: runMetadataStorage.runId,
path: path,
runMetadataStorage: runMetadataStorage
runMetadataStorage: runMetadataStorage,
backend: backend
)
}
} catch {
if analyticsEnabled {
if let backend {
try await dispatchCommandEvent(
timer: timer,
status: .failure("\(error)"),
runId: await runMetadataStorage.runId,
path: path,
runMetadataStorage: runMetadataStorage
runMetadataStorage: runMetadataStorage,
backend: backend
)
}
throw error
Expand All @@ -98,7 +106,8 @@ public class TrackableCommand {
status: CommandEvent.Status,
runId: String,
path: AbsolutePath,
runMetadataStorage: RunMetadataStorage
runMetadataStorage: RunMetadataStorage,
backend: TuistAnalyticsServerBackend
) async throws {
let durationInSeconds = timer.stop()
let durationInMs = Int(durationInSeconds * 1000)
Expand All @@ -114,17 +123,29 @@ public class TrackableCommand {
graph: runMetadataStorage.graph,
binaryCacheItems: runMetadataStorage.binaryCacheItems,
selectiveTestingCacheItems: runMetadataStorage.selectiveTestingCacheItems,
previewId: runMetadataStorage.previewId
previewId: runMetadataStorage.previewId,
resultBundlePath: runMetadataStorage.resultBundlePath
)
let commandEvent = try commandEventFactory.make(
from: info,
path: path
)
try asyncQueue.dispatch(event: commandEvent)
if let command = command as? TrackableParsableCommand, command.analyticsRequired {
asyncQueue.wait()
if (command as? TrackableParsableCommand)?.analyticsRequired == true || ciChecker.isCI() {
ServiceContext.current?.logger?.info("Uploading run metadata...")
do {
let serverCommandEvent: ServerCommandEvent = try await backend.send(commandEvent: commandEvent)
ServiceContext.current?.logger?
.info(
"You can view a detailed run report at: \(serverCommandEvent.url.absoluteString)"
)
} catch let error as ClientError {
ServiceContext.current?.logger?
.warning("Failed to upload run metadata: \(String(describing: error.underlyingError))")
} catch {
ServiceContext.current?.logger?.warning("Failed to upload run metadata: \(String(describing: error))")
}
} else {
asyncQueue.waitIfCI()
try asyncQueue.dispatch(event: commandEvent)
}
}

Expand Down
12 changes: 6 additions & 6 deletions Sources/TuistKit/Commands/TuistCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,17 @@ public struct TuistCommand: AsyncParsableCommand {

let config = try await ConfigLoader(warningController: WarningController.shared).loadConfig(path: path)
let url = try ServerURLService().url(configServerURL: config.url)
let analyticsEnabled: Bool
let backend: TuistAnalyticsServerBackend?
if let fullHandle = config.fullHandle {
let backend = TuistAnalyticsServerBackend(
let tuistAnalyticsServerBackend = TuistAnalyticsServerBackend(
fullHandle: fullHandle,
url: url
)
let dispatcher = TuistAnalyticsDispatcher(backend: backend)
let dispatcher = TuistAnalyticsDispatcher(backend: tuistAnalyticsServerBackend)
try TuistAnalytics.bootstrap(dispatcher: dispatcher)
analyticsEnabled = true
backend = tuistAnalyticsServerBackend
} else {
analyticsEnabled = false
backend = nil
}

try await CacheDirectoriesProvider.bootstrap()
Expand Down Expand Up @@ -116,7 +116,7 @@ public struct TuistCommand: AsyncParsableCommand {
commandArguments: processedArguments
)
try await trackableCommand.run(
analyticsEnabled: analyticsEnabled
backend: backend
)
}
} catch {
Expand Down
4 changes: 3 additions & 1 deletion Sources/TuistKit/Commands/XcodeBuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import TuistHasher
import TuistServer
import XcodeGraph

public struct XcodeBuildCommand: AsyncParsableCommand {
public struct XcodeBuildCommand: AsyncParsableCommand, TrackableParsableCommand {
public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory()
public static var selectiveTestingGraphHasher: SelectiveTestingGraphHashing = EmptySelectiveTestingGraphHasher()
public static var selectiveTestingService: SelectiveTestingServicing = EmptySelectiveTestingService()
Expand All @@ -18,6 +18,8 @@ public struct XcodeBuildCommand: AsyncParsableCommand {
)
}

var analyticsRequired: Bool { true }

public init() {}

@Argument(
Expand Down
42 changes: 40 additions & 2 deletions Sources/TuistKit/Services/XcodeBuildService.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import FileSystem
import Foundation
import Path
import ServiceContextModule
import TuistAutomation
Expand Down Expand Up @@ -43,6 +44,8 @@ struct XcodeBuildService {
private let xcodeGraphMapper: XcodeGraphMapping
private let xcodeBuildController: XcodeBuildControlling
private let configLoader: ConfigLoading
private let cacheDirectoriesProvider: CacheDirectoriesProviding
private let uniqueIDGenerator: UniqueIDGenerating
private let cacheStorageFactory: CacheStorageFactorying
private let selectiveTestingGraphHasher: SelectiveTestingGraphHashing
private let selectiveTestingService: SelectiveTestingServicing
Expand All @@ -52,6 +55,8 @@ struct XcodeBuildService {
xcodeGraphMapper: XcodeGraphMapping = XcodeGraphMapper(),
xcodeBuildController: XcodeBuildControlling = XcodeBuildController(),
configLoader: ConfigLoading = ConfigLoader(warningController: WarningController.shared),
cacheDirectoriesProvider: CacheDirectoriesProviding = CacheDirectoriesProvider(),
uniqueIDGenerator: UniqueIDGenerating = UniqueIDGenerator(),
cacheStorageFactory: CacheStorageFactorying,
selectiveTestingGraphHasher: SelectiveTestingGraphHashing,
selectiveTestingService: SelectiveTestingServicing
Expand All @@ -60,6 +65,8 @@ struct XcodeBuildService {
self.xcodeGraphMapper = xcodeGraphMapper
self.xcodeBuildController = xcodeBuildController
self.configLoader = configLoader
self.cacheDirectoriesProvider = cacheDirectoriesProvider
self.uniqueIDGenerator = uniqueIDGenerator
self.cacheStorageFactory = cacheStorageFactory
self.selectiveTestingGraphHasher = selectiveTestingGraphHasher
self.selectiveTestingService = selectiveTestingService
Expand Down Expand Up @@ -92,7 +99,7 @@ struct XcodeBuildService {
throw XcodeBuildServiceError.schemeNotPassed
}
let graph = try await xcodeGraphMapper.map(at: path)
try await ServiceContext.current?.runMetadataStorage?.update(graph: graph)
await ServiceContext.current?.runMetadataStorage?.update(graph: graph)
let graphTraverser = GraphTraverser(graph: graph)
guard let scheme = graphTraverser.schemes().first(where: {
$0.name == schemeName
Expand Down Expand Up @@ -174,7 +181,14 @@ struct XcodeBuildService {
}

try await xcodeBuildController
.run(arguments: passthroughXcodebuildArguments + skipTestingArguments)
.run(
arguments: [
passthroughXcodebuildArguments,
skipTestingArguments,
resultBundlePathArguments(passthroughXcodebuildArguments: passthroughXcodebuildArguments),
]
.flatMap { $0 }
)

try await storeTestableGraphTargets(
testableGraphTargets,
Expand All @@ -189,6 +203,30 @@ struct XcodeBuildService {
)
}

private func resultBundlePathArguments(
passthroughXcodebuildArguments: [String]
) async throws -> [String] {
if let resultBundlePathString = passedValue(
for: "-resultBundlePath",
arguments: passthroughXcodebuildArguments
) {
let currentWorkingDirectory = try await fileSystem.currentWorkingDirectory()
let resultBundlePath = try AbsolutePath(validating: resultBundlePathString, relativeTo: currentWorkingDirectory)
await ServiceContext.current?.runMetadataStorage?.update(
resultBundlePath: resultBundlePath
)
return []
} else {
let resultBundlePath = try cacheDirectoriesProvider
.cacheDirectory(for: .runs)
.appending(components: uniqueIDGenerator.uniqueID())
await ServiceContext.current?.runMetadataStorage?.update(
resultBundlePath: resultBundlePath
)
return ["-resultBundlePath", resultBundlePath.pathString]
}
}

private func updateRunMetadataStorage(
with testableGraphTargets: [GraphTarget],
selectiveTestingHashes: [GraphTarget: String],
Expand Down
29 changes: 15 additions & 14 deletions Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,36 +54,37 @@ public class TuistAnalyticsServerBackend: TuistAnalyticsBackend {
}

public func send(commandEvent: CommandEvent) async throws {
let _: ServerCommandEvent = try await send(commandEvent: commandEvent)
}

public func send(commandEvent: CommandEvent) async throws -> ServerCommandEvent {
let serverCommandEvent = try await createCommandEventService.createCommandEvent(
commandEvent: commandEvent,
projectId: fullHandle,
serverURL: url
)

let runDirectory = try cacheDirectoriesProvider
let runsDirectory = try cacheDirectoriesProvider
.cacheDirectory(for: .runs)
.appending(component: commandEvent.runId)

let resultBundle = runDirectory
let runDirectory = runsDirectory.appending(component: commandEvent.runId)

let resultBundlePath = commandEvent.resultBundlePath ?? runDirectory
.appending(component: "\(Constants.resultBundleName).xcresult")

if try await fileSystem.exists(resultBundle) {
if try await fileSystem.exists(resultBundlePath) {
try await analyticsArtifactUploadService.uploadResultBundle(
resultBundle,
resultBundlePath,
commandEventId: serverCommandEvent.id,
serverURL: url
)
}

if try await fileSystem.exists(runDirectory) {
try await fileSystem.remove(runDirectory)
if resultBundlePath.parentDirectory.commonAncestor(with: runsDirectory) == runsDirectory,
try await fileSystem.exists(resultBundlePath)
{
try await fileSystem.remove(resultBundlePath)
}

if #available(macOS 13.0, *), ciChecker.isCI() {
ServiceContext.current?.logger?
.info(
"You can view a detailed report at: \(serverCommandEvent.url.absoluteString)"
)
}
return serverCommandEvent
}
}
Loading

0 comments on commit 86553e8

Please sign in to comment.