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:
@@ -135,6 +135,13 @@ public sealed class ArtifactRevisionHashService
|
||||
/// by name (ordinal), so the repository's row order cannot leak into the hash. Anything
|
||||
/// that is not a parseable JSON array is returned verbatim — the hash still changes when
|
||||
/// the payload does, which is the only guarantee this method owes.
|
||||
/// <para>
|
||||
/// The name property is matched CASE-INSENSITIVELY. Today's producer
|
||||
/// (<c>ArtifactDeploymentService.FetchGlobalArtifactsAsync</c>) serializes an anonymous
|
||||
/// type, so the key is PascalCase <c>Name</c> — but a future camelCase naming policy there
|
||||
/// would otherwise silently turn this sort into a no-op and let row order move the hash
|
||||
/// again, with nothing failing.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static string? CanonicalizeMethodDefinitions(string? methodDefinitionsJson)
|
||||
{
|
||||
@@ -150,13 +157,7 @@ public sealed class ArtifactRevisionHashService
|
||||
var ordered = document.RootElement
|
||||
.EnumerateArray()
|
||||
.Select(element => element.Clone())
|
||||
.OrderBy(element =>
|
||||
element.ValueKind == JsonValueKind.Object
|
||||
&& element.TryGetProperty("Name", out var name)
|
||||
&& name.ValueKind == JsonValueKind.String
|
||||
? name.GetString() ?? string.Empty
|
||||
: string.Empty,
|
||||
StringComparer.Ordinal)
|
||||
.OrderBy(MethodSortKey, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return JsonSerializer.Serialize(ordered, CanonicalJsonOptions);
|
||||
@@ -167,6 +168,31 @@ public sealed class ArtifactRevisionHashService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort key for one serialized external-system method: its name property, matched
|
||||
/// case-insensitively so the key survives a naming-policy change at the producer.
|
||||
/// Elements that are not objects, or carry no string name, sort first under an empty key —
|
||||
/// their relative order is then whatever the payload gave, which is the best a nameless
|
||||
/// element allows.
|
||||
/// </summary>
|
||||
private static string MethodSortKey(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(property.Name, "Name", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return property.Value.ValueKind == JsonValueKind.String
|
||||
? property.Value.GetString() ?? string.Empty
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Internal types for deterministic serialization. Properties MUST be declared in
|
||||
// alphabetical (ordinal) order — System.Text.Json emits them in declaration order
|
||||
// and does not sort. See the DETERMINISM CONTRACT note on the class summary.
|
||||
|
||||
Reference in New Issue
Block a user