Roadmap
Status date: 2026-08-13. Living document; the issue tracker is authoritative for day-to-day state, this file is authoritative for shape and sequence.
Visionβ
One engine that moves a bootc system between images, backends, bootloaders, base families, and desktops β safely, with a staged+rollback contract, and with the same evidence-first discipline at every step: report before acting, verify after acting, keep the previous state bootable.
Three deliverables share the code:
| Deliverable | What it is | Stability contract |
|---|---|---|
bootc-migrate | The proven OSTreeβComposeFS migrator (protected MVP) | CLI surface, output, and behavior frozen; its four E2E cells are untouchable regression gates |
bootc-migrate-core | The capability library: phases, preflight, /etc merge, transaction, registry streaming, stores, scan, remap, boot audit, DE stash/restore | Additive growth; everything new lands here first |
bootc-rebase | The universal CLI: routing table Γ strategies | Where new user-facing capability ships |
Where we areβ
M0 (MVP hardening) and M1 (same-backend re-base engine) are done. Every
issue under both milestones is closed. bootc-rebase truthfully routes all
four backend pairs (implemented for three of them; composefsβostree remains
explicitly refused, not silently attempted) and the capability scan (#24)
covers every proposed probe.
In progress, with an explicit boundary between what's landed and what's deliberately deferred β each of M2, M3, and M5 shipped a pure/unit-testable "skeleton" slice first. M2 stopped there, ahead of boot-critical live-system mutation CI cannot validate; M3 and M5 have since grown their live halves, but those run on paths no CI cell reaches (no cross-base E2E cell, no NVRAM mutation, no TUI assertions), so they are landed-but-unproven rather than done. See each milestone below for the specific line and why.
Milestonesβ
M0 β MVP hardening (continuous; protects everything else) β doneβ
All exit-criteria issues closed: #72 (cfs CLI drift, resolved short-term by
probe+delegation, long-term by #13's NativeStore), #22 (E2E rollback proof),
#26 (rollback subcommand), #25 (commit verified fresh-install-identical),
#17 (post-migration /var cleanup), #27 (sleep inhibitor), #18 (pre-baked SSH
E2E image), #12 (phase-module unit tests), #29 (Containerfile-initrd
alternative β evaluated and superseded by the shipped host-side dracut
--rebuild approach, closed as won't-implement).
Exit criteria met: rollback proven in CI; commit/undo produce indistinguishable-from-fresh layouts; no known MVP flakes.
M1 β Same-backend re-base engine (scenarios A / Aβ²) β doneβ
#63 (ostree-rebase E2E cell), #64 (Strategy::OstreeDeploy), #66
(Strategy::ImageSwap), #24 (capability scan: parsers, registry fetch wiring,
bootc-rebase scan subcommand, and the Compatible: YES/NO gate) β all
closed.
Exit criteria met: bootc-rebase --plan truthfully answers all four
backend-pair routes; ostreeβostree proven by its own E2E cell; scan output
drives route refusal with evidence.
M2 β Bootloader migration (scenario B) β skeleton landed, live mutation deferredβ
#65 β the
pure core (BLS entry assembly, kernel-arg carry-over, entry-token derivation)
merged and is unit-tested; bootc-rebase migrate-bootloader exists as a CLI
shape but its run always refuses with "not implemented."
Why stopped here: the remaining work β ESP populate, NVRAM cutover via
efibootmgr with a BootNext one-boot trial before BootOrder promotion,
and the kernel-install resync hook (without which a flipped system silently
boots stale kernels after the next update) β is boot-critical and currently
unvalidatable: no E2E cell exercises it yet, and this isn't something a
compile+unit-test loop can prove correct. A full implementation plan (ESP
layout, NVRAM sequencing, resync-hook mechanism, phase-5 interplay, E2E cell
design) is posted on the issue for whoever picks it up with real
E2E-iteration budget and explicit sign-off on the risk.
Exit criteria (not yet met): a GRUB2 bluefin VM re-bases, boots via
sd-boot, survives a kernel update, and --undo restores GRUB cleanly.
M3 β Cross-base re-base (scenario C) β both parts landed, neither exercised by CIβ
#67 part 1
(remap planner + apply walk over the staged deployment) is done and wired
into OstreeDeploy, gated by is_cross_base + --accept-cross-base.
Part 2 (the cross-base /etc conflict policy) was previously recorded here
as blocked: OstreeDeploy and ImageSwap delegate /etc merging to
bootc switch, not to mergetc, so there was no caller for a mergetc
cross-base extension. That is still true of mergetc β and it turned out to
be the wrong question. The blocker was stated in terms of the call site;
the inputs were never missing. bootc switch stages without rebooting, so
afterwards the source's defaults (<booted>/usr/etc), the user's live
/etc, and the target's defaults (<staged>/usr/etc) all still sit on disk
beside the merge's own output (<staged>/etc).
So the policy landed as bootc-migrate-core::etc_conflict: a narrow
post-merge reconciliation pass, not a second merge. It rewrites only the
paths where all three inputs disagree β the conflict class the native merge
cannot reason about, because within one base lineage "keep the user's value"
is the right answer and across two it is not β and leaves every other path
exactly as bootc switch produced it. Target defaults win; the displaced
value is preserved as a .rebase-old sidecar (the same convention #15
introduced in mergetc); machine-describing paths and the identity DBs are
reported but never replaced. This is the same "adjust the staged deployment
before first boot" seam part 1's remap already uses.
What is not proven: no CI cell is cross-base β all four E2E scenarios are
Fedora-family β Fedora-family, so is_cross_base is false and neither part 1
nor part 2 executes in CI at all. Both are unit-tested (planning, exemptions,
sidecar naming, report/JSON, and a collectβplanβapply round trip over real
trees) and neither has run on a real cross-base system.
Related: #80
confirmed (via reading ostree's merge_configuration_from() source directly)
that bootc switch's native merge does plain whole-file 3-way merge with
no identity-DB (passwd/group/etc.) key-level reconciliation β the
exact class of problem mergetc's union-merge exists to prevent. The
etc_conflict pass deliberately does not close that gap: it holds the
identity DBs exempt (it has no union-merge to rescue them either) and keeps
the existing advisory warning. #80 still needs either an upstream
ostree/bootc change or its own compensating logic.
Exit criteria (not yet met): fedora-family β centos-family E2E cell with
a populated /var: correct ownership after reboot, report lists every
renumbered account, .rebase-old sidecars present where defaults were taken.
M4 β Native store & the generation matrix (the #72 endgame) β not started beyond the feature flagβ
#13's
NativeStore (composefs/composefs-oci crates, no CLI shelling) exists behind
the composefs-native feature flag and is off by default. The default path
still probes host/target/builder for a legacy-CLI-capable bootc and pins
quay.io/fedora/fedora-bootc:42 as a builder when none of the three has it β
visible in every E2E run's Phase 2 log line. Store selection by target
generation, and retiring the pinned legacy builder, have not been picked up.
Exit criteria (not yet met): migration succeeds with no legacy-CLI
bootc anywhere (host, target, builder); BMC_CFS_BUILDER becomes a no-op
escape hatch.
M5 β Desktop & UX (scenario E + human factors) β computable cores landed; the interactive/live pieces exist but are unvalidatedβ
Three issues, same shape: the pure/reusable core shipped first and is
unit-tested. The interactive checklists (#15, #31) and #31's live NVRAM
mutation have since been built on top, but neither is exercisable by this
project's build/clippy/test/fmt + E2E loop β a passing CI run cannot
demonstrate that a checklist UI works, and no E2E cell mutates NVRAM. Both
carry that caveat in their module docs and need manual/corral-VM
validation; #31's efibootmgr path is the same class of risk as #65 and
should not be trusted until it has been run on a real UEFI system.
- #68 β DE
config stash/restore (GNOME dconf/gnome-shell, KDE kdeglobals/plasma,
COSMIC, niri, XFCE), a best-effort portable-preference extractor, and the
pre-switch.d/post-switch.dhook contract are done, unit-tested, and exposed asbootc-rebase de-migrate stash|restore. Target-image DE detection now streams session files, session binaries, and any display-manager default session out of the registry (de_detect), the same decision function classifies the running host, andrebase --de-migrate(off by default) stashes every human account's outgoing DE config before staging and restores a matching stash on the way back. The decision logic is table-driven-tested offline; what has not run is a cross-DE E2E cell (Bluefin GNOME β an Aurora/KDE image) asserting the stash exists on a real system and survives the reboot β that and the portable subset actually being applied by a hook still need live validation. - #15 β the
factory-vs-live
/etcdiff computation is done, exposed asbootc-migrate etc-drift(table or JSON). The interactive checklist UI (etc-drift --interactive, or--review-driftas "Phase 0.5" ahead of a live migration) and its wiring into Phase 4's merge decision (EtcDriftManifest/merge_etc_files_with_overrides, unit-tested) are now implemented. The checklist's terminal event loop itself is the one piece that can't be proven by compile+unit-test β same as this project's other interactive-only work β and needs manual/corral-VM validation. - #31 β the
UEFI boot-entry audit (dead/generic-label/duplicate/firmware-managed
classification) is done, read-only, exposed as
bootc-rebase boot-entries. Interactive selection, live entry removal, and branding-rename are now implemented too, taken on with explicit sign-off on the NVRAM-mutation risk rather than deferred again. The split follows the mitigation below: the whole decision β which entries may be deleted or renamed, and which refusals stop a plan β is a pure, table-tested planner (boot_cleanup::plan), and theefibootmgrexecutor (boot_cleanup::live) only performs what the planner approved. Dry-run is the default;--applyrequires a typed confirmation and writes a restorable NVRAM snapshot first;--undoreplays it. What has not run is theefibootmgrpath itself: no E2E cell mutates NVRAM, so entry deletion, the create-before-delete rename, and--undoneed a real UEFI machine or a corral VM before they are trusted β as does the checklist's terminal event loop, like this project's other interactive-only work.
Exit criteria (not yet met): bluefinβaurora-style switch preserves user data untouched, stashes/restores DE state, swaps DE-scoped flatpaks on request; non-experts can read what will happen before it happens.
Deferred extensions (raised on #30, not yet scoped into a milestone)β
Two directions came up in discussion on the generalize-into-a-re-base-engine RFC but never got a milestone or an issue. Recorded here so they aren't lost, not because either is imminent:
composefsβostree(reverse backend switch) β going back to an OSTree-backed image from a composefs system that never had an OSTree deployment.bootc-rebase's routing table currently refuses this route explicitly (see M1 above) rather than attempting it;undoonly reverts a migration this tool itself performed, which is a much narrower problem (the prior OSTree deployment is already on disk). A generalcomposefsβostreeroute needs to initialize an OSTree repo and bootstrap a deployment from a pulled image with nothing to restore from β mechanically the inverse ofStrategy::OstreeDeploy(M1), not a variant of it.- Cross-family re-base (Fedora β Ubuntu β Arch) β M3's cross-base work
(#67) stays within the Fedora family, where
/etcdefaults, UID/GID allocation, and the init/PAM stack share lineage. A cross-family route would need to treat most of/etcas non-mergeable (drop rather than 3-way-merge family-specific package-manager and service config), carry only universally meaningful state (/var/home, containers, flatpaks, accounts), and regenerate target-family defaults from scratch β closer to a "reinstall with data preservation" than an in-place migration.
1.0 β Universal migratorβ
All routes in the table implemented or explicitly refused with evidence;
MVP binary either retired into bootc-rebase --target-backend composefs
or kept as a thin alias; docs complete (architecture, generations,
recovery, hooks). Version and deprecation policy published.
Dependency graphβ
graph TD
M0["M0 MVP hardening β done"] --> M1["M1 same-backend engine β done"]
M1 --> M2["M2 #65 migrate-bootloader β skeleton done, live mutation deferred"]
M1 --> M3P1["M3 #67 pt1 remap β done, wired into OstreeDeploy"]
M3P1 --> M3P2["M3 #67 pt2 /etc conflict policy β landed post-switch, no cross-base E2E cell"]
M1 --> GAP80["#80 identity-DB merge gap β confirmed, tracked separately"]
M1 --> M4["M4 NativeStore selection / retire legacy builder β not started"]
M1 --> M5A["M5 #68 DE stash/restore β detection+wiring landed, cross-DE E2E cell deferred"]
M1 --> M5B["M5 #15 etc-drift report + TUI + Phase 4 wiring β done, TUI needs manual validation"]
M1 --> M5C["M5 #31 boot-entry audit + cleanup/branding β planner tested, NVRAM path needs a UEFI VM"]
Risks & standing mitigationsβ
- Upstream drift is the norm, not the exception. bootc replaced its cfs CLI once mid-project; assume it will again. Mitigation: the generation matrix (probe, delegate, native), pinned-builder escape hatch, and the empirical harness in docs/cfs-cli-generations.md to re-verify fast.
- MVP regression via shared code. Mitigation: MVP protection rule (frozen behavior, additive-only in core, probe-gated divergence), four untouchable E2E cells.
- Boot-critical work can't be validated by this project's normal loop. Mitigation, applied consistently across M2 and M5: land the pure/testable core, stop before live NVRAM/ESP/interactive-UI mutation, document the exact remaining plan on the issue, and require explicit sign-off + a dedicated E2E cell before attempting it β rather than shipping unvalidated boot-path code just because the rest of a PR's CI run was green.
- CI capacity. Runner starvation observed both 2026-07-19 (this repo's own concurrency groups) and 2026-07-24 (org-wide GitHub Actions queue congestion across most tuna-os repos simultaneously β not fixable from any one repo's side; just wait it out or check org Actions capacity/billing). Mitigation: everything is verified locally (or via a remote build host) before push; heavy validation designed as unit/loopback experiments where possible; E2E cells narrow and dispatchable individually.
- Settings translation temptation (#68). Prior art is unanimous that GNOMEβKDE translation fails; the spec forbids it. Stash/restore only.
Decision log (summary β details in issues)β
- Bootloader on ostreeβostree: migrate to systemd-boot when ready (#64)
- UID/GID divergence: auto-remap with report (#67)
- Cross-base /etc conflicts: target defaults win, user value kept as
.rebase-oldsidecar (#67, part 2 β landed asbootc-migrate-core::etc_conflict, see M3 above; not yet exercised by a cross-base E2E cell) - Store format is defined by the reader at boot (target image) β writer selection follows the target's generation (#13/#72)
- XBOOTLDR GUID-retype: dead (sd-boot β₯258.2 requires vfat) β ESP-copy
- resync instead (#65)
- DE settings: stash/restore, never translate (#68)
- Boot-critical or UI-only remaining scope gets a documented plan on its issue, not a best-effort implementation without validation (#65, #31, #15)
- When boot-critical scope is taken on (#31's cleanup): split the decision from the execution, so every safety rule is a pure unit test and the executor holds no policy; back up before mutating; make the destructive step opt-in and human-gated; and never remove the path back to a bootable state (#31)