18 Release¶
Status: draft M0. Normative for versioning, tags, changelogs, release channels, signing, SBOM, provenance, server images, app distribution, compatibility between app and server and rollback. Flow of a release: docs/operations/release.md. Gates and runners: ADR 0038 and 0039 (team H). API versioning: ADR 0014 (team B).
1. Goals¶
- Every component is versioned and published independently when it changes.
- Every published artifact is signed, has an SBOM and a verifiable provenance.
- An instance operator can tell from the release whether an upgrade needs manual work.
- Old apps work against new servers as long as their protocol version is supported, and otherwise get a clear notice instead of an error.
2. Components¶
The names, directories and tag prefixes are in one place: RELEASE_COMPONENTS in
tools/ci/components.py (ADR 0060). This table reproduces them.
| Component | Directory | Tag prefix | Artifacts | Channels |
|---|---|---|---|---|
bothy |
root | none (vX.Y.Z) |
installation bundle bothy-compose-<version>.tar.gz from make release-build: infra/compose without infra/compose/test (test overlays) and without the source of install.sh, the folders infra/otel and infra/observability that compose.yaml mounts relative to itself, LICENSE, NOTICE, CHANGELOG.md, VERSION; its infra/compose/.env.example contains the pinned server version and the registry (section 2.1); in addition install.sh as a separate file with a built-in version, SHA-256 and download address of the bundle (section 2.1, 16-operations.md, section 3.1) |
stable, beta |
server |
services/server |
server/ |
images bothy-server, bothy-postgres (linux/amd64, linux/arm64), binaries bothy |
stable, beta |
host |
services/host |
host/ |
binaries bothy-host for macOS (arm64, amd64), Linux (amd64, arm64) and Windows (amd64), each with a signature and an SBOM; installation packages of the services (24-terminal.md, 16-operations.md section 4.4) |
stable, beta |
client |
apps/client |
client/ |
iOS, Android, macOS, Windows (one Flutter code base, B5) | stable, beta |
console |
apps/console |
console/ |
web console | with server |
sdk-python |
sdk/python |
sdk-python/ |
package on PyPI | stable, pre-releases |
sdk-typescript |
sdk/typescript |
sdk-typescript/ |
package on npm | stable, pre-releases |
hermes-adapter |
plugins/hermes-adapter |
hermes-adapter/ |
plugin for Hermes Agent | stable, pre-releases |
claude-code-runner |
plugins/claude-code-runner |
claude-code-runner/ |
dev runner for Claude Code | stable, pre-releases |
grafana-bothy-live |
plugins/grafana-bothy-live |
grafana-bothy-live/ |
Grafana panel plugin bothy-live-panel as a ZIP with an SBOM; unsigned until V1 for local provisioning (25-visualization.md, section 8.3) |
stable, pre-releases |
api |
docs/api |
none | OpenAPI and AsyncAPI document; version in the document (ADR 0013, 0014) | with server |
Rules:
- Every component has a file
VERSION(one line, SemVer withoutv) and aCHANGELOG.mdin its directory. The repo structure defines the directories (team H). - Tags are named
<prefix>v<SemVer>, for exampleserver/v1.2.3; the prefix is a rule of this repository. The Go modules reference states two rules without a condition: a module in a subdirectory of its repository has tags prefixed with that subdirectory ("each tag name must be prefixed with the module subdirectory, followed by a slash", hereservices/server/v1.2.3), and from major version 2 on "the module path must end with a major version suffix like/v2". It names their purpose in the same section: "If a module might be depended on by other modules, these rules must be followed so that the go command can find and download the module", and "A module that will never be fetched as a dependency of any other module may use any valid package path for its module path" (https://go.dev/ref/mod, sections "Module paths" and "Mapping versions to commits", retrieved 2026-10-15) [verified]. The server modulegithub.com/OWNER/bothy/services/serveris a main module that no other module requires (PLAN P-05): the go command never maps one of its versions to a tag, and the version of the binary comes fromVERSION(rule 5), not from a tag. So neither rule takes effect, andserver/vX.Y.Zstays (docs/DECISIONS.md D-M7-10-4). Tags are signed. - Should the server module ever become usable as a dependency, that is a separate decision
(tag form and
/v2path according to the Go rule). - The file
VERSIONand the tag match; a gate check verifies this. - The server knows its version from
internal/platform/buildinfo(Version()): a constant equal toservices/server/VERSION, held by a unit test; no linker flags. Reason: ago buildwithout additions yields the same version as the release build. It servesservice.version(17), the comparison withinstance.min_server_version(16, section 10) and the image labels (section 7). - Next to the embedded migrations is the constant
MinServerVersion: the oldest server version that may run with the schema of these migrations. According to rule M1 from upgrade.md this is the previous version; a deliberate break sets its own version; before the first release it is the current one.bothy migrate upwrites it toinstance.min_server_version.
2.1 Server version and registry in the product bundle¶
The bundle of the component bothy determines which server images an installation pulls:
install.sh takes BOTHY_VERSION and BOTHY_REGISTRY unchanged from its
infra/compose/.env.example (16-operations.md, section 3.1, step 4). Compose
pulls the images by their tag with this version, not by digest; what follows from that is in
16-operations.md, section 6.1, rule 2. The checked-in template carries
placeholders (ghcr.io/OWNER, 0.0.0-placeholder), and so does the source of install.sh.
Therefore, for every product release, beta as well as stable:
- Server version. Its only source is the line
BOTHY_VERSION=ininfra/compose/.env.exampleof the tagged commit.make release-prep COMPONENT=bothy SERVER_VERSION=<version>writes it in the same commit asVERSION(ADR 0060). It writes nothing and names the value if the tagserver/v<version>does not exist, if withoutCHANNEL=beta(stable product)<version>carries a pre-release identifier, or ifSERVER_VERSIONis missing while the line carries the placeholder. The gaterelease-pins(tools/ci/components.py pins) checks the committed state in everymake checkand in CI: exactly one line, SemVer withoutvor the placeholder, and no server pre-release in a stable product, which only a product beta (X.Y.Z-beta.N) may pin. Before the tag it runs in the pre-push of the branch with the release commit and in the required checkci-okof its pull request; the maintainer sets the tag only after the merge, on the squash commit onmain(push-discipline.md). The push of the tag alone starts no check: its commit is already onoriginthen, and pre-commit runs no pre-push hook for a push without new commits (function_pre_push_nsin https://github.com/pre-commit/pre-commit/blob/v4.6.2/pre_commit/commands/hook_impl.py, retrieved 2026-10-11) [verified]. After the push of the tag, the jobgatesofrelease.ymlchecks again, before the build.make release-buildtakes the value over unchanged. - Registry.
make release-buildwritesBOTHY_REGISTRY=ghcr.io/<owner>into the.env.exampleof the bundle, with<owner>in lowercase fromGITHUB_REPOSITORY_OWNER, like the image name inrelease.yml. GitHub sets this variable in every step to "The repository owner's name" (https://docs.github.com/en/actions/reference/workflows-and-actions/variables, retrieved 2026-10-11) [verified]; a run outside GitHub Actions sets it itself. Every other file and line of the bundle is the same as in the commit. - Download address.
make release-buildwritesinstall.shas an artifact of its own frominfra/compose/install.shand sets three values: the version of the product, the SHA-256 of the bundle and the download addresshttps://github.com/<repository>/releases/download/v<version>/, with<repository>fromGITHUB_REPOSITORY. GitHub sets this variable in every step to "The owner and repository name" (source as in rule 2) [verified]; a run outside GitHub Actions sets it itself. From this addressinstall.shdownloads the bundle and its signature (16-operations.md, section 3.1, step 2). - Abort.
make release-buildwrites no bundle and noinstall.shand names the value ifBOTHY_VERSIONequals0.0.0-placeholderor is not a SemVer version withoutv, if a stable product (VERSIONwithout a pre-release identifier) pins a server pre-release (second safeguard afterrelease-prepandrelease-pins), ifGITHUB_REPOSITORY_OWNERorGITHUB_REPOSITORYis missing, ifBOTHY_REGISTRYorinstall.shstill containsOWNERor a placeholder of the source afterwards or if one of the images thatinfra/compose/compose.yamlnames with${BOTHY_VERSION}(todaybothy-server:<version>andbothy-postgres:pg18-<version>) is missing underBOTHY_REGISTRY. It reads the list of images fromcompose.yaml, not from a list of its own.
Reason: without this step, every installation from the bundle would pull an image that does not
exist (ghcr.io/OWNER/bothy-server:0.0.0-placeholder), including the portability test
(16-operations.md, section 11.1). The server version is in the commit instead of
being an argument of the build: this way the tag alone determines what the bundle installs, and the
release commit shows the version for review. A server pre-release in the stable product shows up
before the tag already (release-prep, at the latest release-pins in the pull request), because
a pushed tag cannot be taken back. Registry and download address come from the build because they
depend on the location of the repository (fork, replacing OWNER in V1-03), not on the commit:
even a tag whose source still names OWNER delivers an install.sh that finds its bundle. PLAN M7-29
takes on the build (with tests), M7-10 takes on make release-prep itself.
As built (M7-29, docs/DECISIONS.md D-M7-29-2, D-M7-29-3, D-M7-29-5): make
release-build VERSION=<v> OUT=<dir> runs tools/ci/product_release.py build. It reads
VERSION, infra/compose/.env.example, compose.yaml and install.sh from the commit HEAD,
not from the working tree; it checks each image with docker manifest inspect (the release job
logs in to ghcr.io first); it writes the bundle reproducibly (file order, modes and times of
the commit, a gzip header without a time) and install.sh with mode 0755, and only after every
check, so an abort leaves no file. In install.sh the three values are the lines
BOTHY_INSTALL_VERSION=, BOTHY_INSTALL_BUNDLE_SHA256= and BOTHY_INSTALL_BASE_URL=; the
words OWNER and placeholder must not occur in the finished script in any case. Trial only
(D-W-127): RELEASE_TRIAL_REGISTRY=127.0.0.1:<port> replaces ghcr.io/<owner> and
RELEASE_TRIAL_BASE_URL=http://127.0.0.1:<port>/ the download address; both are refused when
GITHUB_ACTIONS is set and for any other host, and the image check then uses
docker manifest inspect --insecure. make release-trial COMPONENT=bothy sets them (section 6.2).
In release.yml the job product-signing (environment release) writes SBOMs, signs bundle,
install.sh and SHA256SUMS with tools/ci/release_chain.py and verifies them like the server
files (section 6.1), and publish takes that set by artifact id.
3. Semantic versioning per component¶
| Component | Major | Minor | Patch |
|---|---|---|---|
server |
removal of a supported API major version or protocol version; removal or reinterpretation of a configuration key; an upgrade that requires manual work on data | new features, new keys, new event types (additive) | bug fix without a change in behavior |
client, console |
the minimum server version rises across a server major version | new features | bug fix |
| SDKs, adapters | incompatible change of the public interface | additive change | bug fix |
Before 1.0.0, SemVer 0.x applies: a minor version may break; the release notes name every break.
4. Changelog and release notes¶
CHANGELOG.mdfollows "Keep a Changelog" with the sections Added, Changed, Deprecated, Removed, Fixed, Security. The entries are in English (the community language).- Commits follow Conventional Commits with the component as the scope. A tool generates a draft
from them; a person or an agent reviews it before the tag. Tool: git-cliff behind
make release-prep(ADR 0060). - Release notes of the server have fixed sections: upgrade notes (manual work yes or no), migrations (expand or contract, duration on large tables), compatibility (supported protocol versions, minimum app version), security, known issues.
5. Channels¶
| Artifact | stable | beta |
|---|---|---|
| Server images | tag X.Y.Z and digest |
tag X.Y.Z-beta.N |
Host service bothy-host |
version X.Y.Z |
X.Y.Z-beta.N |
| iOS | App Store once approved; until then TestFlight group "stable" | TestFlight group "beta" |
| macOS | Sparkle appcast without a channel (default) | Sparkle channel beta |
| Windows | stable.appinstaller |
beta.appinstaller |
| Android | test track "stable" (owner decision, section 8) | internal test track |
| SDKs, adapters | version without a pre-release identifier | X.Y.Z-beta.N (npm tag beta, PyPI pre-release) |
Rules:
- Apps: the version contains no channel. The same build (same build number, same
signature) moves from beta to stable: switch the TestFlight group, remove the element
sparkle:channelin the appcast, letstable.appinstallerpoint to the same MSIX. - Server, host service, SDKs, adapters: beta is a SemVer pre-release (
X.Y.Z-beta.N), because the version is in the binary and in the package. Stable is built from the same commit as the last beta; between the two only the version number changes, and all gates run again. - Images have no moving tag
latest. Compose and OpenTofu pinBOTHY_VERSION; Renovate (ADR 0041) proposes updates. - Sparkle channels are meant for temporary branches, not for permanently parallel editions (https://sparkle-project.org/documentation/publishing/, retrieved 2026-10-10) [verified]. That fits beta; a second permanent edition would need its own appcast.
6. Supply chain¶
| Step | Tool | Result |
|---|---|---|
| SBOM | Syft (server: the pinned binary through tools/ci/release_chain.py sbom; other components: anchore/sbom-action until their release tickets) |
SPDX JSON and CycloneDX JSON per image and platform and per binary, attached to the release |
| Signing images | public: cosign keyless (Fulcio, Rekor), identity = release workflow; private: cosign key pair without a transparency log (ADR 0062) | signature per digest |
| Signing files | cosign sign-blob with a bundle, same procedure as for the images |
*.sigstore.json next to every file |
| Provenance | actions/attest (SLSA Build Provenance) |
attestation in the repository |
| Checksums | SHA-256 | SHA256SUMS, itself signed |
Sources (retrieved 2026-10-10): https://github.com/anchore/sbom-action, https://docs.sigstore.dev/cosign/signing/overview/, https://docs.sigstore.dev/cosign/verifying/verify/, https://github.com/actions/attest-build-provenance [verified].
Limit: artifact attestations are available in GitHub Free, Pro and Team only for public
repositories (https://github.com/actions/attest-build-provenance, retrieved 2026-10-10) [verified].
Until the product repository is public, there are a signature and an SBOM but no attestation,
and signing uses the key pair of the private phase (ADR 0062); verification then uses
cosign verify --key and the public key from the repository. In the public state the
instance operator verifies like this:
cosign verify ghcr.io/<owner>/bothy-server@sha256:<digest> \
--certificate-identity-regexp '^https://github.com/<owner>/bothy/.github/workflows/release.yml@refs/tags/server/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/<owner>/bothy-server@sha256:<digest> --repo <owner>/bothy
The issuer https://token.actions.githubusercontent.com is not confirmed in the sources read
[assumed]; the release ticket checks it at the first real release.
6.1 Server release chain (as built by M7-10)¶
release.yml runs these steps for a tag server/vX.Y.Z, in this order; the steps after the
image builds are commands of tools/ci/release_chain.py, which make release-trial runs the
same way against a local registry (section 6.2):
resolve:tools/ci/components.py releaserejects the tag ifservices/server/VERSIONdiffers from it orservices/server/CHANGELOG.mdhas no section[X.Y.Z]; nothing is built then. The full gate (ci.yml) follows.build: the binariesbothy_<version>_linux_amd64and..._linux_arm64frommake -C services/server release-build, andbothy-serverfor both platforms in one build (it cross compiles), pushed by digest without a tag.postgres-build-amd64andpostgres-build-arm64:bothy-postgresper platform on a native runner (ubuntu-24.04,ubuntu-24.04-arm), pushed by digest, each passing the id of its digest artifact.scancode: ScanCode over the tracked tree with the license gate (make scancode, licenses.md section 4); its report is a release artifact.supply-chain(environmentrelease, afterscancode): join the twobothy-postgresdigests into one multi-arch index (by digest, no tag); vulnerability scan of both images (make trivy-image); SBOMs; signatures of both image digests, both binaries andSHA256SUMS(computed over every file of the release except the signature bundles), with a.sigstore.jsonbundle each; a missing SBOM stops this step before the first signature. In the private phase every bundle is checked to carry no transparency log entry. Then the verification of section 6 or 6.2 against the result, and only then the version tagsbothy-server:X.Y.Zandbothy-postgres:pg18-X.Y.Z.publish: the GitHub release with the binaries, SBOMs, signature bundles,images.txt(name and digest per image),SHA256SUMSand the ScanCode report; Build Provenance only in the public phase.
Signing paths (ADR 0062): in
the private phase cosign signs with the key pair whose private key and password are the
secrets COSIGN_PRIVATE_KEY and COSIGN_PASSWORD of the environment release, and with a
signing configuration from cosign signing-config create --no-default-rekor
--no-default-fulcio --no-default-tsa --no-default-oidc; the public key is
quality/release/cosign-private-phase.pub. In the public phase (github.event.repository.visibility)
cosign signs keyless with the workflow identity, and actions/attest adds Build Provenance per
image digest. tools/ci/check_runners.py (rule signing-outside-release) rejects a signing
step in a job without environment: release.
6.2 Trial release¶
make release-trial COMPONENT=server (docs/DECISIONS.md D-W-127, D-M7-10-1) runs the steps of
6.1 on the build host against throwaway local targets: a registry container pinned by digest
on 127.0.0.1 with a random port, a cosign key pair generated for the run, the signing
configuration of the private phase, SBOMs, SHA256SUMS and its signature in a temporary
directory, then cosign verify --key, cosign verify-blob and sha256sum --check, the
version tags, and a check that every image infra/compose/compose.yaml names with
${BOTHY_VERSION} exists under that version with the signed digest. Three probes must fail: a
changed file against SHA256SUMS, an unsigned digest against cosign verify, a release without
one SBOM against the signing step. It never pushes to another registry, never tags the
repository or creates a release, and refuses to run when GITHUB_ACTIONS is set. Vulnerability
scan and ScanCode are separate targets and not part of the trial. Not proven by the trial:
the keyless path and the attestations of the public phase, and a push to ghcr.io; both run
first with the owner's first tag.
make release-trial COMPONENT=bothy (M7-29) runs the trial of server first and then, against
the same local registry, the product build of section 2.1 from a throwaway repository with the
bundle paths of the commit and BOTHY_VERSION set to the trial server version, with
RELEASE_TRIAL_REGISTRY and RELEASE_TRIAL_BASE_URL pointing at the local registry and a static
HTTP server on 127.0.0.1. It then checks what an installation needs: install.sh and the bundle
downloaded from that address, the bundle's SHA-256 equal to the value in install.sh, registry
and server version of the bundle equal to the trial's, every image of the bundle's
compose.yaml present there with the signed digest, and a docker pull of each by the Docker
daemon. A full installation from that result is the local end-to-end run of M7-14
(16-operations.md 11.1).
7. Server images¶
- Platforms linux/amd64 and linux/arm64, as one multi-arch manifest (B12).
- Never built via emulation:
bothy-servercompiles the Go binary in the builder step on the build platform for the target architecture (without cgo,--platform=$BUILDPLATFORM); images with steps that must run on the target architecture (bothy-postgres) are built per architecture on their own runner. Inrelease.ymlbothy-postgresis built per architecture on a native runner and pushed by digest; the release joins both digests into one multi-arch index (section 6.1). Images are pushed by digest only; the version tag comes last, after the vulnerability scan, ScanCode, SBOMs, signatures and verification. bothy-serveris a static Go binary (without cgo) on a minimal base image without a shell. It runs as an unprivileged user with uid and gid65532;bothy secrets initsets this owner for the secrets ofserverandmigrate(16, section 5.3), and the image createssecrets.signing_key_dirwith this owner and mode0700.HEALTHCHECKand Compose callbothy healthcheck, because there is no shell and no curl in the image.bothy-postgresbuilds on the officialpostgres:18image and contains pgBackRest from 2.59.3 on andbothy-backupd(16-operations.md 8). Its tag ispg18-<version>with the server version, asinfra/compose/compose.yamlnames it (section 2.1); the tag ofbothy-serveris<version>.- Base images are pinned by digest; Renovate keeps them current; a check in the gate rejects every
FROMwithout a digest (SEC-SC-4). - Labels according to OCI (
org.opencontainers.image.source,.version,.revision,.licenses); version, revision and source come as build arguments, a build without a version fails. bothy-serveris built fromservices/server/Dockerfilewith the repository root as build context (the same formake imageandrelease.yml). The fileservices/server/Dockerfile.dockerignoreapplies to this Dockerfile only: it ignores everything and allows onlygo.mod,go.sum,VERSION,api/,cmd/,internal/andmigrations/of the server, without test files andtestdata/. A build whoseVERSIONdiffers fromservices/server/VERSIONfails. For the web console (M6-02) the context also admits the rootpubspec.yaml,pubspec.lockandanalysis_options.yaml, every memberpubspec.yamlof the pub workspace (the root resolution needs them all),apps/consolewithouttest/,build/andcoverage/, thelib/ofapi_client,auth,design_system,domainandl10n, theassets/andfonts/ofdesign_systemanddocs/api/openapi.yaml(forversion.json). The console stage runs on$BUILDPLATFORM(rule 2), because its output is platform independent; it builds with the Flutter version of the workspace (pubspec.yaml), cloned at its tag with the revision checked, on the pinned builder image of the server (rule 5), and the final stage copiesbuild/webto/usr/share/bothy/console, whichBOTHY_CONSOLE__ASSETS_DIRof the image names.
8. App distribution¶
The apps are white label. Whoever signs an app owns its identity (bundle ID, package name, push keys, update feed). Therefore:
- The product delivers source code, build scripts and a template
distribution.yamlwith all values of a publisher: bundle and package IDs, team ID, Sparkle feed URL and public EdDSA key, MSIX publisher, Firebase project, APNs topic, optional Sentry target. - Every organization builds and signs its apps itself with its own accounts. For an instance,
distribution.yamllives in the ops repo. - Push keys and the Firebase project of the instance belong to the publisher of the app (R-Push). An instance can only send push notifications to apps whose keys it has.
| Platform | Route | Sources (retrieved 2026-10-10) |
|---|---|---|
| iOS | TestFlight: up to 100 internal and 10,000 external testers, a build is usable for 90 days, external testers need a Beta App Review for the first build | https://developer.apple.com/help/app-store-connect/test-a-beta-version/testflight-overview/ [verified] |
| macOS | Developer ID, notarization with xcrun notarytool, stapling with xcrun stapler, auto-update with Sparkle 2 (EdDSA-signed archives, channels, phased rollout with sparkle:phasedRolloutInterval) |
https://developer.apple.com/developer-id/, https://sparkle-project.org/documentation/publishing/ [verified] |
| Windows | MSIX with .appinstaller per channel; signing via Azure Artifact Signing (organization in the EU possible) or an OV certificate with an HSM. SmartScreen warns at first even with EV |
https://learn.microsoft.com/en-us/windows/msix/app-installer/app-installer-file-overview, https://learn.microsoft.com/en-us/azure/artifact-signing/quickstart, https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/smartscreen-reputation [verified] |
| Android | Play Internal Testing (up to 100 testers) or Firebase App Distribution; new personal Play accounts need a closed test with 12 testers over 14 days before production | https://support.google.com/googleplay/android-developer/answer/9845334, https://support.google.com/googleplay/android-developer/answer/14151465, https://firebase.google.com/docs/app-distribution [verified] |
| Web console | Proposal: part of the server image, served by the server; this way it is always the same age as the API | decided by ADR 0028 (team E) |
Consequences:
- TestFlight for macOS requires an App Sandbox [assumed, only read via web search]. The Developer ID edition with Sparkle runs without a sandbox. Both would be two builds; the product delivers only the Developer ID edition for macOS.
- The protocol
ms-appinstaller:has been off by default since December 2023; users download the.appinstallerfile and open it (source as above) [verified]. - The owner decides on costs and accounts (Apple Developer Program, Artifact Signing or OV certificate, Play account) (report).
9. Compatibility between app and server¶
- The app reports its protocol version and platform version in
identify(04-sync-protocol.md 6.3). The server supports the protocol versions fromhello.protocol_versions. It closes older ones with4007 protocol_version_unsupported; the app then shows the notice "Update required" instead of an error. instance.discovery.readreturnsclient.min_version.<platform>andclient.recommended_version.<platform>per platform. Below the minimum version the app does not connect and shows the update route of its channel. Below the recommended version it shows a notice.- How long a protocol or API version stays supported after it is superseded is defined by the deprecation policy (ADR 0014, team B). Release notes name the date.
- The server is the leading side: a server upgrade never sets the minimum version higher than the oldest app that is still supported according to the deprecation policy.
- Adapters report their version in the heartbeat in
details.adapter.version(stored inagents.reported_details);reported_runtime_version(heartbeat memberruntime_version) is the version of the runtime (09-agents.md section 4.8). The admin console shows adapters whose SDK version is no longer supported.
9.1 Host service and server¶
The host service reports its version in identify of the control connection. The server accepts
the versions whose protocol version it supports (prologue bothy-terminal/1 and
x-protocol-versions of the channel in asyncapi.yaml); it closes a version
that is too old with 4114, and the console and the host card show "Update host service". The host
service does not update itself. Reason: a program that replaces itself would need
write permissions on its own binary; the service user deliberately does not have them
(24-terminal.md, section 2.2). Updates run through the package route of the
instance operator.
10. Rollback¶
| Artifact | Route | Limit |
|---|---|---|
| Server | start the previous version (BOTHY_VERSION, upgrade.md 3) |
exactly one version back (expand/contract, 16-operations.md 10); further back only via a restore |
| iOS | let the build expire in TestFlight, distribute a corrected build with a higher number | no downgrade on devices |
| macOS | new version with a higher number in the appcast; mark it as a critical update in case of danger | Sparkle does not install an older version |
| Windows | new MSIX with a higher version in the .appinstaller file |
no automatic downgrade |
| Android | new build with a higher versionCode |
no downgrade |
| SDKs | withdraw the version (PyPI yank, npm deprecate), publish a fix | installed copies remain |
Apps are therefore never rolled back but corrected forward. That is why app builds go into beta first and then, with the same build number, into stable.
11. Security releases¶
- Reports come in through the route described in
SECURITY.md(team H). - Fix on a private branch, release with a Security section, notice to instance operators before
the public description. The deadline until publication is in
SECURITY.md. - The instance operator raises affected minimum versions of apps with
client.min_version.<platform>.
12. Configuration values of this chapter¶
| Name | Default | Meaning |
|---|---|---|
client.min_version.ios |
empty | smallest allowed app version on iOS; empty means no limit |
client.min_version.android |
empty | the same for Android |
client.min_version.macos |
empty | the same for macOS |
client.min_version.windows |
empty | the same for Windows |
client.recommended_version.ios |
empty | below it the app shows an update notice |
client.recommended_version.android |
empty | the same for Android |
client.recommended_version.macos |
empty | the same for macOS |
client.recommended_version.windows |
empty | the same for Windows |
13. Decisions of this chapter¶
| No. | Decision | Reason |
|---|---|---|
| R-RL-01 | Version and changelog per component, tags <component>/vX.Y.Z (product bundle vX.Y.Z). |
Independent releases; one source for names and prefixes in tools/ci/components.py; the Go tag rule does not apply to the main module that is never loaded (P-05). |
| R-RL-02 | No tag latest; Compose and OpenTofu pin versions. |
Repeatable installation; an upgrade is a deliberate change. |
| R-RL-03 | Apps: the same build moves from beta to stable. Server, SDKs, adapters: stable from the same commit as the last beta, only the version number changes. | What was tested is what ships; where the version is in the artifact, a rebuild is unavoidable and limited to the number. |
| R-RL-04 | All artifacts signed with an SBOM: in the public state cosign keyless with attestation, in the private state with a key pair without a transparency log (ADR 0062). | Verifiable provenance without key management; attestation depends on the visibility. |
| R-RL-05 | Apps are white label; every organization builds and signs them itself with distribution.yaml. |
Push keys, bundle IDs and update feeds belong to the publisher; no central push forwarding needed. |
| R-RL-06 | macOS only as a Developer ID edition with Sparkle, no second TestFlight edition. | One edition, no forced sandbox, own update channel. |
| R-RL-07 | Apps are corrected forward, never rolled back. | No platform reliably installs older versions. |
| R-RL-08 | Minimum and recommended app version per platform as server configuration. | The instance operator controls security updates without a release of the server. |
| R-RL-09 | The product bundle pins the server version in the tagged commit (BOTHY_VERSION in infra/compose/.env.example, written by make release-prep COMPONENT=bothy SERVER_VERSION=<version>, which rejects a server pre-release without CHANNEL=beta; gate release-pins in the pull request before the tag); make release-build sets BOTHY_REGISTRY from the owner and the download address in install.sh from the name of the repository and aborts on placeholders, on a server pre-release in the stable product and on missing images (2.1). |
install.sh takes over both values unchanged; without a producer, every installation would pull the placeholder image. Version in the commit so that the tag alone determines the bundle, checked before the tag because a pushed tag stays; registry and download address from the build because they depend on the location of the repository. |