The canonical hive release tasks, delivered as a library.
Point a repository's :build alias here and it inherits the whole release path — no build.clj to copy, and no build.clj to drift:
:build {:deps {io.github.hive-agi/hive-build {:mvn/version "0.1.0"}} :ns-default hive-build.api}
Coordinates come from ./version.edn, the version from ./VERSION:
{:lib io.github.hive-agi/hive-thing :minor 1 :license {:name "MIT" :url "https://opensource.org/licenses/MIT"} :scm-url "https://github.com/hive-agi/hive-thing" :src-dirs ["src"] :publish :clojars ; :clojars | :gitea | :gitea-source | :none :aot/java-opts [] ; optional, AOT compile only :aot/elide-meta [] ; optional, [] disables metadata elision :pom-exclude-deps []} ; optional, dropped from the published pom
An untracked ./local.deps.edn may supply a :provided alias (host sources
that must be on the AOT compile classpath but must NOT enter the pom) and an
:aot/preload namespace vector compiled ahead of this lib's own namespaces.
:publish is the ONLY thing that differs between packages — the task names
are identical everywhere, so one CI workflow drives the whole fleet:
:clojars public source jar -> repo.clojars.org :gitea AOT no-source jar -> private Gitea Maven registry :gitea-source source jar -> private Gitea Maven registry :none builds, never ships
Tasks (invoke with clojure -T:build <task>):
clean delete target/
jar source jar + pom
jar-aot AOT no-source jar (own .class + resources only)
install build + install to ~/.m2 (offline)
kondo sync dependency-exported lint configs, then lint
bump rewrite ./VERSION (:level :patch|:minor|:major)
verify-license report LICENSE / version.edn / SPDX agreement (warns)
freeze-check refuse a release that breaks ./freeze-policy.edn (fails)
deploy build + publish per :publish (no-op when :none)
Release flow (what CI runs on a push to main that touches src/deps): clojure -T:build bump :level :patch clojure -T:build deploy
The canonical hive release tasks, delivered as a library.
Point a repository's :build alias here and it inherits the whole release
path — no build.clj to copy, and no build.clj to drift:
:build {:deps {io.github.hive-agi/hive-build {:mvn/version "0.1.0"}}
:ns-default hive-build.api}
Coordinates come from ./version.edn, the version from ./VERSION:
{:lib io.github.hive-agi/hive-thing
:minor 1
:license {:name "MIT" :url "https://opensource.org/licenses/MIT"}
:scm-url "https://github.com/hive-agi/hive-thing"
:src-dirs ["src"]
:publish :clojars ; :clojars | :gitea | :gitea-source | :none
:aot/java-opts [] ; optional, AOT compile only
:aot/elide-meta [] ; optional, [] disables metadata elision
:pom-exclude-deps []} ; optional, dropped from the published pom
An untracked ./local.deps.edn may supply a `:provided` alias (host sources
that must be on the AOT compile classpath but must NOT enter the pom) and an
`:aot/preload` namespace vector compiled ahead of this lib's own namespaces.
`:publish` is the ONLY thing that differs between packages — the task names
are identical everywhere, so one CI workflow drives the whole fleet:
:clojars public source jar -> repo.clojars.org
:gitea AOT no-source jar -> private Gitea Maven registry
:gitea-source source jar -> private Gitea Maven registry
:none builds, never ships
Tasks (invoke with `clojure -T:build <task>`):
clean delete target/
jar source jar + pom
jar-aot AOT no-source jar (own .class + resources only)
install build + install to ~/.m2 (offline)
kondo sync dependency-exported lint configs, then lint
bump rewrite ./VERSION (:level :patch|:minor|:major)
verify-license report LICENSE / version.edn / SPDX agreement (warns)
freeze-check refuse a release that breaks ./freeze-policy.edn (fails)
deploy build + publish per :publish (no-op when :none)
Release flow (what CI runs on a push to main that touches src/deps):
clojure -T:build bump :level :patch
clojure -T:build deployJar file effects: deterministic rewriting, and selective class extraction.
The decisions these functions act on live in hive-build.promote.naming; what is here is only the IO.
Jar file effects: deterministic rewriting, and selective class extraction. The decisions these functions act on live in hive-build.promote.naming; what is here is only the IO.
Everything the freeze gate must read from the world: source text at HEAD and at the previous release tag, that tag's age, and this commit's message.
Isolated here so promote.freeze and promote.api-surface stay pure and
testable without a repository.
Everything the freeze gate must read from the world: source text at HEAD and at the previous release tag, that tag's age, and this commit's message. Isolated here so `promote.freeze` and `promote.api-surface` stay pure and testable without a repository.
Verify the built AOT jar in a fresh JVM containing only its declared dependency classpath. Provided/overlay dependencies are deliberately absent.
Verify the built AOT jar in a fresh JVM containing only its declared dependency classpath. Provided/overlay dependencies are deliberately absent.
Executes a Plan against a handler registry.
Knows nothing about tools.build. A handler is (fn [ctx step] -> result) keyed by :step/kind, which is the seam the whole suite exercises: substitute recording handlers and an entire release runs with no filesystem, no network, and no jar.
Executes a Plan against a handler registry. Knows nothing about tools.build. A handler is (fn [ctx step] -> result) keyed by :step/kind, which is the seam the whole suite exercises: substitute recording handlers and an entire release runs with no filesystem, no network, and no jar.
The tools.build adapter: gathers the Project and Facts a plan is computed from, and supplies the handler that executes each step kind.
This is the only namespace that knows tools.build and deps-deploy exist.
The tools.build adapter: gathers the Project and Facts a plan is computed from, and supplies the handler that executes each step kind. This is the only namespace that knows tools.build and deps-deploy exist.
Filesystem, environment and registry reads.
Every function here is one effect and no decision, so everything above this layer can be pure. Nothing in here interprets what it reads.
Filesystem, environment and registry reads. Every function here is one effect and no decision, so everything above this layer can be pure. Nothing in here interprets what it reads.
Task + Project + Facts -> Plan. Pure and total.
A release is a value before it is an effect. Everything read from the
filesystem or the network already sits in facts, so the whole decision —
which jar, whether to skip, what gets published where — can be asserted
without building anything.
Adding a task is a new steps method; adding a destination is a
publish/register!. Neither edits an existing branch.
Task + Project + Facts -> Plan. Pure and total. A release is a value before it is an effect. Everything read from the filesystem or the network already sits in `facts`, so the whole decision — which jar, whether to skip, what gets published where — can be asserted without building anything. Adding a task is a new `steps` method; adding a destination is a `publish/register!`. Neither edits an existing branch.
A namespace's PUBLIC surface as data, and the difference between two of them.
Pure: takes already-read top-level forms, returns values.
Surface shape: {"ns/name" {:kind :def|:defn|:defmacro|:defprotocol|:defrecord :arities #{0 1 :variadic} :methods {"method" #{1 2}}}}
What counts as public: anything a downstream repo can compile against —
defn/def/defmacro without ^:private (and not defn-), plus every
protocol and its method arities. Private vars are excluded on purpose: they
are not part of the contract and freezing them would freeze refactoring.
A namespace's PUBLIC surface as data, and the difference between two of them.
Pure: takes already-read top-level forms, returns values.
Surface shape:
{"ns/name" {:kind :def|:defn|:defmacro|:defprotocol|:defrecord
:arities #{0 1 :variadic}
:methods {"method" #{1 2}}}}
What counts as public: anything a downstream repo can compile against —
`defn`/`def`/`defmacro` without ^:private (and not `defn-`), plus every
protocol and its method arities. Private vars are excluded on purpose: they
are not part of the contract and freezing them would freeze refactoring.Compiled classes as values: which classes a .class file links against, and which of those links the jar does not satisfy. Pure.
An AOT jar that hardcodes implements hive_addon.protocol.IAddon builds and
publishes in silence, then fails to mount because the host loaded that
protocol from source under a different class identity. The link is visible
in the constant pool long before anyone tries to mount it.
Compiled classes as values: which classes a .class file links against, and which of those links the jar does not satisfy. Pure. An AOT jar that hardcodes `implements hive_addon.protocol.IAddon` builds and publishes in silence, then fails to mount because the host loaded that protocol from source under a different class identity. The link is visible in the constant pool long before anyone tries to mount it.
Source-level metadata elision, applied to a staged copy of the sources before AOT. Pure: text in, text out.
The compiler's :elide-meta reaches def metadata only. An ns form's
docstring is part of a constant map the ns macro emits, so it survives into
the generated __init class verbatim.
Source-level metadata elision, applied to a staged copy of the sources before AOT. Pure: text in, text out. The compiler's :elide-meta reaches `def` metadata only. An `ns` form's docstring is part of a constant map the ns macro emits, so it survives into the generated __init class verbatim.
The freeze policy, evaluated. Pure.
A contract library's whole value is that consumers can pin it and forget it. Two things destroy that: a change they cannot survive, and a cadence that makes them re-pin constantly. This namespace decides both from data.
Policy shape (./freeze-policy.edn in the repo being released):
{:policy/version 1 :cadence {:mode :scheduled | :on-merge :min-days-between-releases 90 :exception-marker "release-exception:"} :compatibility {:additive-only? true :break-marker "BREAKING-CHANGE:"}}
Contract: evaluate never throws and never consults the world — every
observation (the diff, the age, the commit message) is passed in.
The freeze policy, evaluated. Pure.
A contract library's whole value is that consumers can pin it and forget it.
Two things destroy that: a change they cannot survive, and a cadence that
makes them re-pin constantly. This namespace decides both from data.
Policy shape (./freeze-policy.edn in the repo being released):
{:policy/version 1
:cadence {:mode :scheduled | :on-merge
:min-days-between-releases 90
:exception-marker "release-exception:"}
:compatibility {:additive-only? true
:break-marker "BREAKING-CHANGE:"}}
Contract: `evaluate` never throws and never consults the world — every
observation (the diff, the age, the commit message) is passed in.Licence agreement between ./LICENSE, version.edn :license and the SPDX headers in the source tree, as an open rule chain. Pure.
A rule returns nil when satisfied, else a message. Unlike an access decision, every rule runs: a licence report that stopped at the first problem would hide the rest, and a published pom can never be retracted.
Licence agreement between ./LICENSE, version.edn :license and the SPDX headers in the source tree, as an open rule chain. Pure. A rule returns nil when satisfied, else a message. Unlike an access decision, every rule runs: a licence report that stopped at the first problem would hide the rest, and a published pom can never be retracted.
clj-kondo invocations as values. Pure.
Constructing the argument vector is where a lint task silently stops failing, so it is stated here and asserted rather than inlined at the process call.
clj-kondo invocations as values. Pure. Constructing the argument vector is where a lint task silently stops failing, so it is stated here and asserted rather than inlined at the process call.
Addon manifests as values. Pure.
A hive addon ships resources/META-INF/hive-addons/<id>.edn describing itself. The version in that file is the one an addon store reads, so it is derived from the coordinate being built rather than hand-maintained.
Addon manifests as values. Pure. A hive addon ships resources/META-INF/hive-addons/<id>.edn describing itself. The version in that file is the one an addon store reads, so it is derived from the coordinate being built rather than hand-maintained.
Names derived from a coordinate: the Maven symbol, artifact paths, the registry URL a publish check probes, and the namespace/path correspondence AOT compilation depends on. Pure.
Names derived from a coordinate: the Maven symbol, artifact paths, the registry URL a publish check probes, and the namespace/path correspondence AOT compilation depends on. Pure.
The parts of a pom hive-build decides rather than delegates: the declared licence, the dependency set, the source roots, the SCM tag and the repository set. Pure.
The parts of a pom hive-build decides rather than delegates: the declared licence, the dependency set, the source roots, the SCM tag and the repository set. Pure.
The Project value object: raw version.edn data plus a resolved version, with every default applied. Pure.
Nothing downstream reads version.edn again, so a default lives here exactly once.
The Project value object: raw version.edn data plus a resolved version, with every default applied. Pure. Nothing downstream reads version.edn again, so a default lives here exactly once.
Publish destinations as data, behind a registry. Pure.
The registry is the dependency-inversion point: plan and the boundary
depend on the Target schema, never on a particular registry. A new
destination is a register! and no edit anywhere else.
Publish destinations as data, behind a registry. Pure. The registry is the dependency-inversion point: `plan` and the boundary depend on the Target schema, never on a particular registry. A new destination is a `register!` and no edit anywhere else.
What must be rewritten INSIDE a jar for two builds of identical inputs to be byte-identical. Pure.
Fixing ZIP entry timestamps is not enough: build tooling also writes wall clock time and host details into entry content, where a timestamp rewrite cannot reach them. Each such source is a rule, so a newly discovered one is an addition rather than an edit.
What must be rewritten INSIDE a jar for two builds of identical inputs to be byte-identical. Pure. Fixing ZIP entry timestamps is not enough: build tooling also writes wall clock time and host details into entry content, where a timestamp rewrite cannot reach them. Each such source is a rule, so a newly discovered one is an addition rather than an edit.
Semantic versions as values: parse, render, order, bump. Pure.
Semantic versions as values: parse, render, order, bump. Pure.
Malli value objects for the release domain.
Every hive-build contract is stated over these schemas, and the property and mutation facets of the suite are synthesized from them. This namespace has no dependencies beyond clojure.string: schemas are plain data.
Generation: schemas whose validity is a regex carry an explicit
:gen/schema + :gen/fmap so a generator exists without test.chuck.
Malli value objects for the release domain. Every hive-build contract is stated over these schemas, and the property and mutation facets of the suite are synthesized from them. This namespace has no dependencies beyond clojure.string: schemas are plain data. Generation: schemas whose validity is a regex carry an explicit `:gen/schema` + `:gen/fmap` so a generator exists without test.chuck.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |