This document describes how to cut and publish an OHM release. For the 0.8.0 planning notes, see notes/stable-release-0.8.0-plan.md (local, gitignored).
The canonical version lives in pyproject.toml. For the current release number
and its notes, see README.md and CHANGELOG.md;
this document describes the process, not a specific version.
| Item | Value |
|---|---|
| Docker image | touchthesun/openhardwaremanager |
| Image tags | X.Y.Z (exact), X.Y (floating minor), latest |
| Platforms | linux/amd64, linux/arm64 (multi-arch manifest via docker buildx) |
The :0.8 tag below floats to the latest 0.8.x patch, so these commands never
go stale. Pin an exact :X.Y.Z tag for reproducibility.
docker pull touchthesun/openhardwaremanager:0.8
docker run -p 8001:8001 \
-e STORAGE_PROVIDER=local \
-e LLM_ENABLED=false \
touchthesun/openhardwaremanager:0.8
curl -s http://localhost:8001/healthFederation is off by default. To enable it, set OHM_FEDERATION_ENABLED=true and see federation infrastructure.
| Context | Format | Example |
|---|---|---|
| Python package | semver | 0.8.0 |
| Git tag | v + semver |
v0.8.0 |
| Docker image tag | semver (no v) |
0.8.0, 0.8, latest |
| Health API | semver | "version": "0.8.0" |
- Update version in
pyproject.toml(or runuv run python scripts/bump_version.py X.Y.Z). - Run
uv lockanduv sync --extra dev. - Confirm
uv run ohm versionand local/healthreport the new version. - Fold pending entries into
CHANGELOG.md:uv run python scripts/render_changelog.py --consolidate(readschangelog.d/*.md, so no PR hand-edited[Unreleased]directly — seechangelog.d/README.md). Then rename## [Unreleased]to## [X.Y.Z] - YYYY-MM-DDand re-baseline:uv run python scripts/render_changelog.py --refreeze. - Run
make match-harness(offline golden matching). OptionallyMOM_LIVE=1 make match-harnessfor live MoM SPARQL smoke — seetests/matching/. - Merge to
mainand wait for CI to pass (quality, test, contract-stability, security, docker-build-test). - Create git tag
vX.Y.Zon the release commit. - Push the tag — the Release workflow validates the tag, runs tests, builds from
uv.lock, and pushes a multi-arch manifest (linux/amd64,linux/arm64) to Docker Hub. - Push the git tag — the Release workflow creates the GitHub Release automatically after Docker publish (notes from
CHANGELOG.mdviascripts/extract_changelog_section.py). - Smoke-test the pulled image (see below).
- Deploy, deliberately. Publishing is not deploying — see below.
A tag does not deploy. Pushing vX.Y.Z builds, publishes the images, and
creates the GitHub Release with the checksummed installer. It stops there.
Deploying is a separate act: run the Release workflow from the Actions tab with
| input | value |
|---|---|
dry_run |
false |
deploy |
true |
which rolls the worker, then the API, then the frontend.
It used to be. deploy-worker, deploy-azure and deploy-frontend-azure all
ran on github.event_name == 'push', so tagging went to production with
nothing in between — while a manual run already had to ask for deploy: true.
Tags were the unguarded path, which is the wrong way round: a tag is a routine
act and a deploy is not.
The GitHub-native form of this rule is required reviewers on the production
environment, which produces an approval prompt in the Actions UI. That needs
repo admin rights to configure, so it is not set. Anyone with admin can add
it:
gh api -X PUT repos/helpfulengineering/supply-graph-ai/environments/production \
-f 'reviewers[][type]=User' -F 'reviewers[][id]=<numeric-user-id>'Until then, the workflow condition is the mechanism, and it is the thing to
preserve: no deploy job may key on github.event_name == 'push'.
| Artifact | What it is | How you create it |
|---|---|---|
Git tag (v0.8.0) |
A pointer to a commit in git | git tag v0.8.0 && git push origin v0.8.0 |
| GitHub Release | A project page: title, notes, optional assets, zip/tarball source downloads | Created by the github-release job in .github/workflows/release.yml, or manually in the UI |
Pushing a tag starts the Release workflow and publishes Docker images, but GitHub does not list a release until the github-release job runs (or you create one manually). That is why Docker Hub can show 0.8.0 while Releases is empty.
workflow_dispatch with dry_run: false runs the full publish path: Docker images, GitHub Release (v + pyproject version), and Azure deploys (subject to the production environment approval rules). dry_run: true (default) stops after validate/test/smoke.
If v0.8.0 exists but no GitHub Release was created:
uv run python scripts/extract_changelog_section.py 0.8.0 -o /tmp/notes.md
gh release create v0.8.0 --title "Open Hardware Manager 0.8.0" --notes-file /tmp/notes.mdWorkflow file: .github/workflows/release.yml
| Trigger | Behavior |
|---|---|
Push tag v*.*.* |
Validate → test → docker-smoke → publish (API + frontend) → GitHub Release → Azure deploys |
workflow_dispatch |
Same pipeline; dry_run: true (default) skips publish/release/deploy |
| Secret | Purpose |
|---|---|
DOCKERHUB_USERNAME |
Docker Hub account (e.g. touchthesun) |
DOCKERHUB_TOKEN |
Access token with push permission for openhardwaremanager |
Configure under Settings → Secrets and variables → Actions.
The Release workflow only appears in the Actions UI after .github/workflows/release.yml exists on the default branch (main).
uv run python scripts/validate_release_version.py --tag v0.8.0uv sync --extra dev
uv run pytest tests -m "unit and not llm and not quarantine" --maxfail=1
uv run pytest tests -m "contract and not llm and not quarantine" --maxfail=1
uv run pip-audit
docker build --build-arg APP_VERSION=0.8.0 -t openhardwaremanager:0.8.0 .docker pull touchthesun/openhardwaremanager:0.8
docker run -d --name ohm-smoke -p 8001:8001 \
-e STORAGE_PROVIDER=local -e LLM_ENABLED=false \
touchthesun/openhardwaremanager:0.8
curl -sf http://localhost:8001/health
curl -sf http://localhost:8001/health/liveness
# Federation disabled by default:
curl -sf http://localhost:8001/v1/api/federation/identify | head -c 200
docker rm -f ohm-smokeExpect /health to report the version you just published and federation routes to return 404 when federation is disabled.
Release publishes use buildx with platforms: linux/amd64,linux/arm64 so images pull on Intel/AMD servers and Apple Silicon (ARM) Macs.
Smoke tests in CI still run a single amd64 image loaded on the runner (docker-smoke job). The publish job uses QEMU + buildx to push one manifest list for all tags.
To verify locally after publish:
docker buildx imagetools inspect touchthesun/openhardwaremanager:0.8
docker pull touchthesun/openhardwaremanager:0.8 # uses host architectureTo rebuild an already-published tag (e.g. add arm64 to 0.8.0), re-run Release with workflow_dispatch and dry_run: false on main at the release commit (publishes images, creates/updates the GitHub Release, and deploys), or push a new patch tag (v0.8.1).
Pin a known-good image tag instead of latest:
docker pull touchthesun/openhardwaremanager:0.8.0Redeploy using that tag. To republish an older version, check out the corresponding git tag and re-run the Release workflow only if intentional (overwrites moving tags like latest).
- Deploy configs under
deploy/still default toghcr.io/helpfulengineering/supply-graph-ai:latest— migrate separately if cloud deploys should use Docker Hub. - PyPI Beta classifier remains until a true
1.0.0GA release. - Optional GHCR mirror workflow.