Package and Publish Extensions¶
A released extension is a .tar.gz or .zip archive attached to a GitHub release. The marketplace indexes the release, and the Zelos Agent downloads, verifies, and unpacks the archive when a user installs it.
This page covers building the archive, the rules it must follow, versioning, the release workflow in the generated projects, and marketplace submission.
Build the Archive¶
zelos extensions package builds the archive from a project directory:
zelos extensions package .
zelos extensions package . --output ./dist
zelos extensions package . --dry-run
zelos extensions package . --skip-actions-dump
| Option | Effect |
|---|---|
-o, --output <PATH> |
Write the archive to this file, or into this directory when it exists. Default: the project directory. |
--dry-run |
Print the files the archive would contain and exit. Writes nothing. |
--skip-actions-dump |
Do not run the entry module to generate actions.json. An actions.json already in the directory ships unchanged. |
The archive is named <name>-<version>.tar.gz, where <name> is the manifest name in lowercase with runs of other characters replaced by -. For example, name = "My Sensor" and version = "0.1.0" produce my-sensor-0.1.0.tar.gz.
Before it writes the archive, the command checks that:
- The manifest uses
[agent]and[app], not the older form. - Each extra has a
description, andpyproject.tomllists the same packages for it in[project.optional-dependencies]. See Matchpyproject.toml. - The manifest passes the rules that every install checks. See Common Validation Errors.
extension.tomlhas a[package]section.- The agent's
entry, a packagedtab, and theicon,readme,config.schemaandrequirementsfiles exist, and so doesworkdir. - Each panel's
entry,iconandoptions_schemafiles exist. These are build output, so build before you package. - The config schema and each panel's
options_schemahold a JSON object. The config schema is the[config]schemafile, or aconfig.schema.jsonat the project root when[config]names none. - Each
[package].pathsentry exists, and nothing in it is a symbolic link or a special file.
A failed check stops the command. Troubleshooting lists each message. The command also warns about a key that the manifest does not define, such as a misspelled field. A warning does not stop it.
In a generated project, run just package. It runs just build, which builds the web project when there is one. Then it runs zelos extensions package .. The CLI command alone does not build, so run just build before you call it directly.
What Goes Into the Archive¶
The archive holds files at its root, with no wrapping directory. It contains:
extension.toml- The
readme,icon, andconfig.schemafiles the manifest names, when they exist - With an
[agent]:pyproject.toml,uv.lock,actions.json, and therequirementsfile, when they exist - Every file under each
[package].pathsentry
Inside [package] directories, packaging skips hidden entries (names that start with .), node_modules, __pycache__, and .pyc and .pyo files. A symbolic link or a special file under a package path is an error.
Preview the result before you release:
actions.json¶
For an extension with an [agent], zelos extensions package first writes actions.json into the project directory. The file lists the extension's standalone actions, the ones the agent can run while the extension is stopped. It ships in the archive, so the agent never has to import your code to learn about them. See Choices for a Config Field for what standalone actions do.
- The command imports your entry module with
uv runin the project's own environment. - The file addresses each action under the manifest
name, for exampleMy Sensor/list_interfaces. - With no standalone actions, it writes an empty inventory.
- When the step fails, for example because the entry module does not import, the command stops and writes no archive. Any
actions.jsonalready in the directory stays unchanged. - When it cannot write the file, it prints
could not write <path>: <error>and stops. --skip-actions-dumpskips the step.--dry-runnever runs it; it prints a note when packaging would generate the file.zelos extensions install-localwrites the same file, andzelos actions dumprefreshes it by hand.
Keep the entry module's startup code under if __name__ == "__main__":, as the templates do. The import then registers actions without starting your extension.
Archive Rules and Limits¶
These are the only size limits that apply to an extension archive:
| Limit | Value | Enforced by |
|---|---|---|
| Release asset size | 500 MiB | Marketplace, at ingestion. A larger asset is not published. |
| Download size | 2 GiB | Agent, while downloading |
| Extracted size | 2 GiB | Agent, while extracting |
| File count | 10,000 files | Agent, while extracting |
| Download time | 60 seconds | Agent |
The agent reads .tar.gz (and .tgz) and .zip archives. It detects the format from the file contents.
During extraction the agent rejects an archive that contains:
- Symbolic links or hard links
- Special files: block devices, character devices, or FIFOs
- Absolute paths or
..segments - Hidden files or directories, meaning any path segment that starts with
.
The agent also verifies the SHA-256 checksum the marketplace computed for the asset. A mismatch stops the install.
zelos extensions package produces archives that pass these checks. If you build archives another way, remove .git/, .DS_Store, editor folders, and symbolic links first.
Platform-Specific Archives¶
Pure Python extensions need one archive for all platforms. When an extension ships native code, attach one archive per platform to the release. The marketplace reads the platform from each asset's file name:
| Keyword in file name | Detected as |
|---|---|
darwin, macos, osx |
macOS |
linux |
Linux |
win32, windows, .exe |
Windows |
x64, x86_64, amd64 |
x86-64 |
arm64, aarch64 |
ARM64 |
arm, armv7 |
ARM |
Matching ignores case. An asset name with both an OS and an architecture keyword is for that platform. Any other archive is universal.
my-ext-1.0.0-linux-x86_64.tar.gz -> Linux x86-64
my-ext-1.0.0-linux-aarch64.tar.gz -> Linux ARM64
my-ext-1.0.0-macos-arm64.tar.gz -> macOS ARM64
my-ext-1.0.0-windows-x86_64.zip -> Windows x86-64
my-ext-1.0.0.tar.gz -> universal
At install, the agent picks the archive for its own platform first, then a universal archive. With neither, the install fails with Extension doesn't support <os>-<arch>. Available: .... The manifest's targets list does not affect this choice.
Set the Version¶
zelos extensions bump sets the version in every file that carries it:
It writes the new version to extension.toml, and to the [project] table of pyproject.toml when it exists. It also writes it to package.json and package-lock.json, at the project root or in web/, when those exist. It runs npm version for them, so a project with a package.json needs Node and npm. The version must be valid SemVer.
Release From a Generated Project¶
Every generated project has the same just release recipe and the same release workflow, whatever its parts. Each step acts on the parts the project has.
just release stops on uncommitted changes. Then it:
- Runs
zelos extensions bump 0.2.0. - Runs
uv lockwhen the project has an agent runtime. - Runs
just format,just check,just testandjust build. - Commits the changes and tags
v0.2.0.
On the tag, .github/workflows/release.yml:
- Checks that the tag matches the version in
extension.toml, and inpyproject.tomlandweb/package.jsonwhen they exist. - Installs the Zelos CLI and the project's dependencies.
- Runs
just check,just testandjust package. - Creates a GitHub release with the archive attached.
Publish to the Marketplace¶
- Push the project to a GitHub repository, for example
acme/canbus-decoder. - Create a release with at least one archive attached. The generated workflow does this for you.
- Open marketplace.zeloscloud.io and submit the repository. In the Zelos App, Publish Extension in the Marketplace section menu opens the same page.
- Install the Zelos GitHub App on the repository when asked. It has read-only access.
- Wait for approval. After approval, users can install the extension from the Zelos App. Until then, the marketplace does not list it, and installing it fails with "No extension '…' in the marketplace".
The extension ID is owner.repo in lowercase, for example acme.canbus-decoder. An ID allows only lowercase letters, digits, and hyphens in each part, so the agent cannot install a repository whose owner or name has dots or underscores. Name the repository to fit.
What the Marketplace Checks¶
When it ingests a release, the marketplace:
- Reads
extension.tomlfrom the repository root at the release tag, not from the archive. A repository without it at the root is not indexed. - Takes the version from the tag name, without one leading
vorV, and refuses a release whose tag version differs from theversioninextension.toml. - Validates the
[config].schemafile, when declared, as JSON and as a Draft 2020-12 JSON Schema. - Accepts release assets that end in
.tar.gz,.tgz,.zip,.tar.bz2, or.tar.xzand are at most 500 MiB. It computes a SHA-256 checksum for each. Attach only.tar.gz,.tgz, or.ziparchives, because those are the formats the agent can extract. - Refuses a release with no installable assets.
At install, the agent checks that the archive's extension.toml agrees with the marketplace entry. The archive's version must equal the tag version, and its name and the parts it contributes must equal the repository manifest's. The release workflow's tag check prevents a version mismatch.
Updates¶
Users get new releases through Check for Updates in the app or zelos extensions check-updates, then zelos extensions update <id>.
An update:
- Installs the new version next to the old one.
- Refuses a version that is not newer.
- Keeps the parts you chose, when you installed only some, and refuses a version that ships none of them.
- Stops the old version if it is running.
- Moves the extension's
data/directory and saved configuration to the new version. - Starts the new version if the old one was running and the new one has an agent runtime.
- Puts the old version back if any step fails.
Troubleshooting¶
| Problem | Fix |
|---|---|
extension.toml not found in <directory> |
Run the command on the project directory, the one that holds extension.toml. |
extension.toml uses the old [host]/[runtime] form. Move to [agent] and [app]; see docs.zeloscloud.io/sdk/how-to/extension-manifest/ |
See Older Manifests. |
Extra "<name>" needs a description: … |
Write the extra as [extras.<name>] with description and packages. |
Extra "<name>" is not in pyproject.toml [project.optional-dependencies], so uv cannot install it |
Add the group to pyproject.toml. See Match pyproject.toml. |
Extra "<name>" lists <packages> in extension.toml but <packages> in pyproject.toml; make them match |
Make both lists name the same packages. |
Packaging requires a [package] section in extension.toml. Add [package] with explicit paths to define the package boundary. |
Add [package] with paths to extension.toml. |
Manifest entry path does not exist: main.py |
Create the file, or fix entry. |
Manifest icon path does not exist: <path>, and the same for Manifest readme, Manifest config schema and Agent requirements |
Create the file, or fix the field that names it. |
Agent workdir directory does not exist: <path> |
Create the directory, or fix workdir. |
Panel "<id>" file '<file>' is missing; build the panel first |
Run just build, then package again. just package builds first. |
<file> must be a JSON object or <file> is not valid JSON: <error> |
Make the file one JSON object, such as { "type": "object", "properties": {} }. |
Package path does not exist: <path> |
Create the path, or remove it from [package].paths. |
Package path is a symlink: <path>, Package path is a special file: <path>, Archive cannot contain symlinked paths: <path> or Archive cannot contain special entry: <path> |
Replace the link with the file it points to, or remove the entry. |
Unknown key <key> in extension.toml; it is ignored |
A warning. Fix the key's spelling, or remove it. See the Manifest Reference. |
Entry point '<entry>' is not covered by [package].paths |
Add the entry file or its directory to [package].paths. |
| The release does not appear in the marketplace | Check that extension.toml is at the repository root at the tag and that the release has an archive asset. |
Release tag <tag> is version <x>, but extension.toml declares version <y>; they must match |
Tag the release with the manifest's version, for example v1.2.0 for version = "1.2.0". |
Manifest version mismatch: marketplace <x>, archive <y> |
Tag the release with the archive's version, for example v1.2.0 for version = "1.2.0". |
Extension doesn't support <os>-<arch> |
Attach an archive for that platform, or a universal archive. |
standalone actions were not inventoried |
Packaging could not import the entry module. Run uv sync and check that the module imports cleanly. |