diff --git a/Sources/Services/RunnerManager.swift b/Sources/Services/RunnerManager.swift index 3a6325e..a748d2e 100644 --- a/Sources/Services/RunnerManager.swift +++ b/Sources/Services/RunnerManager.swift @@ -319,6 +319,27 @@ class RunnerManager: ObservableObject { } } + func currentWorkflowJob(for runnerID: UUID) -> WorkflowJobSummary? { + activeWorkflowJobs[runnerID] + } + + func openCurrentWorkflowRun(for runnerID: UUID) { + guard let runURL = Self.currentWorkflowRunURL(from: activeWorkflowJobs[runnerID]) else { + return + } + + NSWorkspace.shared.open(runURL) + } + + nonisolated static func currentWorkflowRunURL(from job: WorkflowJobSummary?) -> URL? { + job?.run.htmlURL + } + + nonisolated static func currentWorkflowDisplayName(from job: WorkflowJobSummary?) -> String? { + guard let job else { return nil } + return job.run.name.isEmpty ? job.name : job.run.name + } + private func refreshUpdateStatusMessage() { if let availableUpdate { updateStatusMessage = "Version \(availableUpdate.latestVersion) is available." diff --git a/Sources/Views/MenuBarView.swift b/Sources/Views/MenuBarView.swift index 8f3f72b..60cf945 100644 --- a/Sources/Views/MenuBarView.swift +++ b/Sources/Views/MenuBarView.swift @@ -268,6 +268,25 @@ struct RunnerRow: View { .font(.caption) .foregroundColor(.secondary) + if runner.status == .running, + runner.busy, + let currentJob = runnerManager.currentWorkflowJob(for: runner.id), + let workflowName = RunnerManager.currentWorkflowDisplayName(from: currentJob) { + Button(action: { + runnerManager.openCurrentWorkflowRun(for: runner.id) + }) { + HStack(spacing: 4) { + Image(systemName: "link") + Text("View \(workflowName)") + .lineLimit(1) + } + .font(.caption2) + .foregroundColor(.accentColor) + } + .buttonStyle(.plain) + .help("Open current GitHub Actions run") + } + HStack(spacing: 4) { // Isolation mode indicator if let mode = runner.isolationMode { @@ -348,6 +367,11 @@ struct RunnerRow: View { Button("Stop") { Task { try? await runnerManager.stopRunner(runner.id) } } + if runner.busy, runnerManager.currentWorkflowJob(for: runner.id) != nil { + Button("Open Current Job") { + runnerManager.openCurrentWorkflowRun(for: runner.id) + } + } } else { Button("Start") { Task { try? await runnerManager.startRunner(runner.id) } diff --git a/Tests/MacRunnerTests/MacRunnerTests.swift b/Tests/MacRunnerTests/MacRunnerTests.swift index 8999edc..744ee22 100644 --- a/Tests/MacRunnerTests/MacRunnerTests.swift +++ b/Tests/MacRunnerTests/MacRunnerTests.swift @@ -318,4 +318,36 @@ final class MacRunnerTests: XCTestCase { XCTAssertEqual(payload.body, "omniaura/mac-runner - Nightly (failure)") XCTAssertEqual(payload.runURL, run.htmlURL) } + + func testRunnerManagerCurrentWorkflowHelpersPreferRunNameAndURL() { + let runURL = URL(string: "https://github.com/omniaura/mac-runner/actions/runs/42")! + let job = WorkflowJobSummary( + id: 8, + name: "test", + status: "in_progress", + conclusion: nil, + runnerName: "runner-1", + run: WorkflowRunSummary(id: 42, name: "Nightly", htmlURL: runURL) + ) + + XCTAssertEqual(RunnerManager.currentWorkflowDisplayName(from: job), "Nightly") + XCTAssertEqual(RunnerManager.currentWorkflowRunURL(from: job), runURL) + } + + func testRunnerManagerCurrentWorkflowHelpersFallBackToJobName() { + let job = WorkflowJobSummary( + id: 9, + name: "integration", + status: "in_progress", + conclusion: nil, + runnerName: "runner-1", + run: WorkflowRunSummary( + id: 43, + name: "", + htmlURL: URL(string: "https://github.com/omniaura/mac-runner/actions/runs/43")! + ) + ) + + XCTAssertEqual(RunnerManager.currentWorkflowDisplayName(from: job), "integration") + } }