Skip to content

Commit 6d09511

Browse files
ZhiXiao-LinRoy Lin
andauthored
chore(release): v2.5.0 — CoW snapshot restore + programmable-CI SDK (a3s-box-ci) (#151)
* chore(snapshot): warn when prune keeps in-use CoW lowers When `snapshot prune`/auto-prune cannot reach its limit because the remaining snapshots are all in use as copy-on-write overlay lowers, log a warning with the count so operators understand why fewer were freed than requested. + a test that prune evicts nothing when every snapshot is in use. * chore(release): v2.5.0 — copy-on-write snapshot restore + programmable-CI SDK Bumps the workspace to 2.5.0 (CHANGELOG [Unreleased] -> [2.5.0]). Highlights: CoW snapshot restore (fork a warmed snapshot as an overlay mount, not a full copy), the in-use guard for `snapshot rm`/`prune`, and the new `a3s-box-ci` pipeline SDK. --------- Co-authored-by: Roy Lin <roylin@a3s.box>
1 parent 70075c5 commit 6d09511

4 files changed

Lines changed: 60 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ All notable changes to A3S Box will be documented in this file.
44

55
## [Unreleased]
66

7+
## [2.5.0] — 2026-06-22
8+
9+
Programmable CI on a3s-box: copy-on-write snapshot restore — fork a warmed snapshot as
10+
a near-instant overlay mount instead of a full rootfs copy — plus a new, dependency-free
11+
Rust SDK crate (`a3s-box-ci`) for writing CI pipelines as code, each step in its own
12+
MicroVM. No breaking API changes.
13+
14+
### Added
15+
16+
- **`a3s-box-ci` — programmable CI pipeline SDK.** A pipeline is a Rust program; box is
17+
the execution backend (one kernel per step, exit code = pass/fail). `warm_base`
18+
snapshots a warmed base once, `Base::step` forks it per step, and a content-addressed
19+
`FileCache` skips unchanged steps. A thin, zero-dependency wrapper over the `a3s-box`
20+
CLI; the DAG is the caller's code (no YAML, no engine).
21+
722
### Changed
823

924
- **Snapshot restore is now copy-on-write.** `a3s-box snapshot restore` no longer

src/Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ resolver = "2"
2323
h2 = { path = "third_party/h2" }
2424

2525
[workspace.package]
26-
version = "2.4.0"
26+
version = "2.5.0"
2727
edition = "2021"
2828
authors = ["A3S Lab Team"]
2929
license = "MIT"

src/runtime/src/snapshot.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,15 @@ impl SnapshotStore {
263263
}
264264
// Everything left is protected; can't prune further without
265265
// breaking a live box, so stop (caller stays over the cap).
266-
None => break,
266+
None => {
267+
tracing::warn!(
268+
in_use = snapshots.len(),
269+
"snapshot prune kept {} in-use snapshot(s) (each referenced as a \
270+
copy-on-write overlay lower); requested limit not fully met",
271+
snapshots.len()
272+
);
273+
break;
274+
}
267275
}
268276
}
269277

@@ -668,6 +676,31 @@ mod tests {
668676
assert!(store.get("s4").unwrap().is_some());
669677
}
670678

679+
#[test]
680+
fn prune_keeps_everything_when_all_in_use() {
681+
let tmp = TempDir::new().unwrap();
682+
let store = SnapshotStore::new(&tmp.path().join("snapshots")).unwrap();
683+
let rootfs = make_rootfs(&tmp);
684+
store.save(make_metadata("a", "a"), &rootfs).unwrap();
685+
store.save(make_metadata("b", "b"), &rootfs).unwrap();
686+
687+
// Both snapshots are in use as a box's CoW overlay lower.
688+
for (i, id) in ["a", "b"].iter().enumerate() {
689+
let bd = tmp.path().join("boxes").join(format!("bx{i}"));
690+
std::fs::create_dir_all(&bd).unwrap();
691+
std::fs::write(
692+
bd.join(".snapshot-lower"),
693+
store.rootfs_path(id).to_string_lossy().as_bytes(),
694+
)
695+
.unwrap();
696+
}
697+
698+
// Even asked to keep only 1, prune evicts nothing — both are protected.
699+
let removed = store.prune(1, 0).unwrap();
700+
assert!(removed.is_empty(), "in-use snapshots must never be pruned");
701+
assert_eq!(store.count().unwrap(), 2);
702+
}
703+
671704
#[test]
672705
fn test_snapshot_save_with_empty_rootfs() {
673706
let tmp = TempDir::new().unwrap();

0 commit comments

Comments
 (0)