opsdocsConsole

Ops platform / Publish

Packages

Publish npm packages and Rust crates to ops's own registries, and install them in your builds.

Updated 13 Oct 2026View as Markdown

Ops runs three registries: container images, npm and cargo. Images need nothing from you: every build stores its image, and the project's Packages page lists them with the environments that run each. npm packages and crates are published by your project and installed by any project you share them with.

Claim the names

A project publishes only under names it has claimed. Choose Add resource, then Packages, and list the names:

fieldholds
npmnpm names, in the @syncronet/ scope
cargocrate names
sharednames other projects may install, each with the project slugs that may

A name belongs to one project across all of ops. A name with published versions cannot be released.

Publish from a tag

Push a tag such as v1.4.0 on the project's connected repository. Ops starts a publish run, listed under Deployments beside the builds:

  1. It checks the tag against the version in the Cargo workspace.
  2. It publishes every crate of the workspace, each after the crates it depends on.
  3. It publishes every npm package under layers/ and every modules/<name>/layer.
  4. It registers each package's locale files, see Translations.
  5. It publishes the docs folder of each modules/<name>, see Publish docs.

Nothing publishes if the plan finds a problem first. A run that fails halfway can be run again with Redeploy: it skips what is already published. The run holds a publish token of its own for as long as it runs, so there is no publish key to keep anywhere.

Modules

A repository can group a crate and an npm package that belong together as a module:

text
modules/storage/
  module.ts       export default defineModule({ description: 'Object storage for services.' })
  CHANGELOG.md    a "## 1.4.0" section per version becomes that version's patch notes
  crate/
  layer/
  docs/

defineModule comes from @syncronet/ops-project. A module without its module.ts, or one with no description, refuses the tag before anything publishes.

Publish by hand

How to publish on the Packages page shows the configuration for your ops, with a token from Tokens that holds package.publish:

ini
# .npmrc
@syncronet:registry=https://ops.syncronet.dev/npm/
//ops.syncronet.dev/npm/:_authToken=<token>
toml
# .cargo/config.toml
[registries.ops]
index = "sparse+https://ops.syncronet.dev/cargo/"

[registry]
global-credential-providers = ["cargo:token"]

Then npm publish or cargo publish --registry ops. The credential provider line is needed: without it cargo refuses every read of the index, and only a crate with no dependencies would publish.

A version is published once and never replaced, on both registries. Cargo can yank and unyank a version; npm has no unpublish and no dist tag changes.

Install in a build

Every image build carries a read token for the registries in the build_env build secret, as OPS_NPM_TOKEN and CARGO_REGISTRIES_OPS_TOKEN. Put the token's variable in .npmrc and read the secret in the step that installs:

ini
# .npmrc
@syncronet:registry=https://ops.syncronet.dev/npm/
//ops.syncronet.dev/npm/:_authToken=${OPS_NPM_TOKEN}
dockerfile
RUN --mount=type=secret,id=build_env \
    sh -c 'set -a; . /run/secrets/build_env; set +a; pnpm install --frozen-lockfile'

The build token reads your own project's packages and those another project shared with yours.

Who uses what

Each build of a default branch records which ops packages its lock files resolved. For a project that publishes modules, ops modules lists them with the projects that use each one, and ops modules <name> shows one module with its patch notes:

sh
ops modules --project rungholt
ops modules storage --project rungholt