From 7a036d7af819f8689bb258292e3a9eadf544cf5c Mon Sep 17 00:00:00 2001 From: Craig Campbell Date: Sun, 19 Apr 2026 12:38:42 -0700 Subject: [PATCH 1/2] Add release notes shown on update and in "cmux version" Release notes now live in NOTES.md, shipped as a release asset alongside cmux.sh and VERSION, and sectioned by "## X.Y.Z". On the first cmux invocation after an update, the current version's section is printed as a banner; "cmux version" also prints it. Users on a fresh install or upgrading into this feature stay silent on their first run (no seen-version file yet) to avoid a surprise blast. Also adds tests/ with a simple runner (tests/run.sh) and a first test file covering the new behavior, including the missing-NOTES.md degradation paths. Run with: bash tests/run.sh Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 1 + NOTES.md | 14 +++++ cmux.sh | 63 ++++++++++++++++++-- install.sh | 1 + tests/run.sh | 31 ++++++++++ tests/test_version_notes.sh | 106 ++++++++++++++++++++++++++++++++++ 6 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 NOTES.md create mode 100755 tests/run.sh create mode 100755 tests/test_version_notes.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3f5c42..594c47b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,4 +20,5 @@ jobs: files: | cmux.sh VERSION + NOTES.md install.sh diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..efc7e80 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,14 @@ +# Release notes + +Add a new `## X.Y.Z` section at the top for each release. `cmux` reads this +file to show what's new after an update and in `cmux version`. + +## 0.1.4 +- Release notes: run "cmux version" anytime, and the first run after an update shows what's new +- `cmux new` now branches from the repo's default branch, with a new `--from` flag to override +- `cmux ls` shows merge status per worktree (`[merged]` / `[ahead N]`) +- `cmux new` and `cmux start` accept `-p ` to seed Claude with an initial prompt +- Pass extra flags to the Claude CLI via `--` (e.g. `cmux new foo -- --model sonnet`) +- New `.cmux/teardown` hook runs when a worktree is removed +- Configurable worktree layouts: `nested` (default), `outer-nested`, `sibling` — see `cmux config` +- Fixed: repo-root resolution no longer triggers direnv diff --git a/cmux.sh b/cmux.sh index c636074..7a0abaf 100644 --- a/cmux.sh +++ b/cmux.sh @@ -14,7 +14,7 @@ # cmux init [--replace] — Generate .cmux/setup hook using Claude # cmux config — View or set worktree layout configuration # cmux update — Update cmux to the latest version -# cmux version — Show current version +# cmux version — Show current version and release notes _CMUX_DOWNLOAD_URL="https://github.com/craigsc/cmux/releases/latest/download" CMUX_VERSION="unknown" @@ -25,6 +25,7 @@ cmux() { shift 2>/dev/null _cmux_check_update + _cmux_show_update_notes case "$cmd" in new) _cmux_new "$@" ;; @@ -36,9 +37,9 @@ cmux() { init) _cmux_init "$@" ;; config) _cmux_config "$@" ;; update) _cmux_update "$@" ;; - version) echo "cmux $CMUX_VERSION" ;; + version) _cmux_version "$@" ;; --help|-h|"") - echo "Usage: cmux [branch]" + echo "Usage: cmux [branch]" echo "" echo " new [--from ] [-p ] [-- ]" echo " New worktree + branch, run setup hook, launch Claude" @@ -52,7 +53,7 @@ cmux() { echo " init [--replace] Generate .cmux/setup hook using Claude" echo " config View or set worktree layout configuration" echo " update Update cmux to the latest version" - echo " version Show current version" + echo " version Show current version and release notes" return 0 ;; *) @@ -252,6 +253,59 @@ _cmux_check_update() { disown 2>/dev/null } +# Extract the current version's section from NOTES.md (sectioned by "## X.Y.Z"). +_cmux_version_notes() { + local notes_file="$HOME/.cmux/NOTES.md" + [[ -f "$notes_file" ]] || return 1 + local output + output="$(awk -v hdr="## $CMUX_VERSION" ' + $0 == hdr { in_section = 1; next } + /^## / { in_section = 0 } + in_section { print } + ' "$notes_file")" + [[ -z "$output" ]] && return 1 + printf '%s' "$output" +} + +# Silent on first run (no seen file yet) so fresh installs and pre-existing +# users upgrading into this feature don't get a surprise blast of notes. +_cmux_show_update_notes() { + local seen_file="$HOME/.cmux/.last_seen_version" + local last_seen="" + [[ -f "$seen_file" ]] && last_seen="$(<"$seen_file")" + + if [[ -z "$last_seen" ]]; then + printf '%s' "$CMUX_VERSION" > "$seen_file" + return 0 + fi + + [[ "$last_seen" == "$CMUX_VERSION" ]] && return 0 + + local notes + if notes="$(_cmux_version_notes 2>/dev/null)"; then + echo "" + echo "cmux $CMUX_VERSION — what's new:" + echo "$notes" + echo "" + fi + printf '%s' "$CMUX_VERSION" > "$seen_file" +} + +_cmux_version() { + if [[ "$1" == "--help" || "$1" == "-h" ]]; then + echo "Usage: cmux version" + echo "" + echo " Show the installed cmux version and its release notes." + return 0 + fi + echo "cmux $CMUX_VERSION" + local notes + if notes="$(_cmux_version_notes 2>/dev/null)"; then + echo "" + echo "$notes" + fi +} + # ── Subcommands ────────────────────────────────────────────────────── _cmux_new() { @@ -1033,6 +1087,7 @@ _cmux_update() { if curl -fsSL "${_CMUX_DOWNLOAD_URL}/cmux.sh" -o "$install_path"; then printf '%s' "$remote_version" > "$HOME/.cmux/VERSION" printf '%s' "$remote_version" > "$HOME/.cmux/.latest_version" + curl -fsSL "${_CMUX_DOWNLOAD_URL}/NOTES.md" -o "$HOME/.cmux/NOTES.md" 2>/dev/null source "$install_path" echo "cmux updated to $CMUX_VERSION." else diff --git a/install.sh b/install.sh index a5eded3..3c78617 100755 --- a/install.sh +++ b/install.sh @@ -12,6 +12,7 @@ mkdir -p "$INSTALL_DIR" echo "Downloading cmux..." curl -fsSL "$RELEASE_URL/cmux.sh" -o "$INSTALL_PATH" curl -fsSL "$RELEASE_URL/VERSION" | tr -d '[:space:]' > "$INSTALL_DIR/VERSION" +curl -fsSL "$RELEASE_URL/NOTES.md" -o "$INSTALL_DIR/NOTES.md" 2>/dev/null || true # Clear stale update-check cache from any previous install rm -f "$INSTALL_DIR/.latest_version" "$INSTALL_DIR/.last_check" diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..b7bd2de --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Discover and run every test_*.sh file in this directory. +# Exit nonzero if any fail. Usage: bash tests/run.sh +set -u + +cd "$(dirname "$0")" + +shopt -s nullglob +files=(test_*.sh) +if [[ ${#files[@]} -eq 0 ]]; then + echo "No test files found in tests/" + exit 0 +fi + +failed=0 +for f in "${files[@]}"; do + echo "── $f ──" + if ! bash "$f"; then + failed=$((failed + 1)) + fi + echo "" +done + +total=${#files[@]} +if [[ $failed -eq 0 ]]; then + echo "All $total test file(s) passed." + exit 0 +else + echo "$failed of $total test file(s) failed." + exit 1 +fi diff --git a/tests/test_version_notes.sh b/tests/test_version_notes.sh new file mode 100755 index 0000000..9846f89 --- /dev/null +++ b/tests/test_version_notes.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Tests for cmux version notes. +# Run: bash tests/test_version_notes.sh + +set -u + +CMUX_SH="$(cd "$(dirname "$0")/.." && pwd)/cmux.sh" +SAMPLE_NOTES='## 0.1.3 +- Branch from the repo default branch +- New --from flag + +## 0.1.2 +- Older change' + +fails=0 +passes=0 + +# Run $* in an isolated $HOME with a fake VERSION + NOTES.md so we never +# touch the real ~/.cmux. +isolated() { + local fake_version="$1"; shift + local tmp + tmp="$(mktemp -d)" + mkdir -p "$tmp/.cmux" + printf '%s' "$fake_version" > "$tmp/.cmux/VERSION" + printf '%s\n' "$SAMPLE_NOTES" > "$tmp/.cmux/NOTES.md" + HOME="$tmp" bash -c "source '$CMUX_SH' 2>/dev/null; $*" + local rc=$? + rm -rf "$tmp" + return $rc +} + +# Same, but with NOTES.md removed before the test body runs. +isolated_no_notes() { + local fake_version="$1"; shift + isolated "$fake_version" 'rm -f "$HOME/.cmux/NOTES.md"; '"$*" +} + +check() { + local desc="$1"; shift + if "$@"; then + printf ' ok %s\n' "$desc" + passes=$((passes + 1)) + else + printf ' FAIL %s\n' "$desc" + fails=$((fails + 1)) + fi +} + +# ── _cmux_version_notes ── +check "_cmux_version_notes returns the current version's section" \ + isolated 0.1.3 '[[ "$(_cmux_version_notes)" == *"--from flag"* ]]' + +check "_cmux_version_notes ignores other versions' sections" \ + isolated 0.1.3 '[[ "$(_cmux_version_notes)" != *"Older change"* ]]' + +check "_cmux_version_notes returns nonzero when current version has no notes" \ + isolated 999.0.0 '! _cmux_version_notes >/dev/null 2>&1' + +check "_cmux_version_notes fails gracefully when NOTES.md is missing" \ + isolated_no_notes 0.1.3 '! _cmux_version_notes >/dev/null 2>&1' + +# ── cmux version ── +check "cmux version prints version string" \ + isolated 0.1.3 'echo 0.1.3 > "$HOME/.cmux/.last_seen_version"; [[ "$(cmux version 2>&1)" == *"cmux 0.1.3"* ]]' + +check "cmux version prints notes when they exist" \ + isolated 0.1.3 'echo 0.1.3 > "$HOME/.cmux/.last_seen_version"; [[ "$(cmux version 2>&1)" == *"--from flag"* ]]' + +check "cmux version works even when NOTES.md is missing" \ + isolated_no_notes 0.1.3 'echo 0.1.3 > "$HOME/.cmux/.last_seen_version"; [[ "$(cmux version 2>&1)" == *"cmux 0.1.3"* ]]' + +# ── _cmux_show_update_notes: no seen file ── +check "no seen file: output is silent" \ + isolated 0.1.3 '[[ -z "$(_cmux_show_update_notes 2>&1)" ]]' + +check "no seen file: seen file is created with current version" \ + isolated 0.1.3 '_cmux_show_update_notes; [[ "$(<"$HOME/.cmux/.last_seen_version")" == "0.1.3" ]]' + +# ── _cmux_show_update_notes: seen file matches ── +check "seen file matches current: output is silent" \ + isolated 0.1.3 'echo 0.1.3 > "$HOME/.cmux/.last_seen_version"; [[ -z "$(_cmux_show_update_notes 2>&1)" ]]' + +# ── _cmux_show_update_notes: seen file differs ── +check "seen file older: prints notes" \ + isolated 0.1.3 'echo 0.1.2 > "$HOME/.cmux/.last_seen_version"; [[ "$(_cmux_show_update_notes 2>&1)" == *"what"*"new"* ]]' + +check "seen file older: updates seen file to current version" \ + isolated 0.1.3 'echo 0.1.2 > "$HOME/.cmux/.last_seen_version"; _cmux_show_update_notes >/dev/null 2>&1; [[ "$(<"$HOME/.cmux/.last_seen_version")" == "0.1.3" ]]' + +check "seen file older, no notes for current version: silent but still updates seen file" \ + isolated 999.0.0 'echo 0.1.2 > "$HOME/.cmux/.last_seen_version"; out="$(_cmux_show_update_notes 2>&1)"; [[ -z "$out" && "$(<"$HOME/.cmux/.last_seen_version")" == "999.0.0" ]]' + +check "seen file older, NOTES.md missing: silent and still updates seen file" \ + isolated_no_notes 0.1.3 'echo 0.1.2 > "$HOME/.cmux/.last_seen_version"; out="$(_cmux_show_update_notes 2>&1)"; [[ -z "$out" && "$(<"$HOME/.cmux/.last_seen_version")" == "0.1.3" ]]' + +# ── Summary ── +echo "" +total=$((passes + fails)) +if [[ $fails -eq 0 ]]; then + echo "All $total tests passed." + exit 0 +else + echo "$passes/$total passed, $fails failed." + exit 1 +fi From 08d3bd21750d4ffffd03d2f84fe9d216d6eb4212 Mon Sep 17 00:00:00 2001 From: Craig Campbell Date: Sun, 19 Apr 2026 12:38:48 -0700 Subject: [PATCH 2/2] Run tests on PRs via GitHub Actions Adds a Tests workflow that runs tests/run.sh on pull_request and on push to main, across ubuntu-latest and macos-latest. macOS coverage matters because /bin/bash there is 3.2 (Apple ships the old one for licensing reasons), which has already bitten this repo at least once. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b1780e1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Tests + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Run tests (system bash) + run: /bin/bash tests/run.sh