fix(site-secrets): ship an empty Secrets list, not null, when a site references no secrets

The site applies DeployArtifactsCommand.Secrets as the full set (rows absent from it are
soft-removed and DeployedSecretsMirror deletes them on both nodes), while null means
'central shipped no secrets' and touches nothing. The command builder shipped null for a
site with no references, so dropping the LAST secret: reference never removed the secret
from either site node. Found by the Task 13 live gate (check 5: site-a-a and site-a-b
both still held sb-gate-mxgw r1, deployed_secrets.is_removed=0 after the removal deploy).

Central now always ships a list. Old sites ignore proto field 9; the artifact hash is
unchanged (empty and null hash identically); SecretsDeployed stays null and no
DeploySiteSecrets audit row is written when nothing shipped. Regression test
NoSecretReferences_ShipsEmptySecretsList_SoTheSiteRemovesPreviouslyDeployedOnes (red
before, green after). Spec notes in Component-DeploymentManager/SiteRuntime + the design
doc's as-built section.
This commit is contained in:
Joseph Doherty
2026-09-28 20:43:36 -04:00
parent 1444f85919
commit ad19d4b158
5 changed files with 63 additions and 2 deletions
@@ -375,6 +375,15 @@ out below.
null-when-empty convention for every artifact class), so a site with no
secret references keeps its pre-existing hash across the upgrade that added
this feature.
- **Always a list on the wire.** `DeployArtifactsCommand.Secrets` is never
`null` from a secrets-aware central — a site with no secret references gets an
**empty** list. The site applies it as the full set (every `deployed_secrets`
row absent from it is soft-removed and the mirror deletes it on both nodes),
whereas `null` is the pre-feature "central shipped no secrets" contract that
touches nothing; shipping `null` once the last reference was dropped left the
secret live on both site nodes indefinitely (caught by the Task 13 live gate,
check 5). An old site ignores proto field 9, so the empty list costs it
nothing, and the hash is unaffected (empty == null above).
- **Audit.** One best-effort `DeploySiteSecrets` / `SiteSecrets` audit row is
written per site deploy that carried secrets or a missing-secret failure
(never for a site with no secret references at all) — after-state is `{