carlos / platform Public

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>
Paul Campbell 97f0561e82065c91b28f5ffae415812781587484 parent 701bbd4
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")
+ }
+}