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 workflows 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 . --list
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.
--list Print the files the archive would contain and exit. Writes nothing.
--skip-actions-dump Do not generate actions.json.

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 anything, the command checks the manifest:

  • extension.toml must have a [package] section. Without it the command fails with Packaging requires a [package] section in extension.toml.
  • The entry, icon, readme, config.schema, and requirements files must exist, and so must workdir.

The generated projects wrap the command:

Project Command What it runs
Agent just package zelos extensions package .
App just package npm run build, then npm run package
App npm run package zelos extensions package . only. It does not build, so run npm run build first.

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
  • For agent extensions: 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 --list .

actions.json

For an agent extension, 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.
  • With no standalone actions, it writes an empty inventory.
  • When the import fails, it prints a warning and packages anyway. Any actions.json already in the directory ships unchanged.
  • --skip-actions-dump skips the step. --list 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 package.json, package-lock.json, and the [project] table of pyproject.toml when those exist. The version must be valid SemVer.

Release From a Generated Project

Both templates ship a just release recipe and a GitHub Actions workflow that publishes on a v* tag.

just release 0.2.0
git push --follow-tags

just release stops on uncommitted changes. Then it runs zelos extensions bump, just format, uv lock, just check, and just test, and commits and tags v0.2.0.

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

  1. Checks that the tag matches the version in extension.toml and pyproject.toml.
  2. Installs the Zelos CLI, then runs just check and just test.
  3. Runs just package.
  4. Creates a GitHub release with the archive attached.
just release 0.2.0
git push --follow-tags

just release stops on uncommitted changes. Then it runs zelos extensions bump, just format, just check, and just build, and commits and tags v0.2.0.

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

  1. Checks that the tag matches the version in extension.toml and package.json.
  2. Installs the Zelos CLI, then runs npm run check and npm test.
  3. Builds and packages the extension.
  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.

The extension ID is owner.repo, 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 uppercase letters, 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 a leading v.
  • 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 host type 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, and refuses a change of host type.
  • 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.
  • Puts the old version back if any step fails.

Troubleshooting

Problem Fix
Packaging requires a [package] section Add [package] with paths to extension.toml.
Manifest entry path does not exist: main.py Create the file, or fix entry.
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.
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 the module imports cleanly, or pass --skip-actions-dump.