# Publish a package {#root}

@status:doc/work @audience:user,author

[p01] You wrote a package and want others to install it. This page publishes it to the place your projects install from, as its own repository, tags the version, and checks that a fresh project can install it.

[p02]
```prompt
Publish the in-tree package org.acme/notes of this project, the slot vibevm/vibepacks/org.acme/notes/v0.1.0, to the first registry of this project, using the publish token already in my environment, then create a scratch project elsewhere and install the published package into it to prove it works. Ask me before the actual push.
```

- needs: the vibevm skill installed for your agent; a publish token for the registry's host in the environment or under `~/.vibe/`; the package's `vibe.toml` complete, with a version that has not been published before

outcome: a repository named after the package's coordinate exists in the registry organisation with a tag for the version; a scratch project installs it and `vibe list` shows the version

- assert: `vibe registry publish vibevm/vibepacks/org.acme/notes/v0.1.0 --dry-run`

## What happens {#what-happens}

[p03] The agent runs `vibe registry publish vibevm/vibepacks/org.acme/notes/v0.1.0 --dry-run` first and shows you what would happen: the [registry](../glossary/index.xml#registry), the repository name derived from the [coordinate](../glossary/index.xml#coordinate), the version tag. After your yes it runs the real command: vibe creates the repository in the registry organisation through the host's API if it does not exist, pushes the package's shippable tree, and tags the version. The package is then one more repository the registry's [index](../glossary/index.xml#index-registry) will pick up. To prove it, the agent creates a scratch project and installs the package by coordinate.

> [p04] Each package is its **own** git repository — no monorepo. Per-package maintainer permissions are hosting-native (a package repo's owner controls access); no central merge queue.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#SHAPE-OWN-REPO>

[p05] The publisher is a mechanical tool: it creates the repository, pushes the content and tags the version, and nothing more. The host of the registry's address picks the adapter that creates repositories; GitHub and GitVerse are known, and an unknown host is a clear error rather than a guess. In the repository the package content lies flat at the root, and the version is the tag `v` followed by the version number.

> [p06] **Decision.** Ship a maintainer utility in v1. Scope: mechanical-only publish — **create repo, push contents, tag version**. Semantic review (LLM-backed safety analysis per `VIBEVM-SPEC.md` §8.5) remains v2+.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#PUBLISH-UTILITY>

> [p07] **Adapter selection.** The CLI picks an adapter from the registry URL's host segment. `github.com` (or any subdomain) → `GitHubCreator`; `gitverse.ru` → `GitVerseCreator`; unknown hosts surface a clean error pointing at PROP-002 §2.10 rather than guessing a Gitea-compatible shape that may not match the host's actual API.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#PUB-ADAPTER-SELECTION>

> [p08] **Decision.** A package repository contains the package content flat at the repository root:
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#FLAT-LAYOUT>

> [p09] Version = git tag with `v<semver>` prefix. The tag is a mutable logical label by default: republishing the same version moves it, while the resolved commit and content hash preserve exact identity in each consumer lock.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#LAYOUT-TAG-VERSION>

## By hand {#by-hand}

[p10] 1. Put a publish token where vibe reads it. That is the environment variable `VIBEVM_PUBLISH_TOKEN`, or a file `~/.vibe/<host>.publish.token` such as `~/.vibe/github.publish.token`, readable by you alone. The token needs the right to create repositories in the organisation.

> [p11] 20. Token secrecy and adapter scope
>
> <spec://org.vibevm.core/vibevm/common/PROP-000#token-secrecy>

[p12] vibe looks for the token in a fixed order and takes the first it finds. First comes `VIBEVM_PUBLISH_TOKEN_<HOST>` for the registry's host, such as `VIBEVM_PUBLISH_TOKEN_GITHUB`. Then `VIBEVM_PUBLISH_TOKEN`, then the per-host file, then the older `~/.vibe/git.publish.token`. The token is a surface secret: never printed, never logged, never written to a file vibe writes. It leaves the process only in the request to the host, over an encrypted connection.

> [p13] **Token loading.** The publish token loader (`crate::token::load_token(host)` → `load_token_for_host`) iterates these sources in order, returning the first non-empty value:
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#PUB-TOKEN-LOADING>

> [p14] `VIBEVM_PUBLISH_TOKEN_<HOST>` environment variable — host-specific, and the **highest-precedence source**. The suffix is the uppercased first label of the host (`VIBEVM_PUBLISH_TOKEN_GITHUB` for `github.com`, `VIBEVM_PUBLISH_TOKEN_GITVERSE` for `gitverse.ru`); non-alphanumerics fold to `_` so the name stays a valid POSIX identifier. Lets CI hold tokens for several hosts in the same environment without one host-agnostic variable clobbering them all.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#TOK-HOST-ENV-VAR>

> [p15] `<settings-dir>/<host-prefix>.publish.token` — per-host file. The prefix is the first label of the host (`github` for `github.com`, `gitverse` for `gitverse.ru`, `gitlab` for `gitlab.com`).
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#TOK-PER-HOST-FILE>

> [p16] **Token secrecy invariant.** The token is a surface secret. It is **never** displayed in CLI output, log lines, error messages, JSON event payloads, the lockfile, `.git/config`, or any committed file. The only sanctioned paths through which the value crosses a process boundary are: (a) the GitHub / GitVerse `Authorization: Bearer …` HTTP header, sent over TLS to the hosting API; (b) the `x-access-token:<TOKEN>@…` embed in the URL passed directly to the bounded `git ls-remote` / `git fetch` / `git push` invocations of one publish, never configured as a remote; (c) the in-memory `Token` struct, which redacts on `Display` and `Debug`. The CLI prints the *source* of the token (explicit / env-var / file path) but never the value. Implementations must verify token redaction in unit tests (cf. `vibe_publish::token::tests::debug_redacts_value`).
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#TOKEN-SECRECY-INVARIANT>

[p17] 2. Rehearse:

[p18]
```sh
vibe registry publish vibevm/vibepacks/org.acme/notes/v0.1.0 --dry-run
```

```output
```

[p19] 3. Publish. `--registry` picks a registry by name; without it the first one in the [manifest](../glossary/index.xml#manifest) is used:

[p20]
```sh
vibe registry publish vibevm/vibepacks/org.acme/notes/v0.1.0 --registry local --dry-run
```

```output
```

[p21] 4. Install it from a fresh project to be sure: `vibe init scratch` and `vibe install <group>/notes --path scratch`.

## Publishing a version again {#versions}

[p22] Publishing a version that already exists replaces it: vibe appends a commit with the new content and moves the version tag to it. The version number changes only when you change it in the manifest. A consumer who already resolved that version keeps the exact bytes the [lock file](../glossary/index.xml#lock-file) recorded, and verifies the content [fingerprint](../glossary/index.xml#fingerprint) on every install, so the moved tag is noticed rather than silently accepted; a fresh install gets the new content. When the change matters to consumers, bump the version.

> [p23] **Versions are mutable by default (owner ruling, 2026-09-10).** Publishing an already-present version is the ordinary replacement flow, not a collision and not an implicit request to bump the version. The publisher appends an exact-payload commit on the observed `main`, then moves that version tag to the new commit. A version bump happens only when the author explicitly changes `[package].version`. An identical retry whose payload and selected tag already match is a no-op.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#PUBLISH-MUTABLE-VERSIONS>

> [p24] **2a. Frozen and snapshot versions (owner rulings,
> 2026-08-10; terminology fixed 2026-08-13).** A version is a **snapshot by
> default** — the word carries its Maven sense, *mutable*: content may change
> under the same version string, `vibe update` brings the fresh content without
> regard for hash continuity, and the lockfile pins the delivered capture's
> `content_hash` plus an opaque provider locator for reproduction. The
> **freeze** is the package author's one-way act: `frozen = true` in the
> manifest — never a registry's opinion, never part of the version string. The
> carrier decisions and their reasons: *(i)* the flag lives **inside the hashed
> content**, so a frozen version self-describes even offline and every registry
> serving those bytes necessarily agrees — in a multi-registry world with no
> global journal, content is the only carrier that cannot diverge; registries
> merely *observe* a freeze in their journals and project it into catalogs;
> *(ii)* the version string carries version ordering **only** — two entities
> never share one name, which keeps the full matrix expressible: a frozen
> prerelease (an immutable published beta) and a mutable bare version (being
> stabilised in place) are both legal; *(iii)* the transition is **one-way and
> single** — unfreezing is forbidden, further work is a new version string; a
> registry may never accept a frozen coordinate's re-publication with different
> bytes; *(iv)* same coordinate + different bytes + any party claiming frozen =
> **loud conflict** through the candidate machinery, never a quiet pick.
> **Every surface that shows a version shows its frozen state** — machine
> outputs carry the field by schema; CLI, TUI, GUI and MCP render it always
> (the Maven lesson: mutability a human cannot see is mutability that will
> surprise them). Yank remains journal-borne — it is the act frozen content can
> no longer carry itself.
>
> <spec://org.vibevm.core/vibevm/common/PROP-044#THE-FREEZE-MODEL>

> [p25] a force-pushed tag upstream is caught by the same machinery on the next install.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#EFF-FORCE-PUSH-CAUGHT>

[p26] The publisher never rewrites the registry's history: every publish commit is a child of the head it observed, the tag moves only inside one atomic push guarded by the expected state of both refs, older tags are never touched, and the run stays inside the organisation the manifest names.

> [p27] Never rewrite `main`: every changed publish commit is a child of the exact observed head. Never issue an unconditional force: moving the selected mutable version tag is permitted only inside one `git push --atomic` that carries exact `--force-with-lease` expectations for both `main` and the tag. Never fall back to sequential branch/tag pushes; if atomic push is unsupported or either lease is stale, neither ref moves. Never alter older version tags. Never create a repo in a different org than the configured one unless `--org <other>` is passed explicitly. Never escalate scope: a publish run targets exactly the org named in the project's `[[registry]]` URL — adapters MUST refuse to create or modify anything outside that org.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-registry/PROP-002#PUBLISH-NEVER-RULES>

## Several packages at once {#workspaces}

[p28] A repository that develops several packages publishes them with `vibe workspace publish`. It orders the members by their dependencies on each other and publishes each as its own repository. It stops at the first failure with a report of what was published and what remains. Each published copy carries an `[origin]` table naming the repository and the commit it came from, so a copy can always be traced to its source. `--member` restricts the run to one node; `--dry-run` shows the selection and the order without pushing.

> [p29] **Decision.** `vibe workspace publish` (PROP-007 §2.7) regenerates the boot artifacts of each staged copy for the **published shape** — where dependencies are registry-resolved and version-pinned, not path-sourced.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-workspace/PROP-009#PUBLISH-REGEN>

## Edge cases and rules {#edge-cases}

[p30] `--repo-url` pushes straight to an existing git repository with your local git credentials and loads no publish token; use it for hosts without an API adapter.

[p31] The token is never sent to a package's install script and never printed, even in machine-readable output; if a command prints it, that is a bug to report.

> [p32] The publish token
>   ([PROP-000 §20](../../common/PROP-000.xml#token-secrecy)) is **never** placed in
>   a hook's environment.
>
> <spec://org.vibevm.core/vibevm/modules/vibe-workspace/PROP-020#TOKEN-NEVER-IN-ENV>

[p33] A documentation package is published the same way; what differs is how it is consumed, by `vibe cache add` and the site rather than by `vibe install`.

