Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions .github/actions/check/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ inputs:
description: 'Path to vump.toml, relative to the repository root. The action runs vump from the directory containing this file.'
required: false
default: 'vump.toml'
project:
description: 'Project to verify, in a repository declaring several. Only needed when `version` is a bare version: a tag selects its own project through that project''s tag_pattern. There is deliberately no allow-nested input — a vump.toml nested below another verifies the wrong project, and this action failing is that being caught rather than a limitation.'
required: false
default: ''
vump-version:
description: 'vump release tag to download (e.g. v0.2.0). Defaults to "latest".'
required: false
Expand All @@ -25,9 +29,23 @@ runs:

- name: Run vump check
shell: bash
# Inputs reach the script through the environment rather than being
# interpolated into it: an expression expanded into a shell line is
# expanded before bash sees it, so a value containing shell syntax would
# run as part of this step.
env:
VUMP_VERSION_TO_CHECK: ${{ inputs.version }}
VUMP_CONFIG: ${{ inputs.config }}
VUMP_PROJECT: ${{ inputs.project }}
VUMP_WORKSPACE: ${{ github.workspace }}
run: |
set -euo pipefail

# cd to the directory containing vump.toml so it is found automatically.
CONFIG_DIR=$(dirname "${{ github.workspace }}/${{ inputs.config }}")
cd "$CONFIG_DIR"
"$VUMP_BIN" check "${{ inputs.version }}"
cd "$(dirname "$VUMP_WORKSPACE/$VUMP_CONFIG")"

if [ -n "$VUMP_PROJECT" ]; then
"$VUMP_BIN" check "$VUMP_VERSION_TO_CHECK" --project "$VUMP_PROJECT"
else
"$VUMP_BIN" check "$VUMP_VERSION_TO_CHECK"
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ sandbox/**/obj/
sandbox/**/node_modules/
sandbox/**/.venv/
sandbox/**/__pycache__/
sandbox/**/target/
61 changes: 0 additions & 61 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,6 @@ agreed, at which point it is ranked by value.

Each says what would settle it. Nothing moves up until something does.

### npm workspaces

A Cargo workspace's shared lock is now written per member, matched by the
package name each manifest declares. `package-lock.json` has the same shape and
not the same solution: its `packages` map is keyed by *path*
(`"packages/api"`), not by package name, and a workspace member's version
appears both there and in the member's own `package.json`.

Undecided because no repository here needs it yet, and guessing at the mapping
between a declared manifest path and a `packages` key is exactly the kind of
inference that produced the original lock-file defect.

### Declarative version-file formats

Detection is by filename. Anything else — `gradle.properties`, a `*.gemspec`, a
Expand All @@ -51,55 +39,6 @@ permanently to avoid work that keeps turning out to be small.

What would settle it: a format someone needs that is not worth adding built-in.

### A Rust project in the sandbox

The sandbox covers npm and C#. Rust is missing, and the argument at the time
was that this repository is itself the Cargo example — it tracks `Cargo.toml`
and `Cargo.lock` and is exercised on every release.

That argument is weaker than it looked. The repository demonstrates a
single-crate project only, so the shape actually worth showing by hand — a
workspace, where the lock holds one entry per member and a project writes only
the entries its own manifests name — has no worked example anywhere outside the
test fixtures.

The reason not to rush: a crate inside this repository's tree is not inert the
way an npm or C# project is. It would need excluding from the workspace, and a
mistake there breaks `cargo build` for the tool itself.

### Inputs on the check action

The composite action takes `version`, `config` and `vump-version`. Passing a
tag now selects its own project, so a `project` input is only needed for a
repository that verifies bare versions rather than tags.

There is no `allow-nested` input either, so a repository whose `config` input
points at a nested `vump.toml` now fails in CI. That is arguably the right
outcome — it surfaces the layout rather than verifying quietly against the
wrong project — but it is a consequence that was not chosen deliberately, and
the alternative is one input.

### Pushing what a bump created, separately

A bump that stops at `through = "tag"` leaves a commit and a tag to push by
hand. `git push --follow-tags` does it, but pushes *every* annotated tag
reachable — in a monorepo that can publish another project's tag that happened
to be sitting unpushed. vump's own push names the single ref it created, which
is strictly narrower.

A `vump push` would close that gap: read the current version, render the
project's tag pattern, push `HEAD` and that one tag. It needs no stored state,
since both are derivable from the files and the pattern.

Undecided because the repository that would feel it — a monorepo with several
projects tagged independently — does not exist here yet, and the single-project
case is served by `git push --follow-tags` today.

Not to be confused with a prompt between tagging and pushing, which was
considered and rejected: a blocking prompt owns the terminal, so it cannot
deliver the inspection it appears to offer. Stopping at `tag` and looking
around with a free shell is strictly better.

### Deciding the bump from commit messages

semantic-release and release-please read Conventional Commits — `feat:`, `fix:`,
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ cargo test
CI runs exactly this on Linux, macOS and Windows. Running it locally first is
much faster than waiting for a runner to report the same thing.

`./scripts/sandbox-smoke.sh` is the separate check: it bumps every sandbox and
asks each ecosystem's own toolchain what version it builds, which is the one
thing the suite cannot answer. It needs node, dotnet, uv and cargo, skips what
is missing, and is not part of CI for that reason — run it when a format's
reading or writing changed.

## Working agreement

- **Open a pull request when a coherent set of work is done, then pause for
Expand Down
23 changes: 22 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ In `Cargo.lock` the entries that belong to the repository are the
everything it fetched. A single-crate repository has one, and it needs no
naming.

`package-lock.json` keys its entries by *path* rather than by name — the root
is `""` and a member is `"packages/api"` — but records the name inside each
entry, so a member is still selected by the name its manifest declares. The
root's two records, the top-level `version` and the `""` entry, belong to the
project only when the lock's own name is one the project declares; that is what
keeps a member from rewriting the root it sits under.

`uv.lock` is the same shape with the marker inverted: uv records a `source`
for both, and the entries built from the tree are the ones marked `editable`.
That distinction is load-bearing rather than cosmetic — a published release of
Expand Down Expand Up @@ -519,6 +526,7 @@ vump Interactive bump
vump patch|minor|major Bump a stable version
vump alpha|beta|rc Start or advance a pre-release (--toward required from stable)
vump release Drop the pre-release suffix
vump push Push the commit and tag a bump already created
vump set <version> Write an exact version to every tracked file
vump check <version> Verify tracked files match the given version
vump status Report current versions and sync state
Expand All @@ -532,7 +540,7 @@ vump self list List published releases

| Flag | Applies to | Description |
| ------------------- | --------------- | ---------------------------------------------- |
| `--dry-run` | bump commands | Compute and report the plan, write nothing |
| `--dry-run` | bump commands, `push` | Compute and report the plan, write nothing |
| `--toward <bump>` | pre-release | Stable release the pre-release leads to |
| `--project <name>` | all | Select a project in a multi-project repository |
| `--through <step>` | bump commands | How far to carry the release: `none`, `commit`, `tag`, `push` |
Expand All @@ -556,6 +564,19 @@ one invocation. `--to` takes a version when updating the binary; a bump
command's `--toward` takes a bump name. Reading a name as reserved across the
whole surface is a mistake — the "Applies to" column is what settles it.

**`vump push` finishes a run that stopped at `tag`.** It renders the tag from
the version the files already record and the project's own pattern — the same
derivation the bump used to create it — so it stores nothing and cannot name a
different ref than the one that exists. It refuses when that tag does not
exist rather than pushing the branch alone, which would look like the job was
done. The branch policy is checked here as well: a bump stopping at `tag` was
never checked against it, because nothing had left the machine yet.

The alternative it replaces is `git push --follow-tags`, which pushes every
annotated tag reachable from `HEAD` — in a repository tagging several projects
independently, that publishes whichever other project's tag was sitting
unpushed.

Only files declared in configuration are staged. If the run reaches a commit, a
dirty working tree is a hard failure — a version bump must not sweep unrelated
changes into its commit.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ needs, or it fails saying what is missing.
| `vump patch\|minor\|major` | Bump a stable version |
| `vump alpha\|beta\|rc` | Start or advance a pre-release |
| `vump release` | Drop the pre-release suffix |
| `vump push` | Push the commit and the one tag a bump already created |
| `vump set <version>` | Write an exact version to every tracked file |
| `vump check <version>` | Verify tracked files record this version |
| `vump status` | Report recorded versions and whether they agree |
Expand Down
26 changes: 26 additions & 0 deletions sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This is not test coverage. The suite in `tests/` and the fixtures in
| [`cs/multi-project`](cs/multi-project) | Two C# projects versioned independently |
| [`cs/shared-version`](cs/shared-version) | An executable and its library on one `Directory.Build.props` |
| [`py/single-project`](py/single-project) | One Python project, `pyproject.toml` and `uv.lock` together |
| [`rs/workspace`](rs/workspace) | Two crates sharing one `Cargo.lock`, released together |

## Git is off, deliberately

Expand Down Expand Up @@ -100,6 +101,31 @@ vump minor --allow-nested # pyproject.toml and uv.lock
uv run python -m demo # demo 1.1.0
```

A Cargo workspace shares one lock holding an entry per member, matched by the
name each manifest declares. Declaring both manifests is what moves them as a
unit — `--locked` is the check that vump wrote what cargo expects:

```bash
cd sandbox/rs/workspace
vump minor --allow-nested # both manifests and the shared lock
cargo run --locked --bin sandbox-cli # sandbox-cli 1.1.0
```

## Checking every ecosystem at once

The test suite proves vump writes the bytes it meant to. Only each ecosystem's
own toolchain can say whether those bytes reach a running program:

```bash
./scripts/sandbox-smoke.sh
```

It bumps every project with the vump in this working tree, asks node, dotnet,
uv and cargo what version they build, and reverts afterwards. A toolchain that
is not installed is skipped rather than failed. It is deliberately not part of
`cargo test`, which would need all four on every CI runner to test somebody
else's tool.

## Putting it back

Experiments are meant to be thrown away:
Expand Down
14 changes: 14 additions & 0 deletions sandbox/rs/workspace/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions sandbox/rs/workspace/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# A virtual workspace manifest: it declares members and no package of its own,
# so there is no version here for vump to track. The members carry those.
[workspace]
resolver = "3"
members = ["crates/*"]
8 changes: 8 additions & 0 deletions sandbox/rs/workspace/crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "sandbox-cli"
version = "1.0.0"
edition = "2024"
publish = false

[dependencies]
sandbox-engine = { path = "../engine" }
5 changes: 5 additions & 0 deletions sandbox/rs/workspace/crates/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Both crates are released together, so one version describes the pair.
fn main() {
println!("sandbox-cli {}", env!("CARGO_PKG_VERSION"));
println!("{}", sandbox_engine::greeting());
}
5 changes: 5 additions & 0 deletions sandbox/rs/workspace/crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "sandbox-engine"
version = "1.0.0"
edition = "2024"
publish = false
1 change: 1 addition & 0 deletions sandbox/rs/workspace/crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub fn greeting() -> &'static str { "hello from the engine" }
20 changes: 20 additions & 0 deletions sandbox/rs/workspace/vump.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Two crates in one workspace, released together as a single version.
#
# The workspace shares one Cargo.lock holding an entry per member, matched by
# the name each manifest declares — so a project writes only the entries its
# own manifests name, and members nobody declared are left alone. Declaring
# every manifest here is what makes these two move as a unit.
#
# Git is off throughout the sandbox, and the tag pattern cannot be mistaken
# for vump's own — see ../../npm/single-project/vump.toml.

files = [
"crates/engine/Cargo.toml",
"crates/cli/Cargo.toml",
"Cargo.lock",
]

[git]
through = "none"
tag_pattern = "sandbox-rs-v{new_version}"
commit_message = "chore(sandbox): bump rs/workspace to {new_version}"
96 changes: 96 additions & 0 deletions scripts/sandbox-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
#
# Bumps every sandbox project with the vump in this working tree, then asks
# each ecosystem's own toolchain what version it ends up building.
#
# This is the one thing `cargo test` cannot answer. The test suite proves vump
# writes the bytes it meant to; only dotnet, uv, npm and cargo can say whether
# those bytes reach a running program. It is deliberately not part of `cargo
# test`: it needs four toolchains and a network, which would make every CI job
# slower and flakier in order to test somebody else's tool.
#
# Every project is reverted afterwards, including on failure. An ecosystem
# whose toolchain is missing is reported and skipped, not failed.
set -uo pipefail

cd "$(dirname "$0")/.."
ROOT=$(pwd)
VUMP="$ROOT/target/debug/vump"
FAILED=()
SKIPPED=()

# Bumping the sandbox dirties it, so the script reverts what it did on the way
# out. That is a destructive git command in a real repository, so it refuses to
# start where there is anything there it might throw away.
if [ -n "$(git status --porcelain sandbox/)" ]; then
echo "sandbox/ has uncommitted changes, and this script reverts sandbox/ when"
echo "it finishes. Commit or stash them first:"
git status --short sandbox/ | sed 's/^/ /'
exit 1
fi

cleanup() {
git checkout -- sandbox 2>/dev/null
}
trap cleanup EXIT

echo "building vump"
cargo build -q || { echo "cannot build vump"; exit 1; }

# check <tool> <project> <wanted> <command...>
#
# Skips when the toolchain is absent. Otherwise bumps the project and runs the
# command in it, failing when either step fails or the version does not appear.
check() {
local tool=$1 project=$2 wanted=$3
shift 3

echo
echo "$project"
if ! command -v "$tool" >/dev/null 2>&1; then
echo " skip ($tool not installed)"
SKIPPED+=("$project")
return
fi

local dir="$ROOT/sandbox/$project"
if ! ( cd "$dir" && "$VUMP" minor --allow-nested >/dev/null ); then
echo " FAIL the bump itself failed"
FAILED+=("$project (bump)")
return
fi

local output
if ! output=$( cd "$dir" && "$@" 2>&1 ); then
echo " FAIL $tool could not build or run it:"
printf '%s\n' "$output" | sed 's/^/ /'
FAILED+=("$project ($tool)")
return
fi

if printf '%s' "$output" | grep -qF "$wanted"; then
echo " ok $tool reports $wanted"
else
echo " FAIL $tool does not report $wanted:"
printf '%s\n' "$output" | sed 's/^/ /'
FAILED+=("$project (version)")
fi
}

check node npm/single-project 1.1.0 node index.js
check dotnet cs/single-project 1.1.0 dotnet run --project Demo
check dotnet cs/shared-version 1.1.0 dotnet run --project App
check uv py/single-project 1.1.0 uv run --quiet python -m demo
# --locked is the assertion: cargo refuses a lock it would have rewritten, so a
# passing build proves vump's edit matches what cargo expects.
check cargo rs/workspace 1.1.0 cargo run -q --locked --bin sandbox-cli

echo
if [ ${#SKIPPED[@]} -gt 0 ]; then
echo "skipped: ${SKIPPED[*]}"
fi
if [ ${#FAILED[@]} -gt 0 ]; then
echo "FAILED: ${FAILED[*]}"
exit 1
fi
echo "every sandbox reports the version vump wrote"
Loading
Loading