Upgrading from v0.26 to v0.27.0
This page lists every change in v0.27.0 that can affect code or a deploy that worked on v0.26.1. Each entry is one bullet: what changed, the old code, and the new code.
- Loud changes stop the build. The error names the change and ends with a
link to its entry here, for example
see docs/migration/v0.27.md#value-restriction. - Silent changes compile, but the program behaves differently. The first run
of a new
skyprints these once ("Sky upgraded X -> Y"). Read them before you deploy.
Run sky install once after the upgrade: it regenerates sky-ffi/ in the new
surface format. Then run sky check and fix what it reports. The full story of
each change is in CHANGELOG.md under v0.27.0.
Loud (fails to compile, the error shows the fix)
Value restriction
- A top-level value with no parameters whose body is an application can no longer be used at two types inside a mutable or invariant type (
[E2012]):shared = Task.run (Sync.newRef [])used as aRef (List Int)and aRef (List String)-> give it one type,shared : Result Error (Sync.Ref (List Int)). - A point-free function that is not generic enough is the same error:
render = makePrinter True->render x = makePrinter True x.
any in annotations
anyin your own signature is no longer an unchecked cast. It is a hole the body fills, and callers see the filled type:coerce : a -> anythenString.length (coerce 5)-> write the real type,coerce : a -> a, and fix the caller. Stdlib signatures are not affected.
Comparable bound
<,>,compare,min,max,List.sort, the key ofList.sortByandSetelements need a comparable type ([E2001]):List.sortBy .price itemswithprice : Decimal->List.sortWith (\a b -> Decimal.compare a.price b.price) items.- A signature that sorts must say so:
largest : List a -> Maybe a->largest : List comparable -> Maybe comparable.
Encodable bound
Codec.auto,App.withDurable,Table.table,Jobs.defineandAuth.signTokenrefuse a type that holds a function, aSecret, a key or a runtime handle ([E2001]):Codec.auto { model | proc = Nothing }withproc : Maybe Process-> keep the handle out of the saved record.
AEAD encrypt is a Task
Crypto.aesGcmEncryptandCrypto.chacha20EncryptreturnTask Error String:case Crypto.aesGcmEncrypt key plain of Ok c -> ...->case Task.run (Crypto.aesGcmEncrypt key plain) of Ok c -> ..., or chain it withTask.andThen. Decrypt stays aResult.
FFI Result enforced
- A
sky addbinding returnsResult Error a, and the checker now enforces it:probe : Intandprobe = Hex.encodedLen 3->probe = Hex.encodedLen 3 |> Result.withDefault 0, or handle theErrin acase.
Sky Ffi is stdlib-only
Ffi.kernel,Ffi.call,Ffi.callPureandFfi.callTaskin application code are[E1011]:Ffi.kernel "Crypto_sha256"->Crypto.sha256(the hint names the stdlib function), orsky addthe Go package and call its binding.
Registry packages are checked
- A fetched
.skydepspackage is type-checked with your code and has noSky.Ffi: a package that no longer type-checks fails the build -> update the package, or ask its author for a v0.27.0 release.
FFI opaque Go types
- A Go value that has no Sky form is its own type (
Pkg.Thing), no longer usable at any type:n = case Pkg.mk () of Ok t -> tused as anInt-> annotatet : Pkg.Thingand pass it only to bindings of that package. - A Go value is not a kernel
Valueeither:startServer : Value -> Result Error ()for a router ->startServer : Mux.Router -> Result Error ().
FFI pointer is Maybe
- A Go pointer to a non-opaque type is a
Maybe(nil isNothing) in results, parameters, lists, fields and callbacks:Pkg.setName title p->Pkg.setName (Just title) p, andOk s -> s->Ok (Just s) -> s.
FFI map keys
- A Go map with an
int,floatorboolkey is aDictwith that key type:Dict String Vfor a Gomap[int]V->Dict Int V.
FFI callback result
- A callback passed to Go is typed from the Go signature, result included:
Pkg.apply (\n -> n * 2)for afunc(int) string->Pkg.apply (\n -> String.fromInt n). A zero-parameter Go callback is() -> r.
FFI Go interface params
- A Sky value where Go wants an interface is
[E2013]:Pkg.writeTo "x" s-> pass a Go value that implements the interface (from another binding). This holds through a binding bound to a name (w = Pkg.writeTo) or passed to a function (apply Pkg.writeTo "x" s) too.
FFI surface format 3
- A call through a binding of an older
sky-ffi/surface whose wrapper converted a value unsoundly is refused: oldsky-ffi/-> runsky installto regenerate it. All-native bindings still build, with a warning.
Auth verifyToken JSON
Auth.verifyToken : Secret -> String -> Result Error Json.Value(it returned any type you asked for):verify s t = Auth.verifyToken s ttypedResult Error (Dict String String)->Auth.verifyToken s t |> Result.andThen (Decode.decodeValue (Decode.field "sub" Decode.string)).
Server withCookie typed
Server.withCookieisString -> String -> String -> Response -> Response(it wasany):Server.withCookie someCookie resp->Server.addCookie someCookie resp, andServer.withCookie name value resp->Server.addCookie (Server.cookie name value) resp. The four-argument form is unchanged.
Opaque handle constructors
- The constructors of
WebSocket,WebSocketServer,StreamId,StreamWriterandCacheare hidden:Cache.Cache n,case c of Cache.Cache n -> ...orimport Std.Cache exposing (Cache(..))-> keep and pass the value itself, never build or match it.
New stdlib names
- A bare name that two
exposing (..)imports now both export is[E1012]ambiguous:value "x"withStd.Html.AttributesandJson.Decodeboth exposed ->Attributes.value "x", orimport Std.Html.Attributes exposing (value). The names that collide with an older export:Done,Event,Running,Step,address,close,island,onIslandEvent,raw,rpc,serve,spawn,stop,toMaybe,value,withClientCrypto,withEmbedded,withName,withSessionTransport. The error names both modules.
Stdlib combinators are checked
Result.map,Result.withDefault,Result.andThen,Result.mapError,Maybe.isJust,Maybe.isNothing,identity,alwaysandnothad no signature, so any argument passed. A wrong one is now[E2001]:Ok "s" |> Result.map kwithk : Int -> Int->Ok 1 |> Result.map k, or akthat takes aString.
Recursive type alias
- A recursive
type aliasis[E1016]at its declaration (it passedsky check, thengo buildfailed):type alias Node = { next : Maybe Node }->type Node = Node { next : Maybe Node }. Seedocs/errors/E1016-recursive-type-alias.md.
init takes unit
App.app,App.web,App.cliandApp.tuifixinit's seed to():init : Page -> ( Model, Cmd Msg )->init : () -> ( Model, Cmd Msg ), and read the route withApp.withRoutes/App.withOnNavigate, the request withApp.withRequest.
WebOpts record literals
WebOptshas two new fields: a literalApp.WebOpts { port = 8000, ... }-> addembedded = FalseandsessionTransport = CookieSession, or write{ App.webDefaults | port = 8000 }.WebSocketServerCfghasonFrameandframeMode: a literal cfg -> addonFrame = \_ _ -> Task.succeed ()andframeMode = False, or build it withWs.defaultCfg |> Ws.with....App.DurableWiringhasdiscard: a hand-built record -> adddiscard = Durable.deleteSnapshot db, or useApp.withDurable.
Native permissions
Std.Bundle.Permissionhas six new constructors: acaseoverPermission-> addLocationAlways,PhotoLibrary,Contacts,FaceId,LocalNetworkandBluetooth.- A mobile build refuses a capability without its permission:
Native.authenticatewith noBundle.FaceId-> add|> Bundle.withUsage Bundle.FaceId "<why>"(the same for Camera, Location, and Notifications on Android).
Spa rpc body value
Spa.rpcandSpa.rpcWithtake the request body as a value:Spa.rpc bodyCodec respCodec url (\model -> body) toMsg->Spa.rpc bodyCodec respCodec url body toMsg. Generated split code needs only a rebuild.
Spa split refusals
sky buildof a Sky.Spa split refuses two wire records with one name, one module imported under several aliases, and a server arm it cannot read: the build names the site -> rename the record, use one alias, or split the arm into a named function.
Stdlib module names are reserved
- A package or app module named like a stdlib or kernel module is refused:
src/Std/Log.skyor a dependency'smodule Auth-> rename it (App.Log).
sky check runs the Spa split
sky checkon aweb:apporSpa.appproject fails wheresky buildfailed: a bare data-carrying union in the model -> give the field aCodec, or wrap the union in a record.
fmt refuses a file that does not parse
sky fmt --checkon a file with a syntax error exited 0: it now exits 1 with[E0001]-> fix the syntax.
sky add finds the project root
sky add ../librun insrc/wrotesrc/sky.toml: it now edits the project'ssky.toml, and refuses outside a project -> run it inside the project.
Release refuses local addresses
sky package --releaserefuses a local or private-network backend address:SKY_APP_URL=https://192.168.1.10/-> a public host name inApp.withAppUrlorSKY_APP_URL.
Dependency native code
- A dependency's Android fragment may add only
<uses-permission>,<uses-feature>and<queries>, and a dangerous permission only when the app declares it too: a dependency addingREAD_SMS-> declare it withBundle.withPermissionin the app.
Client crypto keys inside unions
- With
App.withClientCrypto, a key inside a user union is refused in the model and on the wire:boxed : Maybe KeyBoxwithtype KeyBox = KeyBox Noise.Handshake->handshake : Maybe Noise.Handshake.
TestFlight ipa must match the project
--upload testflight --ipa Other.ipaof another app or build is refused -> upload the project's own archive, or fixBundle.withId/Bundle.withBuild.
Upgrades and installs verify checksums
sky upgrade,install.shand the Docker image refuse a download with no or a wrongchecksums.txtentry: a mirror withoutchecksums.txt-> publish the release'schecksums.txtbeside the archives.
Dependency names and paths
sky addrefuses a path or package name with",\or a control character:sky add '../q"uote'-> rename the directory.
Silent (compiles, behaves differently)
Each bullet below is printed once by the first run of the new sky.
Handles belong to their session
- SILENT A process, watcher, WebSocket or stream opened outside a Sky.Live session (in
main) isErr PermissionDeniedinside one:Process.write shared ...fromupdate-> spawn it inside the session that uses it.
Handle ids are random
- SILENT A handle kept in a stored session across a restart or from another replica is
Err"not live in this server":model.procreused after a restart -> spawn or open it again onErr.
CAF handles are process-owned
- SILENT A top-level value that opens a process or watcher (
shell = Task.run (Process.spawn ...)) belonged to the first session that used it: it is now owned by the process, not closed when that session ends.
Set-Cookie header holds every cookie
- SILENT A response that sets several cookies sends all of them now (before, only the first reached the browser), and
resp.headers"Set-Cookie"holds every line joined by a newline, not only the first:Dict.get "Set-Cookie" resp.headers == Just line-> split it on"\n"and check the line you need.
readFileLimit positive limit
- SILENT
File.readFileLimit path 0read the whole file and is nowErr InvalidInput:File.readFileLimit path 0->File.readFileLimit path 1048576.
File permissions no clamp
- SILENT
File.permissionswith a digit outside 0 to 7 gives -1, whichFile.chmodrefuses:File.permissions 9 4 4->File.permissions 7 4 4.
resolveWithin empty root
- SILENT
File.resolveWithin "" pathmeant the working directory and is nowErr InvalidInput:File.resolveWithin "" path->File.resolveWithin "." path.
PTY size bound
- SILENT
Process.resizeandProcess.withPtyabove 500 columns by 200 rows areErr InvalidInput:{ cols = 1000, rows = 400 }-> at most{ cols = 500, rows = 200 }.
Compare derived order
- SILENT
compare,List.sortandSetorder a custom type by constructor declaration order, then the arguments, and a record by field name:compare Red (Green 1)wasEQ-> it isLTwhenRedis declared first;Just _sorts beforeNothing,Ok _beforeErr _, andSet.toListis ascending.
Compare NaN
- SILENT
compareorders Floats totally with NaN greatest:compare nan 1wasEQ->GT;compare nan nanisEQ, andnan < 1andnan == nanstayFalse.
Task parallel panic
- SILENT A panic in a
Task.parallelorTask.parallelNbranch ended the process: the task that waits for the branches now raises it, so a server answers 500 for that request.
Process tree close
- SILENT
Process.closeleft a shell's background jobs running: it ends the whole process tree -> start a job that must outlive its parent outsideSky.Core.Process.
Process release after exit
- SILENT Outside a session a process was kept until
Process.close: it is released after it exits (30 s after, unless its output and status were read) -> read the output within 30 s of the exit.
Field read strict
- SILENT A field read of a value that is not that record gave
nil: it is a classifiedCoerceFailure. Well-typed code needs no change.
Dict Go map keys
- SILENT A Go
map[int]Vfrom FFI became an emptyDict: its keys are converted, and a key that cannot be is a classifiedCoerceFailure.
Watch limit
- SILENT A watch past the descriptor or inotify limit missed a sub-tree without a word: it is
Errat start, or anOverflowbatch -> add large directories toignore.
FFI integer range
- SILENT An out-of-range integer across the Go FFI is an
Err:Pkg.big ()gaveOk -1for auint64above Int ->Err "... out of range for Int".
FFI Sky value to a Go interface
- SILENT A Sky runtime value (a
Secret, aStd.Synchandle, aMaybe, a key) that reaches a Go interface parameter through an annotated generic helper is anErr: it passed when it implemented the interface (Secretis afmt.Stringer) -> pass a Go value from another binding.
toString prints Sky syntax
- SILENT
toStringandDebug.toStringprint Sky syntax:'a'was97,Just 5was5,Truewastrue,["a","b"]was[a b],Nothing,(), anError(IO: disk full) and records (fields sorted) changed too -> for stored or wire text useString.fromInt,String.fromFloat, aCodecor your own formatter.
decodeString rejects trailing text
- SILENT
Json.Decode.decodeStringandCodec.fromJsonrefuse text after the JSON value:decodeString int "3 x"wasOk 3->Err; cut the JSON out of a larger text (an LLM answer) before you decode it.
Json encode NaN
- SILENT
Json.Encode.encodeof a NaN or infiniteFloatreturned""and is a classifiedJsonEncodeFailure: guard the value withMath.isNaNbefore you encode it.
Codec auto union
- SILENT
Codec.autowith a union blank decoded every object as the blank's variant: it returns the variant the JSON names, orErr.
Codec auto unencodable
- SILENT
Codec.autoof a record with aSecret, key or handle field wrote{}: it is a classifiedJsonEncodeFailure-> move the field out of the saved or sent record, or write its codec by hand.
Ed25519 small-order keys
- SILENT
Sign.publicKeyFromBytesrefuses the eight small-order points and non-canonical encodings: it returnedOk-> handle theErrand ask the peer for a real key.
Loopback host guard
- SILENT A loopback server answers only
localhost-styleHostnames outside production: a dev proxy name gets 403 -> setSKY_ALLOWED_HOSTS=<host>orSKY_PUBLIC_URL=https://<host>. Production on loopback behind a proxy is not checked.
Desktop console
- SILENT A
--target desktopapp mounted an open console on its loopback port: it mounts none -> setSKY_CONSOLE_AUTH=tokento use it.
Console access follows the app sign-in
- SILENT Under
SKY_CONSOLE_AUTH=appthe Sky Console cookie kept access for 4 hours after an app sign-out or demotion: the console re-runsApp.withConsoleAuthat most every 60 s and refuses with 403 once it answersNothing(an open console tab stops receiving data and shows the 403 too), and the console sign-out ends the cookie on the server -> no code change; an admin who signs out of the app signs in to it again to reopen the console.
Served app namespace
- SILENT Each
App.serveapp has its own cookiesky_sid_<name>(elsesky_sid_port-<N>), and port 0 with a durable store refuses to start without a name:App.serve app->App.serve (app |> App.withName "admin").
Session rotation status codes
- SILENT For 60 s after sign-in, a request with the old Sky.Live id gets 409
Retry-After: 1(event) or 503Refresh: 1(page), and/_sky/rotateand/_sky/sse-ticketare reserved paths -> keep 5xx alerts from firing on these.
X-Sky-Sid tag
- SILENT
X-Sky-Sidin cookie transport carries a one-way tagt<hex>, never the session id: a tool that read the header as the id -> read the cookie.
Session cookie name
- SILENT Over HTTPS the Sky.Live cookie is
__Host-sky_sid, and its value changes at sign-in:Dict.get "sky_sid" req.cookiesas a key ->Live.sessionKey ().
Spa cookie sky spa
- SILENT The Sky.Spa session cookie is
sky_spa(it sharedsky_sidwith Sky.Live), moved on the next request: a proxy rule or script that readssky_sidfor aweb:appbackend -> readsky_spa.
Spa legacy sessions
- SILENT A pre-v0.27.0 Sky.Spa session is converted once on its first request, so visitors stay signed in: nothing to change, but keep the signing key (
SKY_SPA_SESSION_SECRET) the same across the upgrade.
Spa session key
- SILENT A development Sky.Spa key is
.skydata/spa-session-secret.devand production never reads it: setSKY_SPA_SESSION_SECRET(32+ bytes) in production to keep sessions across deploys and replicas.
Spa sign-out store
- SILENT Sky.Spa in production with no session store and an unwritable data dir refuses to start (it kept sign-outs in memory): set
SKY_LIVE_STORE, orSKY_DATA_DIRto a writable directory.
Spa reload after deploy
- SILENT An open Sky.Spa tab across a deploy that changed the wire lost its follow-up writes: it reloads once and says the last action was not sent. Nothing to change.
Spa server links
- SILENT A Sky.Spa link or
Nav.pushUrlto a path with no client route, or under/_sky/or/_rpc/, is a full page load (it showed the in-app 404): for a path that is both, addSpa.serverRoute "GET /path"towithRoutes.
Spa one event one view
- SILENT In a Sky.Spa app one click runs only the handlers of the view it was delivered to, and a nested outer
onClickcarries the Msg of the clicked view.
Spa Msg order
- SILENT A Sky.Spa
updateruns once per Msg, in arrival order, and Msgs wait while a hold RPC (a server arm whose write needs server data) is in flight: put a long wait in aCmd.perform.
Analytics per visitor
- SILENT
Std.Analyticsin a Sky.Spa backend or HTTP handler shared one anonymous id and one consent for all visitors: each visitor has its own -> callsetConsentper visitor, and re-apply it after a restart.
PubSub reaches every app
- SILENT
Std.PubSub.publishreached only the first Sky.Live app in the process: it reaches every running app.
Default web port
- SILENT A
Std.Appweb app with no port bindssky.toml's port (8000 by default, it was 8080), andport = 0means a free port: keep 8080 with[live] port = 8080.
Ui text wraps
- SILENT
Ui.textoutside a paragraph is its own wrapping box: two texts in a column now sit on two lines ->Ui.textNoWrapfor one-line labels,Ui.paragraphto flow texts together.
sky test exit codes
- SILENT
sky testexits 2 when nothing ran (a build failure), not 1: a CI step that tests for 1 -> test for 2 as well.
sky verify in a project
- SILENT
sky verifyin a project with anexamples/directory verifies the project (fmt, check, tests); it swept the examples and reported green.
sky test json report
- SILENT
sky test --format jsonalso writes theSKY_TEST_JSON=<path>report.
sky doctor auth secret
- SILENT
sky doctorwarns aboutSKY_AUTH_TOKEN_SECRETonly for a project that importsStd.Auth.
Library check
- SILENT
sky checkin a library (noentry, noMain) checks every module and exits 0 when they build (it exited 1 "no entry main").
Dependency version tags
- SILENT A dependency version
"0.1.3"resolves to the tagv0.1.3, and a version with no tag names the fix.
Codec dict on the Spa wire
- SILENT A
Dict k vSky.Spa wire field usesCodec.dict: a JSON array of[key, value]pairs.
New tables in your database
- SILENT The session store creates
sky_session_aliases(andsky_session_bindingsin a revocation Db), andsky db migrate --genskips everysky_*table: the store's database role needsCREATEon first start.
Native shells
- SILENT An iOS or Android shell built by v0.26.1 still gets
Native.notifythrough its old entry point: rebuild and ship the native shells with the backend for the new features.
sky fmt record update layout
- SILENT
sky fmtlays out a record update that does not fit on one line in the standard form, so the first run reformats those lines (layout only, no change in meaning):{ model | a = 1with the next field at the outer indent ->{ modelon its own line, then| a = 1and, b = 2one step in.
Rolling back to v0.26.1
- Sign-outs recorded under v0.27.0 (the Sky.Spa
sidclaim and the alias table) are not enforced by v0.26.1. - A Sky.Spa visitor holds a
sky_spacookie, which v0.26.1 does not read: they sign in again. - A Sky.Live session that holds a
Json.ValueorDecimaldoes not decode on v0.26.1, so that session is lost. - A served app's
sky_sid_<name>cookie is not read by v0.26.1: those sessions start again. - The new tables stay in the database. v0.26.1 ignores them; drop them only after the rollback is final.