Releasing
How a release of brisvox is made: what it consists of, the steps a maintainer takes, what each workflow does, and the registry settings that are entered once. Everything is released together under one version number.
What a release contains
| Artifact | Where | Name | Built and published by |
|---|---|---|---|
Prebuilt brisvox-server | GitHub Release assets | brisvox-server-vX.Y.Z-<target>.tar.gz (.zip on Windows), SHA256SUMS | release.yml: binaries, github-release |
| Container image | ghcr.io/bilal07karadeniz/brisvox-server | tags vX.Y.Z and latest, linux/amd64 + linux/arm64 | release.yml: docker-build, docker-push, docker-manifest |
| Rust crates | crates.io | eleven crates, see below; brisvox is the umbrella | release.yml: crates-dry-run, publish-crates |
| Python SDK | PyPI | brisvox-agent, then the umbrella brisvox | release.yml: python-dist, publish-pypi |
| TypeScript SDK | npm | @brisvox/agent | release.yml: npm-pack, publish-npm |
| Browser module | npm | @brisvox/web | release.yml: npm-pack, publish-npm |
| npm umbrella | npm | brisvox (after the two above) | release.yml: npm-umbrella, publish-npm-umbrella |
| The book | GitHub Pages | https://bilal07karadeniz.github.io/brisvox/ | pages.yml (on every push to main) |
Prebuilt targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, aarch64-apple-darwin
and x86_64-pc-windows-msvc. The Linux binaries are built on Ubuntu 24.04 and need glibc 2.39 or
newer (the prebuilt ONNX Runtime needs 2.38). The binaries and the image are built with the feature
set all-providers,call-control,smart-turn-ort,webrtc (every provider adapter, call control, Smart
Turn on ONNX Runtime, the WebRTC listener); the README says so under "What to build with". The
Smart Turn model is not inside the binary archives (its licence notice and a checksum-verified
downloader are); the container image contains it.
The umbrella packages
The plain name brisvox is a real package on all three registries, so cargo add brisvox,
pip install brisvox and npm i brisvox work next to the specific names. They hold no logic of
their own and are released with everything else, under the same version:
| Registry | Package | Source | What it is |
|---|---|---|---|
| crates.io | brisvox | crates/brisvox | Re-exports brisvox-server (brisvox::server), the tools API (brisvox::agent), the provider adapters (brisvox::providers) and the core types (brisvox::core); brisvox::run_with_tools is a whole main. Forwards the features of brisvox-server by the same names (default: yaml, silero-tract; not the binary's mimalloc). Published last of the crates. |
| PyPI | brisvox | sdk/python-umbrella | Depends on brisvox-agent==X.Y.Z and re-exports its public API (from brisvox import Agent). |
| npm | brisvox | sdk/npm-umbrella | Depends on @brisvox/agent and @brisvox/web at exactly X.Y.Z; brisvox re-exports the agent SDK (Node), brisvox/web the browser client. |
scripts/check_release.py keeps them honest: one version, the exact pins, every feature of
brisvox-server forwarded, the license files in place.
The crates and their publish order
crates.io needs every dependency of a crate to exist before the crate itself, so the crates are
published in this order (python3 scripts/check_release.py --order prints it and
scripts/check_release.py fails when the order stops being a valid dependency order):
brisvox-corebrisvox-audiobrisvox-inferencebrisvox-agentbrisvox-providersbrisvox-simbrisvox-transportbrisvox-enginebrisvox-agent-linkbrisvox-serverbrisvox(the umbrella: it depends onbrisvox-agent,brisvox-core,brisvox-providersandbrisvox-server, so it goes last)
brisvox-bench (the benchmark harness) and everything under examples/ are never published
(publish = false). brisvox-sim is published because brisvox-engine has an optional dependency
on it (its sim feature); it is also the mock providers and simulator for testing an agent. Two
rules in the manifests keep publishing possible: every dependency between workspace crates carries
version next to path, and every dev-dependency between workspace crates is path-only (no
version), so cargo drops it from the published manifest. Several crates are each other's
dev-dependency (brisvox-agent and brisvox-sim), which would otherwise be a cycle.
Each package contains src/, its README and the two license files, nothing else (include in the
manifest): no tests, fixtures, examples or benchmark output. cargo publish prints "ignoring test
... is not included in the published package" for every integration test; that is expected. The
tests run from the repository. The Silero VAD graph that brisvox-inference embeds (MIT) is in the
package under assets/ with its NOTICE.
Making a release
- Bump the version in one commit:
versionin the workspaceCargo.tomland theversion = "..."of the elevenbrisvox*entries in its[workspace.dependencies];sdk/python/pyproject.tomland__version__insdk/python/brisvox_agent/__init__.py;sdk/python-umbrella/pyproject.toml(itsversionand thebrisvox-agent==X.Y.Zpin) and__version__insdk/python-umbrella/brisvox/__init__.py; and insdk/typescriptandsdk/webrunnpm version X.Y.Z --no-git-tag-version(it also updates the lock file). Insdk/npm-umbrellarun the same and set both entries underdependencies(@brisvox/agent,@brisvox/web) toX.Y.Z. Thencargo checksoCargo.lockfollows. - Write the changelog. In
CHANGELOG.mdmove the entries of[Unreleased]under a new## [X.Y.Z] - YYYY-MM-DDheading and update the two compare links at the bottom. The text of that section becomes the GitHub Release notes. - Check locally.
python3 scripts/check_release.py # versions, metadata, licenses, publish order, workflows scripts/publish_crates.sh --dry-run # package and build all eleven crates, no token scripts/verify.sh # the full quality gate - Merge to
mainand wait for thecirun on that commit to finish green. The release workflow refuses a tag whose commit has no successfulcirun. - Rehearse (optional, recommended for the first release and after changing a workflow).
Actions > release > Run workflow on
main. It does everything up to the gate (all binaries, both image platforms, the Python and npm packages, the crate dry run) and publishes nothing. - Tag and push the tag.
A version with a hyphen (git tag -a vX.Y.Z -m "brisvox X.Y.Z" git push origin vX.Y.Z0.2.0-rc.1) is a pre-release: the GitHub Release is marked as one and the image does not get thelatesttag. - Watch the run. If one publish job fails (a registry hiccup, the crates.io rate limit), fix the cause and re-run that job from the Actions page. Publishing is idempotent: crates, PyPI files, npm versions and image tags that already exist are skipped or replaced with the same content.
A published registry version cannot be replaced. A broken release is fixed by publishing the next
patch version; cargo yank, npm deprecate and the PyPI "yank" mark the broken one.
What the workflows do
release.yml (on a v* tag; Run workflow = rehearsal)
Build stage, nothing is published here:
prepare:scripts/check_release.py --tag(the tag names the workspace version; crates, Python and npm packages carry the same version; the changelog has the entry; metadata, license files and publish order are right), extracts the release notes, and requires a successfulcirun on the tagged commit.crates-dry-run:cargo publish --workspace --dry-run, which packages all eleven crates in dependency order and builds each from its package against the ones before it.python-dist: runs the SDK tests, builds the sdist and wheel ofbrisvox-agentand of the umbrellabrisvox,twine check --stricton both, and runs the umbrella's tests with both wheels installed (the umbrella's pin onbrisvox-agentmust resolve to the wheel of this release).npm-pack: tests andnpm packofsdk/typescriptandsdk/web.npm-umbrella: the static checks ofsdk/npm-umbrella,npm packof it, and a fresh install of its tarball together with the two tarballs ofnpm-pack, importingbrisvoxandbrisvox/web.binaries:cargo build --release --lockedon a native runner per target, strip, a smoke test (--version,checkof a reference definition), andscripts/package_release.py(archive plus.sha256).docker-build: the Dockerfile forlinux/amd64andlinux/arm64, each on a native runner, without pushing.gate: succeeds only when every job above did.
Publish stage, tags only, each job needs gate:
github-release: creates the GitHub Release for the tag with the changelog section as notes, the archives andSHA256SUMS.docker-pushanddocker-manifest: build each platform again (from the build cache), push it by digest to GHCR with the repository'sGITHUB_TOKEN, then create one multi-platform image with the tagsvX.Y.Zandlatest.publish-crates:scripts/publish_crates.sh, which publishes the crates in the order above (the umbrellabrisvoxlast), skips versions that already exist and waits out crates.io's limit on new crates.publish-pypi:pypa/gh-action-pypi-publishwith trusted publishing, environmentpypi:brisvox-agent.publish-pypi-umbrella: the same forbrisvox, environmentpypi-brisvox, afterpublish-pypi(PyPI allows one pending publisher per repository + workflow + environment).publish-npm:npm publish --provenance --access publicof the tarballs made bynpm-pack(@brisvox/agent,@brisvox/web).publish-npm-umbrella: the same forbrisvox, only afterpublish-npmsucceeded (it depends on the two scoped packages being on the registry).
pages.yml (on every push to main)
Checks the book structure (scripts/check_book.py), builds it with mdBook (mdbook build book) and
deploys it to GitHub Pages with actions/deploy-pages.
One-time setup
Nothing here is needed again for later releases, except that the first release of each name needs a token (below), and the token is removed afterwards.
Repository settings (GitHub)
- Settings > Pages > Build and deployment > Source: GitHub Actions.
- Settings > Code security > Private vulnerability reporting: enabled (
SECURITY.mdpoints reporters there). - Settings > Actions > General > Workflow permissions: Read repository contents is enough; the workflows declare what they need.
- Settings > Environments: create
pypi(the workflow creates it on first use; add required reviewers if every PyPI upload should wait for a person). - After the first image push: the package
brisvox-serverstarts private. On the repository's Packages page open it, Package settings > Danger Zone > Change visibility: Public, and confirm that the repository is linked (the image carries theorg.opencontainers.image.sourcelabel).
crates.io
Trusted publishing is configured per crate and, as of 2026-10-15, only for a crate that already exists; the crates.io announcement says the first release of a crate is published with a token.
-
Verify the account's email address at https://crates.io/settings/profile: crates.io refuses every publish from an account without a verified email (
400 Bad Request: A verified email address is required), token or not. -
Create a token at https://crates.io/settings/tokens: scopes publish-new and publish-update, crate patterns
brisvox-*andbrisvox(the patternbrisvox-*does not match the umbrella crate's plain name), an expiry of a few weeks. -
Add it as the repository secret
CARGO_REGISTRY_TOKEN(Settings > Secrets and variables > Actions). While this secret exists the workflow uses it instead of OIDC. -
Push the first tag.
publish-cratespublishes all eleven crates; crates.io lets you publish five new crates at once and then one every ten minutes, so the job takes about an hour and waits by itself. (To avoid the wait, write to help@crates.io before the first release for a higher limit.) -
For each of the eleven crates (including
brisvox) open its settings page on crates.io (for the first one the address ends in/crates/brisvox-core/settings) > Trusted Publishing > Add, choose GitHub and enter:Field Value Repository owner bilal07karadenizRepository name brisvoxWorkflow filename release.ymlEnvironment leave empty -
Delete the secret
CARGO_REGISTRY_TOKENand revoke the token. From the next release the job authenticates withrust-lang/crates-io-auth-action(OIDC, a short-lived token). Optionally turn on "Require trusted publishing for new versions" in each crate's settings.
PyPI
PyPI can register a publisher before the project exists (a pending publisher), so no token is
ever needed. There are two projects, so add two pending publishers at
https://pypi.org/manage/account/publishing/ > "Add a new pending publisher" > GitHub, one for each
PyPI project name (brisvox-agent and the umbrella brisvox). PyPI allows only one pending
publisher per repository + workflow + environment, so each uses its own environment:
| Field | Value |
|---|---|
| PyPI project name | brisvox-agent for the first, brisvox for the second |
| Owner | bilal07karadeniz |
| Repository name | brisvox |
| Workflow name | release.yml |
| Environment name | pypi for brisvox-agent, pypi-brisvox for brisvox |
The first successful publish creates each project and turns its pending publisher into an
ordinary trusted publisher. A pending publisher does not reserve the name: if somebody registers
brisvox-agent or brisvox first, it stops working, so publish soon after adding them. The
workflow publishes brisvox-agent first and brisvox second, in two jobs; if only one pending
publisher exists, the other upload fails with a trusted-publishing error and the job can be
re-run after adding it (skip-existing makes the finished one a no-op).
npm
An npm trusted publisher can only be added to a package that already exists, so the first version of
each package is published with a token. The scope needs an owner. There are three packages:
@brisvox/agent, @brisvox/web and the unscoped brisvox (the umbrella, a name on the public
namespace rather than in the @brisvox scope).
-
Create the organization
brisvoxon https://www.npmjs.com/org/create (free for public packages); that owns the@brisvoxscope. -
Create a granular access token (https://www.npmjs.com/settings/~/tokens): read and write, allowed to bypass two-factor authentication (CI has no second factor), a short expiry. Its scope must cover both the
@brisvoxscope and the unscoped packagebrisvox: a token limited to@brisvoxcannot publishbrisvox, which is outside the scope. A granular token can only name packages that already exist, andbrisvoxdoes not yet, so for the first publish give it access to all packages (read and write) with a short expiry, and revoke it in step 5. Add it as the repository secretNPM_TOKEN. -
Push the first tag.
publish-npmpublishes@brisvox/agentand@brisvox/webwith--provenance --access public, thenpublish-npm-umbrellapublishesbrisvoxthe same way. -
For each of the three packages (
@brisvox/agent,@brisvox/web,brisvox) open npmjs.com > the package > Settings > Trusted Publisher > GitHub Actions and enter (case-sensitive; npm does not validate them when you save, a mistake shows at publish time):Field Value Organization or user bilal07karadenizRepository brisvoxWorkflow filename release.ymlEnvironment name leave empty With npm 11.15 or newer and two-factor authentication on the account, the same can be done with
npm trust github @brisvox/agent --repo bilal07karadeniz/brisvox --file release.yml --allow-publish(and the same for@brisvox/webandbrisvox). -
Delete the secret
NPM_TOKENand revoke the token; under each of the three packages' Publishing access choose "Require two-factor authentication and disallow tokens". From the next release npm authenticates by OIDC (npm 11.5.1 and Node 22.14 or newer, which the job has). The package'srepository.urlmust match this repository exactly;package.jsonhas it.
Registry facts
The registry behaviour described here (trusted publishing on crates.io, PyPI and npm; the first release of a name; the crates.io rate limit on new crates; the container registry's default visibility) was read from the registries' own documentation and sources in October 2026. Registries change: when a step stops matching what the website shows, the website is right and this page should be corrected.