fix(site-secrets): contain secret-store outages per site; audit removal-only deploys

Secret-store outage (final review M5): SiteSecretsCollector returns a store
read failure as SiteSecretsCollection.StoreError instead of throwing. A deploy
now fails only the sites that reference a secret (nothing sent, not stamped,
DeploySiteSecrets row with the error), and ComputeCurrentArtifactHashesAsync
omits only those sites (they read Out of date) instead of the exception
aborting the whole sweep and the fleet-wide pending-updates check.

Removal-only deploys: the site reply gains SecretsRemoved (proto field 7,
names only) - the deployed_secrets rows the deploy soft-removed. Central
carries it on SiteArtifactResult and writes the DeploySiteSecrets row with a
removed list whenever it is non-empty, so dropping a site's last secret:
reference is audited centrally rather than only in site logs.
This commit is contained in:
Joseph Doherty
2026-09-28 21:23:52 -04:00
parent ef03a0dca7
commit 29049c4ef6
15 changed files with 875 additions and 423 deletions
@@ -1,9 +1,22 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
/// <summary>
/// A site's reply to a <see cref="DeployArtifactsCommand"/>.
/// </summary>
/// <param name="SecretsApplied">
/// How many of the shipped secrets this node applied and verified; <c>null</c> from a site that
/// predates secret deployment or when central shipped no secret list.
/// </param>
/// <param name="SecretsRemoved">
/// Names of previously-deployed secrets this deploy retired (marked removed in the site's
/// <c>deployed_secrets</c> table because the shipped set no longer contains them). Names only —
/// never values. <c>null</c> when none were removed or the site predates the field.
/// </param>
public record ArtifactDeploymentResponse(
string DeploymentId,
string SiteId,
bool Success,
string? ErrorMessage,
DateTimeOffset Timestamp,
int? SecretsApplied = null);
int? SecretsApplied = null,
IReadOnlyList<string>? SecretsRemoved = null);