Release management
How a change gets from a pull request to people's PCs, who decides each step, and how the release manager integrates work from many parallel workstreams. This page is the process; the live state is in release status, the machine-readable inventory in release-manifest.json, and every Stable build so far in release history.
Release gates are not defined here. The pass/fail gates for a pilot or production release are the readiness checklist owned by the hardening program; this page says when each one is checked.
Two review threads also give each PR a PASS, CONDITIONAL PASS or FAIL: the UX and simplicity review (docs/ux/release-review.md) and the engineering quality review (docs/quality/). A FAIL from either one keeps a PR out of a merge round until its owner fixes it or Chris overrides it. A CONDITIONAL PASS goes into the round with its conditions listed in the merge-round question. Neither review merges or releases anything.
How Fabric Desktop is distributed#
There is no installer, store or package feed. The product is two files published as GitHub release assets, and the app updates itself from them.
| Piece | What it is | Built by |
|---|---|---|
fabric-desktop.exe |
The Tauri 2 shell with the UI (app/) |
CI shell job, cargo build --release --locked on Windows |
fabric-desktop-engine.zip |
python.org's embeddable Python 3.12 plus the fabric_desktop engine (sidecar/) and every pinned package in sidecar/constraints.txt |
CI engine job on Windows |
version.json |
The build's commit, What's new notes, 30 days of history, and the SHA-256 of the exe and engine | CI release job |
| SBOMs | sbom-app.cdx.json, sbom-engine.cdx.json (CycloneDX) |
CI sbom-app and engine jobs |
The headless agent's Linux image (deploy/linux/Dockerfile) is built and started in CI but not published anywhere. The docs site is published to GitHub Pages by docs.yml on every merge that touches docs/.
Channels#
| Channel | Release tag | Moves when | Who moves it |
|---|---|---|---|
| Preview | latest (prerelease) |
Every merge to main whose CI is green and is still main's newest commit when the release job runs |
Automatic (ci.yml release job) |
| Stable (the default) | stable |
Someone runs Promote to stable, which copies the exe, engine and version.json from latest |
The product owner (Chris), by hand, or the release manager on his explicit go for that promotion |
The app reads the commit from the channel's version.json, not from the release's target commit (that field goes stale after a promote). Both channels publish with one shared concurrency group, release-assets, so a promote never copies a half-replaced Preview.
Merging to main is a Preview release. About 5,000 people are expected on launch; Preview is the opt-in channel, but anything merged reaches it within one CI run. Stable is the production release.
Who decides what#
| Step | Decided by | Notes |
|---|---|---|
| Open, rebase, fix a PR in its own area | The owning workstream | Fixes in someone else's area go back to the owner |
| Trial integration, conflict analysis, integration fixes on an integration branch, release docs | Release manager | No approval needed; nothing reaches main |
Merge to main (= Preview release) |
Chris | He merges by hand, or asks for a merge round. Each request covers that round only |
| Promote to Stable (= production release) | Chris | Asked as a separate question, with the candidate commit and its evidence |
| Branch protection, repository variables, signing account | Chris | Repository settings only he can change |
Workflow changes (.github/workflows/) |
Chris reviews | Per AGENTS.md, workflow changes always need maintainer review |
Only pull requests this project opened are merged in a merge round. A PR from someone else (for example a fork) is listed but held until Chris says otherwise.
The integration cycle#
Run on every release-management pass. It is safe to repeat: it changes nothing on main and creates no PRs, tags or releases unless a step below says so.
- Discover. Fetch
mainand every open PR head (git fetch origin '+refs/pull/*/head:refs/pr/*'). List open PRs with base, draft state, author and CI on the head commit. - Check each PR against
main.python3 .github/scripts/integration_trial.py --base origin/main <PR numbers>reports for each PR whether it merges cleanly on its own, and then merges them in the order given onto a throwaway branch and reports the first conflict for each. - Classify every PR (states below) and record it in release-manifest.json and release status.
- Order the batch. Smallest blast radius first: docs-only, then single-module fixes, then features; stacked PRs in stack order; PRs whose CI hasn't finished or is unproven last. Nearly every UI PR appends to
app/tests/ui.test.cjs, so two UI PRs always conflict after the first merges: chain them (each branch merges the previous) so one CI round proves the whole chain. - Validate the integrated result, not just each PR. On the trial branch, run what CI runs that can run on Linux:
node --test app/tests/ui.test.cjscd sidecar && python -m pip install -e ".[dev]" -c constraints.txt && python -m pytest -qWindows-only jobs (engine build, shell build, Windows pytest) are taken from each PR's own CI and are re-proven by the post-merge CI run onmain.
- Repair only what integration broke: a conflict where both sides' intent is clear, resolved so neither change is lost, on the integration branch or as a corrective PR. Anything that changes a feature's behaviour goes back to its owner with a corrective request (format below). Two incompatible designs go to Chris as a decision, never a compromise invented here.
- Ask for the merge round. One question to Chris naming the PRs, the order and the evidence. Nothing merges without his go for that round.
- Merge in order, waiting for each merge's
mainCI where the next PR conflicts with it. Watch the Preview publish ofmain's newest commit. - Verify Preview. The
latestrelease'sversion.jsonnamesmain's tip and itsexe_sha256andengine_sha256equal the GitHub digests of the uploaded assets (curl -s https://api.github.com/repos/KratosDataGod/fabric-desktop/releases/tags/latest | jq '.assets[] | {name, digest}'). If the engine zip is missing, re-run the failed release job ofmain's tip run. - Report in the thread: what was reviewed, merged, fixed, what failed and what is blocked.
PR states#
| State | Means |
|---|---|
| Ready to merge | Real CI green on the head commit (the Codex review check is ignored while its quota is exhausted), merges cleanly onto main or onto the PR before it in the batch, not a draft, nothing it depends on is unmerged |
| Validating | CI on the head commit hasn't finished yet, or the head changed after the last trial |
| Requires integration fix | Acceptable, but conflicts with main or another ready PR in a way the release manager can resolve without changing behaviour |
| Requires developer changes | Needs a change only the owner should make; a corrective request has been sent |
| Blocked by dependency | A PR it builds on (stack base, shared contract) is not merged |
| Failed validation | A real CI check is red on the head, or the integrated trial fails |
| Hold | Draft, awaiting Chris's decision or mockup reaction, or not opened by this project |
| Ready for release | Merged, on Preview, and waiting for the next Stable promotion |
Corrective request format#
Sent to the owning workstream through the project coordinator:
- Affected PR and head commit.
- Problem, as the failing command or conflicting lines.
- Root cause.
- The change needed.
- Acceptance criteria.
- How to validate (exact commands).
What each PR must say (handoff contract)#
The pull request template asks every PR for: what a user sees before and after, the workstream and feature ID, related issue, shared files and contracts touched (engine methods, record schemas, settings, audit.py, server.py, app.js, features.js), PRs it depends on or conflicts with, tests added and run, Preview labelling, permission changes, and release notes in user words. The release manager reads these instead of reconstructing intent from diffs.
Promoting to Stable#
A Stable promotion is the production release. Before asking Chris for it:
- Pick the candidate. The commit now on Preview (
latestversion.jsoncommit). It must bemain's tip or an ancestor with no later merge pending on Preview. - Check the evidence.
mainCI green on that commit; Preview assets match their checksums (step 9 above); the readiness checklist gates that apply to a Stable release are no worse than at the last promotion;FABRIC_CLIENT_IDis still unset (the engine'sengine.jsonclient_idmust be empty, so sign-in uses Microsoft's Fabric CLI client). - Write the notes for people, not developers: one line per change they will notice, from the PR titles and "Release notes" sections since the last Stable commit (
git log --first-parent --format=%s <last stable>..<candidate>). Untested-on-a-tenant features say Preview. - Ask Chris with the candidate commit, the PR list, the evidence and the notes.
- On his go, run Actions > Promote to stable with the notes. Promote copies whatever is on Preview at that moment, so check
lateststill names the candidate immediately before running it (until HD-023 makes promote take a named commit). - Verify Stable:
stableversion.jsonnames the candidate and its checksums match thestableasset digests. - Record it in release history.
Rollback today#
There is no one-click rollback yet. Each channel keeps only its newest build, and the engine always comes from the channel's current release, so an older build can't be pinned (HD-028 in the hardening backlog adds per-build release assets). fabric-desktop.exe.old is deleted at startup and is not a rollback.
What can be done now, in order of preference:
- Fix forward. Revert the offending PR on
main(a revert PR, merged by Chris), let Preview publish, then promote. This is the normal path. - Restore Stable to the previous Stable build. The previous Stable commit is in release history. Its exe and engine are not kept as release assets, so they must be rebuilt: re-run that commit's
mainCI run if its artifacts are still retained (GitHub keeps workflow artifacts 90 days by default), download them, verify against that build's provenance attestation, and upload tostablewith a matchingversion.json. This replaces production files and needs Chris's explicit go. - Organizations on managed updates can redeploy the exe and engine they already hold (enterprise install).
Reverting the app does not revert what a newer build wrote: local settings, schedules, run history, the metadata store and anything in OneLake stay as the newer build left them. Before shipping a change to a stored format, its PR must say whether an older build can still read it.
Versioning#
Builds are identified by their commit. Version numbers in the code do not change between builds today: the shell reports 0.1.0 (app/src-tauri/Cargo.toml, tauri.conf.json) and the engine 0.20.0 (sidecar/pyproject.toml, fabric_desktop/__init__.py), which audit logs, agent status records and support bundles copy. So a support bundle cannot say which build it came from without the commit.
Proposal (RM-003, needs Chris's decision): semantic versions 0.MINOR.PATCH cut at each Stable promotion, with a v0.x.y tag on the promoted commit and the version stamped into the shell and engine at build time from the tag (Preview builds report 0.x.y-preview+<sha7>). 1.0.0 is reserved for the first release that passes every gate in the readiness checklist. This needs workflow changes and pairs with HD-028's per-build assets, so it is a single design with the hardening program.
Need a hand?
Find a fix in troubleshooting, or share a reproducible issue with the community.
Get support ↗Improve this guide on GitHub ↗