Publishing, Licensing and Versioning
Thalweg is a workspace, not a monolith. Most crates are published independently so they can be used on their own — a pure-Rust Kafka client, a sans-io SASL state machine, an Avro-to-Arrow decoder — without taking the rest of the stack.
Licence: MIT OR Apache-2.0
Dual-licensed, at the user’s option. This is the Rust ecosystem convention and it is deliberate:
- Both require attribution. MIT requires the copyright notice and licence text be retained. Apache-2.0 requires retained notices, a statement of changes made, and propagation of any NOTICE file. Either satisfies “attribution back to us”.
- Offering the choice maximises adoption. Apache-2.0 carries an express patent grant, which some organisations require; MIT is simpler and compatible with GPL-2.0-only projects, which Apache-2.0 is not. Dual licensing means a downstream user’s compliance regime is never a reason not to use the crate.
Neither licence compels visible attribution — no “powered by” notice, no advertising clause (the old BSD-4-clause advertising requirement is deprecated and we do not reintroduce it). If visible credit is ever wanted, that is a courtesy request in a README or a trademark policy, not a licence term.
Copyright holder. Currently The Thalweg Authors, which is a placeholder. The
legal entity to name is a decision for the project owners; it appears in both
licence files and should be settled before the first publish.
Every published crate is a standalone product
A crate that is published carries obligations beyond compiling:
- Its own version, and its own release cadence.
versionis deliberately not a workspace-inherited field — a consumer taking one crate should not be forced to track the others. - Its own README, rendered on crates.io. It must say what the crate does without assuming the reader knows what Thalweg is.
- Its own tests, meeting the standard in
testing/TESTING.md— and for sans-io crates, tests that run without a container or a runtime, which is a large part of what makes them reusable at all. - Semver discipline. Below
1.0.0, a minor bump may break; from1.0.0, breaking changes need a major.cargo-public-apisnapshots guard the surface. - Documented MSRV, inherited from the workspace, raised only in a minor bump.
- Complete crates.io metadata: description, keywords, categories, repository, licence, readme.
Four crates are not published, and say so in their manifests: the twg-cli
binary, the two *-arrow composition crates, and the twg-e2e harness. They are
glue and have no standalone use.
API reference is self-hosted, not on docs.rs
docs.rs only builds published crates from crates.io, so it carries nothing
while the workspace is unpublished, and never carries the four internal crates. The
API reference is therefore self-hosted: cargo doc is rendered locally and
committed under docs/site/api/, served at
docs.thalweg.dev/api/ alongside the prose
reference at /reference/. It regenerates on
commit via the pre-commit hook. See
ADR-0059 and
operations/DOCS-DEPLOY.md — contributors need the
Rust toolchain and mdbook installed for local regeneration. Once a crate is
published, docs.rs builds it too; the self-hosted copy remains the one place the
whole workspace (internal crates included) is documented together.
Three tensions this creates
Independent publishing is not free. These are real and are stated here rather than discovered later.
1. The Arrow version chokepoint becomes a public contract
Internally, twg-codec-core holds the only arrow re-export and every codec crate
depends on it, so the workspace pins exactly one Arrow version. That discipline
still holds between our crates — but a downstream consumer mixing
twg-codec-avro with their own arrow dependency can hit a version mismatch, and
Arrow’s frequent breaking releases make this likely rather than theoretical.
Therefore: arrow is a public dependency of the codec crates. Its supported
version must be stated in each crate’s README and treated as part of the API — an
Arrow major bump is a breaking change for us, requiring a major version bump, not
a patch. This is more constraining than it would be for an internal-only crate.
2. Release ordering and cascade
Forty-seven crates in a dependency graph means a change to twg-codec-core
cascades: every codec crate needs a dependency bump and a release. Doing that by
hand does not scale and will be got wrong.
Therefore: release-plz, configured in ../release-plz.toml
and run by the Release workflow. It works out which crates actually changed,
bumps them and their dependents, updates per-crate changelogs, and publishes in
topological order. It honours publish = false, so the four internal crates are
excluded without being listed.
Two jobs: release-pr opens a reviewable PR with the proposed bumps, and
release publishes on merge. Releases are therefore reviewed rather than
automatic.
Publish-by-design is eventual, not immediate. Most publish = true crates are
still unimplemented stubs, so release-plz is currently gated to auto-publish
only the five implemented, publish-ready crates — twg-proto-flatten,
twg-proto-schema, twg-proto-decode, twg-type-map, and twg-udf. The rest
stay gated in ../release-plz.toml until their code
lands; “published independently” describes the design intent for all of them,
not what releases today.
The codec family shares a version group (codec-core, type-map, the five
codec crates, plus twg-proto-schema and twg-proto-decode). They share
arrow as a public dependency through the single
re-export, so an Arrow major bump breaks all of them simultaneously and a change to
the core traits forces a coordinated release anyway — the grouping reflects a real
coupling and collapses a six-crate bump cascade into one. The cost is that a
consumer of twg-codec-json takes a bump when twg-codec-avro changes; that is
acceptable precisely because the shared Arrow contract means they were never
independent. True leaves — twg-wire-sasl, twg-wire-tls,
twg-wire-compression, twg-resource-loader — release independently.
cargo-semver-checks is enabled and blocks a non-major bump that breaks the
public API. That matters more here than in most workspaces: because arrow is a
public dependency of the codec crates, an Arrow type in a signature makes an Arrow
major bump our breaking change, and catching that mechanically beats catching it
in someone else’s build.
Commit messages become load-bearing. release-plz derives bumps and changelogs
from Conventional Commits, with the crate name (minus the twg- prefix) as the
scope. See AGENTS.md. Commits predating the convention are plain prose and are
not retrofitted.
Secrets required
| Secret | Purpose |
|---|---|
CARGO_REGISTRY_TOKEN | crates.io API token, scoped to publish-update and publish-new |
RELEASE_PLZ_TOKEN | PAT or GitHub App token with contents + pull-requests write. The default GITHUB_TOKEN cannot trigger CI on a PR it created, so the release PR would sit untested |
3. Discoverability versus the prefix
twg- is compact and signals provenance, which is right inside the workspace. But
someone searching crates.io for a pure-Rust Kafka client will not search “twg”.
The prefix trades discoverability for consistency and for not squatting generic
names.
This is accepted, with one mitigation: each crate’s README and its keywords must carry the descriptive terms someone would actually search for, since keyword search is how these crates will be found rather than by name.
Before the first publish
- Docs-visibility coupling resolved: the documentation site is public, so
crate READMEs linking to
docs.thalweg.devresolve for anyone arriving from crates.io (seeoperations/DOCS-DEPLOY.md) - Settle the copyright holder in both licence files
- Add the two release secrets, then prove
release-plzwith a dry run before the first real publish - State the supported
arrowversion in each codec crate’s README (Arrow 58, per ADR-0003) - Verify the
twg-prefix is unclaimed for every crate name to be published -
cargo-public-apibaselines committed for crates with a stability promise - Confirm every published crate’s tests pass standalone, outside the workspace