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:
+14
-3
@@ -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]
|
||||
|
||||
+33
@@ -103,6 +103,39 @@ public class ArtifactRevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(a), _sut.ComputeHash(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_ReorderedCamelCaseMethodNames_ProducesSameHash()
|
||||
{
|
||||
// The method-name lookup is case-insensitive so that a naming-policy change at the
|
||||
// producer (PascalCase anonymous type today) cannot silently degrade the sort into a
|
||||
// no-op and let repository row order move the hash again.
|
||||
var a = Command(externalSystems:
|
||||
[
|
||||
new ExternalSystemArtifact("Mes", "https://mes", "ApiKey", null,
|
||||
"[{\"name\":\"MoveOut\"},{\"name\":\"MoveIn\"}]")
|
||||
]);
|
||||
var b = Command(externalSystems:
|
||||
[
|
||||
new ExternalSystemArtifact("Mes", "https://mes", "ApiKey", null,
|
||||
"[{\"name\":\"MoveIn\"},{\"name\":\"MoveOut\"}]")
|
||||
]);
|
||||
|
||||
Assert.Equal(_sut.ComputeHash(a), _sut.ComputeHash(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_UnparseableMethodDefinitions_StillParticipatesInTheHash()
|
||||
{
|
||||
// Non-array / non-JSON payloads pass through verbatim: the order guarantee is lost
|
||||
// (there is nothing to sort), but a content change must still move the hash.
|
||||
var a = Command(externalSystems:
|
||||
[new ExternalSystemArtifact("Mes", "https://mes", "ApiKey", null, "not json")]);
|
||||
var b = Command(externalSystems:
|
||||
[new ExternalSystemArtifact("Mes", "https://mes", "ApiKey", null, "not json either")]);
|
||||
|
||||
Assert.NotEqual(_sut.ComputeHash(a), _sut.ComputeHash(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_DifferentDeploymentIdAndTimestamp_ProducesSameHash()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user