Skip to main content

Publishing

Getting a driver from your machine onto somebody's controller.

There are two ways, and the difference is not quality — it is provenance. Anyone can build, sign off, and ship a Juno driver. What only Juno can do is vouch for where an artifact came from, and that claim is the only thing the certified pipeline adds.

The package​

A .junodrv is a zip:

manifest.toml required
driver.wasm } exactly one kind, matching [driver] runtime
driver.py }
commands.toml }
driver-macos-aarch64.dylib } native drivers ship every platform in one archive
driver-linux-x86_64.so }
driver-linux-aarch64.so }
docs/README.md shown in the controller's driver pane — markdown
icons/128.png optional; 128 / 256 / 512
languages/*.po optional

Build one with junodrv:

junodrv check . # validate against the real proxy contracts
junodrv pack . --out dist # validate, then write the archive

pack validates first and separately, so it never writes an archive that would not install. There is no way to build a package while skipping the check, which is deliberate: the check is the thing standing between a typo and a device that does not work at 9pm.

A package with one driver keeps it in manifest.toml. A package with several puts them all in manifests/ — one place to look, and no file privileged by where it sits.

Shipping it yourself​

A controller installs any .junodrv handed to it. Drag it onto the configurator's driver pane, or drop it in the controller's drivers/ directory and restart.

It is labelled Third-party and never auto-updated. That is not a penalty — it is an honest statement that nothing in the chain between you and that house made any claim about the file, and it means you decide when a house gets a new version rather than the registry deciding for you.

To build one in CI, call the public workflow. It needs no credentials at all:

name: release
on:
push:
branches: [main]
tags: ['v*']

jobs:
driver:
uses: junohouse/driver-ci/.github/workflows/driver.yml@main
permissions:
contents: write

That compiles the driver for every platform a controller runs — macOS ARM, Linux x86-64, Linux ARM — validates the manifest against the same contracts a controller enforces, packs the archive, and publishes it to your own repository's releases. Push to main for a beta, tag v1.2.0 for a release.

It is the identical build a first-party driver gets. Nothing about it is a lesser path.

Getting into the registry​

driver.juno.house is the catalog a controller resolves installs from, and being in it means the artifact came out of Juno's own publishing pipeline. The workflow that does the indexing is public and you are welcome to read it; what you cannot get is the token it dispatches with, which only a repository inside the junohouse organization holds. That is the only thing keeping the word meaning anything.

What certification is:

A provenance claim, not a safety audit. It says the artifact was built by junohouse/driver-ci from a repository Juno controls, and that its manifest validated against the proxy contracts at build time. It does not say the driver is good, correct, or reviewed line by line.

Being precise about that matters, because the controller UI shows the badge to residents.

If you have written a driver you would like carried there, mail hello@juno.house. The path is that the driver moves into junohouse and is maintained there — which is a real commitment on both sides, and the reason the answer is a conversation rather than a form.

Channels​

TriggerChannelVersionLifetime
push to mainbeta1.2.0-beta.41Rolling. Only the newest beta per driver exists.
tag v1.2.0stable1.2.0Kept. Every stable build stays in the index for ever.

A beta version sorts below the release it previews under semver, so a controller following stable can never resolve to one by accident.

Stable accumulates and beta does not, for the same reason in two directions. A rolling beta tag means the previous beta's URL stops resolving the moment a new one is built, so keeping a history of them would be an index full of 404s. Stable keeps everything because the controller decides which build it can run — an index that only offered the newest would strand exactly the installations least able to update.

Versions, and which build a house gets​

The controller resolves this, not the registry:

{ "version": "1.2.0", "core": ">=0.4", "url": "…", "sha256": "…", "size": 214512 }

min_core in your manifest becomes that core requirement. A controller picks the newest build it can run, and shows why a newer one is being withheld rather than hiding it.

So bump min_core only when you actually depend on something newer. Every bump is a set of houses that stop receiving your updates, and they will not be told why unless you were honest in the field.

Which updates install themselves, and which wait​

A house does not press a button for every driver. The controller checks the catalog when it starts, takes the builds it is allowed to take, and leaves a notification about the ones it is not. What decides which is which is your version number — the one thing a version already means:

You publishThe house does
1.2.0 → 1.2.1 (patch)Installs it at the next start, silently.
1.2.0 → 1.3.0 (minor)Installs it at the next start, silently.
1.2.0 → 2.0.0 (major)Waits. The driver pane offers it with your release notes.
0.4.2 → 0.5.0Waits. Before 1.0 the minor is where a break lands.
any → a build needing a newer min_coreNothing. The pane says which core it needs.
a beta buildAlways installs it. Following main continuously is what beta is.

So the rule for you is the ordinary semver one, and it is worth being strict about because a patch release installs itself into every house tonight without anyone reading anything:

Bump the major when something does not carry over. A setting that moved, a pairing that has to be done again, a device that will come back as a different class, a property somebody's automations refer to by name. Not when the code changed a lot — when the house has to change.

A person can always override both directions from the driver pane: install a major before the controller offers to, or roll back to any stable build still in the index. Rolling back holds the driver at that version — the automatic path leaves it alone until somebody clears the hold, because a controller cannot tell a build nobody has seen from one that has already been rejected.

Release notes​

Keep a CHANGELOG.md in the package directory, or at the root of the repo. When you tag a release, CI lifts out the section about that version and puts it in the index, so it can be read on the controller before the download — which is the only moment it is useful for an update that is waiting to be accepted.

## [2.0.0] — 2026-08-01

### Changed
- Setpoints moved from the driver to the house, so every schedule has to be set again.

## [1.2.1] — 2026-07-14

### Fixed
- Input switching on models that report the CNAME late.

Any heading that names the version works — ## 2.0.0, ## v2.0.0, ## [2.0.0] — date — and everything under it up to the next heading of the same level is what a house sees.

Beta builds carry no notes, whatever your file says. A beta is a rolling build of main with a run number on it; there is no released version for a section to be about, and asking every push for release notes only produces notes nobody wrote.

Open and closed source​

Certified drivers are built from repositories in junohouse, and today those are public — you can read the source of everything in the catalog.

That is not what certification means, though, and the index says so rather than leaving it implied. Each entry carries source, filled in by CI from the repository's real visibility at the moment it published, so it cannot drift the way a hand-set flag would. A driver whose source cannot be read is labelled closed source in the catalog and offers no Source link, rather than one that 404s. Certification is unaffected either way: it is a claim about the pipeline an artifact came out of, not about who can read the code.

The one thing that does change with visibility is where the package is downloaded from. A release asset on a private repository needs a token, and a controller in a house has none — so a closed driver's payload has to be served from somewhere anonymous. The index carries whatever URL the build stamped into it; core has no notion of where a driver came from and needs none.

Your own drivers are unaffected by any of this. A .junodrv on your own releases, or on your own web server, installs exactly the same way.

A checklist before you ship​

  • junodrv check passes.
  • Every capability you declared, you can actually deliver. Over-declaring turns into a command the assistant offers a resident and then fails.
  • You notify on every state change, including the ones your own commands caused.
  • min_core is as low as it can honestly be.
  • docs/README.md says what the device is, what to set up on it, and what does not work yet. It is what an integrator reads in the driver pane at the moment they are stuck.
  • The version in manifest.toml matches the tag you are about to push. CI will refuse the build otherwise, which is the friendlier place to find out.
  • The major is bumped if anything about this build does not carry over — see above. A patch or a minor installs itself into every house that starts tonight.
  • CHANGELOG.md has a section for the version you are tagging.