<!-- OpenSmartRoute: Versions and channels. https://opensmartroute.ai/docs/RELEASES -->
# Versions, channels and releases

One version number names everything OpenSmartRoute ships - the Python package, the `osr` command line, the
npm client, the platform API and web images, the Helm chart, the Compose bundle, the Azure Marketplace
package, the browser extension and the desktop app - and every deployment tells you which build it runs. This
page says what the numbers mean, how to read them on each surface, and how a release is made.

## One version

The release version is `X.Y.Z` ([Semantic Versioning](https://semver.org)). Its single source is
`pyproject.toml`; `scripts/release.py` keeps every mirror in step (the `__version__` literals, the platform
and web packages, the TypeScript SDK, the browser extension, the desktop app, the Helm chart, the image tags in
the deploy documents, the Marketplace template) and `release.py check` fails CI when any of them drifts. There
is no separate extension version, desktop version, SDK version or platform version: `pip install
opensmartroute==1.3.0`, `npm install @opensmartroute/sdk@1.3.0`, `osr-platform-api:1.3.0`, chart `1.3.0`, the
extension `1.3.0` and `OpenSmartRoute-1.3.0-win-x64.exe` were all built from the tag `v1.3.0`.

- A **minor** release (`1.3.0` -> `1.4.0`) adds; the public Python API only grows
  ([CONTRIBUTING.md](https://opensmartroute.ai/docs/contributing.md), *Public API*).
- A **patch** release (`1.3.0` -> `1.3.1`) fixes; nothing new is announced.
- A **major** release may remove what was deprecated two minors earlier.
- `SECURITY.md` lists the supported minors: the current one gets fixes, the one before it security fixes only.

## Two channels

Every build is either a **stable release** or a **developer preview**. The pipelines decide with
`scripts/release.py build-version`, from the git tags alone - no one types a build number:

| Channel | Built from | Version | Where it goes |
|---|---|---|---|
| **stable** | the tag `vX.Y.Z` (the *Release* pipeline) | `X.Y.Z` | PyPI, npm, the image tags `X.Y.Z` / `X.Y` / `latest`, the release downloads, `extension/latest.json`, `desktop/latest.json` + `latest*.yml`, the browser stores |
| **developer preview** | any other commit on `main` | `<next>-preview.<N>` | the hosted platform (opensmartroute.ai), `extension/preview.json`, `desktop/preview.json` + `preview*.yml` |

`<next>` is the release the preview leads up to: the pyproject version while its tag does not exist yet
(a merged release pull request, about to be tagged), otherwise the next patch after the last release.
`N` counts the commits since the last release tag, so previews sort and read in order:
`1.3.0` -> `1.3.1-preview.1` ... `1.3.1-preview.89` -> `1.4.0-preview.3` (release PR merged) -> `1.4.0`.

The hosted platform deploys every merge to `main`, so what you use at opensmartroute.ai is always the developer
preview of the next release - the `[Unreleased]` section of the [changelog](https://opensmartroute.ai/docs/changelog.md) is what it carries
beyond the last stable release. Self-hosted installations pull stable images and stay on releases.

Browser manifests take up to four integers, so a preview extension carries `X.Y.Z.N` (`1.3.1.89`) in
`manifest.json`; Chrome and Edge show the readable label (`1.3.1-preview.89`) as `version_name`. A release
carries `X.Y.Z`. Previews are never submitted to the browser stores. The desktop installers carry the label in
their file names (`OpenSmartRoute-1.3.1-preview.89-win-x64.exe`) and a preview build's updater follows the
`preview*.yml` feed, so an installed release only ever updates to the next release.

## Reading the version

| Surface | What it shows |
|---|---|
| `osr --version`, `opensmartroute.__version__` | the package release, `1.3.0` |
| `GET /api/v1/info` -> `build`, `GET /healthz` -> `build` | `{"version": "1.3.1-preview.89", "release": "1.3.0", "channel": "preview", "commit": "4ced2b2..."}` on a pipeline-built image; `channel` is `null` for a checkout or a hand-built image, which report the release alone |
| the website footer | `v<build version>`; a **developer preview** badge when the deployment runs `main` ahead of the next release |
| `GET /api/v1/releases` | every release's notes, the running `build`, and `unreleased` - what has landed since the last release (`GET /api/v1/releases/{version}` with `unreleased` as the version returns that section alone) |
| `GET /extension/release.json` | the extension build offered for download with its `channel`; `?channel=preview` for the preview, `?channel=stable` for the release only |
| `GET /desktop/release.json` | the same for the desktop app's installers |
| the dashboard's *What's new* card and the release mail | the stable release notes, sent once per release version - previews never announce |
| image labels | `org.opencontainers.image.version` and `.revision` on `osr-platform-api` and `osr-platform-web` (`docker inspect`) |

The build stamp reaches the images as build arguments (`OSR_BUILD_VERSION`, `OSR_BUILD_CHANNEL`,
`OSR_BUILD_COMMIT` in `azure.yaml` and the Release pipeline) and the API as the settings
`OSR_PLATFORM_BUILD_VERSION`, `OSR_PLATFORM_BUILD_CHANNEL` and `OSR_PLATFORM_BUILD_COMMIT`. Anyone building the
image by hand can pass the same arguments (`docker build --build-arg OSR_BUILD_VERSION=... `); without them the
image is honest about not knowing its channel.

## Downloads

[opensmartroute.ai/downloads](https://opensmartroute.ai/downloads) always resolves to the current **stable**
release: `/download/<artifact>` redirects to the versioned file and `/download/latest.json` lists them with
SHA-256 checksums. Versioned files never change once published (cached for a year); the `latest.json`
pointers move with each release.

The browser extension follows the same rule with two pointers: `/extension/download/<browser>` offers the
stable release and `/extension/download/<browser>?channel=preview` the developer preview. While no stable
release of the extension has been published yet, the preview stands in and the page says so
([EXTENSION.md](https://opensmartroute.ai/docs/EXTENSION.md), *Where the packages come from*). The desktop app is served the same way from
`/desktop` (`/desktop/release.json?channel=preview`, installers and update feeds under `/desktop/download/`).

## Unreleased changes

Every user-visible change lands as a bullet under `## [Unreleased]` in `CHANGELOG.md` the moment it merges -
that section is the developer preview's release notes and the draft of the next release's. The website
renders it on the changelog page, `GET /api/v1/releases` returns it as `unreleased`, and `release.py prepare`
turns it into the dated `## [X.Y.Z]` section when the release is cut. Nothing is announced to users until then:
the release mail and the *What's new* card key off the stable release version.

## How a release is made

Releases are cut by two Azure Pipelines from `main`; nobody edits a version or a tag by hand
([CONTRIBUTING.md](https://opensmartroute.ai/docs/contributing.md), *Releasing*, has the maintainer detail).

1. **Prepare release** (`azure-pipelines/release-prepare.yml`, run by hand with `bump` = `major` / `minor` /
   `patch` or an explicit version): `release.py prepare` bumps the version everywhere, rolls `[Unreleased]`
   into `[X.Y.Z] - date`, refreshes the compare links and the supported-versions table, regenerates the OpenAPI
   snapshot, and opens the pull request `release/vX.Y.Z`. From the moment that pull request merges, `main`
   builds are `X.Y.Z-preview.N` - release candidates of X.Y.Z.
2. **Release** (`azure-pipelines/release.yml`, run by hand on `main` after the merge): verifies
   `release.py check`, runs the test suite and the routing gates, builds the sdist and wheel, creates the
   annotated tag `vX.Y.Z` (its message is the changelog section), publishes to PyPI and npm, builds the
   `opensmartroute`, `osr-platform-api` and `osr-platform-web` images stamped `stable`, packages the Compose
   bundle, Helm chart and Marketplace zip into the release downloads, queues the *Extension* and *Desktop*
   pipelines on the tag (stable packages behind `latest.json`, store submissions), then fetches everything back
   the way a user would - the PyPI files must match the built checksums, the image must report the version and
   answer `/readyz`.
3. **Announce.** The next deploy of the hosted platform runs the new version; its release-news job mails the
   digest of the changelog section to opted-in users once and the dashboard shows the *What's new* card
   ([PLATFORM.md](https://opensmartroute.ai/docs/PLATFORM.md), *Campaigns*).

A red verification stage means the release is not usable as published: fix forward with a patch release,
never by moving a tag.

## Maintainer commands

```bash
python scripts/release.py version                 # 1.3.0
python scripts/release.py check                   # every mirror agrees, the changelog has the section
python scripts/release.py build-version --json    # what a build of this checkout is called
python scripts/release.py notes 1.3.0             # the changelog section (the tag message)
python scripts/release.py prepare --bump minor    # what the Prepare release pipeline runs
```

`build-version` needs the tags (`git fetch --tags`; pipelines check out with `fetchDepth: 0` and
`fetchTags: true`). `--channel stable` or `--channel preview` forces the answer, `--build N` the preview number -
the pipelines use `auto`.
