fix(deploy): persist the artifact deployment record before the best-effort site stamp

Both repositories are scoped over one DbContext, so a failed stamp save left the
Site entries tracked as Modified and the deployment record's (uncaught)
SaveChangesAsync rethrew it — losing the audit row and failing a deploy in which
every site succeeded. Ordering rule now: audit row first, stamp after, inside its
own try/catch. Same in RetryForSiteAsync, where the site load is caught too so
nothing on the stamp path can reach the method's catch.

Also: the method-definitions canonicaliser matches the name property
case-insensitively (a naming-policy change at the producer would otherwise
silently turn the sort into a no-op), and ComputeCurrentArtifactHashesAsync
documents that a per-site query failure aborts the whole batch by design.

Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
Joseph Doherty
2026-09-14 13:51:34 -04:00
parent fc48db480c
commit 0287f8f64f
4 changed files with 156 additions and 43 deletions
@@ -354,10 +354,13 @@ public class ArtifactDeploymentServiceTests : TestKit
}
[Fact]
public async Task DeployToAllSitesAsync_StampSaveThrows_DeployStillReportsSuccess()
public async Task DeployToAllSitesAsync_StampSaveThrows_DeploymentRecordStillPersistedAndSuccessReported()
{
// Audit trail beats staleness bookkeeping: the artifacts DID reach the site, so a
// failure to record the stamp is logged and swallowed, never reported as a failure.
// ORDERING RULE: audit row first, best-effort stamp after. Both repositories are
// scoped over ONE DbContext, so a failed stamp save leaves the Site entries tracked
// as Modified — stamping first would let that failure rethrow out of the deployment
// record's own (uncaught) SaveChangesAsync and lose the audit row for a deploy where
// every site succeeded. The record must therefore be saved BEFORE the stamp is tried.
var site = new Site("Site One", "site-1") { Id = 1 };
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<Site> { site });
_siteRepo.SaveChangesAsync(Arg.Any<CancellationToken>())
@@ -370,6 +373,14 @@ public class ArtifactDeploymentServiceTests : TestKit
Assert.True(result.IsSuccess);
Assert.Equal(1, result.Value.SuccessCount);
await _deploymentRepo.Received(1).AddSystemArtifactDeploymentAsync(
Arg.Any<SystemArtifactDeploymentRecord>(), Arg.Any<CancellationToken>());
await _deploymentRepo.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
Received.InOrder(() =>
{
_deploymentRepo.SaveChangesAsync(Arg.Any<CancellationToken>());
_siteRepo.UpdateSiteAsync(site, Arg.Any<CancellationToken>());
});
}
[Fact]