---
url: https://docs.repoos.org/deployments-and-releases.md
---
# Deployments and releases

Both of these surfaces are **opt-in and off by default**. A repo with no
`[release]` block and no `[[deployments]]` rows shows no Releases or Deployments
item in the navigation, and exposes no release or deploy action — the features
are entirely dormant. This page covers what you get once you configure them.

The two solve different problems: a **release** is one versioned artifact; a
**deployment** is a service tracking a branch. They're independent — configure
one, the other, or both.

## Releases

The only provider is `git-tag`, and it deliberately stays small: after a green
set of passing checks, RepoOS bumps the version, pushes the branch and an annotated tag, and
**your CI does the actual build and publish**. RepoOS never builds the GitHub
release itself, which is what lets it stay dependency-free.

```toml
[release]
enabled     = true
provider    = "git-tag"
branch      = "main"          # example: replace with your release branch
versionFile = "package.json"  # example: the file holding your semantic version
tagPrefix   = "v"
remote      = "origin"
```

The full field list, defaults, and examples are in the
[repoos.toml reference](/configuration#releases-deployments-and-distribution)
and the [Environment and secrets](/environment-and-secrets) guide. The
`Releases` nav item only appears when `release.enabled = true`.

### The Releases page

It shows the version RepoOS would ship, the latest tag and when/where it was cut,
the latest *stable* tag, and any blockers. A release can't be cut unless:

* the checkout is on the configured `branch`,
* the working tree is clean, and
* the tag doesn't already exist.

Cutting a release asks you to confirm the exact tag, then runs the same checks as
task close-out: bump the version file, write and commit the release changelog
(`release-history.json` and `CHANGELOG.md`), rebuild, run `repoos check`, push
the branch, create the annotated tag, and push the tag. If the tag push fails,
the local tag is removed so nothing is left half-done. Pushing the tag is what
triggers your CI workflow.

The release record is written before the checks, so the docs site, the
`/changelog` page, `repoos.org` and the generated llms files all show the new
version and date as soon as they are built — and the GitHub release body is
read from the same entry. When you have `[[deployments]]` rows, the cut also
fast-forwards and pushes each deployed branch that is behind the release branch
(for example `prod` ← `main`), which is what publishes the docs and landing
sites before anyone visits them. A failed deploy is reported but does not fail
the release: the tag is already public, so retry that branch from the
Deployments page.

Tags with a suffix — `v1.2.0-beta.1`, `v1.2.0-rc.1` — are treated as
**prereleases**: they're flagged as such on GitHub so `GET /releases/latest`
never hands a prerelease to a stable user, and they don't hide the last stable
tag in the UI.

### The Cut a release panel

Cutting a release opens a **side panel** on the Releases page, styled like the
task drawer. Closing it — the × in the upper right, a click on the scrim, or
**Cancel** — never resets anything: your typed version and notes, the run log,
and any in-flight cut or draft are all still there when you reopen. While a
cut runs, the page's button relabels to **View progress** and reopens the
panel against the live run. The form only clears once a cut **succeeds**, so
the next cut starts fresh.

The panel states **Currently published** and **Suggested next**, then offers
one band with two ways to pick the version:

* **OR · Cut Next** applies the suggested version for you — the everyday
  "next patch" release needs no typing at all. Publish then behaves exactly as
  if you had typed that version, tag prefix and prerelease rules included.
* Typing into **New version** stays the path for everything else (`1.4.0`,
  `2.0.0-rc.1`, `1.4.0-beta.1`, …); the `→` preview always shows the exact tag
  that will be pushed. Cut Next is a shortcut, never a requirement.

**Release notes** are optional. **Generate with AI** starts a **tracked
draft run**: the button returns at once and the field shows "Drafting…" while
the agent works, so closing the panel mid-run loses nothing — reopening shows
the draft in progress, and the draft is dropped into the editable text area
the moment the run finishes. Clicking Generate while a draft is already being
generated can't start a duplicate run.

Successful drafts are saved to `<cacheDir>/release-notes.json`
(`.repoos/release-notes.json` by default — derived state, gitignored, safe to
delete), keyed by the **release-relevant commits** in the range. Task-file
bookkeeping commits (`docs(NNNN): add task`, status flips) touch only the work
dir, so they don't count: a saved draft survives them and Generate stays
instant. Generate again after a failed cut and RepoOS reuses that saved text
instead of spending another minute or two on the agent — the panel says
**Reused saved notes**. A source commit, a new release tag, or deleting the
file simply means the next generate runs the agent again.

If a draft was generated but the cut then failed, it stays visible: a **Saved
AI draft not yet released** card sits below the notes field with how long ago
it was generated, how far `main` has moved since (and what changed), the full
text, and a **Use these notes** button that puts it back in the editor. That
means a retry after a failed cut — including one where you only tweaked
`repoos.toml` in between — doesn't spend another minute or two on the agent.
The card disappears once a cut succeeds and the draft has shipped.

### Distribution destinations: "Published to"

A release is one versioned artifact; the places users install it from are its
**distribution destinations**. When you declare any, the Releases page grows a
**Published to** section listing each one, the commands to install from it, and
whether the version it currently publishes matches the release you're looking
at. This is deliberately separate from [Deployments](#deployments) — those are
environments *you* ship to, these are registries *users* install from.

```toml
[[distribution]]
name    = "npm"
kind    = "npm"                        # npm | homebrew | github-release | custom
package = "@scope/name"
url     = "https://www.npmjs.com/package/@scope/name"
install = ["npm install -g @scope/name", "bun add -g @scope/name"]

[[distribution]]
name       = "Homebrew"
kind       = "homebrew"
versionUrl = "https://raw.githubusercontent.com/owner/homebrew-tap/main/Formula/name.rb"
url        = "https://github.com/owner/homebrew-tap/blob/main/Formula/name.rb"
install    = ["brew install owner/tap/name"]
```

Each entry is independent: `name` is required, `url` is the source link you
click through to (it may contain `{tag}` or `{version}`), and `install` is a
one-line list of commands — every one gets its own copy button. Keep the array
on a single line; RepoOS's config reader doesn't accept multi-line arrays.

For the version summary, `kind` picks a public lookup (npm registry, a Homebrew
formula file, a GitHub release, or a `custom` URL + regex). The check is
time-bounded and per channel: a slow registry shows as **Check failed** on its
own row rather than breaking the page, and a channel with no `kind` simply
reads **Unverified** — RepoOS never claims a channel is current when it can't
confirm the version. No registry token or credential is ever sent to the
browser. CI finishing means it handed the release to each destination; npm,
Homebrew, and CDNs can take several more minutes to make it installable. The
page re-checks when RepoOS finishes cutting the tag, and **Check again** refreshes
the downstream status whenever you want.

Leaving the section out entirely keeps the plain Releases page — no empty
marketing box.

## Deployments

Deployments model a grid of services: one `[[deployments]]` row per
service-and-branch, where each branch is an environment. The names below are
examples: map your own development, staging, and production branches.

```toml
[[deployments]]
name          = "Dashboard (prod)"
branch        = "prod"
provider      = "cloudflare-workers"
url           = "https://app.example.com"
dashboard_url = ""            # optional; paste from the provider dashboard
subdir        = "app"         # optional; scopes freshness to this tree

[[deployments]]
name     = "Dashboard (dev)"
branch   = "main"
provider = "cloudflare-workers"
url      = "https://main-app.example.workers.dev"
subdir   = "app"
```

`url` is the live link, `provider` is a plain label for you, and `subdir` scopes
the "last push" lookup to the part of the repo that row deploys. `dashboard_url`
is optional and manual — RepoOS doesn't call any provider API.

### The Deployments page

For each row it links to the live URL, shows the last commit that touched the
row's `subdir`, and shows branch health against `origin` (how far ahead or
behind). Deployment is plain git: **Deploy** pushes the branch to `origin`, and
the git-connected provider (for example Cloudflare Workers) rebuilds from that
push. RepoOS doesn't confirm whether the provider's build succeeded — the push is
the end of its involvement.

A deploy fast-forwards first when it safely can: if a branch is strictly behind
another configured branch (a `prod` row behind `main`), RepoOS fast-forwards the
local branch to that source before pushing. There is no force-push path — if the
branches have diverged, the deploy fails loudly with git's own refusal rather
than overwriting anything. A checkout with uncommitted changes can't deploy at
all.

Both surfaces read from the **primary checkout**, even when the server is serving a
preview from a task worktree, because branch health and dirty state belong to the
checkout you actually manage.

Each configured target also has a **Local environment** selector. This saves a
`deployment:<target-name>` binding in RepoOS's local environment metadata; it
does not inject values into the RepoOS server, change a provider destination,
or upload/synchronize variables. The selected name is a record of which
project environment the deployment is intended to consume. RepoOS currently
has no provider variable publish/sync operation, so provider-managed variables
must be configured in that provider's own tools. Selecting `production` never
propagates production values. Selecting **Not selected** clears the binding.
If an existing binding points to an environment that has been removed or
renamed, RepoOS marks it as unknown and refuses to push that branch until the
binding is repaired or cleared.

This binding is a local record of intent only. Deploying pushes the configured
git branch; RepoOS does not upload or synchronize environment values to the
provider, and selecting `production` does not grant production credentials to
the deploy process. Configure provider runtime variables in the provider's
own tools. See [Environment and secrets](/environment-and-secrets) for how
local dev, preview, and check consumers receive explicit grants.
