release: pin that canary never jumps to stable
The jump that would make the ladder decorative, and the one a production app parked on a migration canary channel would make if nothing stopped it — which is the exact situation both go.tito.io and keymail.dev are in today. Found while moving keymail onto stable: the promotion succeeded when it should have been refused, and the cause was a stale carlos binary on the operator's path, not the ladder. The rule was always right; nothing asserted it from this direction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 file changed,
+26
−0
internal/release/canarystable_test.go+26 −0
diff --git a/internal/release/canarystable_test.go b/internal/release/canarystable_test.go| new file mode 100644 |
| index 000000000..e9cbdfd6d |
| --- /dev/null |
| +++ b/internal/release/canarystable_test.go |
| @@ -0,0 +1,26 @@ |
| +package release_test |
| + |
| +import ( |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// canary -> stable is the jump that would make the ladder decorative: it is |
| +// exactly what a production app parked on a migration canary channel would do |
| +// if nothing stopped it. |
| +func TestCanaryToStableIsRefused(t *testing.T) { |
| + err := release.PromotionAllowed(release.RungCanary, release.RungStable) |
| + if err == nil { |
| + t.Fatal("canary -> stable was ALLOWED — the ladder is decorative") |
| + } |
| + if !strings.Contains(err.Error(), "-hotfix") { |
| + t.Errorf("refusal %q does not name -hotfix", err) |
| + } |
| + t.Logf("refusal: %v", err) |
| + // And the same jump for beta, for completeness. |
| + if release.PromotionAllowed(release.RungCanary, release.RungBeta) == nil { |
| + t.Error("canary -> beta was allowed") |
| + } |
| +} |