Publish a plugin
Push to GitLab, CI ships your plugin to every lly install in the world.
Plugins extend the lly CLI with new frontends, runtimes, and tools. Today the registry is restricted to org members on the internal GitLab. A public marketplace is on the roadmap, and the publish flow described here will not change when it lands — only the repository host and the access policy of the GCS trust root.
Read Plugins first if you have not seen how the resolver, the GCS bucket behind rc.lilylabs.io, and the per-user install directory at ~/.lly/plugins/ fit together. This page is the producer side of that picture.
1. Bootstrap the repo
Every plugin lives in its own repo under the lily1/store group on the internal GitLab at 10.0.1.3. The repo name is the plugin name. Pick something short and kebab-case: nextjs, void, frontend-c. The name becomes the binary name, the install directory, and the argument to lly plugin install, so it has to be stable from day one.
2. The plugin shape
A plugin is a single statically-linked binary plus a manifest. The binary lives at bin/<plugin>for each target. The manifest declares the name, the semver-shaped version, the entry binary, and the targets you ship. Anything else — config, embedded assets, sub-commands — is the plugin's own business.
{
"name": "myplugin",
"version": "0.1.0",
"entry": "bin/myplugin",
"description": "Short one-liner shown by 'lly plugin info'.",
"targets": ["x86_64-linux", "aarch64-macos"],
"extensions": [".mp"],
"lly_min_version": "0.8.0"
}The extensions array is optional. If present, lly compile will dispatch files with those extensions to your plugin, and lly plugin which .mpwill resolve to you. lly_min_versionis the CLI version your plugin requires — the resolver refuses to install if the user's lly is older.
3. Add the CI pipeline
Drop a .gitlab-ci.yml in the repo root. Two parallel build jobs produce the Linux and macOS binaries, a third publish job uploads both binaries plus the manifest to the gs://lly-plugins/ bucket under a version-stamped prefix. The bucket is the trust root: only the CI service account has write access.
stages: [build, publish]
build:linux:
stage: build
image: rust:1.82-bullseye
script:
- cargo build --release --target x86_64-unknown-linux-gnu
- cp target/x86_64-unknown-linux-gnu/release/myplugin bin/myplugin
artifacts:
paths: [bin/myplugin, manifest.json]
build:macos:
stage: build
tags: [macos-arm64]
script:
- cargo build --release --target aarch64-apple-darwin
- cp target/aarch64-apple-darwin/release/myplugin bin/myplugin
artifacts:
paths: [bin/myplugin, manifest.json]
publish:
stage: publish
image: google/cloud-sdk:slim
script:
- V=$(jq -r .version manifest.json)
- gsutil cp manifest.json gs://lly-plugins/myplugin/$V/manifest.json
- gsutil cp bin/myplugin gs://lly-plugins/myplugin/$V/x86_64-linux/myplugin
- gsutil cp bin/myplugin gs://lly-plugins/myplugin/$V/aarch64-macos/myplugin
only: [main]The CI service account credentials are injected as the GCP_SA_KEY variable at the group level — you do not configure it per repo. If you fork a plugin into a new repo, ask infra to enable the variable for the new path.
4. Push to main
Tag the version in manifest.json, commit, push. The pipeline runs on the protected main branch only. Once publish turns green, the binary sits in the bucket waiting to be picked up.
On rc-1, a systemd timer fires every 60 seconds and runs the registry puller. It walks the bucket, compares versions to the live registry, and ingests anything new. Within a minute of the green CI run, rc.lilylabs.io serves the new manifest and binary to every lly install in the world.
5. Verify the install
From a clean machine — or any machine other than yours — run the install. The resolver fetches the manifest, downloads the binary for the local target, verifies the SHA against the manifest entry, and drops it into ~/.lly/plugins/myplugin/.
Iterating
Bump the version in manifest.json for every release. The puller is version-keyed, so a republish of the same version is a no-op — uploads with a duplicate version are rejected at the bucket. Users get the new version on their next lly plugin install <name>, or implicitly on lly plugin update. Old versions stay in the bucket and remain installable by explicit pin: lly plugin install myplugin@0.1.0.
For local iteration before publishing, point lly at an unpacked plugin directory with lly plugin install --from ./. That bypasses the resolver entirely and is the fastest way to test a new build. See Plugins for the resolver internals and lly plugin for the full subcommand surface.