diff --git a/Makefile b/Makefile index 7a4a5d0..021ba3a 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal .DEFAULT_GOAL := help -.PHONY: help build banger bangerd test fmt tidy clean install uninstall lint lint-go lint-shell coverage coverage-html coverage-total smoke smoke-build smoke-coverage-html smoke-clean +.PHONY: help build banger bangerd test fmt tidy clean install uninstall lint lint-go lint-shell coverage coverage-html coverage-total smoke smoke-build smoke-coverage-html smoke-clean smoke-fresh help: @printf '%s\n' \ @@ -50,6 +50,7 @@ help: ' make tidy Run go mod tidy' \ ' make clean Remove built Go binaries and coverage artefacts' \ ' make smoke Build instrumented binaries, run scripts/smoke.sh, report coverage (needs KVM + sudo)' \ + ' make smoke-fresh smoke-clean + smoke — forces first-install paths (migrations, image pull) into the coverage stamp' \ ' make smoke-coverage-html HTML coverage report from the last smoke run' \ ' make smoke-clean Remove the smoke build tree' @@ -147,6 +148,15 @@ smoke-coverage-html: smoke smoke-clean: rm -rf "$(SMOKE_DIR)" +# smoke-fresh wipes everything under $(SMOKE_DIR) (instrumented +# binaries, coverage pods, persisted XDG state) and runs a full +# smoke from scratch. Useful before a release tag: the regular +# `make smoke` reuses the XDG state across runs to skip the ~290MB +# image pull, which is fast but leaves migrations and image-upsert +# paths cold on every run after the first. smoke-fresh pays the +# time cost to stamp those paths into the coverage report too. +smoke-fresh: smoke-clean smoke + install: build mkdir -p "$(DESTDIR)$(BINDIR)" mkdir -p "$(DESTDIR)$(LIBDIR)/banger" diff --git a/internal/daemon/workspace.go b/internal/daemon/workspace.go index c17e622..9e0e97d 100644 --- a/internal/daemon/workspace.go +++ b/internal/daemon/workspace.go @@ -116,11 +116,21 @@ func (s *WorkspaceService) ExportVMWorkspace(ctx context.Context, params api.Wor // Mechanics: seed a temp index from diffRef's tree via git read-tree, // restage the working tree into that temp index with GIT_INDEX_FILE, // then emit the diff. The temp index is rm'd on exit via trap. +// +// The temp index must live on the same filesystem as the repo's +// real .git directory. `git read-tree --index-output=PATH` uses a +// lockfile + rename + hardlink sequence that fails with "unable to +// write new index file" when PATH is on a different filesystem — +// reliably reproducible on Debian bookworm guests where /tmp is +// tmpfs and the workspace overlay is on a separate FS. mktemp'ing +// inside `$(git rev-parse --git-dir)` keeps the temp index on the +// same FS as .git/index without polluting the working tree. func exportScript(guestPath, diffRef, diffFlag string) string { return fmt.Sprintf( "set -euo pipefail\n"+ "cd %s\n"+ - "tmp_idx=\"$(mktemp \"${TMPDIR:-/tmp}/banger-export.XXXXXX\")\"\n"+ + "git_dir=\"$(git rev-parse --git-dir)\"\n"+ + "tmp_idx=\"$(mktemp \"$git_dir/banger-export-idx.XXXXXX\")\"\n"+ "trap 'rm -f \"$tmp_idx\"' EXIT\n"+ "git read-tree %s --index-output=\"$tmp_idx\"\n"+ "GIT_INDEX_FILE=\"$tmp_idx\" git add -A\n"+ diff --git a/scripts/smoke.sh b/scripts/smoke.sh index c0c2490..6ad7e58 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -141,6 +141,39 @@ grep -q 'untracked-marker' <<<"$inc_out" || die "--include-untracked didn't ship # Restore repo to tracked-only state for any later scenarios. rm -f "$repodir/smoke-untracked.txt" +# --- workspace export round-trip -------------------------------------- +# Exercises ExportVMWorkspace: create a VM, prepare the workspace, +# write a new file inside the guest, then export and assert the +# emitted patch sees the guest-side change. If the export pipeline +# (temp-index, git add -A, diff --binary) ever stops capturing +# guest-side changes, this scenario catches it. +log 'workspace export: create + prepare + guest edit + export + assert marker' +export_vm='smoke-export' +cleanup_export_vm() { + "$BANGER" vm delete "$export_vm" >/dev/null 2>&1 || true +} +# Chain the VM cleanup with the existing runtime_dir trap so a mid- +# scenario failure still tears the VM down before the script exits. +# shellcheck disable=SC2064 +trap "cleanup_export_vm; rm -rf '$runtime_dir'" EXIT + +"$BANGER" vm create --name "$export_vm" --image debian-bookworm >/dev/null \ + || die "export: vm create exit $?" +"$BANGER" vm workspace prepare "$export_vm" "$repodir" >/dev/null \ + || die "export: workspace prepare exit $?" +"$BANGER" vm ssh "$export_vm" -- sh -c 'echo guest-edit > /root/repo/new-guest-file.txt' \ + || die "export: guest-side file write exit $?" +export_patch="$runtime_dir/smoke-export.diff" +"$BANGER" vm workspace export "$export_vm" --output "$export_patch" \ + || die "export: workspace export exit $?" +[[ -s "$export_patch" ]] || die "export: patch file empty at $export_patch" +grep -q 'new-guest-file.txt' "$export_patch" \ + || die "export: patch missing new-guest-file.txt marker (head: $(head -c 400 "$export_patch"))" + +cleanup_export_vm +# shellcheck disable=SC2064 +trap "rm -rf '$runtime_dir'" EXIT + # --- concurrent vm runs ----------------------------------------------- # Stresses per-VM lock scoping, the tap pool warm-up path, and # createVMMu's narrow reservation window. Two `vm run --rm` invocations