feat(deploymentmanager): DeployAreaAsync bulk-deploys an area subtree through the shared DeploySiteAsync core; PreviewSubtreeDeployAsync returns confirm counts (Gitea #40)

DeploySiteAsync's body moves into a private core taking the site plus an
optional area-id filter; site deploy behaviour is unchanged. The subtree is
one in-memory ParentAreaId walk (cycle-safe) shared by the deploy and the
preview. DeploymentService gains an ITemplateEngineRepository dependency for
the area lookups.
This commit is contained in:
Joseph Doherty
2026-10-22 14:07:33 -04:00
parent b9751473a6
commit eb1c9ea7f9
9 changed files with 568 additions and 20 deletions
@@ -219,6 +219,30 @@ runs for it. An escape from phase 1 or phase 3 unwinds every not-yet-finalised
entry the same way: `Failed` status plus lock release, so no instance touched by
a cancelled bulk deploy can be left holding its lock.
### Area-subtree deployment (`DeployAreaAsync`, Gitea #40)
`DeployAreaAsync(areaId, user, force)` is the same bulk deploy narrowed to one
area **subtree** — the area itself plus every area nested beneath it at any
depth — surfaced as the `MgmtDeployArea` management command, the CLI
`deploy area --id`, and the Central UI Topology page's **Deploy all…** context
item on area nodes (the site node's item calls `DeploySiteAsync`). Both entry
points run one private core (`DeployInstancesOfSiteAsync`) that loads the site's
instances, keeps only those whose `AreaId` is in the subtree (area-less instances
belong to the whole-site scope only), and then applies everything above
unchanged: the Disabled skip (even under `force`), the up-to-date skip unless
`force`, the three phases, not-all-or-nothing, and the same
`SiteDeploymentSummary` with `SkippedInstances`. An unknown area is a failure
result, not an empty success.
The subtree is resolved from ONE read of the site's areas
(`GetAreasBySiteIdAsync`) walked in memory over `ParentAreaId`; each area is
visited at most once, so a corrupt parent chain that loops cannot hang the walk.
`PreviewSubtreeDeployAsync(siteId, areaId?)` shares that walk to return the
confirm dialog's `SubtreeDeployPreview(Total, Disabled)` counts (`areaId` null =
the whole site; an area of another site is a failure) — a plain DB read with no
staleness probe, so the up-to-date count is only known once the deploy reports
its skipped rows.
`DeployInstanceAsync` is composed from the same three phase helpers with a batch of
one, so the two entry points cannot drift on deployment identity, idempotency, lock
coverage, or optimistic concurrency.