RFD 0061 — A single VERSION file is the source of truth for the toolchain version
- State: accepted
- Depends on: RFD 0032 (version-coupled VS Code extension — the
.vsixcarries the toolchain version), the on-demand-tag release model (RELEASING.md) - Tracks: the release-version arc (supersedes the stored-version-of-record machinery)
- Prior art: a checked-in
VERSIONfile read at build time (the Linux-kernel / many-C-project pattern — the version lives in the tree, not the VCS metadata); SemVer pre-release / build-metadata grammar (X.Y.Z-dev,+g<sha>); Cargo’s build-scriptcargo:rustc-envmechanism. This RFD supersedes the git-tag-derived design (git describe, the Go/setuptools-scm/vergenpattern) that an earlier revision adopted.
Question
Argon is Sharpe-internal and never published to crates.io; consumers pin git revs. The toolchain version was once stored as a version of record in eight places — compiler/Cargo.toml [workspace.package].version, oxup/Cargo.toml, the VS Code package.json, two Cargo.locks, and three editor stamps — kept in lockstep by cargo xtask bump / cargo xtask check-versions and a post-tag bump PR. That stored version bought nothing (nothing is published to a registry that reads the field; internal path-deps ignore it) and cost a recurring post-tag bump PR that starved main, a lockstep gate that could drift, and a “bump, PR, merge, then tag” release dance.
The first fix collapsed all of that into the git tag: git describe derived the version at build time, so cutting a release was just git tag && git push. That keeps the single-source win but makes every build git-dependent — git describe needs a .git with reachable tags. A source tarball, a shallow CI checkout, an in-tree copy without .git, or any build off the VCS metadata then can’t resolve the version and falls back to a 0.0.0 placeholder; the version is also not human-readable from the tree (you must run git to know it).
So: where should the single source of truth live so a build is git-independent — resolvable from a plain tarball, reproducible, and human-readable — while keeping the “no lockstep, no bump PR” win?
Decision
A single VERSION file at the repo root is the source of truth for the toolchain version. It holds the bare current dev line (e.g. 0.3.4) on one line. Every binary reads it at compile time via include_str! — no git, no git describe, no fetched tags — and surfaces the composed version through --version and the LSP serverInfo.version. The git tag is cut from VERSION; the only “bump” is an atomic advance of that file after a cut.
Why a file, not the tag: the version must resolve with no .git at all (tarball, shallow checkout, in-tree copy). A checked-in file is git-independent, reproducible from the source alone, and human-readable (cat VERSION). The tag still exists — it is what stable points at — but it is downstream of the file, not the source.
The derivation (build.rs)
Each binary crate (oxc-driver — the ox/oxc/ox-lsp/oxfmt bins — and oxup), plus the two library crates that surface the version on the wire (oxc-lsp, oxc-serve), runs a build.rs that reads VERSION via include_str! and emits cargo:rustc-env=ARGON_VERSION:
| build | composed ARGON_VERSION |
|---|---|
| dev (default), git reachable | <base>-dev+g<shortsha> |
| dev (default), no git at all | <base>-dev |
| stable channel signal | bare <base> |
ARGON_VERSION set directly | that value, verbatim |
where <base> is the trimmed VERSION contents. The core version always resolves from VERSION — the +g<shortsha> is pure build metadata, attached only when git happens to be reachable and never required. A build with .git absent or unreadable still produces <base>-dev; it never errors and never falls back to a placeholder (the file is always present in the tree).
Channel selection is by explicit signal, never inference: ARGON_RELEASE_CHANNEL=stable (set by the release stable build) selects the bare <base>; a non-empty ARGON_VERSION baked directly wins outright (the release pipeline bakes the fully-resolved label — bare X.Y.Z for stable, X.Y.Z-dev.<ts> / X.Y.Z-nightly.<date> for dev/nightly). Local / dev / no-signal builds get the -dev form. build.rs also emits ARGON_VERSION_STRING — the human --version banner composing the version, the build sha, and a dev/toolchain note (the #234 provenance string).
build.rs emits cargo:rerun-if-changed on the repo-root VERSION (located by walking up from CARGO_MANIFEST_DIR, so it is correct for every includer crate’s depth) and rerun-if-env-changed on ARGON_VERSION / ARGON_RELEASE_CHANNEL / ARGON_TOOLCHAIN_VERSION / ARGON_BUILD_SHA. A light rerun-if-changed on the resolved HEAD ref is kept only because the optional +g<sha> metadata moves with it; absent git it is simply skipped.
The shared body lives in compiler/version_build.rs, include!d by the three compiler-workspace crates; oxup keeps an identical copy (it is a deliberately standalone workspace and does not cross-include).
The placeholders (never edited)
compiler/Cargo.toml [workspace.package].versionandoxup/Cargo.toml [package].versionare0.0.0, forever. Cargo requires the field; nothing reads it. (Normal dev never editsVERSIONeither — only the release cut’s atomic advance does.)- The three editor stamps (
editors/nvim/lua/argon/version.lua,editors/emacs/argon-version.el,editors/vscode/src/argon-version.ts) are0.0.0-devplaceholders. The.vsixis stamped fromARGON_VERSIONat package time (release.ymlbuild-vsix); a plain checkout keeps the placeholder. The editor version-skew nudge (#675) treats a0.0.0/-devstamp as “unstamped” and suppresses the warning. - The VS Code
package.jsonmarketplace version is stamped fromARGON_VERSIONat package time.
What stays deleted
cargo xtask bumpandcargo xtask check-versions(the entirecompiler/xtask/src/version.rs).- The
check-versionsstep incheck.yml’sdriftjob. - The post-tag bump PR machinery in
release-cut.yml(the bump-PR step).
The cut procedure + the atomic advance
Cutting a stable release (release-cut.yml, workflow_dispatch from main) is:
- gate — enforce zero open
P0-blockerissues (RELEASING.md); - tag —
git tag "v$(cat VERSION)"at the greenmaintip and push it (which firesrelease.yml’s stable path; the stable build reports the bareX.Y.Z). An explicitversioninput escalates to a minor/major (must be>=the file line); - atomic advance — bump the patch (
X.Y.Z→X.Y.(Z+1)), write it back toVERSION, and fast-forward push it tomaindirectly as the bypass bot (aprotect-mainbypass actor — no PR, no merge queue, no starvation). The push is rebase-retried in casemainmoved (it is the only writer during a cut). This single commit is the entire bump — there is no lockstep gate and nothing else to advance.
A dev build off the freshly-advanced main then reports X.Y.(Z+1)-dev automatically — newer than the just-cut release, not yet the next one.
Out of scope
The .oxbin / 4-axis artifact-schema versioning and the ARGON_ACCEPT_SCHEMA_CHANGE gate are a separate artifact axis, unrelated to the toolchain semver, and are untouched.