using System.CommandLine; using System.CommandLine.Parsing; using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; namespace ZB.MOM.WW.ScadaBridge.CLI.Commands; public static class DeployCommands { /// /// Builds the deploy command group with all sub-commands. /// /// Global management URL option. /// Global output format option. /// Global username option. /// Global password option. /// The configured deploy with all sub-commands attached. public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var command = new Command("deploy") { Description = "Deployment operations" }; command.Add(BuildInstance(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildSite(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildArtifacts(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildStatus(urlOption, formatOption, usernameOption, passwordOption)); return command; } private static Command BuildInstance(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Instance ID", Required = true }; var cmd = new Command("instance") { Description = "Deploy a single instance" }; cmd.Add(idOption); cmd.SetAction(async (ParseResult result) => { var id = result.GetValue(idOption); return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeployInstanceCommand(id)); }); return cmd; } /// /// Builds deploy site — bulk-deploy every instance at one site. /// /// /// Sits under deploy alongside deploy instance and /// deploy artifacts, naming the SCOPE of the deploy as the verb, which /// is the convention the group already follows. Note it is deliberately NOT /// fleet-wide when --site-id is omitted (the pattern deploy /// artifacts uses): a bulk instance deploy is far more consequential than /// an artifact push, so the target site is required rather than defaulted. /// /// private static Command BuildSite(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var siteIdOption = new Option("--site-id") { Description = "Target site ID", Required = true }; // A flag, not a value option: absent means the default (skip unchanged // instances), present means redeploy them. Disabled instances are skipped // either way — force is not an override for that. var forceOption = new Option("--force") { Description = "Redeploy instances whose config is unchanged (default: skip them — a redeploy " + "resets static overrides and native alarm state at the site)", }; var cmd = new Command("site") { Description = "Deploy every deployable instance at a site" }; cmd.Add(siteIdOption); cmd.Add(forceOption); cmd.SetAction(async (ParseResult result) => { var siteId = result.GetValue(siteIdOption); var force = result.GetValue(forceOption); return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeploySiteCommand(siteId, force), // A bulk deploy is N instance round-trips; the default 30 s client // timeout would abandon the request while the server is still // applying. Matches the server-side long-running Ask window. timeout: BulkDeployTimeout, onSuccess: json => HandleSiteDeploymentResponse(json, CommandHelpers.ResolveFormat(result, formatOption, CliConfig.Load()))); }); return cmd; } /// /// The server owns the bounded overall bulk deadline; do not abandon it with a shorter client timer. /// internal static readonly TimeSpan BulkDeployTimeout = Timeout.InfiniteTimeSpan; internal static int HandleSiteDeploymentResponse(string json, string format) { CommandHelpers.HandleResponse(new ManagementResponse(200, json, null, null), format); try { using var payload = JsonDocument.Parse(json); if (payload.RootElement.TryGetProperty("failureCount", out var failures) && failures.TryGetInt32(out var count) && count >= 0) return count == 0 ? 0 : 1; } catch (JsonException) { } OutputFormatter.WriteError("The server returned an invalid site deployment summary.", "INVALID_RESPONSE"); return 1; } private static Command BuildArtifacts(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var siteIdOption = new Option("--site-id") { Description = "Target site ID (all sites if omitted)" }; var cmd = new Command("artifacts") { Description = "Deploy artifacts to site(s)" }; cmd.Add(siteIdOption); cmd.SetAction(async (ParseResult result) => { var siteId = result.GetValue(siteIdOption); return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeployArtifactsCommand(siteId)); }); return cmd; } private static Command BuildStatus(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var instanceIdOption = new Option("--instance-id") { Description = "Filter by instance ID" }; // The valid set is named in the help text rather than enforced with // AcceptOnlyFromAmong: that check is case-SENSITIVE, and the server has // always accepted `--status success`. The server rejects an unrecognised // value with the same list (register row 88) and is the authoritative // check — HTTP API callers never pass through this option at all. var statusOption = new Option("--status") { Description = "Filter by status (Pending, InProgress, Success, Failed; case-insensitive)", }; var pageOption = new Option("--page") { Description = "Page number" }; pageOption.DefaultValueFactory = _ => 1; var pageSizeOption = new Option("--page-size") { Description = "Page size (1-1000; 0 or unset uses 50)" }; pageSizeOption.DefaultValueFactory = _ => 50; var cmd = new Command("status") { Description = "Query deployment status" }; cmd.Add(instanceIdOption); cmd.Add(statusOption); cmd.Add(pageOption); cmd.Add(pageSizeOption); cmd.SetAction(async (ParseResult result) => { var instanceId = result.GetValue(instanceIdOption); var status = result.GetValue(statusOption); var page = result.GetValue(pageOption); var pageSize = result.GetValue(pageSizeOption); return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new QueryDeploymentsCommand(instanceId, status, page, pageSize), tableProjector: DeploymentStatusTableProjection.ProjectRows); }); return cmd; } }