# Project releases

Projects can distribute versioned software releases from their Git repositories.
Each release has a unique version, title, Markdown changelog, optional source
commit, independent public/private visibility, draft/published status, prerelease flag and up to 100 attached binary files.
Release versions are labels: creating a release does not create a Git tag or
deploy an app. A repository supports up to 1,000 retained release records.

The canonical project repository is selected by default. Supply `repo_id` to
select another Git repository belonging to the same project. Releases require
initialized Rogue-hosted Git storage; legacy providers and managed system
repositories do not expose this feature.

## CLI workflow

SDK/CLI 0.24.0 and API contract 2.10.0 support independent release visibility:

```sh
rog releases list PROJECT_UUID
rog releases create PROJECT_UUID v1.0.0 --changelog CHANGELOG.md --commit COMMIT_SHA --private --yes
rog releases upload PROJECT_UUID RELEASE_UUID --file ./app-linux.tar.gz --yes
rog releases get PROJECT_UUID RELEASE_UUID
rog releases publish PROJECT_UUID RELEASE_UUID --public --yes
rog releases download PROJECT_UUID RELEASE_UUID ASSET_UUID --out ./download.tar.gz
rog repos usage
```

`create` returns a draft release UUID. `upload` calculates the complete file's
SHA-256 while streaming, prepares an asset with a stable UUID, sends chunks, and
prints the final asset record. Downloads verify size and SHA-256 and refuse to
overwrite an existing output. Use `--repo REPO_UUID` for an additional repository.
Use `--public` or `--private` with create, edit or publish. Publishing without a
visibility flag preserves its setting. `edit ... --private --yes` revokes public
reads without changing draft status. Read `rog releases help` for all commands.

After an interrupted upload, inspect `get`, then explicitly rerun `upload` with
the same file and `--asset-id ASSET_UUID --yes`. It resumes at `uploaded_bytes`.
There are no automatic mutation retries. Supply `--release-id UUID` to recover an
uncertain create. Delete unwanted partial uploads with `delete-file`; they retain
their occupied quota until removed. Files on a published release cannot change;
first set the release to draft, make the intended changes, then publish again.

## Shared Git storage quota

The per-repository ceiling covers **reachable Git history + actual release binary
bytes**, including incomplete uploads. The account ceiling sums that combined
usage across owned repositories. Managed Skills Git history still counts.

| Plan | Per repository, including releases | Across owned repositories |
| --- | --- | --- |
| Free | 100 MiB | 200 MiB |
| Pro | 1 GiB | 5 GiB |
| Active administrator | Unlimited | Unlimited |

Git's 5 MiB Free / 24 MiB Pro per-file limits apply to Git blobs. Release files
can use the repository's remaining capacity. HTTP uploads use chunks of at most
32 MiB, regardless of the total binary size. Preparation checks capacity but
does not reserve it; every chunk is checked against fresh actual usage.

`get_git_storage_usage` / `GET /api/v1/me/git-storage` return `size_bytes` for
combined usage and `git_size_bytes` / `release_size_bytes` for the breakdown,
both for the account and each repository. A deleted release/file frees its bytes
after physical removal. Deleting a repository removes its releases too. Changing
draft/publication status does not free storage. Unknown storage fails closed.

## MCP and SDKs

The main MCP publishes `list_project_releases`, `get_project_release`,
`create_project_release`, `update_project_release`, `delete_project_release`,
`prepare_project_release_asset` and `delete_project_release_asset`. All SDKs
generate these helpers from the same contract. The project-scoped MCP includes
the list and get tools with the project already selected.

```js
const release = await client.create_project_release({
  id: projectId,
  release: { version: "v1.0.0", changelog_md: "## Changes\nFirst release." }
});
const prepared = await client.prepare_project_release_asset({
  id: projectId, release_id: release.id,
  asset: { asset_id: stableAssetId, name: "app.tar.gz", size_bytes: totalSize, sha256 }
});
// Use the authenticated SDK HTTP transport for raw chunks; see the REST table.
// Inspect uploaded_bytes after an uncertain response before explicitly resuming.
await client.update_project_release({
  id: projectId, release_id: release.id, release: { draft: false }
});
```

Binary transfers use HTTP rather than embedding base64 in MCP JSON. Each SDK's
same-origin HTTP transport can upload raw bytes and download the returned URL;
the CLI provides the complete local-file workflow. Never forward credentials to
a different origin or automatically execute downloaded binaries.

## REST contract

All routes start with `/api/v1/projects/{id}/releases`. Most responses use
`{data,meta}`; file downloads return raw bytes. The optional `repo_id` query
selects a repository; creation accepts it in the JSON body.

| Method | Suffix | Action |
| --- | --- | --- |
| GET | (collection) | List with `cursor` and `limit` (1–100) |
| POST | (collection) | Create metadata; optional stable `release_id` |
| GET / PATCH / DELETE | `/{releaseId}` | Read, edit/publish, permanently delete |
| POST | `/{releaseId}/assets` | Prepare `name`, `size_bytes`, `sha256`, optional `asset_id` |
| PUT | `/{releaseId}/assets/{assetId}?offset=N&size=N` | Stream exactly the declared raw bytes, at most 32 MiB |
| GET | `/{releaseId}/assets/{assetId}` | Download a ready file |
| DELETE | `/{releaseId}/assets/{assetId}` | Delete a draft file or partial upload |

Each upload response includes `uploaded_bytes` and `state` (`uploading` or
`ready`). A stale offset returns 409. Exceeded quota, incorrect lengths or failed
checksums return 400; a checksum failure discards the partial file and resets its
offset to zero. Finalization verifies the hash before exposing the download.
Pending uploads prevent publication. Portable filenames contain ASCII letters,
digits, `.`, `_`, `+` or `-`, begin with a letter/digit and are at most 200 characters.

Each release has `visibility: public|private`, separate from `draft`. A public,
published release and its files are readable anonymously through their UUID URLs,
even inside a private project/repository. This never exposes Git, project details,
issues, or other private releases. Private releases and all drafts require project
membership plus repository read access. New releases default to private unless
both their source project and repository are public. Explicitly choose public
when distributing software from private source. Omitted visibility on updates
preserves the setting. Legacy records created before contract 2.10 keep inherited
access until an owner edits the record; choose visibility explicitly when migrating.
Writes require an active account, `repos:write`, membership and repository access.
Suspended owners and deleted repositories hide releases. Knowing a private release
UUID does not grant access. Public release documents omit private project metadata.
Metadata and changelogs are untrusted user content. Responses never cache private
files; downloads use attachment disposition, binary content type and `nosniff`.

Public discovery is available at `/projects/{id}/releases` and
`/projects/{id}/releases/{releaseId}`, with changelogs and file download links.
The administrator's repository inspector includes drafts, checksums and uploaded
byte counts without executing files.

## Operations and verification

The private Git origin stores release metadata and files in
`<repo UUID>.releases` alongside `<repo UUID>.git`. Keep both with the repository
volume during any future storage migration. Independent app sharing uses database
migration 0054; release visibility lives in origin metadata. No new bucket is needed. Deploy `git-releases.mjs` with the other origin modules before deploying
the Worker; old origins cannot serve releases or report their storage breakdown.
The existing origin storage-write lock serializes Git pushes, source commits,
skill writes, upload chunks, release edits and deletions. Cold starts recount
actual binaries; partial files and upload leftovers continue to count.

Run `npm run verify:project-releases`, `verify:git-limits`,
`verify:git-account-storage`, types and lint. Tests use an isolated database and
temporary Git service. Follow the guarded [operations runbook](OPERATIONS.md) and
[Git service runbook](../infra/git/README.md) for coordinated deployment.
