Contributing
Thanks for your interest in improving bootc-migrate.
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 |
just e2e-tui | Bluefin stable β Dakota, driven through the TUI wizard | 40 GB | E2E_MODE=tui-migrate |
These are the local recipes and they deliberately differ from the CI
matrix β just e2e-lts runs XFS at 20 GB to exercise the ext4-loopback store
on a small disk, while CI's LTS cell runs ext4 at 40 GB. The seven-cell CI
matrix lives in .github/workflows/e2e-tests.yml, which is authoritative;
README.md reproduces it. Don't sync these two tables into one β they answer
different questions.
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 /usr/local/bin/"
# Option 2: If architectures match, just ship the binary
base64 target/release/bootc-migrate | corral ssh tui-e2e --user root -c \
"base64 -d > /usr/local/bin/bootc-migrate && \
chmod +x /usr/local/bin/bootc-migrate"
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', ['bootc-migrate'])
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.
- The change satisfies the Definition of Done (REVIEW.md): every claim in the PR matches the diff, no undischarged validation caveats remain, and all required checks have been observed passing on the head commit.
Code reviewβ
Please read REVIEW.md β it describes the Definition of Done (DoD),
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, and discharge all validation steps).
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.