// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json.Serialization; using EpicGames.Horde.Artifacts; using EpicGames.Horde.Commits; using EpicGames.Horde.Common; using EpicGames.Horde.Jobs.Bisect; using EpicGames.Horde.Jobs.Templates; using EpicGames.Horde.Streams; using EpicGames.Horde.Users; #pragma warning disable CA1056 // Change the type of property 'JobContainerOptions.ImageUrl' from 'string' to 'System.Uri' #pragma warning disable CA2227 // Change 'Outcomes' to be read-only by removing the property setter namespace EpicGames.Horde.Jobs { /// /// State of the job /// public enum JobState { /// /// Waiting for resources /// Waiting, /// /// Currently running one or more steps /// Running, /// /// All steps have completed /// Complete, } /// /// Read-only interface for job options /// public interface IReadOnlyJobOptions { /// /// Name of the executor to use /// string? Executor { get; } /// /// Whether to execute using Wine emulation on Linux /// bool? UseWine { get; } /// /// Executes the job lease in a separate process /// bool? RunInSeparateProcess { get; } /// /// What workspace materializer to use in WorkspaceExecutor. Will override any value from workspace config. /// string? WorkspaceMaterializer { get; } /// /// Options for executing a job inside a container /// IReadOnlyJobContainerOptions Container { get; } /// /// Number of days after which to expire jobs /// int? ExpireAfterDays { get; } /// /// Name of the driver to use /// string? Driver { get; } } /// /// Options for executing a job /// public class JobOptions : IReadOnlyJobOptions { /// public string? Executor { get; set; } /// public bool? UseWine { get; set; } /// public bool? RunInSeparateProcess { get; set; } /// public string? WorkspaceMaterializer { get; set; } /// public JobContainerOptions Container { get; set; } = new JobContainerOptions(); /// public int? ExpireAfterDays { get; set; } /// public string? Driver { get; set; } IReadOnlyJobContainerOptions IReadOnlyJobOptions.Container => Container; /// /// Merge defaults from another options object /// public void MergeDefaults(JobOptions other) { Executor ??= other.Executor; UseWine ??= other.UseWine; RunInSeparateProcess ??= other.RunInSeparateProcess; WorkspaceMaterializer ??= other.WorkspaceMaterializer; Container.MergeDefaults(other.Container); ExpireAfterDays ??= other.ExpireAfterDays; Driver ??= other.Driver; } } /// /// Options for a job container /// public interface IReadOnlyJobContainerOptions { /// /// Whether to execute job inside a container /// public bool? Enabled { get; } /// /// Image URL to container, such as "quay.io/podman/hello" /// public string? ImageUrl { get; } /// /// Container engine executable (docker or with full path like /usr/bin/podman) /// public string? ContainerEngineExecutable { get; } /// /// Additional arguments to pass to container engine /// public string? ExtraArguments { get; } } /// /// Options for executing a job inside a container /// public class JobContainerOptions : IReadOnlyJobContainerOptions { /// /// Whether to execute job inside a container /// public bool? Enabled { get; set; } /// /// Image URL to container, such as "quay.io/podman/hello" /// public string? ImageUrl { get; set; } /// /// Container engine executable (docker or with full path like /usr/bin/podman) /// public string? ContainerEngineExecutable { get; set; } /// /// Additional arguments to pass to container engine /// public string? ExtraArguments { get; set; } /// /// Merge defaults from another options object /// public void MergeDefaults(JobContainerOptions other) { Enabled ??= other.Enabled; ImageUrl ??= other.ImageUrl; ContainerEngineExecutable ??= other.ContainerEngineExecutable; ExtraArguments ??= other.ExtraArguments; } } /// /// Query selecting the base changelist to use /// public class ChangeQueryMessage : IChangeQuery { /// public string? Name { get; set; } /// public Condition? Condition { get; set; } /// public TemplateId? TemplateId { get; set; } /// public string? Target { get; set; } /// public List? Outcomes { get; set; } IReadOnlyList? IChangeQuery.Outcomes => Outcomes; /// public CommitTag? CommitTag { get; set; } /// /// Constructor /// public ChangeQueryMessage() { } /// /// Constructor /// public ChangeQueryMessage(IChangeQuery other) { Name = other.Name; Condition = other.Condition; TemplateId = other.TemplateId; Target = other.Target; Outcomes = other.Outcomes?.ToList(); CommitTag = other.CommitTag; } } /// /// Parameters required to create a job /// public class CreateJobRequest { /// /// The stream that this job belongs to /// [Required] public StreamId StreamId { get; set; } /// /// The template for this job /// [Required] public TemplateId TemplateId { get; set; } /// /// Name of the job /// public string? Name { get; set; } /// /// The changelist number to build. Can be null for latest. /// [Obsolete("Use CommitId instead")] public int? Change { get => _change ?? _commitId?.GetPerforceChangeOrMinusOne(); set => _change = value; } int? _change; /// /// The changelist number to build. Can be null for latest. /// public CommitId? CommitId { get => _commitId ?? CommitId.FromPerforceChange(_change); set => _commitId = value; } CommitId? _commitId; /// /// Parameters to use when selecting the change to execute at. /// [Obsolete("Use ChangeQueries instead")] public ChangeQueryMessage? ChangeQuery { get => (ChangeQueries != null && ChangeQueries.Count > 0) ? ChangeQueries[0] : null; set => ChangeQueries = (value == null) ? null : new List { value }; } /// /// List of change queries to evaluate /// public List? ChangeQueries { get; set; } /// /// The preflight changelist number /// [Obsolete("Use PreflightCommitId instead")] public int? PreflightChange { get => _preflightChange ?? _preflightCommitId?.GetPerforceChangeOrMinusOne(); set => _preflightChange = value; } int? _preflightChange; /// /// The preflight commit /// public CommitId? PreflightCommitId { get => _preflightCommitId ?? CommitId.FromPerforceChange(_preflightChange); set => _preflightCommitId = value; } CommitId? _preflightCommitId; /// /// Job options /// public JobOptions? JobOptions { get; set; } /// /// Priority for the job /// public Priority? Priority { get; set; } /// /// Whether to automatically submit the preflighted change on completion /// public bool? AutoSubmit { get; set; } /// /// Whether to update issues based on the outcome of this job /// public bool? UpdateIssues { get; set; } /// /// Values for the template parameters /// public Dictionary? Parameters { get; set; } /// /// Arguments for the job /// public List? Arguments { get; set; } /// /// Additional arguments for the job /// public List? AdditionalArguments { get; set; } /// /// Targets for the job. Will override any parameters specified in the Arguments or Parameters section if specified. /// public List? Targets { get; set; } /// /// The parent job id if any /// public string? ParentJobId { get; set; } /// /// The parent step id if any /// public string? ParentJobStepId { get; set; } /// /// Run the job as the scheduler for debugging purposes, requires debugging ACL permission /// public bool? RunAsScheduler { get; set; } /// /// Private constructor for serialization /// public CreateJobRequest(StreamId streamId, TemplateId templateId) { StreamId = streamId; TemplateId = templateId; } } /// /// Response from creating a new job /// public class CreateJobResponse { /// /// Unique id for the new job /// public string Id { get; set; } /// /// Constructor /// /// Unique id for the new job public CreateJobResponse(string id) { Id = id; } } /// /// Updates an existing job /// public class UpdateJobRequest { /// /// New name for the job /// public string? Name { get; set; } /// /// New priority for the job /// public Priority? Priority { get; set; } /// /// Set whether the job should be automatically submitted or not /// public bool? AutoSubmit { get; set; } /// /// Mark this job as aborted /// public bool? Aborted { get; set; } /// /// Optional reason the job was canceled /// public string? CancellationReason { get; set; } /// /// New list of arguments for the job. Only -Target= arguments can be modified after the job has started. /// public List? Arguments { get; set; } } /// /// Job metadata put request /// public class PutJobMetadataRequest { /// /// Meta data to append to the job /// public List? JobMetaData { get; set; } /// /// Step meta data to append, JobStepId => list of meta data strings /// public Dictionary>? StepMetaData { get; set; } } /// /// Placement for a job report /// public enum JobReportPlacement { /// /// On a panel of its own /// Panel = 0, /// /// In the summary panel /// Summary = 1 } /// /// Information about a report associated with a job /// public class GetJobReportResponse { /// /// Name of the report /// public string Name { get; set; } /// /// Report placement /// public JobReportPlacement Placement { get; set; } /// /// The artifact id /// public string? ArtifactId { get; set; } /// /// Content for the report /// public string? Content { get; set; } /// /// Constructor /// public GetJobReportResponse(string name, JobReportPlacement placement) { Name = name; Placement = placement; } } /// /// Information about a job /// public class GetJobResponse { /// /// Unique Id for the job /// public JobId Id { get; set; } /// /// Name of the job /// public string Name { get; set; } /// /// Unique id of the stream containing this job /// public StreamId StreamId { get; set; } /// /// The changelist number to build /// [Obsolete("Use CommitId instead")] public int Change { get => _change ?? _commitId?.GetPerforceChangeOrMinusOne() ?? 0; set => _change = value; } int? _change; /// /// The commit to build /// public CommitIdWithOrder CommitId { get => _commitId ?? CommitIdWithOrder.FromPerforceChange(_change) ?? CommitIdWithOrder.Empty; set => _commitId = value; } CommitIdWithOrder? _commitId; /// /// The code changelist /// [Obsolete("Use CodeCommitId instead")] public int? CodeChange { get => _codeChange ?? _codeCommitId?.GetPerforceChangeOrMinusOne(); set => _codeChange = value; } int? _codeChange; /// /// The code commit to build /// public CommitIdWithOrder? CodeCommitId { get => _codeCommitId ?? CommitIdWithOrder.FromPerforceChange(_codeChange); set => _codeCommitId = value; } CommitIdWithOrder? _codeCommitId; /// /// The preflight changelist number /// [Obsolete("Use PreflightCommitId instead")] public int? PreflightChange { get => _preflightChange ?? _preflightCommitId?.GetPerforceChangeOrMinusOne(); set => _preflightChange = value; } int? _preflightChange; /// /// The preflight commit /// public CommitId? PreflightCommitId { get => _preflightCommitId ?? Horde.Commits.CommitId.FromPerforceChange(_preflightChange); set => _preflightCommitId = value; } CommitId? _preflightCommitId; /// /// Description of the preflight /// public string? PreflightDescription { get; set; } /// /// The template type /// public TemplateId TemplateId { get; set; } /// /// Hash of the actual template data /// public string? TemplateHash { get; set; } /// /// Hash of the graph for this job /// public string? GraphHash { get; set; } /// /// The user that started this job [DEPRECATED] /// public string? StartedByUserId { get; set; } /// /// The user that started this job [DEPRECATED] /// public string? StartedByUser { get; set; } /// /// The user that started this job /// public GetThinUserInfoResponse? StartedByUserInfo { get; set; } /// /// Bisection task id that started this job /// public BisectTaskId? StartedByBisectTaskId { get; set; } /// /// The user that aborted this job [DEPRECATED] /// public string? AbortedByUser { get; set; } /// /// The user that aborted this job /// public GetThinUserInfoResponse? AbortedByUserInfo { get; set; } /// /// Optional reason the job was canceled /// public string? CancellationReason { get; set; } /// /// Priority of the job /// public Priority Priority { get; set; } /// /// Whether the change will automatically be submitted or not /// public bool AutoSubmit { get; set; } /// /// The submitted changelist number /// public int? AutoSubmitChange { get; set; } /// /// Message produced by trying to auto-submit the change /// public string? AutoSubmitMessage { get; set; } /// /// Time that the job was created /// public DateTimeOffset CreateTime { get; set; } /// /// The global job state /// public JobState State { get; set; } /// /// Array of jobstep batches /// public List? Batches { get; set; } /// /// List of labels /// public List? Labels { get; set; } /// /// The default label, containing the state of all steps that are otherwise not matched. /// public GetDefaultLabelStateResponse? DefaultLabel { get; set; } /// /// List of reports /// public List? Reports { get; set; } /// /// Artifacts produced by this job /// public List? Artifacts { get; set; } /// /// Parameters for the job /// public Dictionary Parameters { get; set; } = new Dictionary(); /// /// Command line arguments for the job /// public List Arguments { get; set; } = new List(); /// /// Additional command line arguments for the job for when using the parameters block /// public List AdditionalArguments { get; set; } = new List(); /// /// Custom list of targets for the job /// public List? Targets { get; set; } /// /// List of metadata for this job /// public List? Metadata { get; set; } /// /// The last update time for this job /// public DateTimeOffset UpdateTime { get; set; } /// /// Whether to use the V2 artifacts endpoint /// public bool UseArtifactsV2 { get; set; } /// /// Whether issues are being updated by this job /// public bool UpdateIssues { get; set; } /// /// Whether the current user is allowed to update this job /// public bool CanUpdate { get; set; } = true; /// /// The parent job id, if any /// public string? ParentJobId { get; set; } /// /// The parent job step id, if any /// public string? ParentJobStepId { get; set; } /// /// Constructor /// public GetJobResponse(JobId jobId, StreamId streamId, TemplateId templateId, string name) { Id = jobId; StreamId = streamId; TemplateId = templateId; Name = name; } /// /// Default constructor needed for JsonSerializer /// [JsonConstructor] public GetJobResponse() { Name = ""; } } /// /// Response describing an artifact produced during a job /// /// Identifier for this artifact, if it has been produced /// Name of the artifact /// Artifact type /// Description to display for the artifact on the dashboard /// Keys for the artifact /// Metadata for the artifact /// Step producing the artifact public record class GetJobArtifactResponse ( ArtifactId? Id, ArtifactName Name, ArtifactType Type, string? Description, List Keys, List Metadata, JobStepId StepId ); /// /// The timing info for a job /// public class GetJobTimingResponse { /// /// The job response /// public GetJobResponse? JobResponse { get; set; } /// /// Timing info for each step /// public Dictionary Steps { get; set; } /// /// Timing information for each label /// public List Labels { get; set; } /// /// Constructor /// /// The job response /// Timing info for each steps /// Timing info for each label public GetJobTimingResponse(GetJobResponse? jobResponse, Dictionary steps, List labels) { JobResponse = jobResponse; Steps = steps; Labels = labels; } } /// /// The timing info for /// public class FindJobTimingsResponse { /// /// Timing info for each job /// public Dictionary Timings { get; set; } /// /// Constructor /// /// Timing info for each job public FindJobTimingsResponse(Dictionary timings) { Timings = timings; } } /// /// Request used to update a jobstep /// public class UpdateStepRequest { /// /// The new jobstep state /// public JobStepState State { get; set; } = JobStepState.Unspecified; /// /// Outcome from the jobstep /// public JobStepOutcome Outcome { get; set; } = JobStepOutcome.Unspecified; /// /// If the step has been requested to abort /// public bool? AbortRequested { get; set; } /// /// Optional reason the job step was canceled /// public string? CancellationReason { get; set; } /// /// Specifies the log file id for this step /// public string? LogId { get; set; } /// /// Whether the step should be re-run /// public bool? Retry { get; set; } /// /// New priority for this step /// public Priority? Priority { get; set; } /// /// Properties to set. Any entries with a null value will be removed. /// public Dictionary? Properties { get; set; } } /// /// Response object when updating a jobstep /// public class UpdateStepResponse { /// /// If a new step is created (due to specifying the retry flag), specifies the batch id /// public string? BatchId { get; set; } /// /// If a step is retried, includes the new step id /// public string? StepId { get; set; } } /// /// Reference to the output of a step within the job /// /// Step producing the output /// Index of the output from this step public record struct JobStepOutputRef(JobStepId StepId, int OutputIdx); /// /// Returns information about a jobstep /// public class GetJobStepResponse { /// /// The unique id of the step /// public JobStepId Id { get; set; } /// /// Index of the node which this jobstep is to execute /// public int NodeIdx { get; set; } /// /// The name of this node /// public string Name { get; set; } = String.Empty; /// /// Whether this node can be run multiple times /// public bool AllowRetry { get; set; } /// /// This node can start running early, before dependencies of other nodes in the same group are complete /// public bool RunEarly { get; set; } /// /// Whether to include warnings in the output (defaults to true) /// public bool Warnings { get; set; } /// /// References to inputs for this node /// public List? Inputs { get; set; } /// /// List of output names /// public List? OutputNames { get; set; } /// /// Indices of nodes which must have succeeded for this node to run /// public List? InputDependencies { get; set; } /// /// Indices of nodes which must have completed for this node to run /// public List? OrderDependencies { get; set; } /// /// List of credentials required for this node. Each entry maps an environment variable name to a credential in the form "CredentialName.PropertyName". /// public IReadOnlyDictionary? Credentials { get; set; } /// /// Annotations for this node /// public IReadOnlyDictionary? Annotations { get; set; } /// /// List of metadata for this step /// public List? Metadata { get; set; } /// /// Current state of the job step. This is updated automatically when runs complete. /// public JobStepState State { get; set; } /// /// Current outcome of the jobstep /// public JobStepOutcome Outcome { get; set; } /// /// Error describing additional context for why a step failed to complete /// public JobStepError Error { get; set; } /// /// If the step has been requested to abort /// public bool AbortRequested { get; set; } /// /// Name of the user that requested the abort of this step [DEPRECATED] /// public string? AbortByUser { get; set; } /// /// The user that requested this step be run again /// public GetThinUserInfoResponse? AbortedByUserInfo { get; set; } /// /// Optional reason the job step was canceled /// public string? CancellationReason { get; set; } /// /// Name of the user that requested this step be run again [DEPRECATED] /// public string? RetryByUser { get; set; } /// /// The user that requested this step be run again /// public GetThinUserInfoResponse? RetriedByUserInfo { get; set; } /// /// The log id for this step /// public string? LogId { get; set; } /// /// Time at which the batch was ready (UTC). /// public DateTimeOffset? ReadyTime { get; set; } /// /// Time at which the batch started (UTC). /// public DateTimeOffset? StartTime { get; set; } /// /// Time at which the batch finished (UTC) /// public DateTimeOffset? FinishTime { get; set; } /// /// List of reports /// public List? Reports { get; set; } /// /// List of spawned job ids /// public List? SpawnedJobs { get; set; } /// /// User-defined properties for this jobstep. /// public Dictionary? Properties { get; set; } } /// /// The state of a particular run /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum JobStepBatchState { /// /// Waiting for dependencies of at least one jobstep to complete /// Waiting = 0, /// /// Ready to execute /// Ready = 1, /// /// Preparing to execute work /// Starting = 2, /// /// Executing work /// Running = 3, /// /// Preparing to stop /// Stopping = 4, /// /// All steps have finished executing /// Complete = 5 } #pragma warning disable CA1027 /// /// Error code for a batch not being executed /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum JobStepBatchError { /// /// No error /// None = 0, /// /// The stream for this job is unknown /// UnknownStream = 1, /// /// The given agent type for this batch was not valid for this stream /// UnknownAgentType = 2, /// /// The pool id referenced by the agent type was not found /// UnknownPool = 3, /// /// There are no agents in the given pool currently online /// NoAgentsInPool = 4, /// /// There are no agents in this pool that are onlinbe /// NoAgentsOnline = 5, /// /// Unknown workspace referenced by the agent type /// UnknownWorkspace = 6, /// /// Cancelled /// Cancelled = 7, /// /// Lost connection with the agent machine /// LostConnection = 8, /// /// Lease terminated prematurely but can be retried. /// Incomplete = 9, /// /// An error ocurred while executing the lease. Cannot be retried. /// ExecutionError = 10, /// /// The change that the job is running against is invalid /// UnknownShelf = 11, /// /// Step was no longer needed during a job update /// NoLongerNeeded = 12, /// /// Syncing the branch failed /// SyncingFailed = 13, /// /// Legacy alias for /// [Obsolete("Use SyncingFailed instead")] AgentSetupFailed = SyncingFailed, } #pragma warning restore CA1027 /// /// Request to update a jobstep batch /// public class UpdateBatchRequest { /// /// The new log file id /// public string? LogId { get; set; } /// /// The state of this batch /// public JobStepBatchState? State { get; set; } } /// /// Information about a jobstep batch /// public class GetJobBatchResponse { /// /// Unique id for this batch /// public JobStepBatchId Id { get; set; } /// /// Index of the group being executed /// public int GroupIdx { get; set; } /// /// The agent type /// public string AgentType { get; set; } = String.Empty; /// /// The agent assigned to execute this group /// public string? AgentId { get; set; } /// /// Rate for using this agent (per hour) /// public double? AgentRate { get; set; } /// /// The agent session holding this lease /// public string? SessionId { get; set; } /// /// The lease that's executing this group /// public string? LeaseId { get; set; } /// /// The unique log file id /// public string? LogId { get; set; } /// /// The state of this batch /// public JobStepBatchState State { get; set; } /// /// Error code for this batch /// public JobStepBatchError Error { get; set; } /// /// The priority of this batch /// public int WeightedPriority { get; set; } /// /// Time at which the group started (UTC). /// public DateTimeOffset? StartTime { get; set; } /// /// Time at which the group finished (UTC) /// public DateTimeOffset? FinishTime { get; set; } /// /// Time at which the group became ready (UTC). /// public DateTimeOffset? ReadyTime { get; set; } /// /// Steps within this run /// public List Steps { get; set; } = new List(); } /// /// State of an aggregate /// public enum LabelState { /// /// Aggregate is not currently being built (no required nodes are present) /// Unspecified, /// /// Steps are still running /// Running, /// /// All steps are complete /// Complete } /// /// Outcome of an aggregate /// public enum LabelOutcome { /// /// Aggregate is not currently being built /// Unspecified, /// /// A step dependency failed /// Failure, /// /// A dependency finished with warnings /// Warnings, /// /// Successful /// Success, } /// /// State of a label within a job /// public class GetLabelStateResponse { /// /// Name to show for this label on the dashboard /// public string? DashboardName { get; set; } /// /// Category to show this label in on the dashboard /// public string? DashboardCategory { get; set; } /// /// Name to show for this label in UGS /// public string? UgsName { get; set; } /// /// Project to display this label for in UGS /// public string? UgsProject { get; set; } /// /// State of the label /// public LabelState? State { get; set; } /// /// Outcome of the label /// public LabelOutcome? Outcome { get; set; } /// /// Steps to include in the status of this label /// public List Steps { get; set; } = new List(); } /// /// Information about the default label (ie. with inlined list of nodes) /// public class GetDefaultLabelStateResponse : GetLabelStateResponse { /// /// List of nodes covered by default label /// public List Nodes { get; set; } = new List(); } /// /// Information about the timing info for a particular target /// public class GetTimingInfoResponse { /// /// Wait time on the critical path /// public float? TotalWaitTime { get; set; } /// /// Sync time on the critical path /// public float? TotalInitTime { get; set; } /// /// Duration to this point /// public float? TotalTimeToComplete { get; set; } /// /// Average wait time by the time the job reaches this point /// public float? AverageTotalWaitTime { get; set; } /// /// Average sync time to this point /// public float? AverageTotalInitTime { get; set; } /// /// Average duration to this point /// public float? AverageTotalTimeToComplete { get; set; } } /// /// Information about the timing info for a particular target /// public class GetStepTimingInfoResponse : GetTimingInfoResponse { /// /// Name of this node /// public string? Name { get; set; } /// /// Average wait time for this step /// public float? AverageStepWaitTime { get; set; } /// /// Average init time for this step /// public float? AverageStepInitTime { get; set; } /// /// Average duration for this step /// public float? AverageStepDuration { get; set; } } /// /// Information about the timing info for a label /// public class GetLabelTimingInfoResponse : GetTimingInfoResponse { /// /// Name of the label /// [Obsolete("Use DashboardName instead")] public string? Name => DashboardName; /// /// Category for the label /// [Obsolete("Use DashboardCategory instead")] public string? Category => DashboardCategory; /// /// Name of the label /// public string? DashboardName { get; set; } /// /// Category for the label /// public string? DashboardCategory { get; set; } /// /// Name of the label /// public string? UgsName { get; set; } /// /// Category for the label /// public string? UgsProject { get; set; } } /// /// Describes the history of a step /// public class GetJobStepRefResponse { /// /// The job id /// public JobId JobId { get; set; } /// /// The batch containing the step /// public JobStepBatchId BatchId { get; set; } /// /// The step identifier /// public JobStepId StepId { get; set; } /// /// The change number being built /// [Obsolete("Use CommitId instead")] public int Change { get => _change ?? _commitId?.GetPerforceChangeOrMinusOne() ?? 0; set => _change = value; } int? _change; /// /// The commit being built /// public CommitIdWithOrder CommitId { get => _commitId ?? CommitIdWithOrder.FromPerforceChange(_change) ?? CommitIdWithOrder.Empty; set => _commitId = value; } CommitIdWithOrder? _commitId; /// /// The step log id /// public string? LogId { get; set; } /// /// The pool id /// public string? PoolId { get; set; } /// /// The agent id /// public string? AgentId { get; set; } /// /// Outcome of the step, once complete. /// public JobStepOutcome? Outcome { get; set; } /// /// The issues which affected this step /// public List? IssueIds { get; set; } /// /// The step meta data for this step /// public List? Metadata { get; set; } /// /// Time at which the job started /// public DateTimeOffset JobStartTime { get; set; } /// /// Time at which the step started. /// public DateTimeOffset StartTime { get; set; } /// /// Time at which the step finished. /// public DateTimeOffset? FinishTime { get; set; } } }