Tag a commit with a v-prefixed SemVer 2.0 string to trigger release.yml:
git tag v1.2.3 && git push origin v1.2.3- Tags containing
-are drafts (and push the sdist to TestPyPI). - Bare
vX.Y.Ztags publish immediately (and push the sdist to production PyPI). - Assets:
geniex-sdk-{linux,windows}-arm64-<tag>.zip,geniex-cli-linux-arm64-<tag>.tar.gz,geniex-cli-setup-windows-arm64-<tag>.exe,geniex-android-aar-<tag>.aar,geniex-bench-{linux,windows,android}-arm64-<tag>.{tar.gz,zip},geniex-pysdist{,-llama_cpp,-qairt}-<tag>.tar.gz, and per-file.sha256sidecars. - The CPU-only variant (#1217) adds a
-cpuinfix on the Linux and Android assets:geniex-{sdk,cli}-linux-arm64-cpu-<tag>.*andgeniex-android-aar-cpu-<tag>.aar. Docker mirrors it as the-cputag; Maven Central publishes only the default AAR. Nogeniex-bencharchive: on Linux the SDK zip already shipsbin/geniex-bench, and no device in the benchmark fleet needs a CPU-only build. - Re-run a tag via Actions → Release → Run workflow: set Use workflow from to the tag (Tags tab) and pass the same tag as
release_tag.resolve-tagrejects a dispatch whose commit differs from the tag's, and refuses a stable tag whose release already has assets. Prereleases can be re-run freely; a shipped stable tag can't, so cut a higher one. - S3 mirror at
s3://qaihub-public-assets/qai-hub-geniex/— see § S3 mirror & manifest below.
For the developer/user side of HTP signing (cert import, test-signing), see run.md § Self-signed fallback.
Tags follow SemVer 2.0 with a v prefix: vX.Y.Z for stable, vX.Y.Z-<channel>.<n> for pre-releases.
| Bump | Meaning | Example triggers |
|---|---|---|
MAJOR (X) |
Breaking change to any public surface. Consumers must adapt. | CLI flag removed/renamed, SDK header signature changed, Python API removed, config key renamed. |
MINOR (Y) |
Backwards-compatible feature addition. | New runtime, new model support, new CLI subcommand, new SDK function. |
PATCH (Z) |
Backwards-compatible fix or cleanup. | Bug fix, dependency bump, doc/CI-only change, internal refactor. |
Pre-1.0 rule. The project is still pre-1.0 (X = 0). Do not bump MAJOR while private/unreleased — keep X = 0. Breaking changes bump MINOR (0.Y → 0.(Y+1), resetting Z to 0) and must be flagged in the release notes. The project graduates to X = 1 only on first public release.
Pre-release channels communicate how ready a build is. Ordering: alpha < beta < rc < stable.
| Channel | Purpose | Allowed branch | Still allowed to change |
|---|---|---|---|
alpha.n |
Share in-progress builds; feature shape may still move. | feature branch or main | Anything, including breaking. |
beta.n |
Feature-complete for the target X.Y.Z; seeking feedback. |
main |
Bug fixes and polish only. |
rc.n |
Release candidate — "will ship unless we find a bug". | main |
Bug fixes only. |
| stable | Published release. | main |
Nothing — cut a new bump. |
Rules:
- Always pass through at least one
-rc.nonmainbefore stable. - A stable tag must be on a commit whose HTP bundle is Microsoft-signed, not self-signed. Check the last rc: its release body's "Hexagon HTP" section says which, and a self-signed release carries
ggml-htp-v1.cerand a-selfsignedSDK zip (gh release view <rc> --json assets). If it's self-signed, finish Hexagon HTP signing first. - Never re-use or move a published tag. Retract via a new patch.
alpha.ntags on feature branches are disposable — don't rebase the tagged commit afterward.
This is the algorithm /release follows. Apply in order.
-
Find the latest stable tag
v0.A.Bafter fetching (stale local tags give wrong proposals):git fetch --tags --prune origin git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1
Don't use
git describe— it only looks at HEAD's ancestor chain and will miss stable tags cut on side branches. If the command prints nothing, targetv0.1.0and skip to step 3. -
Pick the target
X.Y.Zfromgit log v0.A.B..HEAD --no-merges --format="%h %s" --stat(subject + touched files in one pass; rungit show <sha>only if that's still ambiguous):- any commit with a breaking change →
v0.(A+1).0(whileX = 0, breaking bumps MINOR); - else any feature commit →
v0.(A+1).0; - else →
v0.A.(B+1).
Read both subjects and diffs when the subject is ambiguous — Conventional-Commits prefixes (
feat:,fix:,feat!:) are a hint, not a contract; the repo does not enforce them. - any commit with a breaking change →
-
Pick the channel by cycle position for that target:
- first tag toward a new target, on a feature branch →
alpha.1; - first tag toward a new target, on
main→rc.1(skip beta unless explicitly requested; most cycles go straight to rc); - already in cycle → advance within the channel (
-rc.1→-rc.2) or move forward one channel (-beta.3→-rc.1, resettingn). Channels only move forward on a givenX.Y.Z; - all
-rc.ngreen + HTP Microsoft-signed → cut barevX.Y.Z.
- first tag toward a new target, on a feature branch →
-
If a breaking change lands mid-cycle and raises the target: abandon the current
X.Y.Z(leave existing tags in place — don't retract) and restart atv0.(new)-alpha.1or-rc.1. -
Counter
nresets perX.Y.Zand per channel:0.4.0-alpha.{1,2}→0.4.0-beta.{1}→0.4.0-rc.{1,2}→0.4.0.
- Latest stable
v0.3.2.git log v0.3.2..HEADhas onefix:and onedocs:. Onmain, first tag →v0.3.3-rc.1, thenv0.3.3. - Latest stable
v0.3.2. Log contains afeat:adding a new runtime. On a feature branch →v0.4.0-alpha.1; after merge tomain→v0.4.0-rc.1→v0.4.0. - On
v0.4.0-rc.2, a CLI flag rename (breaking) lands. Target rises from0.4.0to0.5.0. Leave-rc.2alone; next tag isv0.5.0-alpha.1orv0.5.0-rc.1depending on the branch.
A subset of every release is mirrored to s3://qaihub-public-assets/qai-hub-geniex/ (anonymous-GET via https://qaihub-public-assets.s3.us-west-2.amazonaws.com/qai-hub-geniex/...) so that apps and installers can fetch artifacts without GitHub-API rate limits.
All objects live directly under the prefix — no <tag>/ subdirectories. The <tag> in each filename disambiguates versions.
| Object | Per tag? | Cache | Purpose |
|---|---|---|---|
geniex-sdk-windows-arm64-<tag>.zip(.sha256) |
every tag | default | Windows SDK |
geniex-sdk-linux-arm64-<tag>.zip(.sha256) |
every tag | default | Linux SDK |
geniex-sdk-linux-arm64-cpu-<tag>.zip(.sha256) |
every tag | default | CPU-only Linux SDK, fetched by GENIEX_SDK_VARIANT=cpu |
geniex-cli-setup-windows-arm64-<tag>.exe(.sha256) |
every tag | default | Windows CLI installer (versioned) |
geniex-cli-linux-arm64-<tag>.tar.gz(.sha256) |
every tag | default | Linux CLI archive (versioned) |
geniex-cli-linux-arm64-cpu-<tag>.tar.gz(.sha256) |
every tag | default | CPU-only Linux CLI archive (versioned) |
install-<tag>.sh(.sha256) |
every tag | default | Linux install script (versioned, pinned via --version) |
geniex-cli.exe |
stable only | no-cache |
Mutable pointer to latest stable Windows installer |
geniex-cli-linux-arm64.tar.gz(.sha256) |
stable only | no-cache |
Mutable pointer consumed by install.sh |
geniex-cli-linux-arm64-cpu.tar.gz(.sha256) |
stable only | no-cache |
Mutable pointer consumed by install.sh --cpu-only |
install.sh |
stable only | no-cache |
Mutable install script — curl ... | sh entrypoint |
manifest-<tag>.json |
every tag | immutable |
Per-tag asset listing |
index.json |
every tag | no-cache |
Full version catalogue |
latest.json |
stable only | no-cache |
Pointer to the latest stable manifest |
windows-signed.txt |
stable only | no-cache |
Code-signing gate for the latest Windows installer — see § Windows installer signing gate |
Other assets (AAR, sdist, HTP cert/to-sign zips) ship via GitHub Releases / Maven Central / PyPI (stable) / TestPyPI (prerelease) only — not via S3.
S3 publishing runs in the geniex repo (the IAM role's OIDC trust only allows qcom-ai-hub/geniex); this repo's publish-s3 job just dispatches it. The schema lives in release_s3_manifest.py on geniex's chore/publish-s3 branch; all files carry schema_version: 1.
-
Update check (lightest) — GET
latest.json. Comparetagwith the locally installed version. Cachedno-cache, so the response is always current. Only present once a stable tag has shipped. -
Version picker (history / rollback) — GET
index.json:{ "schema_version": 1, "updated_at": "2026-05-19T08:23:11Z", "latest_stable": "v0.1.5", "latest_prerelease": "v0.1.6-rc.2", "versions": [ { "tag": "v0.1.6-rc.2", "is_prerelease": true, "released_at": "...", "manifest": "manifest-v0.1.6-rc.2.json" }, { "tag": "v0.1.5", "is_prerelease": false, "released_at": "...", "manifest": "manifest-v0.1.5.json" } ] } -
Asset download — GET the per-tag manifest referenced by
versions[].manifest:{ "schema_version": 1, "tag": "v0.1.5", "is_prerelease": false, "released_at": "...", "llama_sha": "abc123", "htp_signed": true, "assets": [ { "name": "geniex-sdk-windows-arm64-v0.1.5.zip", "url": "https://qaihub-public-assets.s3.us-west-2.amazonaws.com/qai-hub-geniex/geniex-sdk-windows-arm64-v0.1.5.zip", "size": 123456789, "sha256": "...", "kind": "sdk", "platform": "windows", "arch": "arm64" } ] }assets[].urlalways points at the versioned object (never at the mutablegeniex-cli.exe/geniex-cli-linux-arm64.tar.gz/install.sh), so the referenced bytes are immutable and the listedsha256is authoritative.kindis one ofsdk/cli-installer/cli-archive/install-script/sha256.
The per-tag manifest is byte-stable across workflow re-runs of the same tag — released_at is preserved from the first publish, so clients can cache it forever.
The CPU-only objects are on S3 but deliberately absent from the manifest: their metadata would be identical to the default build's, so a kind/platform/arch lookup could hand a normal Snapdragon device the slow artifact. Nothing discovers them that way anyway — install.sh --cpu-only and GENIEX_SDK_VARIANT=cpu both build the URL from the naming convention.
The Windows ARM64 SDK ships libggml-htp.cat plus libggml-htp-v{73,75,79,81,85}.so — Windows refuses to load them unsigned. Release CI runs an overlay-htp job before build-cli that sparse-checks-out sdk/signed-htp/libggml-htp-<sha>.zip from the chore/signed-htp-lfs-store branch of qcom-ai-hub/geniex (LFS-tracked), where <sha> is the first 6 characters of the third-party/llama.cpp commit. Both the installer and the SDK zip end up with the same HTP files:
- Hit — overlay the Microsoft-signed files into the SDK artifact;
build-clipackages them into the installer; release normally. - Miss — keep the self-signed build. The SDK name gets a
-selfsignedsuffix, and the release also carriesggml-htp-v1.cer(users import it) andlibggml-htp-to-sign-<sha>.zip(operators submit it for signing).
The signed bundle must contain exactly these six files at the zip root: libggml-htp.cat and libggml-htp-v{73,75,79,81,85}.so — no .inf.
The cross-repo checkout reuses secrets.GH_PAT (already scoped for cross-repo access to qcom-ai-hub/geniex — see publish-s3 below). If CI reports signed=false but the bundle is on chore/signed-htp-lfs-store, first check that GH_PAT has not expired.
- Download
libggml-htp-to-sign-<sha>.zipfrom the draft release. - Get it Microsoft (attestation) signed through the internal signing service. The steps are in the team's internal runbook, not in this public repo.
- Repack the signed
libggml-htp.catand.sofiles (no.inf) at the zip root, and commit it toqcom-ai-hub/geniexatsdk/signed-htp/libggml-htp-<sha>.zipon thechore/signed-htp-lfs-storebranch (LFS). - Re-run the Release workflow for the same prerelease tag (see the re-run rule above).
The Windows installer is published before it is Authenticode-signed, so geniex update gates on windows-signed.txt to avoid pushing an unsigned build. On Windows, once a newer version is found, the updater GETs this file (see cli/cmd/geniex/update.go, isWindowsSigned): contents true → proceed with the download; anything else or missing → treat as up-to-date and skip.
Release-side: upload windows-signed.txt with true after the latest stable installer is signed; set it back to a non-true value before publishing the next, not-yet-signed installer.
-rc.nonmain, HTP Microsoft-signed (promote first if not).- Smoke-test the rc on CPU / GPU / NPU with one small GGUF and one AI Hub qairt model.
- Cut bare
vX.Y.Zon the same commit. - Manual follow-ups the workflow doesn't do: click Publish for the Maven Central deployment, get the Windows installer Authenticode-signed, replace it on S3, then set
windows-signed.txttotrue.