feat(cli): deploy area --id <areaId> [--force] bulk-deploys an area subtree with deploy site's deadline and exit-code contract (Gitea #40)

This commit is contained in:
Joseph Doherty
2026-10-22 14:12:46 -04:00
parent f72c72c52f
commit 74d5fba2b0
5 changed files with 165 additions and 3 deletions
@@ -93,6 +93,7 @@ internal static class CommandHelpers
// Every alias of a deploy contract shares the same policy. The server bounds
// the operation; a shorter client deadline can report failure while it applies.
var requestTimeout = timeout ?? (command is MgmtDeployInstanceCommand or MgmtDeployArtifactsCommand or MgmtDeploySiteCommand
or MgmtDeployAreaCommand
? Timeout.InfiniteTimeSpan
: TimeSpan.FromSeconds(30));
var response = await client.SendCommandAsync(commandName, command, requestTimeout);
@@ -21,6 +21,7 @@ public static class DeployCommands
command.Add(BuildInstance(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSite(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildArea(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildArtifacts(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildStatus(urlOption, formatOption, usernameOption, passwordOption));
@@ -84,6 +85,41 @@ public static class DeployCommands
return cmd;
}
/// <summary>
/// Builds <c>deploy area</c> — bulk-deploy every instance in one area subtree
/// (the area plus all of its descendant areas), Gitea #40.
///
/// <para>
/// <c>deploy site</c> narrowed to an area: same skip rules (Disabled always,
/// up-to-date unless <c>--force</c>), same not-all-or-nothing summary, same
/// server-owned deadline and the same exit-code contract.
/// </para>
/// </summary>
private static Command BuildArea(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Area ID (its whole subtree is deployed)", Required = true };
var forceOption = new Option<bool>("--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("area") { Description = "Deploy every deployable instance in an area and its descendant areas" };
cmd.Add(idOption);
cmd.Add(forceOption);
cmd.SetAction(async (ParseResult result) =>
{
var areaId = result.GetValue(idOption);
var force = result.GetValue(forceOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new MgmtDeployAreaCommand(areaId, force),
timeout: BulkDeployTimeout,
onSuccess: json => HandleSiteDeploymentResponse(json, CommandHelpers.ResolveFormat(result, formatOption, CliConfig.Load())));
});
return cmd;
}
/// <summary>
/// The server owns the bounded overall bulk deadline; do not abandon it with a shorter client timer.
/// </summary>
@@ -91,8 +127,8 @@ public static class DeployCommands
/// <summary>
/// Renders a deployment summary and turns its <c>failureCount</c> into the exit code:
/// 0 when every site/instance succeeded, 1 when any failed. Shared by <c>deploy site</c>
/// and both artifact-deploy commands (<c>deploy artifacts</c>, <c>site deploy-artifacts</c>),
/// 0 when every site/instance succeeded, 1 when any failed. Shared by <c>deploy site</c>,
/// <c>deploy area</c> and both artifact-deploy commands (<c>deploy artifacts</c>, <c>site deploy-artifacts</c>),
/// whose summaries all carry <c>failureCount</c> — a per-site failure is a successful HTTP
/// call, so without this the process exited 0 and scripts could not detect it.
/// </summary>
+20 -1
View File
@@ -1101,6 +1101,25 @@ Output is a per-instance result matrix (`instanceId`, `uniqueName`, `deploymentI
`success`, `errorMessage`) plus `successCount` / `failureCount`, and `skippedInstances`
(`instanceId`, `uniqueName`, `reason`).
#### `deploy area`
Deploy every deployable instance in one **area subtree** — the area itself plus every
area nested beneath it at any depth (Gitea #40). It is `deploy site` narrowed to that
subtree, with identical rules: Disabled instances are always skipped, up-to-date ones
are skipped unless `--force`, the batch is not all-or-nothing, and the output is the
same summary (`siteIdentifier` is the area's site). Instances with no area are only
reachable through `deploy site`. Same deadline policy as `deploy site`, and the same
exit-code rule: `1` when any instance failed (or the summary is invalid), else `0`.
```sh
scadabridge --url <url> deploy area --id <areaId> [--force]
```
| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Area ID. A site-scoped (non-Administrator) Deployer must name an area of an in-scope site; an unknown area is denied to them exactly like an out-of-scope one. |
| `--force` | no | Redeploy instances whose config is unchanged (default: skip them). Disabled instances are still skipped. |
#### `deploy artifacts`
Deploy compiled artifacts to one or all sites (same as `site deploy-artifacts`).
@@ -2665,7 +2684,7 @@ run's outbound API / DB / notification rows, which are always audited.
## Deployment deadlines
`deploy instance`, `instance deploy`, `deploy artifacts`, `site deploy-artifacts`,
and `deploy site` defer the overall deadline to the Management API. They keep the
`deploy site` and `deploy area` defer the overall deadline to the Management API. They keep the
bounded socket-connect timeout but do not impose the ordinary 30-second client
request deadline on a deployment that may still be applying. The API's configured
long-running timeout remains authoritative. Ordinary commands retain a 30-second