Documentation menu

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#

  1. Requires [target], or one or more [[target]] tables, each with an environment. The registry keys each entry by its target ID. main is not required to publish, except for Alloy and Loom.

  2. For language = "alloy" or "loom", runs alloy build or loom build in the package folder. Publish then refuses when main is missing, does not end in .luau, or does not exist after the build. See Languages.

  3. Checks [package] logo: a .png, .jpg, or .webp file that exists and is at most 512 KiB.

  4. Validates [package] authors are GitHub-username-shaped — fails before packing otherwise.

  5. Stops on an XML model (.rbxmx). Save it again in Roblox Studio as a binary .rbxm. See Models in packages.

  6. 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.toml always 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.

  7. Enforces the registry's 10 MB cap client-side.

  8. --dry-run stops here and prints the destination, archive name and size, and the full file list.

  9. Authenticates: the stored login from ~/.ember/credentials.toml (see embr 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.

  10. Uploads. The API validates the tarball, checks its .rbxm models 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 install installs the change.
  • embr yank and embr deprecate do 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#

StatusMeaning
401bad token
400malformed tarball/ember.toml, bad authors, or a model that breaks the model rules
403scope owned by someone else
409version already exists (versions are immutable)
413over 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.