Files
fruix/docs/reports/phase15-base-upgrades-freebsd.md

5.8 KiB

Phase 15.2: side-by-side native base versions and rollback-friendly redeploy

Date: 2026-04-03

Goal

Phase 15.2 demonstrated that Fruix can keep at least two distinct declarative FreeBSD base builds in /frx/store at the same time and switch between them through the normal system rebuild/image/boot flow.

For this first upgrade-story validation, both declared bases still point at the same local /usr/src, but they carry distinct declarative version labels:

  • current base: 15.0-STABLE
  • candidate base: 15.0-STABLE-p1

That is enough to prove the Fruix properties needed here:

  • distinct content-addressed outputs
  • side-by-side coexistence
  • no in-place mutation of the older base closure
  • rollback to the earlier closure using the normal deployment path

New files

Added:

  • tests/system/run-phase15-base-coexistence.sh
  • tests/system/run-phase15-base-rollback-qemu.sh
  • tests/system/run-phase15-base-rollback-xcpng.sh

Validation model

Current base declaration

(freebsd-base
  #:name "stable-default"
  #:version-label "15.0-STABLE"
  #:release "15.0-STABLE"
  #:branch "stable/15"
  ...)

Candidate base declaration

(freebsd-base
  #:name "stable-canary"
  #:version-label "15.0-STABLE-p1"
  #:release "15.0-STABLE"
  #:branch "stable/15"
  ...)

Both declarations use the same validated native Phase 14 package composition:

  • kernel from freebsd-native-kernel-for
  • bootloader from freebsd-native-bootloader-for
  • runtime from freebsd-native-system-packages-for
  • shepherd-pid1

Side-by-side build validation

Passing run:

  • PASS phase15-base-coexistence
  • workdir: /tmp/phase15-2-coexist-1775202833

Confirmed:

current_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
candidate_closure=/frx/store/dc40b1b7a76084e140d0457f3b7f6c5d4acc185f0d6cee0b161c9775d5fb3bec-fruix-system-fruix-freebsd
current_base_version_label=15.0-STABLE
candidate_base_version_label=15.0-STABLE-p1
side_by_side_base_versions=ok
rollback_rebuild_path=ok

Current native base stores:

/frx/store/d9785661ea4829d51fbf545c2607a5691af2cc33c8ef3cd44de7ad5626685098-freebsd-native-kernel-15.0-STABLE
/frx/store/b448c822302ccdfb2f06da811fb224a044c51a9935bbfcd77a71a25d02f228f1-freebsd-native-bootloader-15.0-STABLE
/frx/store/ac3ba684020e70d3c76e593fd687cef8ab5e148958baabb477b7ef3d2647c5cd-freebsd-native-runtime-15.0-STABLE

Candidate native base stores:

/frx/store/05bee8ffbe8c43242ffd97da4dc305f2921612a660cbcb48c3a3536bfac07079-freebsd-native-kernel-15.0-STABLE-p1
/frx/store/8955f1bfe89321e6e1e628c59376f2092547523f48a773974cc259963adac184-freebsd-native-bootloader-15.0-STABLE-p1
/frx/store/30314f17fd8ff4a1a3eff31c8c5048f15f67c46d1132d5b8c45fd9768742665e-freebsd-native-runtime-15.0-STABLE-p1

Important result:

  • the older current closure stayed in /frx/store
  • the candidate closure appeared beside it
  • rebuilding the current declaration returned the exact original current closure path again

Local QEMU rollback validation

Passing run:

  • PASS phase15-base-rollback-qemu
  • workdir: /tmp/phase15-2-qemu2-1775204321

Validation sequence:

  1. boot current base
  2. boot candidate base
  3. boot current base again

Confirmed:

current_first_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
candidate_closure=/frx/store/dc40b1b7a76084e140d0457f3b7f6c5d4acc185f0d6cee0b161c9775d5fb3bec-fruix-system-fruix-freebsd
rollback_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
current_base_version_label=15.0-STABLE
candidate_base_version_label=15.0-STABLE-p1
rollback_base_version_label=15.0-STABLE
base_rollforward_and_rollback=ok

This showed that the booted system could move forward to the candidate base and then return to the earlier closure without mutating it in place.

Real XCP-ng rollback validation

Passing run:

  • PASS phase15-base-rollback-xcpng
  • workdir: /tmp/phase15-2-xcpng-1775204839

Validation sequence:

  1. boot candidate base on the approved VM/VDI
  2. boot current base again on the same approved VM/VDI

Confirmed:

candidate_closure=/frx/store/dc40b1b7a76084e140d0457f3b7f6c5d4acc185f0d6cee0b161c9775d5fb3bec-fruix-system-fruix-freebsd
rollback_closure=/frx/store/9f57ecc6481e271811ceb53ac21a3b2aef4ef329f82b7d4788622315db1f0e43-fruix-system-fruix-freebsd
candidate_base_version_label=15.0-STABLE-p1
rollback_base_version_label=15.0-STABLE
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
vdi_id=0f1f90d3-48ca-4fa2-91d8-fc6339b95743
base_rollforward_and_rollback=ok

Both boots also preserved the already-hardened guest properties:

  • shepherd_pid=1
  • sshd_status=running
  • compat_prefix_shims=absent
  • guile_module_smoke=ok

Result

Phase 15.2 is complete.

Fruix now has a real declarative rebuild/redeploy/rollback story for the FreeBSD base at the store/model layer:

  • two declared base versions can coexist side by side in /frx/store
  • the candidate deployment does not overwrite the current one in place
  • rebuilding the earlier declaration returns to the earlier closure path
  • the same story works both locally under QEMU and on the approved XCP-ng VM/VDI path

Scope note

This first upgrade-story validation still uses the same local /usr/src as the underlying source tree for both declarations. What changed is the declared base identity and therefore the store/model/deployment identity.

That is sufficient for this phase because the requirement was to establish the upgrade semantics:

  • explicit base declaration
  • side-by-side outputs
  • rollback-friendly closures

The next improvement beyond Phase 15 would be to make acquiring or selecting distinct source trees/releases more reproducible and less tied to a single host checkout.