docs(comments): strip internal task/milestone/bundle bookkeeping from code comments

Remove project bookkeeping citations from shipped code comments across the
solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/
issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels,
and C/D/K/S/T phase labels.

Comment text only — no code logic, string/log literals, or XML-doc structure
changed. Genuine descriptions are preserved (only the citation is stripped),
and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365,
UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker
TaskReferenceInComment / TrackingReferenceInComment checks plus targeted
grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
Joseph Doherty
2026-07-07 11:03:26 -04:00
parent 67005ca4c0
commit 9cff87fe85
435 changed files with 2338 additions and 2547 deletions
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Communication;
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
/// <summary>
/// WP-7: System-wide artifact deployment.
/// System-wide artifact deployment.
/// Broadcasts artifacts (shared scripts, external systems, DB connections, and
/// data connections) to all sites with per-site tracking.
///
@@ -47,7 +47,7 @@ public class ArtifactDeploymentService
/// <param name="templateRepo">Repository for templates.</param>
/// <param name="externalSystemRepo">Repository for external systems.</param>
/// <param name="notificationRepo">
/// DeploymentManager-025: retained on the signature for DI/source compatibility but
/// Retained on the signature for DI/source compatibility but
/// intentionally NOT consumed. Notification lists and SMTP configuration are
/// central-only and are never shipped to sites, so the artifact path must not read
/// the notification repository at all.
@@ -71,7 +71,7 @@ public class ArtifactDeploymentService
_deploymentRepo = deploymentRepo;
_templateRepo = templateRepo;
_externalSystemRepo = externalSystemRepo;
// DeploymentManager-025: notificationRepo is deliberately not stored — notification
// notificationRepo is deliberately not stored — notification
// lists and SMTP configs are central-only and are never fetched for shipping to sites.
_ = notificationRepo;
_communicationService = communicationService;
@@ -87,14 +87,14 @@ public class ArtifactDeploymentService
/// <param name="siteId">The DB id of the site whose data connections are collected.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="deploymentId">
/// DeploymentManager-010: the logical deployment id for this artifact deployment. All per-site
/// The logical deployment id for this artifact deployment. All per-site
/// commands of one <see cref="DeployToAllSitesAsync"/> call share this id so the audit log,
/// UI summary, and persisted record correlate. When <c>null</c> a fresh id is minted (used by
/// single-site retries).
/// </param>
/// <returns>A deployment artifacts command for the site.</returns>
/// <remarks>
/// DeploymentManager-023: this convenience overload runs the global artifact queries
/// This convenience overload runs the global artifact queries
/// for a single site (used by <see cref="RetryForSiteAsync"/>). The multi-site
/// <see cref="DeployToAllSitesAsync"/> path hoists the global queries OUT of the
/// per-site loop and calls the prefetched-globals overload to avoid the N+1
@@ -115,12 +115,12 @@ public class ArtifactDeploymentService
/// DB connections). Only the per-site data-connection query runs here.
/// </summary>
/// <remarks>
/// DeploymentManager-023: separating the global fetch from the per-site build lets
/// Separating the global fetch from the per-site build lets
/// <see cref="DeployToAllSitesAsync"/> issue the global queries exactly once across
/// the whole multi-site sweep, eliminating the N+1 re-query of shared scripts,
/// external systems, methods, and DB connections.
///
/// DeploymentManager-025: the command's <c>NotificationLists</c> and
/// The command's <c>NotificationLists</c> and
/// <c>SmtpConfigurations</c> fields are always sent <c>null</c> — notification
/// delivery is central-only and no notification artifact or SMTP credential is
/// ever distributed to a site. The fields remain on the contract only for
@@ -143,10 +143,10 @@ public class ArtifactDeploymentService
globals.SharedScripts,
globals.ExternalSystems,
globals.DatabaseConnections,
// DeploymentManager-025: notification lists are central-only — never shipped to sites.
// Notification lists are central-only — never shipped to sites.
NotificationLists: null,
dataConnectionArtifacts,
// DeploymentManager-025: SMTP config (incl. credentials) is central-only — never shipped to sites.
// SMTP config (incl. credentials) is central-only — never shipped to sites.
SmtpConfigurations: null,
DateTimeOffset.UtcNow);
}
@@ -158,11 +158,11 @@ public class ArtifactDeploymentService
/// once before the per-site loop.
/// </summary>
/// <remarks>
/// DeploymentManager-023: the per-site artifact build path previously re-issued
/// The per-site artifact build path previously re-issued
/// every one of these queries per site (≈ N + M·N round trips for N sites
/// and M external systems). Hoisting them here drops that to a single fetch.
///
/// DeploymentManager-025: notification lists and SMTP configurations are NOT
/// Notification lists and SMTP configurations are NOT
/// fetched here. Notification delivery is central-only, so they are never
/// shipped to sites — the artifact path must not even read them.
/// </remarks>
@@ -208,9 +208,9 @@ public class ArtifactDeploymentService
/// <summary>
/// Bag of the global artifact sets that do not vary per site, captured once at
/// the start of <see cref="DeployToAllSitesAsync"/> and reused for every per-site
/// command build (DeploymentManager-023). Notification lists and SMTP
/// command build. Notification lists and SMTP
/// configurations are deliberately absent — they are central-only and never
/// shipped to sites (DeploymentManager-025).
/// shipped to sites.
/// </summary>
private sealed record GlobalArtifactSnapshot(
IReadOnlyList<SharedScriptArtifact> SharedScripts,
@@ -235,15 +235,15 @@ public class ArtifactDeploymentService
var deploymentId = Guid.NewGuid().ToString("N");
var perSiteResults = new Dictionary<string, SiteArtifactResult>();
// DeploymentManager-023: hoist the system-wide artifact queries (shared scripts,
// Hoist the system-wide artifact queries (shared scripts,
// external systems + methods, DB connections) OUT of the per-site loop so they
// run ONCE instead of once per site. Only data connections legitimately vary
// per site, so they stay inside the loop. (Notification lists and SMTP config
// are central-only and not fetched at all — DeploymentManager-025.)
// are central-only and not fetched at all.)
var globals = await FetchGlobalArtifactsAsync(cancellationToken);
// Build per-site commands sequentially (DbContext is not thread-safe).
// DeploymentManager-010: every per-site command carries the SAME logical
// Every per-site command carries the SAME logical
// deploymentId, so the per-site commands, audit log, persisted record,
// and UI summary all reference one id instead of N+1 unrelated GUIDs.
var siteCommands = new Dictionary<int, DeployArtifactsCommand>();
@@ -301,7 +301,7 @@ public class ArtifactDeploymentService
}
// Persist the system artifact deployment record.
// DeploymentManager-010: SystemArtifactDeploymentRecord has no dedicated
// SystemArtifactDeploymentRecord has no dedicated
// DeploymentId column (adding one is a Commons/ConfigurationDatabase
// schema change outside this module). The logical deploymentId is
// embedded in the PerSiteStatus payload so the persisted record can be
@@ -333,7 +333,7 @@ public class ArtifactDeploymentService
}
/// <summary>
/// WP-7: Retry artifact deployment to a specific site that previously failed.
/// Retry artifact deployment to a specific site that previously failed.
/// </summary>
/// <param name="siteDbId">The database identifier of the site.</param>
/// <param name="siteIdentifier">The site identifier string.</param>
@@ -371,7 +371,7 @@ public class ArtifactDeploymentService
}
/// <summary>
/// WP-7: Per-site result for artifact deployment.
/// Per-site result for artifact deployment.
/// </summary>
public record SiteArtifactResult(
string SiteId,
@@ -380,7 +380,7 @@ public record SiteArtifactResult(
string? ErrorMessage);
/// <summary>
/// WP-7: Summary of system-wide artifact deployment with per-site results.
/// Summary of system-wide artifact deployment with per-site results.
/// </summary>
public record ArtifactDeploymentSummary(
string DeploymentId,
@@ -6,15 +6,15 @@ namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
public class DeploymentManagerOptions
{
/// <summary>
/// WP-6: Timeout for a lifecycle command round-trip (disable, enable, delete).
/// Timeout for a lifecycle command round-trip (disable, enable, delete).
/// Applied as a linked-CTS deadline in <c>DeploymentService</c> so a hung or
/// unreachable site does not hold the per-instance operation lock indefinitely.
/// </summary>
public TimeSpan LifecycleCommandTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>WP-7: Timeout per site for system-wide artifact deployment.</summary>
/// <summary>Timeout per site for system-wide artifact deployment.</summary>
public TimeSpan ArtifactDeploymentTimeoutPerSite { get; set; } = TimeSpan.FromSeconds(120);
/// <summary>WP-3: Timeout for acquiring an operation lock on an instance.</summary>
/// <summary>Timeout for acquiring an operation lock on an instance.</summary>
public TimeSpan OperationLockTimeout { get; set; } = TimeSpan.FromSeconds(5);
}
@@ -19,20 +19,20 @@ using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
/// <summary>
/// WP-1: Central-side deployment orchestration service.
/// Central-side deployment orchestration service.
/// Coordinates the full deployment pipeline:
/// 1. Validate instance state transition (WP-4)
/// 2. Acquire per-instance operation lock (WP-3)
/// 3. Flatten configuration via TemplateEngine (captures template state at time of flatten -- WP-16)
/// 1. Validate instance state transition
/// 2. Acquire per-instance operation lock
/// 3. Flatten configuration via TemplateEngine (captures template state at time of flatten)
/// 4. Validate flattened configuration
/// 5. Compute revision hash and diff
/// 6. Stage a PendingDeployment row + send RefreshDeploymentCommand (notify-and-fetch; site fetches the config over HTTP)
/// 7. Track deployment status with optimistic concurrency (WP-4)
/// 8. Store deployed config snapshot (WP-8)
/// 7. Track deployment status with optimistic concurrency
/// 8. Store deployed config snapshot
/// 9. Audit log all actions
///
/// WP-2: Each deployment has a unique deployment ID (GUID) + revision hash.
/// WP-16: Template state captured at flatten time -- last-write-wins on templates is safe.
/// Each deployment has a unique deployment ID (GUID) + revision hash.
/// Template state captured at flatten time -- last-write-wins on templates is safe.
/// </summary>
public class DeploymentService
{
@@ -52,7 +52,7 @@ public class DeploymentService
/// <summary>
/// Prefix written to <see cref="DeploymentRecord.ErrorMessage"/> when a
/// deployment fails because the site command timed out or was cancelled.
/// Used by the query-before-redeploy trigger (DeploymentManager-006) to tell
/// Used by the query-before-redeploy trigger to tell
/// a timeout-induced failure apart from other deployment errors.
/// </summary>
private const string TimeoutFailurePrefix = "Communication failure:";
@@ -111,7 +111,7 @@ public class DeploymentService
}
/// <summary>
/// CentralUI-006: raises a push notification that a deployment record's
/// Raises a push notification that a deployment record's
/// status was just persisted, so the Central UI deployment-status page can
/// re-render over its SignalR circuit instead of polling. Called at every
/// point a <see cref="DeploymentRecord"/> status is written.
@@ -124,7 +124,7 @@ public class DeploymentService
/// Resolves the site's string identifier from the numeric DB ID.
/// The communication layer routes by string identifier (e.g. "site-a"), not DB ID.
///
/// DeploymentManager-021: when the <see cref="Site"/> row is missing (FK was
/// When the <see cref="Site"/> row is missing (FK was
/// deleted, race with admin delete, DB inconsistency) the previous behaviour
/// silently substituted the numeric id rendered as a string — every
/// downstream `CommunicationService` call then failed with a confusing
@@ -146,12 +146,12 @@ public class DeploymentService
}
/// <summary>
/// WP-1: Deploy an instance to its site.
/// WP-2: Generates unique deployment ID, computes revision hash.
/// WP-4: Validates state transitions, uses optimistic concurrency.
/// WP-5: Site-side apply is all-or-nothing (handled by DeploymentManagerActor).
/// WP-8: Stores deployed config snapshot on success.
/// WP-16: Captures template state at time of flatten.
/// Deploy an instance to its site.
/// Generates unique deployment ID, computes revision hash.
/// Validates state transitions, uses optimistic concurrency.
/// Site-side apply is all-or-nothing (handled by DeploymentManagerActor).
/// Stores deployed config snapshot on success.
/// Captures template state at time of flatten.
/// </summary>
/// <param name="instanceId">The database ID of the instance to deploy.</param>
/// <param name="user">The username initiating the deployment, recorded in the audit log.</param>
@@ -167,19 +167,19 @@ public class DeploymentService
if (instance == null)
return Result<DeploymentRecord>.Failure($"Instance with ID {instanceId} not found.");
// WP-4: Validate state transition
// Validate state transition
var transitionError = StateTransitionValidator.ValidateTransition(instance.State, "deploy");
if (transitionError != null)
return Result<DeploymentRecord>.Failure(transitionError);
// WP-3: Acquire per-instance operation lock
// Acquire per-instance operation lock
using var lockHandle = await _lockManager.AcquireAsync(
instance.UniqueName, _options.OperationLockTimeout, cancellationToken);
// WP-2: Generate unique deployment ID
// Generate unique deployment ID
var deploymentId = Guid.NewGuid().ToString("N");
// WP-1/16: Flatten configuration (captures template state at this point in time)
// Flatten configuration (captures template state at this point in time)
var flattenResult = await _flatteningPipeline.FlattenAndValidateAsync(instanceId, cancellationToken);
if (flattenResult.IsFailure)
return Result<DeploymentRecord>.Failure($"Validation failed: {flattenResult.Error}");
@@ -190,7 +190,7 @@ public class DeploymentService
if (!validationResult.IsValid)
{
// Followup #8: return a grouped/summarized error (leading count + per-module
// Return a grouped/summarized error (leading count + per-module
// rollup, capped) instead of a flat semicolon-joined dump that becomes a wall
// of text for instances with dozens of unbound attributes. The full per-entry
// list still goes to the deploy log for operators who need every clause.
@@ -208,7 +208,7 @@ public class DeploymentService
// snapshot on success / reconciliation).
var configJson = JsonSerializer.Serialize(flattenedConfig);
// DeploymentManager-006: query-the-site-before-redeploy idempotency.
// Query-the-site-before-redeploy idempotency.
// If a prior deployment for this instance is stuck InProgress or Failed
// due to a timeout, the site may have actually applied the config. Query
// the site for its currently-applied revision before re-sending so a
@@ -230,13 +230,13 @@ public class DeploymentService
return Result<DeploymentRecord>.Failure(
"CentralFetchBaseUrl is not configured — required for deployment (notify-and-fetch).");
// WP-4: Create the deployment record directly in InProgress.
// Create the deployment record directly in InProgress.
//
// DeploymentManager-022: the previous code wrote the record as Pending,
// The previous code wrote the record as Pending,
// then immediately updated it to InProgress with no work in between
// (flattening, validation, and reconciliation all completed above). The
// back-to-back write cost an extra SaveChangesAsync round-trip, an
// extra IDeploymentStatusNotifier push (CentralUI-006 rendered a
// extra IDeploymentStatusNotifier push (which rendered a
// Pending→InProgress flicker for ~ms), and an extra row-version bump
// for nothing. The transient Pending slot carried no operational
// meaning — it was set and immediately overwritten — so dropping it
@@ -290,12 +290,12 @@ public class DeploymentService
// CommunicationOptions.PendingDeploymentPurgeInterval cadence.
var response = await _communicationService.RefreshDeploymentAsync(siteId, command, cancellationToken);
// WP-1: Update status based on site response.
// Update status based on site response.
record.Status = response.Status;
record.ErrorMessage = response.ErrorMessage;
record.CompletedAt = DateTimeOffset.UtcNow;
// DeploymentManager-003: once the site has confirmed the apply,
// Once the site has confirmed the apply,
// commit the deployment record's terminal status BEFORE touching
// instance state and the deployed-config snapshot. If a later write
// (instance update / snapshot store) fails, the recorded fact that
@@ -342,12 +342,12 @@ public class DeploymentService
}
catch (Exception ex)
{
// DeploymentManager-001: any exception out of the try (timeout,
// Any exception out of the try (timeout,
// cancellation, transport, serialization, DB) must leave the
// deployment record as Failed -- the design requires an interrupted
// deployment to be treated as failed, never stuck in InProgress.
//
// DeploymentManager-002: the failure-status write must NOT use the
// The failure-status write must NOT use the
// operation's cancellation token. If the operation was cancelled or
// timed out, that token is already cancelled and the cleanup writes
// would themselves throw before the Failed status is persisted.
@@ -398,7 +398,7 @@ public class DeploymentService
}
/// <summary>
/// WP-6: Disable an instance. Stops Instance Actor, retains config, S&amp;F drains.
/// Disable an instance. Stops Instance Actor, retains config, S&amp;F drains.
/// </summary>
/// <param name="instanceId">The database ID of the instance to disable.</param>
/// <param name="user">The username initiating the operation, recorded in the audit log.</param>
@@ -424,7 +424,7 @@ public class DeploymentService
var siteId = await ResolveSiteIdentifierAsync(instance.SiteId, cancellationToken);
var command = new DisableInstanceCommand(commandId, instance.UniqueName, DateTimeOffset.UtcNow);
// WP-6: bound the round-trip with the configured lifecycle timeout so a
// Bound the round-trip with the configured lifecycle timeout so a
// hung/unreachable site does not block the operation lock indefinitely.
InstanceLifecycleResponse response;
try
@@ -435,7 +435,7 @@ public class DeploymentService
}
catch (Exception ex) when (ex is TimeoutException or OperationCanceledException)
{
// DeploymentManager-019: a lifecycle command timeout produced no
// A lifecycle command timeout produced no
// audit row pre-fix — the operator saw a timeout in the UI but
// the audit trail showed nothing happened, contrary to the
// design's "audit logging for all instance lifecycle changes"
@@ -468,7 +468,7 @@ public class DeploymentService
}
/// <summary>
/// WP-6: Enable an instance. Re-creates Instance Actor from stored config.
/// Enable an instance. Re-creates Instance Actor from stored config.
/// </summary>
/// <param name="instanceId">The database ID of the instance to enable.</param>
/// <param name="user">The username initiating the operation, recorded in the audit log.</param>
@@ -494,7 +494,7 @@ public class DeploymentService
var siteId = await ResolveSiteIdentifierAsync(instance.SiteId, cancellationToken);
var command = new EnableInstanceCommand(commandId, instance.UniqueName, DateTimeOffset.UtcNow);
// WP-6: bound the round-trip with the configured lifecycle timeout.
// Bound the round-trip with the configured lifecycle timeout.
InstanceLifecycleResponse response;
try
{
@@ -504,7 +504,7 @@ public class DeploymentService
}
catch (Exception ex) when (ex is TimeoutException or OperationCanceledException)
{
// DeploymentManager-019: emit an audit entry on lifecycle timeout
// Emit an audit entry on lifecycle timeout
// so the operator's attempted Enable is recorded; see the matching
// comment in DisableInstanceAsync for the full rationale.
await TryLogLifecycleTimeoutAsync(
@@ -532,7 +532,7 @@ public class DeploymentService
}
/// <summary>
/// WP-6: Delete an instance. Stops the site actor, removes site config, and
/// Delete an instance. Stops the site actor, removes site config, and
/// removes the central instance record (deployment history, snapshot,
/// overrides, and connection bindings go with it). S&amp;F NOT cleared.
/// Delete fails if the site is unreachable within
@@ -563,7 +563,7 @@ public class DeploymentService
var siteId = await ResolveSiteIdentifierAsync(instance.SiteId, cancellationToken);
var command = new DeleteInstanceCommand(commandId, instance.UniqueName, DateTimeOffset.UtcNow);
// WP-6: bound the round-trip with the configured lifecycle timeout.
// Bound the round-trip with the configured lifecycle timeout.
InstanceLifecycleResponse response;
try
{
@@ -573,7 +573,7 @@ public class DeploymentService
}
catch (Exception ex) when (ex is TimeoutException or OperationCanceledException)
{
// DeploymentManager-019: emit an audit entry on lifecycle timeout
// Emit an audit entry on lifecycle timeout
// so the operator's attempted Delete is recorded; see the matching
// comment in DisableInstanceAsync for the full rationale.
await TryLogLifecycleTimeoutAsync(
@@ -590,7 +590,7 @@ public class DeploymentService
// Deployment records, snapshot, overrides, and connection bindings
// are removed with it (see repository implementation).
//
// DeploymentManager-004: the site has already destroyed the Instance
// The site has already destroyed the Instance
// Actor and removed its config. If the central record removal now
// fails (DB error / concurrency), the exception must NOT escape
// uncaught -- that would leave the central record orphaned and
@@ -630,7 +630,7 @@ public class DeploymentService
}
/// <summary>
/// WP-8: Get the deployed config snapshot and compare with current
/// Get the deployed config snapshot and compare with current
/// template-derived state. Produces both a staleness flag and — per the
/// design's "Diff View" — a structured <see cref="ConfigurationDiff"/> of
/// added/removed/changed attributes, alarms, and scripts (including data
@@ -658,7 +658,7 @@ public class DeploymentService
// I-1 (latent): the snapshot's ConfigurationJson + RevisionHash froze the
// FLATTENED config at deploy time. The current config is a FRESH flatten,
// now always in native List form (#93 consolidated element-type/coercion
// now always in native List form (consolidated element-type/coercion
// into AttributeValueCodec, which emits native-form JSON arrays). A List
// attribute deployed in the OLD quoted form (e.g. ["10","20"]) therefore
// both (a) hashes differently from the native re-flatten — a spurious
@@ -669,7 +669,7 @@ public class DeploymentService
// equal to the native re-flatten, then drive BOTH the staleness hash and
// the diff off that normalized snapshot. Scalars are left untouched.
//
// DeploymentManager-007: a snapshot that cannot be deserialized (corrupt /
// A snapshot that cannot be deserialized (corrupt /
// older schema) still yields the frozen-hash staleness result, with a
// null diff.
var deployedRevisionHash = snapshot.RevisionHash;
@@ -781,7 +781,7 @@ public class DeploymentService
}
/// <summary>
/// WP-2: Returns the current persisted <see cref="DeploymentRecord"/> for
/// Returns the current persisted <see cref="DeploymentRecord"/> for
/// the given deployment ID from the configuration database. This is a pure
/// local DB read — it does not contact the site. The query-the-site-before-
/// redeploy reconciliation (design: "Deployment Identity &amp; Idempotency")
@@ -799,7 +799,7 @@ public class DeploymentService
}
/// <summary>
/// DeploymentManager-006: query-the-site-before-redeploy reconciliation.
/// Query-the-site-before-redeploy reconciliation.
///
/// The site query is issued ONLY when a prior <see cref="DeploymentRecord"/>
/// for this instance is stuck <see cref="DeploymentStatus.InProgress"/>, or
@@ -810,11 +810,11 @@ public class DeploymentService
///
/// Reconciliation: if the site already has the TARGET revision hash, the
/// prior record is marked <see cref="DeploymentStatus.Success"/> (with its
/// <see cref="DeploymentRecord.RevisionHash"/> corrected to the target
/// DeploymentManager-016) and returned (the caller must NOT re-send the
/// <see cref="DeploymentRecord.RevisionHash"/> corrected to the target)
/// and returned (the caller must NOT re-send the
/// deploy). The same post-success side effects as the normal deploy path
/// are applied — instance <see cref="InstanceState.Enabled"/> and a stored
/// <see cref="DeployedConfigSnapshot"/> (DeploymentManager-015) — so central
/// <see cref="DeployedConfigSnapshot"/> — so central
/// and site state do not diverge. Otherwise <c>null</c> is returned and the
/// normal deploy proceeds.
///
@@ -875,7 +875,7 @@ public class DeploymentService
prior.Status = DeploymentStatus.Success;
prior.ErrorMessage = null;
prior.CompletedAt = DateTimeOffset.UtcNow;
// DeploymentManager-016: the prior record can legitimately carry a
// The prior record can legitimately carry a
// different (stale) revision hash than the current target. The site
// confirmed it is running the target revision, so the persisted
// record, the audit entry below, and the site must all agree.
@@ -884,13 +884,13 @@ public class DeploymentService
await _repository.SaveChangesAsync(cancellationToken);
NotifyStatusChange(prior);
// DeploymentManager-015: a reconciled deployment must perform the
// A reconciled deployment must perform the
// SAME post-success side effects as the normal deploy path — set
// the instance State to Enabled and store/refresh the deployed
// config snapshot — otherwise the central state machine and the
// deployed-snapshot invariant diverge from what the site is running.
//
// DeploymentManager-018: the reconciliation path runs only when the
// The reconciliation path runs only when the
// prior record is InProgress or timeout-Failed — exactly the cases
// that survive a central failover. The in-memory operation lock is
// lost on failover, so an operator may have legitimately invoked
@@ -906,7 +906,7 @@ public class DeploymentService
instance, prior.DeploymentId, targetRevisionHash, configJson,
forceEnabledState: false, cancellationToken);
// DeploymentManager-020: attribute the audit row to the user driving
// Attribute the audit row to the user driving
// THIS redeploy (the caller of DeployInstanceAsync), not the user
// who issued the original timed-out / stuck deployment. The original
// deployer is preserved in the detail object so forensics can still
@@ -930,7 +930,7 @@ public class DeploymentService
}
/// <summary>
/// DeploymentManager-006: the site is queried before a re-deploy only when a
/// The site is queried before a re-deploy only when a
/// prior record is stuck <see cref="DeploymentStatus.InProgress"/>, or is
/// <see cref="DeploymentStatus.Failed"/> because the site command timed out
/// (detected via the <see cref="TimeoutFailurePrefix"/> error-message
@@ -944,12 +944,12 @@ public class DeploymentService
/// <summary>
/// Post-success side effects shared by the normal deploy path and the
/// DeploymentManager-006 reconciliation path: set the instance
/// <see cref="InstanceState.Enabled"/> (WP-4) and store/refresh the
/// deployed config snapshot (WP-8). Factored into one helper so the two
/// paths cannot drift (DeploymentManager-015).
/// reconciliation path: set the instance
/// <see cref="InstanceState.Enabled"/> and store/refresh the
/// deployed config snapshot. Factored into one helper so the two
/// paths cannot drift.
///
/// DeploymentManager-018: <paramref name="forceEnabledState"/> distinguishes
/// <paramref name="forceEnabledState"/> distinguishes
/// the two callers. The normal deploy path passes <c>true</c> — a fresh
/// successful apply legitimately puts the instance into <see cref="InstanceState.Enabled"/>
/// (the documented "Deploy on a Disabled instance also enables it" semantics
@@ -977,8 +977,8 @@ public class DeploymentService
{
try
{
// WP-4: Update instance state to Enabled on successful deployment.
// DeploymentManager-018: on the reconciliation path
// Update instance state to Enabled on successful deployment.
// On the reconciliation path
// (forceEnabledState=false) only promote NotDeployed → Enabled,
// preserving an intentional Disabled state set between the original
// timed-out deploy and the redeploy.
@@ -988,7 +988,7 @@ public class DeploymentService
}
await _repository.UpdateInstanceAsync(instance, cancellationToken);
// WP-8: Store deployed config snapshot
// Store deployed config snapshot
await StoreDeployedSnapshotAsync(
instance.Id, deploymentId, revisionHash, configJson, cancellationToken);
@@ -1005,7 +1005,7 @@ public class DeploymentService
}
/// <summary>
/// DeploymentManager-019: write a "&lt;Action&gt;TimedOut" audit entry on
/// Write a "&lt;Action&gt;TimedOut" audit entry on
/// behalf of a lifecycle command (Disable / Enable / Delete) whose site
/// round-trip exceeded <see cref="DeploymentManagerOptions.LifecycleCommandTimeout"/>.
///
@@ -1094,10 +1094,10 @@ public class DeploymentService
}
/// <summary>
/// WP-8: Result of comparing deployed vs template-derived configuration.
/// Result of comparing deployed vs template-derived configuration.
/// </summary>
/// <param name="Diff">
/// DeploymentManager-007: structured added/removed/changed detail for
/// Structured added/removed/changed detail for
/// attributes, alarms, and scripts. Null only when the deployed snapshot could
/// not be deserialized (corrupt / older schema), in which case
/// <see cref="IsStale"/> still reflects the hash comparison.
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
/// Default <see cref="IDeploymentStatusNotifier"/> implementation. A simple
/// in-process event broadcaster: registered as a DI singleton so it is shared
/// between the central-process <see cref="DeploymentService"/> and the Central
/// UI's Blazor circuits (CentralUI-006).
/// UI's Blazor circuits.
///
/// A throwing subscriber must never break the deployment pipeline, so each
/// handler is invoked individually and its exceptions are caught and logged.
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
/// Orchestrates the TemplateEngine services (FlatteningService, ValidationService, RevisionHashService)
/// into a single pipeline for deployment use.
///
/// WP-16: This captures template state at the time of flatten, ensuring that concurrent template edits
/// This captures template state at the time of flatten, ensuring that concurrent template edits
/// (last-write-wins) do not conflict with in-progress deployments.
/// </summary>
public class FlatteningPipeline : IFlatteningPipeline
@@ -31,7 +31,7 @@ public class FlatteningPipeline : IFlatteningPipeline
/// <param name="validationService">Service that performs semantic validation on the flattened config.</param>
/// <param name="revisionHashService">Service that computes the revision hash for staleness detection.</param>
/// <param name="sharedSchemaRepo">
/// M9-T32b: repository backing the JSON-Schema <c>$ref</c> resolution seam. Used to
/// Repository backing the JSON-Schema <c>$ref</c> resolution seam. Used to
/// look up <c>lib:Name</c> library references so a dangling reference in any validated
/// script schema becomes a deploy-blocking error.
/// </param>
@@ -135,7 +135,7 @@ public class FlatteningPipeline : IFlatteningPipeline
.Select(c => c.Name)
.ToHashSet(StringComparer.Ordinal);
// M2.8 (#23): the set of data-connection names that actually exist on the
// The set of data-connection names that actually exist on the
// target site, used to verify each bound connection resolves to a real site
// connection. Same StringComparer.Ordinal as the rest of the binding-resolution
// path (connection names are matched as-authored throughout the pipeline).
@@ -143,7 +143,7 @@ public class FlatteningPipeline : IFlatteningPipeline
.Select(c => c.Name)
.ToHashSet(StringComparer.Ordinal);
// M9-T32b: build the JSON-Schema $ref resolution seam from the shared-schema
// Build the JSON-Schema $ref resolution seam from the shared-schema
// library. The seam ValidationService consumes is synchronous, so the library is
// pre-loaded once into a name→JSON map here (avoiding sync-over-async) and the
// seam is a pure in-memory lookup. An unresolved {"$ref":"lib:Name"} in any
@@ -16,7 +16,7 @@ public readonly record struct DeploymentStatusChange(
DeploymentStatus Status);
/// <summary>
/// CentralUI-006: push-based deployment-status change notification.
/// Push-based deployment-status change notification.
///
/// The design (Component-CentralUI "Real-Time Updates") requires deployment
/// status transitions to push to the UI immediately via SignalR, with no
@@ -3,14 +3,14 @@ using System.Collections.Concurrent;
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
/// <summary>
/// WP-3: Per-instance operation lock. Only one mutating operation (deploy, disable, enable, delete)
/// Per-instance operation lock. Only one mutating operation (deploy, disable, enable, delete)
/// may be in progress per instance at a time. Different instances can proceed in parallel.
///
/// Implementation: ConcurrentDictionary of ref-counted SemaphoreSlim(1,1) keyed by instance
/// unique name. The lock is released on completion, timeout, or failure.
/// Lost on central failover (acceptable per design -- in-progress treated as failed).
///
/// DeploymentManager-005: each entry is ref-counted. The semaphore is created on the
/// Each entry is ref-counted. The semaphore is created on the
/// first acquire/wait, shared while there are waiters or a holder, and removed +
/// <see cref="IDisposable.Dispose"/>d when the last reference is released — so the dictionary
/// does not accumulate one kernel wait handle per distinct instance name forever.
@@ -22,7 +22,7 @@ public class OperationLockManager
/// <summary>
/// Number of lock entries currently tracked. Used for diagnostics and to
/// verify that semaphores are reclaimed (DeploymentManager-005).
/// verify that semaphores are reclaimed.
/// </summary>
public int TrackedLockCount
{
@@ -25,13 +25,13 @@ public static class ServiceCollectionExtensions
/// <returns>The <paramref name="services"/> instance for chaining.</returns>
public static IServiceCollection AddDeploymentManager(this IServiceCollection services)
{
// DeploymentManager-008: ensure the options class is always resolvable.
// Ensure the options class is always resolvable.
// The Host binds OptionsSection to appsettings.json; absent that binding
// the declared option-class defaults apply.
services.AddOptions<DeploymentManagerOptions>();
services.AddSingleton<OperationLockManager>();
// CentralUI-006: push-based deployment-status notification. Registered
// Push-based deployment-status notification. Registered
// as a singleton so the scoped DeploymentService and the Central UI's
// scoped Blazor page component share one instance — both run in the
// same central Host process. The deployment-status page subscribes to
@@ -42,7 +42,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<DeploymentService>();
services.AddScoped<ArtifactDeploymentService>();
// #16 (M8 D2): expose the flattening pipeline's revision-hash computation
// Expose the flattening pipeline's revision-hash computation
// to the Transport bundle importer through the Commons IStaleInstanceProbe
// seam, so a template overwrite can enumerate the deployed instances whose
// flattened config has drifted from their deployed snapshot. Scoped so it
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
/// <summary>
/// WP-4: State transition matrix for instance lifecycle.
/// State transition matrix for instance lifecycle.
///
/// State | Deploy | Disable | Enable | Delete
/// ----------|--------|---------|--------|-------
@@ -42,8 +42,8 @@ public static class StateTransitionValidator
/// undeployed instance would otherwise linger as an unremovable orphan record.
/// Delete from <c>NotDeployed</c> is a central-side record cleanup (no live site
/// config to tear down). This matches the state-transition matrix in
/// Component-DeploymentManager.md ("Delete from Not deployed = Yes") — reconciled
/// in M2.17 (#31); the deliberate behaviour was introduced in commit 1d5465f3.
/// Component-DeploymentManager.md ("Delete from Not deployed = Yes") — reconciled;
/// the deliberate behaviour was introduced in commit 1d5465f3.
/// </remarks>
public static bool CanDelete(InstanceState currentState) =>
currentState is InstanceState.NotDeployed or InstanceState.Enabled or InstanceState.Disabled;