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.tomlmust have a[package]section. Without it the command fails withPackaging requires a [package] section in extension.toml.- The
entry,icon,readme,config.schema, andrequirementsfiles must exist, and so mustworkdir.
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, andconfig.schemafiles the manifest names, when they exist - For agent extensions:
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 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 runin 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.jsonalready in the directory ships unchanged. --skip-actions-dumpskips the step.--listnever 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 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 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:
- Checks that the tag matches the version in
extension.tomlandpyproject.toml. - Installs the Zelos CLI, then runs
just checkandjust test. - Runs
just package. - Creates a GitHub release with the archive attached.
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:
- Checks that the tag matches the version in
extension.tomlandpackage.json. - Installs the Zelos CLI, then runs
npm run checkandnpm test. - Builds and packages the extension.
- 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.
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.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 a leading
v. - 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 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. |