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.
[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 and the 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 — those are environments you ship to, these are registries users install from.
[[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.
[[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 for how local dev, preview, and check consumers receive explicit grants.