Add integration tests for migration categories and execution
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
- Implemented MigrationCategoryTests to validate migration categorization for startup, release, seed, and data migrations. - Added tests for edge cases, including null, empty, and whitespace migration names. - Created StartupMigrationHostTests to verify the behavior of the migration host with real PostgreSQL instances using Testcontainers. - Included tests for migration execution, schema creation, and handling of pending release migrations. - Added SQL migration files for testing: creating a test table, adding a column, a release migration, and seeding data.
This commit is contained in:
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Extensions;
|
||||
using StellaOps.Cli.Plugins;
|
||||
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
||||
|
||||
@@ -5182,13 +5183,13 @@ internal static class CommandFactory
|
||||
Description = "Image digests to test (can be specified multiple times).",
|
||||
AllowMultipleArgumentsPerToken = true
|
||||
};
|
||||
imagesOption.IsRequired = true;
|
||||
imagesOption.Required = true;
|
||||
|
||||
var scannerOption = new Option<string>("--scanner", "-s")
|
||||
{
|
||||
Description = "Scanner container image reference."
|
||||
};
|
||||
scannerOption.IsRequired = true;
|
||||
scannerOption.Required = true;
|
||||
|
||||
var policyBundleOption = new Option<string?>("--policy-bundle")
|
||||
{
|
||||
@@ -5582,13 +5583,13 @@ internal static class CommandFactory
|
||||
{
|
||||
Description = "Start timestamp (ISO-8601). Required."
|
||||
};
|
||||
fromOption.IsRequired = true;
|
||||
fromOption.Required = true;
|
||||
|
||||
var toOption = new Option<DateTimeOffset>("--to")
|
||||
{
|
||||
Description = "End timestamp (ISO-8601). Required."
|
||||
};
|
||||
toOption.IsRequired = true;
|
||||
toOption.Required = true;
|
||||
|
||||
var logsTenantOption = new Option<string?>("--tenant", "-t")
|
||||
{
|
||||
@@ -6544,7 +6545,7 @@ internal static class CommandFactory
|
||||
var secretsInjectRefOption = new Option<string>("--secret-ref")
|
||||
{
|
||||
Description = "Secret reference (provider-specific path).",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var secretsInjectProviderOption = new Option<string>("--provider")
|
||||
@@ -6844,19 +6845,18 @@ internal static class CommandFactory
|
||||
|
||||
return CommandHandlers.HandleExceptionsListAsync(
|
||||
services,
|
||||
tenant,
|
||||
vuln,
|
||||
scopeType,
|
||||
scopeValue,
|
||||
statuses,
|
||||
owner,
|
||||
effect,
|
||||
expiringDays,
|
||||
expiringDays.HasValue ? DateTimeOffset.UtcNow.AddDays(expiringDays.Value) : null,
|
||||
includeExpired,
|
||||
pageSize,
|
||||
pageToken,
|
||||
tenant,
|
||||
json,
|
||||
csv,
|
||||
json || csv,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
@@ -6977,7 +6977,8 @@ internal static class CommandFactory
|
||||
var effect = parseResult.GetValue(createEffectOption) ?? string.Empty;
|
||||
var justification = parseResult.GetValue(createJustificationOption) ?? string.Empty;
|
||||
var owner = parseResult.GetValue(createOwnerOption) ?? string.Empty;
|
||||
var expiration = parseResult.GetValue(createExpirationOption);
|
||||
var expirationStr = parseResult.GetValue(createExpirationOption);
|
||||
var expiration = !string.IsNullOrWhiteSpace(expirationStr) && DateTimeOffset.TryParse(expirationStr, out var exp) ? exp : (DateTimeOffset?)null;
|
||||
var evidence = parseResult.GetValue(createEvidenceOption) ?? Array.Empty<string>();
|
||||
var policy = parseResult.GetValue(createPolicyOption);
|
||||
var stage = parseResult.GetValue(createStageOption);
|
||||
@@ -6987,17 +6988,17 @@ internal static class CommandFactory
|
||||
|
||||
return CommandHandlers.HandleExceptionsCreateAsync(
|
||||
services,
|
||||
tenant ?? string.Empty,
|
||||
vuln,
|
||||
scopeType,
|
||||
scopeValue,
|
||||
effect,
|
||||
justification,
|
||||
owner,
|
||||
owner ?? string.Empty,
|
||||
expiration,
|
||||
evidence,
|
||||
policy,
|
||||
stage,
|
||||
tenant,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
@@ -7042,9 +7043,9 @@ internal static class CommandFactory
|
||||
return CommandHandlers.HandleExceptionsPromoteAsync(
|
||||
services,
|
||||
exceptionId,
|
||||
target,
|
||||
comment,
|
||||
tenant,
|
||||
target ?? "active",
|
||||
comment,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
@@ -7128,10 +7129,10 @@ internal static class CommandFactory
|
||||
|
||||
return CommandHandlers.HandleExceptionsImportAsync(
|
||||
services,
|
||||
tenant ?? string.Empty,
|
||||
file,
|
||||
stage,
|
||||
source,
|
||||
tenant,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
@@ -7184,11 +7185,13 @@ internal static class CommandFactory
|
||||
|
||||
return CommandHandlers.HandleExceptionsExportAsync(
|
||||
services,
|
||||
output,
|
||||
tenant,
|
||||
statuses,
|
||||
format,
|
||||
output,
|
||||
false, // includeManifest
|
||||
signed,
|
||||
tenant,
|
||||
false, // json output
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
@@ -7470,13 +7473,13 @@ internal static class CommandFactory
|
||||
var backfillFromOption = new Option<DateTimeOffset>("--from")
|
||||
{
|
||||
Description = "Start date/time for backfill (ISO 8601 format).",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var backfillToOption = new Option<DateTimeOffset>("--to")
|
||||
{
|
||||
Description = "End date/time for backfill (ISO 8601 format).",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var backfillDryRunOption = new Option<bool>("--dry-run")
|
||||
@@ -7732,19 +7735,19 @@ internal static class CommandFactory
|
||||
var quotaSetTenantOption = new Option<string>("--tenant")
|
||||
{
|
||||
Description = "Tenant ID.",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var quotaSetResourceTypeOption = new Option<string>("--resource-type")
|
||||
{
|
||||
Description = "Resource type (api_calls, data_ingested_bytes, items_processed, backfills, concurrent_jobs, storage_bytes).",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var quotaSetLimitOption = new Option<long>("--limit")
|
||||
{
|
||||
Description = "Quota limit value.",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var quotaSetPeriodOption = new Option<string>("--period")
|
||||
@@ -7800,13 +7803,13 @@ internal static class CommandFactory
|
||||
var quotaResetTenantOption = new Option<string>("--tenant")
|
||||
{
|
||||
Description = "Tenant ID.",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var quotaResetResourceTypeOption = new Option<string>("--resource-type")
|
||||
{
|
||||
Description = "Resource type to reset.",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var quotaResetReasonOption = new Option<string?>("--reason")
|
||||
@@ -9547,7 +9550,7 @@ internal static class CommandFactory
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output path for the downloaded spec (file or directory).",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var serviceOption = new Option<string?>("--service", "-s")
|
||||
|
||||
@@ -48,43 +48,72 @@ namespace StellaOps.Cli.Commands;
|
||||
internal static class CommandHandlers
|
||||
{
|
||||
private const string KmsPassphraseEnvironmentVariable = "STELLAOPS_KMS_PASSPHRASE";
|
||||
private static readonly JsonSerializerOptions KmsJsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private static async Task VerifyBundleAsync(string path, ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
// Simple SHA256 check using sidecar .sha256 file if present; fail closed on mismatch.
|
||||
var shaPath = path + ".sha256";
|
||||
if (!File.Exists(shaPath))
|
||||
{
|
||||
logger.LogError("Checksum file missing for bundle {Bundle}. Expected sidecar {Sidecar}.", path, shaPath);
|
||||
Environment.ExitCode = 21;
|
||||
throw new InvalidOperationException("Checksum file missing");
|
||||
}
|
||||
|
||||
var expected = (await File.ReadAllTextAsync(shaPath, cancellationToken).ConfigureAwait(false)).Trim();
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
var actual = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogError("Checksum mismatch for {Bundle}. Expected {Expected} but found {Actual}", path, expected, actual);
|
||||
Environment.ExitCode = 22;
|
||||
throw new InvalidOperationException("Checksum verification failed");
|
||||
}
|
||||
|
||||
logger.LogInformation("Checksum verified for {Bundle}", path);
|
||||
}
|
||||
private static readonly JsonSerializerOptions KmsJsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
public static async Task HandleScannerDownloadAsync(
|
||||
IServiceProvider services,
|
||||
string channel,
|
||||
string? output,
|
||||
bool overwrite,
|
||||
bool install,
|
||||
/// <summary>
|
||||
/// Standard JSON serializer options for CLI output.
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// JSON serializer options for output (alias for JsonOptions).
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions JsonOutputOptions = JsonOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the verbosity level for logging.
|
||||
/// </summary>
|
||||
private static void SetVerbosity(IServiceProvider services, bool verbose)
|
||||
{
|
||||
// Configure logging level based on verbose flag
|
||||
var loggerFactory = services.GetService<ILoggerFactory>();
|
||||
if (loggerFactory is not null && verbose)
|
||||
{
|
||||
// Enable debug logging when verbose is true
|
||||
var logger = loggerFactory.CreateLogger("StellaOps.Cli.Commands.CommandHandlers");
|
||||
logger.LogDebug("Verbose logging enabled");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task VerifyBundleAsync(string path, ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
// Simple SHA256 check using sidecar .sha256 file if present; fail closed on mismatch.
|
||||
var shaPath = path + ".sha256";
|
||||
if (!File.Exists(shaPath))
|
||||
{
|
||||
logger.LogError("Checksum file missing for bundle {Bundle}. Expected sidecar {Sidecar}.", path, shaPath);
|
||||
Environment.ExitCode = 21;
|
||||
throw new InvalidOperationException("Checksum file missing");
|
||||
}
|
||||
|
||||
var expected = (await File.ReadAllTextAsync(shaPath, cancellationToken).ConfigureAwait(false)).Trim();
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
var actual = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogError("Checksum mismatch for {Bundle}. Expected {Expected} but found {Actual}", path, expected, actual);
|
||||
Environment.ExitCode = 22;
|
||||
throw new InvalidOperationException("Checksum verification failed");
|
||||
}
|
||||
|
||||
logger.LogInformation("Checksum verified for {Bundle}", path);
|
||||
}
|
||||
|
||||
public static async Task HandleScannerDownloadAsync(
|
||||
IServiceProvider services,
|
||||
string channel,
|
||||
string? output,
|
||||
bool overwrite,
|
||||
bool install,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -114,29 +143,29 @@ internal static class CommandHandlers
|
||||
|
||||
CliMetrics.RecordScannerDownload(channel, result.FromCache);
|
||||
|
||||
if (install)
|
||||
{
|
||||
await VerifyBundleAsync(result.Path, logger, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var installer = scope.ServiceProvider.GetRequiredService<IScannerInstaller>();
|
||||
await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false);
|
||||
CliMetrics.RecordScannerInstall(channel);
|
||||
}
|
||||
if (install)
|
||||
{
|
||||
await VerifyBundleAsync(result.Path, logger, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var installer = scope.ServiceProvider.GetRequiredService<IScannerInstaller>();
|
||||
await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false);
|
||||
CliMetrics.RecordScannerInstall(channel);
|
||||
}
|
||||
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to download scanner bundle.");
|
||||
if (Environment.ExitCode == 0)
|
||||
{
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to download scanner bundle.");
|
||||
if (Environment.ExitCode == 0)
|
||||
{
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleTaskRunnerSimulateAsync(
|
||||
@@ -264,15 +293,15 @@ internal static class CommandHandlers
|
||||
{
|
||||
var console = AnsiConsole.Console;
|
||||
|
||||
console.MarkupLine($"[bold]Scan[/]: {result.ScanId}");
|
||||
console.MarkupLine($"Image: {result.ImageDigest}");
|
||||
console.MarkupLine($"Generated: {result.GeneratedAt:O}");
|
||||
console.MarkupLine($"Outcome: {result.Graph.Outcome}");
|
||||
|
||||
if (result.BestPlan is not null)
|
||||
{
|
||||
console.MarkupLine($"Best Terminal: {result.BestPlan.TerminalPath} (conf {result.BestPlan.Confidence:F1}, user {result.BestPlan.User}, cwd {result.BestPlan.WorkingDirectory})");
|
||||
}
|
||||
console.MarkupLine($"[bold]Scan[/]: {result.ScanId}");
|
||||
console.MarkupLine($"Image: {result.ImageDigest}");
|
||||
console.MarkupLine($"Generated: {result.GeneratedAt:O}");
|
||||
console.MarkupLine($"Outcome: {result.Graph.Outcome}");
|
||||
|
||||
if (result.BestPlan is not null)
|
||||
{
|
||||
console.MarkupLine($"Best Terminal: {result.BestPlan.TerminalPath} (conf {result.BestPlan.Confidence:F1}, user {result.BestPlan.User}, cwd {result.BestPlan.WorkingDirectory})");
|
||||
}
|
||||
|
||||
var planTable = new Table()
|
||||
.AddColumn("Terminal")
|
||||
@@ -284,15 +313,15 @@ internal static class CommandHandlers
|
||||
|
||||
foreach (var plan in result.Graph.Plans.OrderByDescending(p => p.Confidence))
|
||||
{
|
||||
var confidence = plan.Confidence.ToString("F1", CultureInfo.InvariantCulture);
|
||||
planTable.AddRow(
|
||||
plan.TerminalPath,
|
||||
plan.Runtime ?? "-",
|
||||
plan.Type.ToString(),
|
||||
confidence,
|
||||
plan.User,
|
||||
plan.WorkingDirectory);
|
||||
}
|
||||
var confidence = plan.Confidence.ToString("F1", CultureInfo.InvariantCulture);
|
||||
planTable.AddRow(
|
||||
plan.TerminalPath,
|
||||
plan.Runtime ?? "-",
|
||||
plan.Type.ToString(),
|
||||
confidence,
|
||||
plan.User,
|
||||
plan.WorkingDirectory);
|
||||
}
|
||||
|
||||
if (planTable.Rows.Count > 0)
|
||||
{
|
||||
@@ -6860,7 +6889,7 @@ internal static class CommandHandlers
|
||||
}
|
||||
|
||||
AnsiConsole.Write(violationTable);
|
||||
}
|
||||
}
|
||||
|
||||
private static int DetermineVerifyExitCode(AocVerifyResponse response)
|
||||
{
|
||||
@@ -10895,13 +10924,10 @@ stella policy test {policyName}.stella
|
||||
Code = diag.Code,
|
||||
Message = diag.Message,
|
||||
Severity = diag.Severity.ToString().ToLowerInvariant(),
|
||||
Line = diag.Line,
|
||||
Column = diag.Column,
|
||||
Span = diag.Span,
|
||||
Suggestion = diag.Suggestion
|
||||
Path = diag.Path
|
||||
};
|
||||
|
||||
if (diag.Severity == PolicyDsl.DiagnosticSeverity.Error)
|
||||
if (diag.Severity == PolicyIssueSeverity.Error)
|
||||
{
|
||||
errors.Add(diagnostic);
|
||||
}
|
||||
@@ -10939,7 +10965,7 @@ stella policy test {policyName}.stella
|
||||
InputPath = fullPath,
|
||||
IrPath = irPath,
|
||||
Digest = digest,
|
||||
SyntaxVersion = compileResult.Document?.SyntaxVersion,
|
||||
SyntaxVersion = compileResult.Document?.Syntax,
|
||||
PolicyName = compileResult.Document?.Name,
|
||||
RuleCount = compileResult.Document?.Rules.Length ?? 0,
|
||||
ProfileCount = compileResult.Document?.Profiles.Length ?? 0,
|
||||
@@ -10985,24 +11011,14 @@ stella policy test {policyName}.stella
|
||||
|
||||
foreach (var err in errors)
|
||||
{
|
||||
var location = err.Line.HasValue ? $":{err.Line}" : "";
|
||||
if (err.Column.HasValue) location += $":{err.Column}";
|
||||
AnsiConsole.MarkupLine($"[red]error[{Markup.Escape(err.Code)}]{location}: {Markup.Escape(err.Message)}[/]");
|
||||
if (!string.IsNullOrWhiteSpace(err.Suggestion))
|
||||
{
|
||||
AnsiConsole.MarkupLine($" [cyan]suggestion: {Markup.Escape(err.Suggestion)}[/]");
|
||||
}
|
||||
var location = !string.IsNullOrWhiteSpace(err.Path) ? $" at {err.Path}" : "";
|
||||
AnsiConsole.MarkupLine($"[red]error[{Markup.Escape(err.Code)}]{Markup.Escape(location)}: {Markup.Escape(err.Message)}[/]");
|
||||
}
|
||||
|
||||
foreach (var warn in warnings)
|
||||
{
|
||||
var location = warn.Line.HasValue ? $":{warn.Line}" : "";
|
||||
if (warn.Column.HasValue) location += $":{warn.Column}";
|
||||
AnsiConsole.MarkupLine($"[yellow]warning[{Markup.Escape(warn.Code)}]{location}: {Markup.Escape(warn.Message)}[/]");
|
||||
if (!string.IsNullOrWhiteSpace(warn.Suggestion))
|
||||
{
|
||||
AnsiConsole.MarkupLine($" [cyan]suggestion: {Markup.Escape(warn.Suggestion)}[/]");
|
||||
}
|
||||
var location = !string.IsNullOrWhiteSpace(warn.Path) ? $" at {warn.Path}" : "";
|
||||
AnsiConsole.MarkupLine($"[yellow]warning[{Markup.Escape(warn.Code)}]{Markup.Escape(location)}: {Markup.Escape(warn.Message)}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13248,18 +13264,6 @@ stella policy test {policyName}.stella
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetVexStatusMarkup(string status)
|
||||
{
|
||||
return status?.ToLowerInvariant() switch
|
||||
{
|
||||
"affected" => "[red]affected[/]",
|
||||
"not_affected" => "[green]not_affected[/]",
|
||||
"fixed" => "[blue]fixed[/]",
|
||||
"under_investigation" => "[yellow]under_investigation[/]",
|
||||
_ => Markup.Escape(status ?? "(unknown)")
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Vulnerability Explorer (CLI-VULN-29-001)
|
||||
@@ -14543,13 +14547,13 @@ stella policy test {policyName}.stella
|
||||
var fixText = obs.Fix?.Available == true ? "[green]available[/]" : "[grey]none[/]";
|
||||
|
||||
table.AddRow(
|
||||
Markup.Escape(obs.ObservationId),
|
||||
Markup.Escape(sourceVendor),
|
||||
Markup.Escape(aliasesText),
|
||||
Markup.Escape(severityText),
|
||||
new Markup(Markup.Escape(obs.ObservationId)),
|
||||
new Markup(Markup.Escape(sourceVendor)),
|
||||
new Markup(Markup.Escape(aliasesText)),
|
||||
new Markup(Markup.Escape(severityText)),
|
||||
new Markup(kevText),
|
||||
new Markup(fixText),
|
||||
obs.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture));
|
||||
new Markup(Markup.Escape(obs.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture))));
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
@@ -15386,12 +15390,12 @@ stella policy test {policyName}.stella
|
||||
var size = FormatSize(snapshot.SizeBytes);
|
||||
|
||||
table.AddRow(
|
||||
Markup.Escape(snapshot.SnapshotId.Length > 20 ? snapshot.SnapshotId[..17] + "..." : snapshot.SnapshotId),
|
||||
Markup.Escape(snapshot.CaseId),
|
||||
new Markup(Markup.Escape(snapshot.SnapshotId.Length > 20 ? snapshot.SnapshotId[..17] + "..." : snapshot.SnapshotId)),
|
||||
new Markup(Markup.Escape(snapshot.CaseId)),
|
||||
new Markup(statusMarkup),
|
||||
artifactCount,
|
||||
size,
|
||||
snapshot.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture));
|
||||
new Markup(Markup.Escape(artifactCount)),
|
||||
new Markup(Markup.Escape(size)),
|
||||
new Markup(Markup.Escape(snapshot.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture))));
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
|
||||
Reference in New Issue
Block a user