Contributing
Thanks for your interest in improving bootc-migrate-composefs.
Note: this tool performs an in-place, hard-to-reverse migration of a real system. Treat changes to the migration phases (
src/migration/) with extra care and exercise them through the end-to-end suite before merging.
Development setupβ
You need a recent stable Rust toolchain (the crate targets edition 2024,
rust-version = 1.88.0) and just.
$ cargo build
$ just check # clippy + rustfmt + unit tests + shellcheck β run before every PR
IDE setupβ
Standard rust-analyzer works out of the box. The crate uses clippy with
several extra lints enabled (see Cargo.toml); run just check rather than
cargo clippy alone to get the full lint set.
Running the end-to-end testsβ
The E2E harness boots a real QEMU VM, runs the full migration, reboots, and validates the result. It needs additional host packages:
# Fedora/RHEL
sudo dnf install qemu-system-x86_64 edk2-ovmf podman cryptsetup lvm2 swtpm
# Ubuntu/Debian
sudo apt install qemu-system-x86 ovmf podman cryptsetup-bin lvm2 swtpm
You also need root (for loop mounts and pflash) and outbound registry access
(ghcr.io) to pull the Bluefin/Dakota images. On a fresh machine, seed the
local registry cache first (saves ~8 GB of re-pulls on every run):
$ just registry-start # start a local OCI registry on localhost:5000
$ just registry-cache # pull Bluefin + Dakota; push to local registry
E2E scenariosβ
| Recipe | What it tests | Disk | Notes |
|---|---|---|---|
just e2e | Bluefin stable β Dakota (btrfs, x86_64) | 20 GB | Default; fastest |
just e2e-lts | Bluefin LTS β Dakota (XFS + ext4 loopback) | 20 GB | LTS base |
just e2e-luks | Bluefin LTS β Dakota (XFS + LUKS + swtpm) | 40 GB | Encrypted root |
just e2e-lvm | Bluefin LTS β Dakota (LVM-on-LUKS, separate /var) | 40 GB | Most complex |
Run the default scenario:
$ just e2e
Watch progress in another terminal:
$ just watch # tails the latest .log; exits on errors or idle timeout
Or ssh into the running VM to poke around:
$ just e2e-ssh # opens an interactive SSH session to port 2222
Debugging a failed E2E runβ
$ just e2e-failures # grep log for failures/errors
$ just e2e-composefs # grep for composefs-related boot messages
$ just e2e-tail # tail the QEMU serial console (high-signal lines only)
$ just e2e-status # show disk.raw status + QEMU/SSH availability
To reproduce a failure starting from after the migration (skipping setup):
$ SKIP_SETUP=1 just e2e-reboot-test
Using Corral VMs for interactive testingβ
Corral is a VM manager that provisions KubeVirt (or local QEMU) VMs from bootc container images. It's useful for interactive TUI testing and exploratory debugging where the scripted QEMU harness is too rigid.
Setup β install the corral binary (see Corral's README), then create a
Bluefin VM for testing:
$ corral create tui-e2e --image ghcr.io/projectbluefin/bluefin:stable \
--cpu 2 --memory 4Gi --disk 40Gi --efi
$ corral start tui-e2e
SSH into the VM:
$ corral ssh tui-e2e --user root
Deploy a local build to the VM (cross-compile or build on the VM):
# Option 1: Build on the VM (Rust must be installed in the VM)
tar czf /tmp/bmc-src.tar.gz --exclude=target --exclude=.git .
base64 /tmp/bmc-src.tar.gz | corral ssh tui-e2e --user root -c \
"base64 -d > /tmp/src.tar.gz && mkdir -p /tmp/bmc && \
tar xzf /tmp/src.tar.gz -C /tmp/bmc && cd /tmp/bmc && \
cargo build --release && \
cp target/release/bootc-migrate-composefs /usr/local/bin/"
# Option 2: If architectures match, just ship the binary
base64 target/release/bootc-migrate-composefs | corral ssh tui-e2e --user root -c \
"base64 -d > /usr/local/bin/bootc-migrate-composefs && \
chmod +x /usr/local/bin/bootc-migrate-composefs"
Capture TUI screenshots (the VM won't have tmux on an immutable OS, but Python3 is available for PTY capture):
corral ssh tui-e2e --user root -c "python3 << 'EOF'
import pty, os, time, select, re, struct, fcntl, termios, sys
rows, cols = 30, 100
pid, fd = pty.fork()
if pid == 0:
ws = struct.pack('HHHH', rows, cols, 0, 0)
fcntl.ioctl(sys.stdout.fileno(), termios.TIOCSWINSZ, ws)
os.environ['TERM'] = 'xterm-256color'
os.execvp('bootc-migrate-composefs', ['bootc-migrate-composefs'])
else:
ws = struct.pack('HHHH', rows, cols, 0, 0)
fcntl.ioctl(fd, termios.TIOCSWINSZ, ws)
time.sleep(2)
out = b''
while select.select([fd],[],[],1)[0]:
out += os.read(fd, 65536)
os.write(fd, b'q'); time.sleep(0.5)
os.write(fd, b'h'); time.sleep(0.3)
os.write(fd, b'\r'); time.sleep(0.5)
os.waitpid(pid, 0)
text = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', out.decode('utf-8', errors='replace'))
print(text)
EOF"
Corral VMs in CI β the CI matrix uses the scripted QEMU harness
(tests/run-e2e.sh) for reproducibility. Corral VMs are for developer
convenience only and are not required to contribute.
Cleaning up after E2Eβ
$ just cleanup # kill QEMU, prune podman, remove disk.raw and .log files
Adding a new E2E scenarioβ
- Add a new recipe in
justfilemodelled one2e-luksore2e-lvm. - Add the scenario to the CI matrix in
.github/workflows/e2e-tests.yml(follow the existinginclude:pattern, setname,filesystem,disk-size, and any env overrides). - Update the CI matrix table in AGENTS.md.
- Document the scenario in docs/filesystem-support.md.
Before you open a PRβ
just checkpasses (clippy + rustfmt + unit tests + shellcheck β this is what CI'svalidatejob runs).cargo deny checkpasses if you touched dependencies.- Commits follow the
component: Summaryconvention described in REVIEW.md; fixups are squashed before merge. - New non-trivial logic has unit tests (prefer table-driven, per REVIEW.md), and migration-path changes are exercised by at least the default E2E scenario.
- If your change affects the kernel command line, boot artifacts, or any phase output, run the full E2E matrix locally or wait for CI to do it on your PR.
Code reviewβ
Please read REVIEW.md β it describes the testing, code-quality, and
commit-message expectations applied here. AI-assisted contributions must follow
AGENTS.md (no automatic Signed-off-by; add an Assisted-by:
trailer).
Dependency update policyβ
Dependency updates come through Renovate (see
renovate.json). Patch-level updates are auto-merged if CI is green. Minor and
major updates get a PR for human review. When reviewing Renovate PRs:
- Check the changelog / release notes for breaking changes.
- Verify
cargo deny checkstill passes. - Run
just checklocally if the changed crate is a key dependency (rustix,clap,anyhow,serde_json).
Licenseβ
By contributing, you agree that your contributions are dual-licensed under the MIT and Apache-2.0 licenses.