Skip to content

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, and pyproject.toml lists the same packages for it in [project.optional-dependencies]. See Match pyproject.toml.
  • The manifest passes the rules that every install checks. See Common Validation Errors.
  • extension.toml has a [package] section.
  • The agent's entry, a packaged tab, and the icon, readme, config.schema and requirements files exist, and so does workdir.
  • Each panel's entry, icon and options_schema files exist. These are build output, so build before you package.
  • The config schema and each panel's options_schema hold a JSON object. The config schema is the [config] schema file, or a config.schema.json at the project root when [config] names none.
  • Each [package].paths entry 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, and config.schema files the manifest names, when they exist
  • With an [agent]: pyproject.toml, uv.lock, actions.json, and the requirements file, when they exist
  • Every file under each [package].paths entry

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:

zelos extensions package --dry-run .

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 run in the project's own environment.
  • The file addresses each action under the manifest name, for example My 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.json already in the directory stays unchanged.
  • When it cannot write the file, it prints could not write <path>: <error> and stops.
  • --skip-actions-dump skips the step. --dry-run never runs it; it prints a note when packaging would generate the file.
  • zelos extensions install-local writes the same file, and zelos actions dump refreshes 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:

zelos extensions bump 1.2.0
zelos extensions bump 1.2.0 --path ./my-extension

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 0.2.0
git push --follow-tags

just release stops on uncommitted changes. Then it:

  1. Runs zelos extensions bump 0.2.0.
  2. Runs uv lock when the project has an agent runtime.
  3. Runs just format, just check, just test and just build.
  4. Commits the changes and tags v0.2.0.

On the tag, .github/workflows/release.yml:

  1. Checks that the tag matches the version in extension.toml, and in pyproject.toml and web/package.json when they exist.
  2. Installs the Zelos CLI and the project's dependencies.
  3. Runs just check, just test and just package.
  4. Creates a GitHub release with the archive attached.

Publish to the Marketplace

  1. Push the project to a GitHub repository, for example acme/canbus-decoder.
  2. Create a release with at least one archive attached. The generated workflow does this for you.
  3. Open marketplace.zeloscloud.io and submit the repository. In the Zelos App, Publish Extension in the Marketplace section menu opens the same page.
  4. Install the Zelos GitHub App on the repository when asked. It has read-only access.
  5. 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.toml from 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 v or V, and refuses a release whose tag version differs from the version in extension.toml.
  • Validates the [config].schema file, 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.xz and are at most 500 MiB. It computes a SHA-256 checksum for each. Attach only .tar.gz, .tgz, or .zip archives, 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.