---
title: Packages
description: Publish npm packages and Rust crates to ops's own registries, and install them in your builds.
order: 1
---

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:

| field | holds |
| --- | --- |
| `npm` | npm names, in the `@syncronet/` scope |
| `cargo` | crate names |
| `shared` | names 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](translations.md).
5. It publishes the `docs` folder of each `modules/<name>`, see [Publish docs](docs.md).

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:

```
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](../develop/tokens.md) 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
```
