diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 202610d..b88e55c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,42 @@ jobs: # Build the binary the e2e harness spawns BEFORE running tests. - name: build treehouse binary run: cargo build --bin treehouse + # --no-fail-fast is load-bearing, not a preference. cargo otherwise aborts + # at the FIRST failing test target, so a failure in an early target hides + # every later one. That is not hypothetical: the 2 failures in the + # `treehouse` bin target meant treehouse-core's 17 lib tests never ran on + # Windows at all, and the 2 lib failures meant the update_e2e integration + # tests never ran on Linux — all of it green-looking for months. Every + # target must run before this job can be called green. - name: test workspace - run: cargo test --workspace + run: cargo test --workspace --no-fail-fast - name: test with hardening feature - run: cargo test -p treehouse-core --features hardening + run: cargo test -p treehouse-core --features hardening --no-fail-fast + + # `toon` is feature-gated and OFF by default, so no job above compiles the + # TOON encoder — release.yml's `--features toon` build is the only place it is + # compiled today, and finding out it is broken by pushing a tag is far too + # late. Single runner on purpose: the encoder is portable Rust with no + # platform-specific code, so macOS/Windows buys no signal over one compile, + # and its feature set is disjoint from every other job's (hence no `needs:`). + # + # This job needs network and gets it for the same reason checkout does: + # `tru` is a git dependency (Cargo.lock:2597 pins it to an immutable rev of + # Dicklesworthstone/toon_rust), fetched by cargo from github.com. + toon: + name: build + test with toon encoder + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: build workspace with toon + run: cargo build --workspace --features toon + # The e2e suites spawn target/debug/treehouse (tests/e2e/common.rs + # `treehouse_bin` asserts it exists), and the release binary is the one + # that has to carry the encoder — so build it here, before the tests, + # rather than letting them pick up a default-feature build. + - name: build treehouse binary with toon + run: cargo build --bin treehouse --features toon + - name: test workspace with toon + run: cargo test --workspace --features toon --no-fail-fast diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b208be..dc37938 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,11 +73,23 @@ jobs: case "${{ matrix.suffix }}" in windows-*) BIN_NAME="treehouse.exe" ;; esac + # `toon` is a FEATURE-gated encoder, and without it `--format toon` + # silently falls back to serde_json — a shipped binary that prints JSON + # for a flag the README documents as TOON. The feature is off by + # default so an offline checkout still compiles, which means every + # build that ships has to ask for it explicitly. + # + # NOTE: `tru` is a git dependency (pinned rev, Cargo.toml + # workspace.dependencies). Under `cross` the fetch happens INSIDE the + # container, so this job needs the container to reach github.com. That + # is also why ci.yml needs its own `--features toon` job — this is the + # only place the encoder is compiled today, and finding out it is + # broken by pushing a tag is far too late. if [[ "${{ matrix.use_cross }}" == "true" ]]; then - cross build --release --target "${{ matrix.target }}" --bin treehouse + cross build --release --target "${{ matrix.target }}" --bin treehouse --features toon BIN="target/${{ matrix.target }}/release/${BIN_NAME}" else - cargo build --release --bin treehouse + cargo build --release --bin treehouse --features toon BIN="target/release/${BIN_NAME}" fi echo "BIN=$BIN" >> "$GITHUB_ENV" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c81fab3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,216 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +Nothing yet. + +## [0.2.0] - 2026-10-01 + +A 0.x minor, so it carries breaking changes. The two that matter are the exit +status and `--env-path`; both are documented in `RELEASE_NOTES.md` and both have +a one-release migration path. + +### Breaking + +#### Exit 3 is now the default when a dirty worktree is left unreturned + +`treehouse return` previously exited 0 even when it declined to clean a dirty +worktree, so a caller reading exit 0 as "the slot was released" was reading a +bug. It now exits 3 — the status Go shipped in v3.0.0 — and keeps the slot held. + +`TREEHOUSE_EXIT_STRICT=0` restores the old exit 0 and its byte-identical output +for one minor release. The variable changed polarity: it used to be opt-in and is +now opt-out, so only a value Go's `strconv.ParseBool` reads as false (`0`, `f`, +`false`, `off`, `n`, `no`) restores exit 0. An unrecognised value is deliberately +not the opt-out — `TREEHOUSE_EXIT_STRICT=1` left over from 0.1.x keeps exit 3, +because handing the escape hatch to a typo is the one direction that can +silently under-report a slot that was never released. + +#### `--env-path` is now an alias of `--root` + +`--env-path ` used to be accepted and ignored, falling back to +`~/.treehouse`. It is now a visible alias of the global `--root` flag, so it +resolves to `/.treehouse` (`/.treehouse` for a relative `DIR`). +Precedence is `--root` > `TREEHOUSE_ROOT` > `treehouse.toml` `root` > +`~/.treehouse`, matching Go. + +### Fixed + +Four paths could destroy work a user had not asked treehouse to destroy. + +- **A non-interactive confirmation no longer reads as consent.** At stdin EOF + `read_line` returns `Ok(0)`, not `Err`, so `treehouse return + < /dev/null` used to take the documented `[Y/n]` bare-Enter default and clean + uncommitted work. The bytes actually read now decide the outcome, and EOF is + a refusal. This is the fix with the widest blast radius — it can turn a script + that used to succeed into one that stops. +- **An unlanded-commit guard now runs before reset.** A worktree holding commits + that exist nowhere else was eligible to be reset away. `is_worktree_safe_to_reset` + refuses unless the worktree is verified clean, and an unverifiable worktree is + refused rather than assumed safe. +- **Pools are scoped by clone identity, not by origin URL alone.** Two clones of + one origin used to resolve to the same pool and contend for the same slots. + The requester's clone identity is resolved once, before the state lock, and a + slot owned by a different clone is classified `OtherClone` and skipped rather + than handed out. An identity that cannot be read is `Unverified` and is also + skipped — never guessed at. +- **Destructive git calls carry a marker precondition.** `require_worktree_marker` + gates the operations that rewrite a checkout, so a path that is not a + treehouse worktree fails closed instead of being operated on. A marker that + cannot be resolved — a dangling symlink, for instance — is a read failure, not + an absent marker. + +### Added + +#### VCS seam + +The `GitBackend` trait every backend must implement, with a registry, per-marker +backend resolution (`.git` → git, `.jj` → jj), and `destructive_backend_for_worktree` +as the single dispatch point for operations that rewrite a checkout. A path +carrying no recognised marker is refused by name rather than answered by git. +Every trait method has a default that refuses with a clear "the +backend does not support ", so adding a backend needs no trait edit +and no dispatch change. The jj backend is registered in the same registry and +implements 21 of the trait's 24 methods; see "Known gaps" for the three it leaves +on their defaults and for why none of that has been run against a real jj yet. + +#### Worktree seeding + +`GitBackend::seed_worktree` copies backend-specific ignored files into a new or +recycled worktree and returns the paths it wrote, so cleanup trusts that +inventory instead of re-reading mutable worktree metadata. The git backend reads +a committed `.worktreeinclude` manifest from the destination worktree's HEAD. +`.worktreeinclude` can never widen what gets copied into a worktree: it can only +authorize paths that are already ignored, and an uncommitted manifest authorises +nothing. + +#### Commands + +- `treehouse lease ` — durably lease an existing worktree by name without + touching it. +- `treehouse doctor [--strict]` — read-only health report; `--strict` treats any + warning as a failure. +- `treehouse gc [--all] [--prune-orphans] [-v] [--yes]` — reclaim stale, + orphaned, and dead-owner worktrees. Dry-run by default. +- `treehouse run [--ttl ] [--lease-holder ] -- ` — acquire, run a + command in the worktree, and guarantee cleanup on every exit. Everything from + the first non-flag token on is the command, so its own flags are never eaten + by treehouse's parser. + +#### Flags + +- `--format human|json|toon`, global, on agent-facing commands. `toon` is a + token-oriented encoding for the list-shaped output `status` and + `lease --json` produce. **It is not smaller than `json` here** — on a measured + `status` payload the `toon` rendering came out *larger* (335 B against 319 B + for one slot, on a pool with long paths; 291 B against 275 B on another). The + absolute counts move with pool path length, but the direction did not across + either fixture: `toon` lost by a consistent ~16 bytes. The usual "40–60% + smaller" folklore does not hold for this payload, so `json` is the format to + benchmark against and `toon` is worth reaching for only after measuring your + own. +- `--root `, global (see the breaking change above). +- `get --lease`, `--lease-holder `, `--ttl `, `--json` — non-interactive + acquire that prints only the path. +- `get --base ` — cut from this branch instead of the repository default. + Distinct from `-b/--branch`, which *creates* a branch; they are kept separate + all the way down, because `--base main -b main` otherwise reads as "create the + branch the worktree is already on". +- `get --no-fetch` — skip the origin fetch, for offline and air-gapped use. +- `get --worktree-path