Versioning & Releases

heliaCORE uses release-please to manage versions. Every merge to main triggers .github/workflows/release.yml:

  1. release-please scans conventional-commit subjects since the last tag, decides whether to open a “Release PR” (bumping the version), or — when that PR is itself merged — to create a tag and a GitHub Release.

  2. publish-staticlibs builds static libraries and SDK tarballs for each Cortex-M target across GCC, ATfE, and armclang, then publishes SDK tarballs plus per-toolchain staticlib bundles with SHA-256 checksums.

  3. publish-ci-image builds and pushes ghcr.io/ambiqai/ns-cmsis-nn-ci:vX.Y.Z so consumers can pin a reproducible build environment.

  4. publish-pack generates Ambiq.NS-CMSIS-NN.<version>.pack, schema-validates it against PACK.xsd, and uploads it.

  5. release-unit-tests and release-helia-core-tester run against the freshly-built artifacts to verify the release actually works before customers see it.

Tag scheme

v<MAJOR>.<MINOR>.<PATCH>
  • MAJOR bumps on feat!: / fix!: / BREAKING CHANGE: footers.

  • MINOR bumps on any feat:.

  • PATCH bumps on fix: and similar.

Other conventional types (docs:, chore:, refactor:, test:, ci:, build:, perf:) do not trigger a release on their own, but their bodies still appear in CHANGELOG.md.

How the version gets stamped

release-please bumps .release-please-manifest.json and CHANGELOG.md, and every other file that hardcodes the release version is listed in release-please-config.json under extra-files. Those all use the generic updater, which rewrites a line only if that same line carries an x-release-please-version (or -major/-minor/-patch) comment. There is no error when a listed file has no such comment: the updater changes nothing and the release proceeds, so the file appears covered and drifts a release behind. Two checks close that gap, from opposite directions:

  • scripts/check_pdsc.py asserts every extra-files path exists, carries a working annotation, and agrees with Include/arm_nn_types.h. It sees everything on the list.

  • scripts/check_stale_version_refs.py reads the previous release out of CHANGELOG.md and asserts no tracked file still mentions it, outside an allowlist of files whose job is to discuss release history (this page and the recovery commands below are on it). It sees the files that are not on the list.

Adding a new file that hardcodes the version means adding it to extra-files and putting the annotation on the value’s own line. Where the value sits somewhere a comment cannot go — a JSON line in a fenced example, say — state the version in prose next to the block, with the annotation on that prose line, and leave the block version-free.

What’s in a release

Every GitHub Release contains:

Asset

Purpose

Ambiq.NS-CMSIS-NN.<version>.pack

CMSIS-Pack

ns-cmsis-nn-<cpu>-<toolchain>-<version>.tar.gz

SDK tarball (CMake users)

ns-cmsis-nn-staticlibs-<toolchain>-<version>.zip

Bare static archives by CPU

*.sha256

SHA-256 of each artifact

<cpu> is one of cortex-m0, cortex-m4, cortex-m55. <toolchain> is one of gcc, atfe, armclang.

Note

Cortex-M0/M0+ naming The release asset name uses cortex-m0 for the baseline ARMv6-M package. Use that artifact for Cortex-M0/M0+ class Apollo targets.

Required vs optional assets

Not every asset is equally load-bearing. If a release is missing something, use this table to know whether that’s a release-blocking bug or an expected gap (see AmbiqAI/ns-cmsis-nn#228):

Asset

Status

Why

vX.Y.Z tag + GitHub Release (source)

Required

Created by release-please; the release exists once this lands.

Ambiq.NS-CMSIS-NN.<version>.pack

Required

Only depends on the gcc static libs.

ns-cmsis-nn-<cpu>-gcc-<version>.tar.gz

Required

Free toolchain, no license dependency.

ns-cmsis-nn-<cpu>-atfe-<version>.tar.gz

Required

Free toolchain (LLVM-Embedded-Toolchain-for-Arm), no license dependency.

ns-cmsis-nn-staticlibs-gcc-<version>.zip

Required

Same as above.

ns-cmsis-nn-staticlibs-atfe-<version>.zip

Required

Same as above.

ns-cmsis-nn-<cpu>-armclang-<version>.tar.gz

Optional

Promoted to required only when the repository variable ARMCLANG_REQUIRED is true. Not keyed on whether a licence is configured.

ns-cmsis-nn-staticlibs-armclang-<version>.zip

Optional

Same as above.

ghcr.io/ambiqai/ns-cmsis-nn-ci:vX.Y.Z image

Required

Needed by release-unit-tests / release-helia-core-tester.

GitHub Pages docs update

Required

Rides along with publish-pack.

Armclang is licensed through Arm’s User-Based Licensing: the ARM_UBL_LICENSE_IDENTIFIER secret is an activation code, redeemed once per release run by armlm activate --code in the publish-staticlibs-armclang job before any compilation happens. That job builds all three Cortex-M targets on a single runner precisely so the licence is activated once and the Arm Compiler archive is downloaded once.

The eight armclang assets are optional unless the repository variable ARMCLANG_REQUIRED is set to true. Set it and release-verify promotes them, failing the release if they are missing and taking the required bar from 17 assets to 25; leave it unset and the bar stays at 17.

The gate is deliberately an operator variable rather than an inference from whether ARM_UBL_LICENSE_IDENTIFIER is configured. That secret was configured throughout the period in which armclang published nothing at all, so keying off it would promote assets this pipeline has never once produced — and a required asset the pipeline cannot produce makes every release red with no way out, because the recovery such a failure prescribes re-runs the same job. Turn ARMCLANG_REQUIRED on only after a release run has actually published the eight, and turn it off to ride out an Arm licensing outage.

Independently of that switch: when no licence is configured, resolve-release-capabilities reports the capability as unavailable, every step of publish-staticlibs-armclang self-skips (the job succeeds as a no-op and self-reports via ::notice::/::warning:: annotations), and the armclang bundle is omitted from publish-staticlib-bundles. Either way the required gcc/atfe assets are unaffected: they come from a different job entirely, and publish-staticlibs-armclang is continue-on-error: true, so armclang can never hold up the tag, pack, gcc/atfe libs, CI image, or tests.

Before v7.30.0 this gate read ARMLMD_LICENSE_FILE, the legacy FlexLM licence-file variable, which has never been configured on this repository — which is why no release from v7.24.1 onward published any armclang asset (see AmbiqAI/ns-cmsis-nn#275).

Nightly published-release audit

release-verify (in release.yml) enforces the table above at release time, but only for a run that actually reaches it — v7.26.0 through v7.29.1 all shipped with zero assets before it existed, and a cancelled run or a later gh release upload --clobber failure (which deletes an asset before replacing it) leaves nothing red behind (AmbiqAI/ns-cmsis-nn#274). The release-assets-audit job in nightly.yml therefore re-checks the published release objects themselves every night, via scripts/ci/audit_release_assets.sh:

  • Every non-draft, non-prerelease release from v7.26.0 (the first cut under the current asset contract) onward must carry the 17 required assets for its version. armclang assets are never audited by the nightly sweep at all – even for releases cut with ARMCLANG_REQUIRED=true – because the variable is point-in-time and the audit keeps no per-tag record of which contract was in force; post-publication loss of armclang assets is covered only by release-verify in the release run itself.

  • Failures land in one rolling issue (Published release(s) missing required assets, label release-audit), which comments on subsequent red nights and closes on the first clean audit — separate from the nightly code-health issue, because the fix is a recovery dispatch, not a code change.

  • To record a deliberate decision not to repair a historical release, add its tag to the repository variable RELEASE_AUDIT_WAIVED_TAGS (space- or comma-separated). Waived gaps are reported but do not fail the audit.

Recovering assets for an existing tag

release.yml also accepts a manual, idempotent recovery run via workflow_dispatch with a recover_tag input (e.g. v7.29.2). This never creates, moves, or re-publishes a tag — it only re-runs the staticlib/bundle/pack jobs against an already-published release and re-uploads (gh release upload --clobber) any assets that are missing or need updating:

gh workflow run release.yml --ref main -f recover_tag=v7.29.2

The run fails fast if recover_tag isn’t already a published GitHub Release, so it can’t be used to sneak out a brand-new tag. See Contributing → Release recovery for the full recovery runbook.

Every recovery run — and every normal release run — resolves recover_tag (or the freshly-cut tag) to its exact target commit once, via the GitHub API. A recovery dispatch’s own triggering ref (whatever branch was selected when running the workflow, typically main) is never used to build release assets; only the commit the tag itself points at is.

Historical recovery restores customer GitHub Release assets only: static-library archives/bundles and the CMSIS-Pack. It deliberately skips publish-ci-image, release-unit-tests, and release-helia-core-tester. The CI image is build infrastructure rather than a release asset, and its tool acquisition is qualified independently of historical asset recovery. Skipping it also makes recovery incapable of publishing an old versioned image or repointing :latest. Normal new releases continue to require the versioned CI image and both container test suites.

Immutable CI tool acquisition

Normal-release CI images install their build and simulation tools from ci/tools/manifest.json. Each entry records an exact version, vendor HTTPS URL, SHA-256 digest, extraction rule, executable probe, environment export, and license classification. scripts/install_ci_tools.sh validates the manifest, verifies every archive before extraction, and emits /opt/ns-cmsis-nn/tool-env.json with the resolved paths and provenance.

This repository-owned manifest replaces the retired vcpkg-artifacts/vcpkg-ce registry. The image build has no package-solver or floating-registry dependency. Armclang remains installed from Arm’s vendor-hosted archive, but licensed execution is capability-gated separately through ARM_UBL_LICENSE_IDENTIFIER. Updating any tool requires a reviewed manifest version, URL, and digest change plus a clean image build and version smoke test.

Recovering a genuinely old tag surfaces two more subtleties, both handled automatically via an explicit two-tree checkout architecture: a trusted/current tree (this repo’s current main, whatever ref/commit triggered the run) supplies recovery tooling — helper scripts and the hardened Dockerfile — while a distinct, immutable/historical tree (pinned to the resolved historical commit) supplies the only source payload that is ever built, packaged, or uploaded. The two trees always use different on-disk paths (never the same checkout directory), so neither can silently overwrite the other:

  • CI-image tooling vs. source. The reusable Docker build workflow checks out the repository twice, into two different directories: an unpinned checkout at _tooling/ (whatever ref/commit triggered the run) purely to obtain scripts/ci/resolve_image_tags.sh — a historical tag can predate that helper script entirely — and a second checkout pinned to the resolved historical commit at _source/ for the actual Docker build context. docker build is then invoked as -f _tooling/.devcontainer/Dockerfile _source: the Dockerfile always comes from the current, hardened tree (so a historical, pre-fix Dockerfile bootstrap bug can never resurface), while the build context is always the immutable historical source (so the built image still reflects exactly what that historical tag shipped). Giving each checkout its own path: is required, not cosmetic: an earlier revision of this recovery path had both checkouts default to the same directory, so the historical, pinned checkout silently clobbered the current tooling checkout’s files — including the Dockerfile — right before the build ran (AmbiqAI/ns-cmsis-nn#228). Normal push/direct-dispatch/schedule runs are unaffected by this split: with no recover_tag, both _tooling and _source resolve to the same github.sha, so the two trees are identical content living at two paths, and the build behaves exactly as it always has.

  • Lightweight vs. annotated tags for gen_pack.sh. Release Please creates lightweight tags, but Open-CMSIS-Pack’s gen-pack (run with PACK_CHANGELOG_MODE=tag) requires an annotated tag with a non-empty message. publish-pack’s only long-lived checkout is pinned directly to the resolved historical commit — that historical tree is the only source gen_pack.sh ever packages. Because scripts/ci/ensure_local_tag_annotation.sh is itself part of the current repository, publish-pack also checks out the current, merged repository into a second, unpinned directory (_tooling/) purely to obtain that helper, then invokes it explicitly against the historical checkout’s working directory (bash _tooling/scripts/ci/ensure_local_tag_annotation.sh "$TAG" "$GITHUB_REPOSITORY" "$GITHUB_WORKSPACE"). The helper — for a lightweight tag only — creates a local-only annotated tag at the exact same commit (message sourced from the existing GitHub Release body, XML-escaped, or a deterministic fallback) so gen_pack.sh succeeds. It never runs git push; the remote/immutable tag is untouched, already-annotated tags are left completely unaltered, and _tooling/ is never read by gen-pack-action or packaged into the .pack output — it exists solely to supply the helper script. The Pack dry-run workflow rehearses this same tag-mode path per PR: it creates a synthetic, local-only annotated tag at HEAD via the same helper, so the changelog path is exercised without cutting a release (AmbiqAI/ns-cmsis-nn#395).

See also

  • Maintainer release notes in Contributing explain how to recover when the pipeline fails.

  • Toolchain identity is recorded in manifest.json inside each tarball. See Toolchain Pinning.