Commands
embr publish
Last updated October 23, 2026
embr publish [--dry-run] [--target <id>]... [--local]
Publishes the current package to the Ember registry (api.luaupm.com). The registry's rules — scopes, immutability, co-ownership — have their own page.
What publish does, in order#
-
Requires
[target], or one or more[[target]]tables, each with anenvironment. The registry keys each entry by its target ID.mainis not required to publish, except for Alloy and Loom. -
For
language = "alloy"or"loom", runsalloy buildorloom buildin the package folder. Publish then refuses whenmainis missing, does not end in.luau, or does not exist after the build. See Languages. -
Checks
[package] logo: a.png,.jpg, or.webpfile that exists and is at most 512 KiB. -
Validates
[package] authorsare GitHub-username-shaped — fails before packing otherwise. -
Stops on an XML model (
.rbxmx). Save it again in Roblox Studio as a binary.rbxm. See Models in packages. -
Packs a
.tar.gz: everything in the project except.git,.ember-staging,.ember-patch,ember.lock,target,node_modules, and the packages-out folders; filtered by[target] includes/excludes(see the manifest).ember.tomlalways ships.The packed ember.toml is regenerated — comments are not preserved, workspace dependencies are rewritten to registry requirements (see Workspaces), and the
[overrides]and[patches]tables are stripped: both are the installing project's business, never a published package's. -
Enforces the registry's 10 MB cap client-side.
-
--dry-runstops here and prints the destination, archive name and size, and the full file list. -
Authenticates: the stored login from
~/.ember/credentials.toml(seeembr auth), or the GitHub device flow (opens github.com/login/device with a code). The flow requests no OAuth scopes — the registry only maps the token to a GitHub login. The OAuth client id comes from the index's config, not the binary. If the registry answers 401 (revoked or expired token), Ember clears the stored token, re-runs the device flow, and retries once. -
Uploads. The API validates the tarball, checks its
.rbxmmodels against the model rules, stores it on the CDN, and writes the index entry.
Several targets#
A manifest with several [[target]]
tables publishes one archive for each target. Each archive holds the files
of its own target, and an ember.toml with that one target as a plain
[target] table. Alloy and Loom build once for each publish. A target that
fails does not stop the others.
--target <id> publishes only the target with that ID, such as roblox or
roblox-alloy. Repeat it to publish more than one.
Local packages#
--local publishes to the local registry in ~/.ember/local. A local
package is a package that only this computer can install. There is no login
and no upload.
cd my-lib && embr publish --local
cd my-game && embr add my-lib
- The archive is the same as for a registry publish, with each target,
--target, the build and the checks. - A publish of the same version replaces the archive. The next
embr installinstalls the change. embr yankandembr deprecatedo not change a local package.
Provenance from GitHub Actions#
A publish that runs inside a GitHub Actions workflow can carry a verifiable "built from repo X at commit Y by workflow Z" proof. Give the publishing job one permission line:
permissions:
id-token: write
That is the whole setup. The CLI notices it is running in Actions, requests the workflow's OIDC token, and sends it with the upload; the registry verifies the token and records the repository, commit, and workflow run, which the version's package page shows in its provenance panel.
Publishing from a laptop, or from a workflow without that permission, simply produces a version with no provenance panel. That is normal, not a warning sign. Authentication is unchanged either way: the OIDC token is additional, and the publishing identity is still the GitHub login.
Private packages#
private = true under [package] makes publish refuse: it prints
"package is private, refusing to publish" and succeeds doing nothing.
Error statuses#
| Status | Meaning |
|---|---|
| 401 | bad token |
| 400 | malformed tarball/ember.toml, bad authors, or a model that breaks the model rules |
| 403 | scope owned by someone else |
| 409 | version already exists (versions are immutable) |
| 413 | over 10 MB |
The API's human-readable message is surfaced directly.
Hooks#
prepublish runs before the archive is packed, which is the point of
it — that is where you build what ships. postpublish runs only after a
successful publish. See Lifecycle hooks.
prepublish runs on --dry-run too, since a dry run exists to show
the archive a real publish would produce. The Alloy and Loom build runs on
--dry-run for the same reason.
Workspaces#
In a workspace root, publish walks the whole workspace — root first, then every member. See Workspaces for skip rules and how workspace dependencies become registry requirements.