Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .vsix carries 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 VERSION file 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-script cargo:rustc-env mechanism. This RFD supersedes the git-tag-derived design (git describe, the Go/setuptools-scm/vergen pattern) 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-dependentgit 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:

buildcomposed ARGON_VERSION
dev (default), git reachable<base>-dev+g<shortsha>
dev (default), no git at all<base>-dev
stable channel signalbare <base>
ARGON_VERSION set directlythat 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].version and oxup/Cargo.toml [package].version are 0.0.0, forever. Cargo requires the field; nothing reads it. (Normal dev never edits VERSION either — 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) are 0.0.0-dev placeholders. The .vsix is stamped from ARGON_VERSION at package time (release.yml build-vsix); a plain checkout keeps the placeholder. The editor version-skew nudge (#675) treats a 0.0.0 / -dev stamp as “unstamped” and suppresses the warning.
  • The VS Code package.json marketplace version is stamped from ARGON_VERSION at package time.

What stays deleted

  • cargo xtask bump and cargo xtask check-versions (the entire compiler/xtask/src/version.rs).
  • The check-versions step in check.yml’s drift job.
  • 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:

  1. gate — enforce zero open P0-blocker issues (RELEASING.md);
  2. taggit tag "v$(cat VERSION)" at the green main tip and push it (which fires release.yml’s stable path; the stable build reports the bare X.Y.Z). An explicit version input escalates to a minor/major (must be >= the file line);
  3. atomic advance — bump the patch (X.Y.ZX.Y.(Z+1)), write it back to VERSION, and fast-forward push it to main directly as the bypass bot (a protect-main bypass actor — no PR, no merge queue, no starvation). The push is rebase-retried in case main moved (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.