How to version Clojure libraries and applications, drive releases from Git, and automate builds with current Clojure CLI workflows.
Versioning and release management in Clojure is mostly about contract discipline. The language does not need a special theory of release engineering. What matters is whether your public API, artifacts, and deployment workflow make change predictable for the people who depend on your code.
Older Clojure content often over-focuses on Leiningen plugins or Travis CI recipes. Those can still appear in established codebases, but current official guidance is built around the Clojure CLI, deps.edn, and tools.build. That shift matters because it changes how teams should think about repeatable builds: less hidden plugin behavior, more explicit scripts and version sources.
Before choosing tooling, decide what a version communicates.
For a reusable library, semantic versioning is usually the clearest default:
MAJOR for intentionally breaking public changesMINOR for backward-compatible additionsPATCH for backward-compatible fixesThis matters most when:
For a deployable service, the important thing is traceability. Many teams still use SemVer for applications, but calendar versions or Git-derived versions can also work if they map cleanly to deployed artifacts and incident timelines.
The weak choice is not SemVer versus calendar versioning. The weak choice is a version that nobody can connect to source, changelog, or deployment state.
A sound release pipeline usually starts with Git, not with a build plugin mutating version state in hidden ways.
Good release flow:
That gives you one anchor for:
For Clojure libraries, that is more important than whether the jar was produced by one tool or another.
The current official build direction for new Clojure projects is the Clojure CLI plus tools.build. The key benefit is visibility: your build becomes ordinary Clojure code instead of opaque plugin configuration.
1(ns build
2 (:require [clojure.tools.build.api :as b]))
3
4(def lib 'com.acme/reporting)
5(def version (or (System/getenv "RELEASE_VERSION")
6 "0.4.0-SNAPSHOT"))
7(def class-dir "target/classes")
8(def basis (delay (b/create-basis {:project "deps.edn"})))
9(def jar-file (format "target/%s-%s.jar" (name lib) version))
10
11(defn clean [_]
12 (b/delete {:path "target"}))
13
14(defn jar [_]
15 (clean nil)
16 (b/copy-dir {:src-dirs ["src" "resources"]
17 :target-dir class-dir})
18 (b/jar {:class-dir class-dir
19 :jar-file jar-file
20 :basis @basis
21 :lib lib
22 :version version}))
This example is intentionally simple. In a real pipeline you would likely also:
One common mistake is conflating your application’s version with the versions of the libraries it consumes.
1{:paths ["src" "resources"]
2 :deps {org.clojure/clojure {:mvn/version "1.12.4"}
3 com.github.seancorfield/next.jdbc {:mvn/version "1.3.1048"}}}
These dependency versions matter for reproducibility, but they are not your release identity. Your release identity comes from the artifact you publish, the tag you cut, and the contract you maintain.
You will still encounter:
project.cljlein releaselein uberjarThat is normal in older systems. The practical rule is:
The guide should teach the modern default without pretending older systems vanished overnight.
Strong Clojure release management usually includes:
For libraries, add one more question: what exactly counts as a breaking change? In Clojure that can include:
ex-dataIf you do not treat those as release-contract questions, semantic versioning becomes decorative.
flowchart LR
A["Reviewed Commit"] --> B["CI Validation"]
B --> C["Git Tag / Release Version"]
C --> D["Build Artifact"]
D --> E["Publish Artifact"]
E --> F["Release Notes and Changelog"]
The important point is sequence. A trustworthy release is not a jar that happens to exist. It is an artifact tied to a validated commit and a versioned release decision.
tools.build scripts for new build examples.