Releasing guide
There are two kinds of release. A store release ships the host and the built-in bundle. An OTA release ships only a new web bundle: fixes, content and changes to features the reviewed build already has. New features go out with a store release. Most releases are OTA.
Bundles are signed with Ed25519. Generate a key once:
node valence/valence.mjs keygen- The private key is written to
~/.valence/keys/<appId>.<keyId>.pemwith mode0600. The public key goes intovalence/keyring.json, which is compiled into the host. keygenrefuses to write a private key inside a git work tree, links included.- It refuses to reuse a
keyIdalready inkeyring.json. To rotate keys, ship a host that has both keys, then switch signing to the new one. - In CI, set the
VALENCE_SIGNING_KEYsecret to the PEM contents instead of keeping a file. Do not also pass--key.
Never commit the private key, print it or paste it into a prompt. valence bundle also refuses a key whose public half does not match keyring.json.
Store release
Section titled “Store release”For a submission to the stores (the first release, a new native capability or a new feature area), build the built-in bundle with --store:
node valence/valence.mjs bundle --store--store records the routes and the bridge schema the stores reviewed in .valence/baseline.json. From then on codegen keeps the schema additive-only and bundle warns about routes the stores have not seen. A plain --builtin build does not record a baseline, so if your CI builds store releases, have that job run --store. Commit baseline.json with the release.
OTA release
Section titled “OTA release”-
Check.
codegen --checkpasses and your web tests pass. -
Build and sign. Build the web app, then:
Terminal window node valence/valence.mjs bundleThis writes the signed bundle to
.valence/dist-ota/<appId>/. It rejects native code, warns about a new top-level route since the last store build (stop and decide whether the change needs a store update), and re-verifies its own output. -
Commit
.valence/baseline.json. It holdslastSeq, which keepsseqincreasing across machines and CI. -
Verify (optional but cheap):
Terminal window node valence/valence.mjs verify --manifest .valence/dist-ota/<appId>/manifests/<seq>.json --files .valence/dist-ota/<appId>/files -
Stage the pointer at a small rollout:
Terminal window node valence/valence.mjs pointer --channel production --rollout 10Without
--seq, the pointer uses the highest manifest indist-ota. -
Upload, in this order: files, then manifests, then the pointer last, so no phone ever sees a pointer to missing files.
-
Ramp the rollout from 10 to 50 to 100 by running
pointeragain with a higher--rolloutand uploading the pointer. Watch for ready timeouts and crash reports between steps.
Upload layout
Section titled “Upload layout”<OTA_BASE>/<appId>/channels/<channel>.json pointer Cache-Control: max-age=60<OTA_BASE>/<appId>/manifests/<seq>.json manifest immutable<OTA_BASE>/<appId>/manifests/<seq>.json.sig signature immutable<OTA_BASE>/<appId>/files/<sha256-hex> file bytes immutable, content-addressedWith any static host, the same shape applies. For example, with the AWS CLI:
OUT=.valence/dist-ota/com.example.appaws s3 sync "$OUT/files" s3://my-ota/com.example.app/files --cache-control "public, max-age=31536000, immutable"aws s3 sync "$OUT/manifests" s3://my-ota/com.example.app/manifests --cache-control "public, max-age=31536000, immutable"aws s3 sync "$OUT/channels" s3://my-ota/com.example.app/channels --cache-control "max-age=60"Files are content-addressed, so uploading an unchanged file again is harmless. wrangler r2 object put, gsutil rsync and rclone work the same way.
Options on bundle
Section titled “Options on bundle”| Flag | Use |
|---|---|
--floor N |
Raise the lowest seq a phone may run after this release. Use it only when a release fixes a security bug. It can never go back down. |
--requires-bridge N |
Set the minimum host bridge version this UI needs. Defaults to the schema version. |
--urgent |
Tell the host to emit bundle.updateReady so the web app can offer to apply the update now. Otherwise it applies at the next launch. |
--key <file> |
Sign with a specific private key file. |
What phones do with it
Section titled “What phones do with it”Each phone checks the pointer on UI start, on resume and every 30 minutes in the foreground. A phone joins the rollout when fnv1a32(installId + ":" + seq) % 100 is below the pointer’s rollout. The install id is a random UUID created on first launch, never a device id. The new bundle becomes pending, runs as a trial on the next launch and commits when the page calls ready(). See the update protocol.
Roll back
Section titled “Roll back”Rolling back means rolling forward.
- Check out the last good web commit and build it.
- Run
valence bundle. It gets a new, higherseq. - Point the channel at it and upload.
Pointing the channel at an older seq only cancels downloads that are pending. Phones that already committed the bad bundle keep it until something newer arrives. Phones still trying a bad bundle revert on their own after two failed starts.
What the pointer can and cannot do
Section titled “What the pointer can and cannot do”The pointer is unsigned, so the rollout can change without re-signing. If your CDN is compromised, an attacker can freeze phones on their current bundle or change who is in the rollout. They cannot serve unsigned code, change any signed field, push a phone below its floor or serve a manifest under a different seq.