Skip to main content

Release policy

AiSOC ships often. This page is the contract that makes that safe to live with: what a version number means, which tags move on their own, how long a release keeps getting security fixes, and how much warning you get before something you depend on goes away.

It is enforced, not merely stated. scripts/check_release_policy.py runs on every pull request and again at the moment a tag is pushed, and a release whose version and notes disagree does not publish.

A major version means an operator must act​

The version number answers one question: do I have to do something before upgrading?

ChangeVersion
You must change configuration, run a command, or accept behaviour you cannot opt out ofmajor
New capability, new endpoint, new connector, anything additiveminor
A fix that changes nothing you were relying onpatch

That is a narrower rule than "big release". A major is a bill for your time, so a release that costs you nothing does not get one however large it is.

Concretely, these are majors: a removed or renamed API field, a removed endpoint, a configuration key that no longer works, a default that flips in a way you cannot turn back, a migration you must run out of band, a minimum version of PostgreSQL or Kubernetes going up.

These are not: a new endpoint, a new optional setting, a new connector, a default that changes only for deployments that never set it, a performance change, or anything behind a feature flag that ships off.

Both directions are enforced​

A major bump requires a ### BREAKING section in that version's changelog entry, and a ### BREAKING section requires a major bump.

The second half matters at least as much as the first. Semantic versioning is the only signal most automated upgrade tooling reads, and a minor is the version people let a bot merge unattended. A break announced in a note that nobody opens, on a version everything treats as safe, arrives through an unreviewed dependency update.

A major with no breaking notes is the mirror image: a version number that tells an operator to read upgrade instructions that do not exist, so they upgrade assuming the major was cosmetic.

The rule applies from 10.0.0 onward, which is the first major that carried a breaking section. Eight earlier majors do not, and the gate prints them as exempt on every run rather than quietly skipping them. Published history is not rewritten.

python3 scripts/check_release_policy.py # the whole changelog
python3 scripts/check_release_policy.py --tag v12.0.0

Tags and channels​

Container images​

Every release publishes ghcr.io/beenuar/aisoc-<service>:vX.Y.Z, and moves two channel tags.

TagMovesUse it when
vX.Y.ZNeverAlways, in production. Pin it.
latestEvery release, majors includedYou want the newest thing and will read the notes
stableMinors and patches automatically; a major only when promoted deliberatelyYou pull by tag and do not want to be moved across a break without deciding to
demo / vX.Y.Z-demoDemo console onlyNever in a real deployment. Demo mode is baked in at build time

stable exists because latest cannot be both "newest" and "safe to pull unattended". A major stops stable where it is until a maintainer dispatches the release workflow with promote_stable: true.

The rule is about a sequence, not a release. Refusing vX.0.0 is not enough on its own: v16.0.0 correctly declines the tag and v16.1.0 is a minor, so it took it the next day and the channel crossed the major with nobody deciding to. That is how stable reached v12, v13 and v15. The workflow now resolves which major the channel is on -- from the registry, because a tag is a registry fact -- and moves it only within that major.

tests/test_release_channel_tags.py lifts the tag-selection block out of release.yml and runs it under bash rather than describing it a second time, and it replays whole release ladders, because every question asked one release at a time had the right answer while the sequence did not.

Neither channel tag is ever applied by a repair run. Re-publishing the images for an older tag moves nothing, because handing a self-hoster older content on the tag they pull by default is the defect this arrangement exists to prevent.

The Helm chart​

The chart is published to an OCI registry on every release:

helm show chart oci://ghcr.io/beenuar/charts/aisoc --version '7.x'
helm install aisoc oci://ghcr.io/beenuar/charts/aisoc --version <chart-version> -n aisoc --create-namespace
--version stable does not work, and never did

This page used to say --version stable. Helm's --version takes a semver constraint, not a tag, so it rejects a channel name client-side:

Error: improper constraint: stable

That is a helm limitation rather than a missing tag — latest is refused the same way, while 7.x and ^7.4.0 both resolve. The images' stable tag works because Docker addresses any tag; helm does not.

Use a constraint instead. --version '7.x' gives you exactly what the channel was meant to give you: the newest chart within major 7, and nothing from major 8 until you choose it. Quote it, or your shell will glob.

The chart follows the chart's major rather than the application's — the break it protects an operator from is a values-schema break, and the chart versions independently of appVersion. A chart stable tag is published for OCI tooling that can address tags directly (docker buildx imagetools inspect, oras), and a chart major stops its promotion until a maintainer dispatches with promote_stable: true, exactly as for images.

Pin an explicit version in production; a constraint is for the case where you would otherwise have pinned nothing at all.

The chart's version and its appVersion move independently. appVersion names the images an unpinned install pulls; version is the chart's own. A chart change that does not touch the services does not force a service release, and the reverse.

The release job packages and lints the chart on every run and pushes only on a tag, so the chart cannot quietly stop being publishable between releases. It also re-checks that appVersion names images that exist, because a chart whose default tag resolves to nothing installs straight into ImagePullBackOff.

Security fixes: what gets one, and for how long​

Security fixes land on the current minor and are backported to the previous minor for 90 days after it is superseded. Older lines get nothing.

In practice that is a rolling window of two minors. Given the release cadence, staying inside it means upgrading a minor within about a quarter.

Two things this is not. It is not a paid support commitment, and nothing here creates one: AiSOC is MIT-licensed software maintained by its community, and the same MIT warranty disclaimer applies to this page as to the code. And it is not a statement about compliance certification of any kind.

Report a vulnerability through GitHub's private vulnerability reporting on the repository, not as a public issue. SECURITY.md has the detail.

Deprecation: one minor of warning, minimum​

Nothing is removed without a release in which it still works and says it is going.

  1. Minor N ships the deprecation. The old thing keeps working. It warns at the point of use: a response header, a log line at warning, a console notice, or a DeprecationWarning, whichever a user of that surface would actually see. The changelog entry names the replacement.
  2. Minor N+1 at the earliest, or any later release, removes it. Removal is a ### BREAKING entry and therefore a major, by the rule above.

So the minimum notice is one minor and the removal always costs a major. A deprecation that a user cannot discover by running the software does not count as warning them: "it was in the changelog" is not a deprecation path.

Security is the one exception. A control that has to be removed or tightened because leaving it is unsafe ships immediately, with the reason stated.

What a release actually publishes​

A release is the set of artefacts it published, not the set of jobs that went green. That distinction is load-bearing here: a release once reported success having built thirty-two images and published none of them, because a skipped upstream job propagates the whole way down a needs chain.

So release.yml ends by asking the registry rather than itself (scripts/check_published_images.py --require-fresh --require-network), and the release is not considered done until every reference in docker-compose.yml, the chart and the tracked documentation resolves to an image that exists and carries the version its tag names.

ArtefactPublished
16 service images, linux/amd64 + linux/arm64Every release
Cosign signatures and SBOM attestationsEvery release
Helm chart, to oci://ghcr.io/beenuar/charts/aisocEvery release
Source tarball and SPDX SBOM on the GitHub ReleaseEvery release
npm and PyPI packagesBuilt and validated every release; uploaded only once the registries are configured. See docs/operations/publishing.md

Upgrading​

Every pull request that touches a migration runs .github/workflows/upgrade-test.yml: the previous minor's published image applies its own migration chain, data is seeded, and then the chain from the pull request is applied over the populated schema. It asserts the rows that were there are still there by id, that the runtime role can still read them, and that re-applying the chain is a no-op.

A fresh install applies every migration to an empty schema, which is the one case that cannot go wrong. The failures that reach operators need data to be present, so the test carries some.

Operational steps for an upgrade are in Upgrades; the Kubernetes specifics are in Kubernetes.