653 lines
23 KiB
C#
653 lines
23 KiB
C#
// -----------------------------------------------------------------------------
|
|
// TaskRunnerCommandGroup.cs
|
|
// Sprint: SPRINT_20260117_021_CLI_taskrunner
|
|
// Tasks: TRN-001 through TRN-005 - TaskRunner management commands
|
|
// Description: CLI commands for TaskRunner service operations
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System.CommandLine;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Cli.Commands;
|
|
|
|
/// <summary>
|
|
/// Command group for TaskRunner operations.
|
|
/// Implements status, tasks, artifacts, and logs commands.
|
|
/// </summary>
|
|
public static class TaskRunnerCommandGroup
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
WriteIndented = true,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
/// <summary>
|
|
/// Build the 'taskrunner' command group.
|
|
/// </summary>
|
|
public static Command BuildTaskRunnerCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var taskrunnerCommand = new Command("taskrunner", "TaskRunner service operations");
|
|
|
|
taskrunnerCommand.Add(BuildStatusCommand(verboseOption, cancellationToken));
|
|
taskrunnerCommand.Add(BuildTasksCommand(verboseOption, cancellationToken));
|
|
taskrunnerCommand.Add(BuildArtifactsCommand(verboseOption, cancellationToken));
|
|
taskrunnerCommand.Add(BuildLogsCommand(verboseOption, cancellationToken));
|
|
|
|
return taskrunnerCommand;
|
|
}
|
|
|
|
#region TRN-001 - Status Command
|
|
|
|
private static Command BuildStatusCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var formatOption = new Option<string>("--format", ["-f"])
|
|
{
|
|
Description = "Output format: table (default), json"
|
|
};
|
|
formatOption.SetDefaultValue("table");
|
|
|
|
var statusCommand = new Command("status", "Show TaskRunner service status")
|
|
{
|
|
formatOption,
|
|
verboseOption
|
|
};
|
|
|
|
statusCommand.SetAction((parseResult, ct) =>
|
|
{
|
|
var format = parseResult.GetValue(formatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
var status = new TaskRunnerStatus
|
|
{
|
|
Health = "healthy",
|
|
Version = "2.1.0",
|
|
Uptime = TimeSpan.FromDays(12).Add(TimeSpan.FromHours(5)),
|
|
Workers = new WorkerPoolStatus
|
|
{
|
|
Total = 8,
|
|
Active = 3,
|
|
Idle = 5,
|
|
MaxCapacity = 16
|
|
},
|
|
Queue = new QueueStatus
|
|
{
|
|
Pending = 12,
|
|
Running = 3,
|
|
Completed24h = 847,
|
|
Failed24h = 3
|
|
}
|
|
};
|
|
|
|
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(status, JsonOptions));
|
|
return Task.FromResult(0);
|
|
}
|
|
|
|
Console.WriteLine("TaskRunner Status");
|
|
Console.WriteLine("=================");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Health: {status.Health}");
|
|
Console.WriteLine($"Version: {status.Version}");
|
|
Console.WriteLine($"Uptime: {status.Uptime.Days}d {status.Uptime.Hours}h");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Worker Pool:");
|
|
Console.WriteLine($" Total: {status.Workers.Total}");
|
|
Console.WriteLine($" Active: {status.Workers.Active}");
|
|
Console.WriteLine($" Idle: {status.Workers.Idle}");
|
|
Console.WriteLine($" Capacity: {status.Workers.MaxCapacity}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Queue:");
|
|
Console.WriteLine($" Pending: {status.Queue.Pending}");
|
|
Console.WriteLine($" Running: {status.Queue.Running}");
|
|
Console.WriteLine($" Completed/24h: {status.Queue.Completed24h}");
|
|
Console.WriteLine($" Failed/24h: {status.Queue.Failed24h}");
|
|
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
return statusCommand;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region TRN-002/TRN-003 - Tasks Commands
|
|
|
|
private static Command BuildTasksCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var tasksCommand = new Command("tasks", "Task operations");
|
|
|
|
tasksCommand.Add(BuildTasksListCommand(verboseOption));
|
|
tasksCommand.Add(BuildTasksShowCommand(verboseOption));
|
|
tasksCommand.Add(BuildTasksCancelCommand(verboseOption));
|
|
|
|
return tasksCommand;
|
|
}
|
|
|
|
private static Command BuildTasksListCommand(Option<bool> verboseOption)
|
|
{
|
|
var statusOption = new Option<string?>("--status", ["-s"])
|
|
{
|
|
Description = "Filter by status: pending, running, completed, failed"
|
|
};
|
|
|
|
var typeOption = new Option<string?>("--type", ["-t"])
|
|
{
|
|
Description = "Filter by task type"
|
|
};
|
|
|
|
var fromOption = new Option<string?>("--from")
|
|
{
|
|
Description = "Start time filter"
|
|
};
|
|
|
|
var toOption = new Option<string?>("--to")
|
|
{
|
|
Description = "End time filter"
|
|
};
|
|
|
|
var limitOption = new Option<int>("--limit", ["-n"])
|
|
{
|
|
Description = "Maximum number of tasks to show"
|
|
};
|
|
limitOption.SetDefaultValue(20);
|
|
|
|
var formatOption = new Option<string>("--format", ["-f"])
|
|
{
|
|
Description = "Output format: table (default), json"
|
|
};
|
|
formatOption.SetDefaultValue("table");
|
|
|
|
var listCommand = new Command("list", "List tasks")
|
|
{
|
|
statusOption,
|
|
typeOption,
|
|
fromOption,
|
|
toOption,
|
|
limitOption,
|
|
formatOption,
|
|
verboseOption
|
|
};
|
|
|
|
listCommand.SetAction((parseResult, ct) =>
|
|
{
|
|
var status = parseResult.GetValue(statusOption);
|
|
var type = parseResult.GetValue(typeOption);
|
|
var from = parseResult.GetValue(fromOption);
|
|
var to = parseResult.GetValue(toOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var format = parseResult.GetValue(formatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
var tasks = GetSampleTasks()
|
|
.Where(t => string.IsNullOrEmpty(status) || t.Status.Equals(status, StringComparison.OrdinalIgnoreCase))
|
|
.Where(t => string.IsNullOrEmpty(type) || t.Type.Equals(type, StringComparison.OrdinalIgnoreCase))
|
|
.Take(limit)
|
|
.ToList();
|
|
|
|
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(tasks, JsonOptions));
|
|
return Task.FromResult(0);
|
|
}
|
|
|
|
Console.WriteLine("Tasks");
|
|
Console.WriteLine("=====");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"{"ID",-20} {"Type",-15} {"Status",-12} {"Duration",-10} {"Started"}");
|
|
Console.WriteLine(new string('-', 75));
|
|
|
|
foreach (var task in tasks)
|
|
{
|
|
var duration = task.Duration.HasValue ? $"{task.Duration.Value.TotalSeconds:F0}s" : "-";
|
|
Console.WriteLine($"{task.Id,-20} {task.Type,-15} {task.Status,-12} {duration,-10} {task.StartedAt:HH:mm:ss}");
|
|
}
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Total: {tasks.Count} tasks");
|
|
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
return listCommand;
|
|
}
|
|
|
|
private static Command BuildTasksShowCommand(Option<bool> verboseOption)
|
|
{
|
|
var taskIdArg = new Argument<string>("task-id")
|
|
{
|
|
Description = "Task ID to show"
|
|
};
|
|
|
|
var formatOption = new Option<string>("--format", ["-f"])
|
|
{
|
|
Description = "Output format: text (default), json"
|
|
};
|
|
formatOption.SetDefaultValue("text");
|
|
|
|
var showCommand = new Command("show", "Show task details")
|
|
{
|
|
taskIdArg,
|
|
formatOption,
|
|
verboseOption
|
|
};
|
|
|
|
showCommand.SetAction((parseResult, ct) =>
|
|
{
|
|
var taskId = parseResult.GetValue(taskIdArg) ?? string.Empty;
|
|
var format = parseResult.GetValue(formatOption) ?? "text";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
var task = new TaskDetails
|
|
{
|
|
Id = taskId,
|
|
Type = "scan",
|
|
Status = "completed",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
CompletedAt = DateTimeOffset.UtcNow.AddMinutes(-2),
|
|
Duration = TimeSpan.FromMinutes(3),
|
|
Input = new { Image = "myapp:v1.2.3", ScanType = "full" },
|
|
Steps = [
|
|
new TaskStep { Name = "pull-image", Status = "completed", Duration = TimeSpan.FromSeconds(15) },
|
|
new TaskStep { Name = "generate-sbom", Status = "completed", Duration = TimeSpan.FromSeconds(45) },
|
|
new TaskStep { Name = "vuln-scan", Status = "completed", Duration = TimeSpan.FromMinutes(2) },
|
|
new TaskStep { Name = "upload-results", Status = "completed", Duration = TimeSpan.FromSeconds(5) }
|
|
],
|
|
Artifacts = ["sbom.json", "vulns.json", "scan-report.html"]
|
|
};
|
|
|
|
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(task, JsonOptions));
|
|
return Task.FromResult(0);
|
|
}
|
|
|
|
Console.WriteLine($"Task Details: {taskId}");
|
|
Console.WriteLine(new string('=', 15 + taskId.Length));
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Type: {task.Type}");
|
|
Console.WriteLine($"Status: {task.Status}");
|
|
Console.WriteLine($"Started: {task.StartedAt:u}");
|
|
Console.WriteLine($"Completed: {task.CompletedAt:u}");
|
|
Console.WriteLine($"Duration: {task.Duration?.TotalMinutes:F1} minutes");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Steps:");
|
|
foreach (var step in task.Steps)
|
|
{
|
|
var icon = step.Status == "completed" ? "✓" : step.Status == "running" ? "▶" : "○";
|
|
Console.WriteLine($" {icon} {step.Name}: {step.Duration?.TotalSeconds:F0}s");
|
|
}
|
|
Console.WriteLine();
|
|
Console.WriteLine("Artifacts:");
|
|
foreach (var artifact in task.Artifacts)
|
|
{
|
|
Console.WriteLine($" • {artifact}");
|
|
}
|
|
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
return showCommand;
|
|
}
|
|
|
|
private static Command BuildTasksCancelCommand(Option<bool> verboseOption)
|
|
{
|
|
var taskIdArg = new Argument<string>("task-id")
|
|
{
|
|
Description = "Task ID to cancel"
|
|
};
|
|
|
|
var gracefulOption = new Option<bool>("--graceful")
|
|
{
|
|
Description = "Graceful shutdown (wait for current step)"
|
|
};
|
|
|
|
var forceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Force immediate termination"
|
|
};
|
|
|
|
var cancelCommand = new Command("cancel", "Cancel a task")
|
|
{
|
|
taskIdArg,
|
|
gracefulOption,
|
|
forceOption,
|
|
verboseOption
|
|
};
|
|
|
|
cancelCommand.SetAction((parseResult, ct) =>
|
|
{
|
|
var taskId = parseResult.GetValue(taskIdArg) ?? string.Empty;
|
|
var graceful = parseResult.GetValue(gracefulOption);
|
|
var force = parseResult.GetValue(forceOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
Console.WriteLine("Task Cancellation");
|
|
Console.WriteLine("=================");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Task ID: {taskId}");
|
|
Console.WriteLine($"Mode: {(force ? "force" : graceful ? "graceful" : "default")}");
|
|
Console.WriteLine();
|
|
|
|
if (force)
|
|
{
|
|
Console.WriteLine("Task terminated immediately.");
|
|
}
|
|
else if (graceful)
|
|
{
|
|
Console.WriteLine("Waiting for current step to complete...");
|
|
Console.WriteLine("Task cancelled gracefully.");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Task cancellation requested.");
|
|
}
|
|
|
|
Console.WriteLine($"Final Status: cancelled");
|
|
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
return cancelCommand;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region TRN-004 - Artifacts Commands
|
|
|
|
private static Command BuildArtifactsCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var artifactsCommand = new Command("artifacts", "Task artifact operations");
|
|
|
|
artifactsCommand.Add(BuildArtifactsListCommand(verboseOption));
|
|
artifactsCommand.Add(BuildArtifactsGetCommand(verboseOption));
|
|
|
|
return artifactsCommand;
|
|
}
|
|
|
|
private static Command BuildArtifactsListCommand(Option<bool> verboseOption)
|
|
{
|
|
var taskOption = new Option<string>("--task", ["-t"])
|
|
{
|
|
Description = "Task ID to list artifacts for",
|
|
Required = true
|
|
};
|
|
|
|
var formatOption = new Option<string>("--format", ["-f"])
|
|
{
|
|
Description = "Output format: table (default), json"
|
|
};
|
|
formatOption.SetDefaultValue("table");
|
|
|
|
var listCommand = new Command("list", "List task artifacts")
|
|
{
|
|
taskOption,
|
|
formatOption,
|
|
verboseOption
|
|
};
|
|
|
|
listCommand.SetAction((parseResult, ct) =>
|
|
{
|
|
var taskId = parseResult.GetValue(taskOption) ?? string.Empty;
|
|
var format = parseResult.GetValue(formatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
var artifacts = new List<ArtifactInfo>
|
|
{
|
|
new() { Id = "art-001", Name = "sbom.json", Type = "application/json", Size = "245 KB", Digest = "sha256:abc123..." },
|
|
new() { Id = "art-002", Name = "vulns.json", Type = "application/json", Size = "128 KB", Digest = "sha256:def456..." },
|
|
new() { Id = "art-003", Name = "scan-report.html", Type = "text/html", Size = "89 KB", Digest = "sha256:ghi789..." }
|
|
};
|
|
|
|
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(artifacts, JsonOptions));
|
|
return Task.FromResult(0);
|
|
}
|
|
|
|
Console.WriteLine($"Artifacts for Task: {taskId}");
|
|
Console.WriteLine(new string('=', 20 + taskId.Length));
|
|
Console.WriteLine();
|
|
Console.WriteLine($"{"ID",-12} {"Name",-25} {"Type",-20} {"Size",-10} {"Digest"}");
|
|
Console.WriteLine(new string('-', 85));
|
|
|
|
foreach (var artifact in artifacts)
|
|
{
|
|
Console.WriteLine($"{artifact.Id,-12} {artifact.Name,-25} {artifact.Type,-20} {artifact.Size,-10} {artifact.Digest}");
|
|
}
|
|
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
return listCommand;
|
|
}
|
|
|
|
private static Command BuildArtifactsGetCommand(Option<bool> verboseOption)
|
|
{
|
|
var artifactIdArg = new Argument<string>("artifact-id")
|
|
{
|
|
Description = "Artifact ID to download"
|
|
};
|
|
|
|
var outputOption = new Option<string?>("--output", ["-o"])
|
|
{
|
|
Description = "Output file path"
|
|
};
|
|
|
|
var getCommand = new Command("get", "Download an artifact")
|
|
{
|
|
artifactIdArg,
|
|
outputOption,
|
|
verboseOption
|
|
};
|
|
|
|
getCommand.SetAction((parseResult, ct) =>
|
|
{
|
|
var artifactId = parseResult.GetValue(artifactIdArg) ?? string.Empty;
|
|
var output = parseResult.GetValue(outputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
var outputPath = output ?? $"{artifactId}.bin";
|
|
|
|
Console.WriteLine("Downloading Artifact");
|
|
Console.WriteLine("====================");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Artifact ID: {artifactId}");
|
|
Console.WriteLine($"Output: {outputPath}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Downloading... done");
|
|
Console.WriteLine("Verifying digest... ✓ verified");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Artifact saved to: {outputPath}");
|
|
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
return getCommand;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region TRN-005 - Logs Command
|
|
|
|
private static Command BuildLogsCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var taskIdArg = new Argument<string>("task-id")
|
|
{
|
|
Description = "Task ID to show logs for"
|
|
};
|
|
|
|
var followOption = new Option<bool>("--follow", ["-f"])
|
|
{
|
|
Description = "Stream logs continuously"
|
|
};
|
|
|
|
var stepOption = new Option<string?>("--step", ["-s"])
|
|
{
|
|
Description = "Filter by step name"
|
|
};
|
|
|
|
var levelOption = new Option<string?>("--level", ["-l"])
|
|
{
|
|
Description = "Filter by log level: error, warn, info, debug"
|
|
};
|
|
|
|
var outputOption = new Option<string?>("--output", ["-o"])
|
|
{
|
|
Description = "Save logs to file"
|
|
};
|
|
|
|
var logsCommand = new Command("logs", "Show task logs")
|
|
{
|
|
taskIdArg,
|
|
followOption,
|
|
stepOption,
|
|
levelOption,
|
|
outputOption,
|
|
verboseOption
|
|
};
|
|
|
|
logsCommand.SetAction((parseResult, ct) =>
|
|
{
|
|
var taskId = parseResult.GetValue(taskIdArg) ?? string.Empty;
|
|
var follow = parseResult.GetValue(followOption);
|
|
var step = parseResult.GetValue(stepOption);
|
|
var level = parseResult.GetValue(levelOption);
|
|
var output = parseResult.GetValue(outputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
Console.WriteLine($"Logs for Task: {taskId}");
|
|
Console.WriteLine(new string('-', 50));
|
|
|
|
var logs = new[]
|
|
{
|
|
"[10:25:01] INFO [pull-image] Pulling image myapp:v1.2.3...",
|
|
"[10:25:15] INFO [pull-image] Image pulled successfully",
|
|
"[10:25:16] INFO [generate-sbom] Generating SBOM...",
|
|
"[10:25:45] INFO [generate-sbom] Found 847 components",
|
|
"[10:25:46] INFO [vuln-scan] Starting vulnerability scan...",
|
|
"[10:27:30] WARN [vuln-scan] Found 3 high severity vulnerabilities",
|
|
"[10:27:45] INFO [vuln-scan] Scan complete: 847 components, 3 high, 12 medium, 45 low",
|
|
"[10:27:46] INFO [upload-results] Uploading results...",
|
|
"[10:27:50] INFO [upload-results] Results uploaded successfully"
|
|
};
|
|
|
|
foreach (var log in logs)
|
|
{
|
|
if (!string.IsNullOrEmpty(step) && !log.Contains($"[{step}]"))
|
|
continue;
|
|
if (!string.IsNullOrEmpty(level) && !log.Contains(level.ToUpperInvariant()))
|
|
continue;
|
|
|
|
Console.WriteLine(log);
|
|
}
|
|
|
|
if (follow)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("(streaming logs... press Ctrl+C to stop)");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(output))
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Logs saved to: {output}");
|
|
}
|
|
|
|
return Task.FromResult(0);
|
|
});
|
|
|
|
return logsCommand;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Sample Data
|
|
|
|
private static List<TaskInfo> GetSampleTasks()
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
return
|
|
[
|
|
new TaskInfo { Id = "task-001", Type = "scan", Status = "running", StartedAt = now.AddMinutes(-2), Duration = null },
|
|
new TaskInfo { Id = "task-002", Type = "attest", Status = "running", StartedAt = now.AddMinutes(-1), Duration = null },
|
|
new TaskInfo { Id = "task-003", Type = "scan", Status = "pending", StartedAt = now, Duration = null },
|
|
new TaskInfo { Id = "task-004", Type = "scan", Status = "completed", StartedAt = now.AddMinutes(-10), Duration = TimeSpan.FromMinutes(3) },
|
|
new TaskInfo { Id = "task-005", Type = "verify", Status = "completed", StartedAt = now.AddMinutes(-15), Duration = TimeSpan.FromSeconds(45) },
|
|
new TaskInfo { Id = "task-006", Type = "attest", Status = "failed", StartedAt = now.AddMinutes(-20), Duration = TimeSpan.FromMinutes(2) }
|
|
];
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region DTOs
|
|
|
|
private sealed class TaskRunnerStatus
|
|
{
|
|
public string Health { get; set; } = string.Empty;
|
|
public string Version { get; set; } = string.Empty;
|
|
public TimeSpan Uptime { get; set; }
|
|
public WorkerPoolStatus Workers { get; set; } = new();
|
|
public QueueStatus Queue { get; set; } = new();
|
|
}
|
|
|
|
private sealed class WorkerPoolStatus
|
|
{
|
|
public int Total { get; set; }
|
|
public int Active { get; set; }
|
|
public int Idle { get; set; }
|
|
public int MaxCapacity { get; set; }
|
|
}
|
|
|
|
private sealed class QueueStatus
|
|
{
|
|
public int Pending { get; set; }
|
|
public int Running { get; set; }
|
|
public int Completed24h { get; set; }
|
|
public int Failed24h { get; set; }
|
|
}
|
|
|
|
private sealed class TaskInfo
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string Type { get; set; } = string.Empty;
|
|
public string Status { get; set; } = string.Empty;
|
|
public DateTimeOffset StartedAt { get; set; }
|
|
public TimeSpan? Duration { get; set; }
|
|
}
|
|
|
|
private sealed class TaskDetails
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string Type { get; set; } = string.Empty;
|
|
public string Status { get; set; } = string.Empty;
|
|
public DateTimeOffset StartedAt { get; set; }
|
|
public DateTimeOffset? CompletedAt { get; set; }
|
|
public TimeSpan? Duration { get; set; }
|
|
public object? Input { get; set; }
|
|
public List<TaskStep> Steps { get; set; } = [];
|
|
public string[] Artifacts { get; set; } = [];
|
|
}
|
|
|
|
private sealed class TaskStep
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Status { get; set; } = string.Empty;
|
|
public TimeSpan? Duration { get; set; }
|
|
}
|
|
|
|
private sealed class ArtifactInfo
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Type { get; set; } = string.Empty;
|
|
public string Size { get; set; } = string.Empty;
|
|
public string Digest { get; set; } = string.Empty;
|
|
}
|
|
|
|
#endregion
|
|
}
|