Skip to main content

Required Intermediate Build

Overview​

A required intermediate build is a version that every client must install before it is allowed to jump past it. When an install on an old version checks for updates, faynoSync offers the intermediate instead of the latest release. Once the client is on the intermediate, the next check offers the latest one.

You need this when a release cannot be skipped:

  • the intermediate version migrates a local database, config file or cache format that later versions no longer know how to read;
  • it rotates keys, certificates or an update feed URL that newer versions depend on;
  • it ships a fix that has to run before a newer installer can work at all.

Without an enforced path, a user who has been offline for months jumps straight from 1.0.0 to 3.0.0 and skips the migration in 2.0.0. A required intermediate build makes that impossible: the decision happens on the server, so the client needs no special logic.

How the server picks the version​

On every update check (/checkVersion, the SDKs, and the dynamic Velopack feed), faynoSync looks for versions of the same app that are:

  1. marked as intermediate (intermediate: true);
  2. published;
  3. in the same channel the client asked for;
  4. carrying an artifact for the client's platform and arch;
  5. newer than the client's version and not newer than the latest version.

If several versions match, the lowest one is offered. The response then describes that version entirely: its artifacts, its changelog, its critical flag and its rollout percentage, plus is_intermediate_required: true.

If nothing matches, the client gets the latest version as usual and is_intermediate_required is left out of the response.

Example: versions 1.0.0, 1.0.1 (intermediate), 1.0.2 and 1.0.3 (intermediate), 1.0.4 are published on stable.

Client versionOffered versionis_intermediate_required
1.0.01.0.1true
1.0.11.0.3true
1.0.21.0.3true
1.0.31.0.4absent
1.0.4no updateabsent

Each install moves one step per update, so a client that is several intermediates behind walks the chain in order.

Marking a version as intermediate​

On upload​

Add "intermediate": true to the data JSON of /upload:

curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/myapp-1.0.1.deb"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"1.0.1\",\"channel\":\"stable\",\"publish\":true,\"intermediate\":true,\"platform\":\"linux\",\"arch\":\"amd64\",\"changelog\":\"Migrates the local database to the v2 schema\"}"'

With the faynoSync CLI, pass --intermediate:

faynosync upload \
--app myapp \
--file ./myapp-1.0.1.deb \
--version 1.0.1 \
--channel stable \
--platform linux \
--arch amd64 \
--publish \
--intermediate \
--changelog "Migrates the local database to the v2 schema"

For presigned uploads, intermediate is taken from the upload that creates the version, like publish, critical and rollout.

On an existing version​

Set the flag later with /apps/update:

curl --location 'http://localhost:9000/apps/update' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'data="{\"id\": \"653a6268f51dee6a99a3d88c\", \"app_name\": \"myapp\", \"version\": \"1.0.1\", \"channel\": \"stable\", \"publish\": true, \"intermediate\": true, \"platform\": \"linux\", \"arch\": \"amd64\", \"changelog\": \"\"}"'
Calling /apps/update directly

The dashboard always sends the current intermediate value, so editing a version there keeps the flag. When you call /apps/update yourself, include intermediate in data: a missing field is treated as false.

Response examples​

Intermediate step required​

A client on 0.0.1 checking for updates, while 0.0.5 is a required intermediate and 0.0.8 is the latest:

{
"changelog": "Example changelog\n",
"critical": true,
"is_intermediate_required": true,
"update_available": true,
"update_url_deb": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.5.deb",
"update_url_rpm": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.5.rpm"
}

No intermediate step​

The same client after it has installed 0.0.5:

{
"changelog": "Example changelog\n",
"critical": false,
"update_available": true,
"update_url_deb": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.8.deb",
"update_url_rpm": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.8.rpm"
}

The JavaScript SDK exposes the flag as isIntermediateRequired, the Go SDK as IsIntermediateRequired. Use it in the UI, for example to explain that another update will follow right after this one.

Support by updater​

UpdaterIntermediate buildsNotes
Manual (/checkVersion, SDKs)YesThe response points at the intermediate's artifacts.
electron-builderYesThe yml package URL belongs to the intermediate's version folder, so electron-updater downloads that version. See the Electron example.
Squirrel macOS / Squirrel WindowsYes, via /checkVersionSame server decision, returned in the updater's response format.
TauriYes, via /checkVersionSame server decision.
VelopackDynamic feed onlyPass localVersion to the dynamic feed; faynoSync caps the feed at the intermediate. The static CDN feed cannot make per-client decisions.
SparkleNot yetNeeds a dynamic appcast route; the static appcast is the same for every client.

Edge cache​

With the edge and S3 response cache, manifests are stored per client version (.../{client_version}.json), so a client on 1.0.0 and a client on 1.0.2 read different cached answers and the intermediate decision stays correct. An upload or an /apps/update call clears the app's manifests for that channel, so a newly marked intermediate takes effect on the next miss (allow up to a minute for a CDN in front of the bucket).

Staged rollouts​

An intermediate carries its own rollout percentage. The rollout of the final version only applies once the install is already on the intermediate, and chained rollouts multiply. See Interaction with required intermediate versions.

Use cases​

  1. Breaking changes: a new version changes a file format or protocol, and only the intermediate knows how to convert the old one.
  2. Database migrations: local schema changes have to run in order.
  3. Security updates: a key rotation or a new trust anchor has to be installed before later releases are accepted.
  4. Feature dependencies: new features rely on data or settings created by the intermediate.

Best practices​

  1. Record in the changelog why a version is a required step. The client shows the intermediate's changelog, not the latest one.
  2. Keep the chain short. Every intermediate is one more download and restart for users who are far behind.
  3. Make the intermediate stable and well tested. Users cannot skip it, so a broken intermediate blocks everyone behind it.
  4. Upload the intermediate for every platform and arch you ship. A client whose platform/arch has no artifact on the intermediate is not stepped through it and gets the latest version directly.
  5. Do not unpublish an intermediate while installs older than it still exist. An unpublished version is ignored, so those installs skip straight to the latest.
  6. Mark the version critical as well if it must be installed promptly; intermediate controls the order, not the urgency.

Troubleshooting​

The latest version is offered instead of the intermediate. Check that the intermediate is published, is in the same channel the client sends, has an artifact for the client's platform and arch, and is newer than the client's version. If you edit versions through the API, also check that the flag is still set: an /apps/update call without intermediate resets it to false.

Clients on the intermediate are not moving on. That is the expected behavior while the next version is under a staged rollout and the device's bucket is outside it. See Staged rollouts.

A Velopack client skips the intermediate. The client is reading the static CDN feed. Point it at the dynamic feed and send localVersion.

Limitations​

  1. One step per check: only the lowest matching intermediate is offered; the client reaches the next one on its following update.
  2. Versions are compared numerically (1.0.10 is newer than 1.0.9), so they have to be valid version strings.
  3. Sparkle and the static Velopack feed do not support intermediate builds yet.