P2.3: publish enforces channel-name target + release-id uniqueness
Close the remaining publish aggregate-consistency edges (review round 2, F1 continued): - chan.name == release.channel — the promoted channel must be the one the release declares as its target; promoting a "beta" channel for a release whose channel is "stable" committed an edge contradicting the release's own target. Now rejected with Integrity + rollback. - release.id must be new — a colliding id would shadow the existing release (get_release resolves to the OLD one), so the channel edge would silently point at a different release than the one published. Now rejected with Integrity + rollback. tests/repo_transaction.sx: add a channel-name-mismatch case and a release-id-collision case (both assert Integrity + model unchanged); existing fully-consistent publish still commits. Both new cases fail on the pre-fix repo.sx and pass after.
This commit is contained in:
@@ -26,8 +26,9 @@
|
||||
// Failure classes for the publish transaction. `Validation` = a release /
|
||||
// artifact / channel failed domain validation; `Integrity` = the published
|
||||
// aggregate is cross-identity inconsistent — the release names an app that
|
||||
// doesn't exist, or an artifact / promoted channel whose app_id or release_id
|
||||
// doesn't match the release being published.
|
||||
// doesn't exist, a release id that already exists, an artifact whose app_id /
|
||||
// release_id doesn't match the release, or a promoted channel that belongs to
|
||||
// a different app or is not the channel the release targets (name mismatch).
|
||||
PublishErr :: error {
|
||||
Validation,
|
||||
Integrity,
|
||||
@@ -194,11 +195,15 @@ Repo :: struct {
|
||||
// in the repo (the "no dangling release" invariant).
|
||||
//
|
||||
// The published aggregate must also form ONE consistent identity graph,
|
||||
// else committing it would create a cross-app dangling edge. So, as an
|
||||
// Integrity precondition: the release's app must exist, the promoted
|
||||
// channel must belong to that app (chan.app_id == release.app_id), and
|
||||
// every artifact must belong to that app AND name this release
|
||||
// (a.app_id == release.app_id and a.release_id == release.id).
|
||||
// else committing it would create a dangling or ambiguous edge. So, as
|
||||
// Integrity preconditions: the release's app must exist; the release id
|
||||
// must be new (a colliding id would shadow the existing release, leaving
|
||||
// the channel edge pointing at a different release than the one
|
||||
// published); the promoted channel must belong to that app
|
||||
// (chan.app_id == release.app_id) AND be the channel this release targets
|
||||
// (chan.name == release.channel); and every artifact must belong to that
|
||||
// app AND name this release (a.app_id == release.app_id and
|
||||
// a.release_id == release.id).
|
||||
//
|
||||
// Rollback is by snapshot: List appends only bump `len`, so undoing them
|
||||
// is a `len` reset; an updated existing channel is restored from a saved
|
||||
@@ -218,14 +223,23 @@ Repo :: struct {
|
||||
validate_release(release) catch { failed = true; };
|
||||
|
||||
// Cross-entity identity preconditions for the whole aggregate: the
|
||||
// release's app must exist and the promoted channel must belong to
|
||||
// that same app. (Per-artifact identity is checked in the loop.)
|
||||
// release's app must exist, the release id must be new (else the
|
||||
// channel edge would resolve to a pre-existing release, not this one),
|
||||
// and the promoted channel must belong to that same app AND be the
|
||||
// channel this release targets (chan.name == release.channel).
|
||||
// (Per-artifact identity is checked in the loop.)
|
||||
if !failed {
|
||||
if self.get_app(release.app_id) == null { integrity = true; failed = true; }
|
||||
}
|
||||
if !failed {
|
||||
if self.get_release(release.id) != null { integrity = true; failed = true; }
|
||||
}
|
||||
if !failed {
|
||||
if chan.app_id != release.app_id { integrity = true; failed = true; }
|
||||
}
|
||||
if !failed {
|
||||
if chan.name != release.channel { integrity = true; failed = true; }
|
||||
}
|
||||
|
||||
if !failed {
|
||||
self.releases.append(release, self.own_allocator);
|
||||
|
||||
Reference in New Issue
Block a user