feat(transport): export site/instance selection end-to-end via CLI + ManagementActor (M8 B4)

This commit is contained in:
Joseph Doherty
2026-06-18 06:14:39 -04:00
parent d7dae24355
commit bdbf5cdab0
16 changed files with 450 additions and 12 deletions
@@ -2305,6 +2305,7 @@ public class ManagementActor : ReceiveActor
var externalRepo = sp.GetRequiredService<IExternalSystemRepository>();
var notifRepo = sp.GetRequiredService<INotificationRepository>();
var inboundRepo = sp.GetRequiredService<IInboundApiRepository>();
var siteRepo = sp.GetRequiredService<ISiteRepository>();
var templates = await templateRepo.GetAllTemplatesAsync();
var sharedScripts = await templateRepo.GetAllSharedScriptsAsync();
@@ -2314,6 +2315,10 @@ public class ManagementActor : ReceiveActor
var smtpConfigs = await notifRepo.GetAllSmtpConfigurationsAsync();
// Inbound API keys are not transported between environments (re-arch C4); only methods.
var apiMethods = await inboundRepo.GetAllApiMethodsAsync();
// M8 (B4): site/instance-scoped selection. Sites match on SiteIdentifier
// (preferred) or Name; instances match on UniqueName.
var sites = await siteRepo.GetAllSitesAsync();
var instances = await templateRepo.GetAllInstancesAsync();
int[] ResolveIds<T>(IReadOnlyList<T> all, IReadOnlyList<string>? names,
Func<T, string> getName, Func<T, int> getId, string entityType)
@@ -2333,6 +2338,33 @@ public class ManagementActor : ReceiveActor
return matched;
}
// Sites accept EITHER the SiteIdentifier (preferred) or the friendly Name
// per requested token, so this needs a two-key matcher rather than the
// single-key ResolveIds above. Under All=true every site is included.
int[] ResolveSiteIds()
{
if (cmd.All) return sites.Select(s => s.Id).ToArray();
var names = cmd.SiteNames;
if (names is null || names.Count == 0) return Array.Empty<int>();
var matchedIds = new List<int>();
var unmatched = new List<string>();
foreach (var token in names)
{
var site = sites.FirstOrDefault(s =>
string.Equals(s.SiteIdentifier, token, StringComparison.Ordinal))
?? sites.FirstOrDefault(s =>
string.Equals(s.Name, token, StringComparison.Ordinal));
if (site is null) unmatched.Add(token);
else matchedIds.Add(site.Id);
}
if (unmatched.Count > 0)
{
throw new ManagementCommandException(
$"Unknown site identifier/name(s): {string.Join(", ", unmatched.OrderBy(n => n, StringComparer.Ordinal))}.");
}
return matchedIds.Distinct().ToArray();
}
var selection = new ExportSelection(
TemplateIds: ResolveIds(templates, cmd.TemplateNames, t => t.Name, t => t.Id, "template"),
SharedScriptIds: ResolveIds(sharedScripts, cmd.SharedScriptNames, s => s.Name, s => s.Id, "shared script"),
@@ -2343,7 +2375,11 @@ public class ManagementActor : ReceiveActor
// preview row shows the Host value, so the CLI uses Host too.
SmtpConfigurationIds: ResolveIds(smtpConfigs, cmd.SmtpConfigurationNames, s => s.Host, s => s.Id, "SMTP configuration"),
ApiMethodIds: ResolveIds(apiMethods, cmd.ApiMethodNames, m => m.Name, m => m.Id, "API method"),
IncludeDependencies: cmd.IncludeDependencies);
IncludeDependencies: cmd.IncludeDependencies,
// M8 (B4): site/instance-scoped selection. Under All=true include every
// site + instance, mirroring how All includes every template/etc.
SiteIds: ResolveSiteIds(),
InstanceIds: ResolveIds(instances, cmd.InstanceNames, i => i.UniqueName, i => i.Id, "instance"));
var exporter = sp.GetRequiredService<IBundleExporter>();
await using var stream = await exporter.ExportAsync(