Skip to content
GitmoapdocsBeta
Guide· How do I…?

Releases

Overview

Cut a release when a commit stops being a commit and becomes a version other people depend on. A release is a title, a set of markdown notes and a set of downloadable files, all attached to a git tag — so whoever consumes your project gets one page saying what changed and one link to download the binary, instead of a 40-character hash and an invitation to build it themselves.

It works for people and for machines in the same way. Anyone who can read the repository can fetch the newest published version from one stable URL that never changes as you ship, and a build can cut the release, attach the artifacts it just produced and publish it in four calls, holding a token that can do nothing else.

Quick start

Quick start3 steps

Publish a release

You’ll need Developer on the repository.

Start at Releases in the side navigation.

  1. Choose New release.

  2. Pick a tag, or type a new one and the branch to cut it from. Write the notes — the editor previews the markdown as you go.

  3. Choose Publish release, then attach any binaries from the release's own page.

Before you start

  • A repository, and a commit worth naming.A release always points at a tag. When the tag already exists you attach to it; when it does not, name the branch or commit to cut it from and the tag is created for you as part of the same call.
  • Access on the repository.Reporter to read a release and download from it, and Developer to create, edit, publish and delete releases and their files. A public repository serves its published releases to anyone, including people who are not signed in; an internal one serves them to anyone signed in; a private one serves them only to its members.
  • A token with the right scope, for the API and the CLI: release:read to list, read, download and fetch source archives; release:create to create a release and register a file on it; release:update to edit or publish; release:delete to remove a release or one of its files. Scopes are chosen when a token is minted and cannot be added afterwards, so a job that only ships builds should carry release:create and release:update and nothing else.
  • The repository's id.The API paths are addressed by id. The CLI's --repo-id also accepts an owner/repo path, so you can hand it something a human typed.
  • An HTTP client that can PUT a file, if you are attaching binaries. The bytes never travel through the API — you register the file, get a short-lived upload URL back, and send the bytes straight to storage with curl or the equivalent.
  • Room in the storage budget.Attached files are stored bytes and are charged to the namespace budget the repository's whole group shares. See Limits.

How it works

A release annotates a tag, and the tag outlives it

The tag is the release's identity. There is exactly one release per tag, it is addressed by tag name in every URL, and the tag name cannot be changed afterwards — a release renamed is a release deleted and cut again.

The two live at different layers, which is what makes the following true and occasionally surprising:

  • Deleting a release leaves the tag. The tag is a real git ref that clones and checkouts still resolve; the release was the notes and files layered on top of it. Removing the tag as well is a separate deletion in the repository's tag list.
  • Deleting or moving the tag does not delete the release. The release keeps its notes and its files, and keeps reporting the commit the tag pointed at when it was cut. Source archives do not follow that snapshot — they are built from the tag as it stands now, so deleting the tag makes them answer 404, and moving it makes them serve the new commit while the release still names the old one.
  • Force-moving a tag does not rewrite history on the release. The commit shown on the release is a snapshot taken at the moment you cut it. Cut a new release rather than moving a tag under a published one.

Draft, pre-release and "latest"

Two independent flags decide who sees a release and whether it answers as the newest one:

StateWho can see itIn the repository's release listAnswers the latest-release lookup
DraftDeveloper and above onlyonly for Developer and above, and only when drafts are explicitly requestedno
Published, pre-releaseeveryone the repository's visibility allowsyesno
Publishedeveryone the repository's visibility allowsyesyes — the newest one wins

A draft answers 404 to everyone below Developer, on a public repository as much as a private one, so you can stage release notes in the open without leaking them. Publishing is an edit that clears the draft flag, and it stamps the moment of publication once. Turning a published release back into a draft and publishing it a second time keeps that first stamp, so a version you pull and restore does not jump ahead of everything shipped in between.

The latest-release lookup orders by when a release was published, not by when it was created and not by version order. Publishing a draft you wrote three weeks ago makes it the newest release today. Flag a version as a pre-release when you want it downloadable and listed but never served as the newest — release candidates and nightlies are the reason the flag exists.

The release list orders by a different axis: when each release was created. The two agree as long as you publish in the order you create, and diverge the moment you publish an old draft — it becomes the newest release without moving up the list. So the release the latest lookup serves is not always the one at the top of a page of releases.

Attaching a file is two steps, and the budget is taken at the first

1. register 2. upload 3. confirmed
name, type, size ──▶ PUT to the signed URL ──▶ state: available
storage reserved bytes go straight to download link appears
state: uploading storage, never via the API downloads start working

Registering a file reserves its declared size against the namespace budget and hands back a URL signed for 15 minutes and bound to the exact size and content type you declared. The file becomes downloadable only once the upload is confirmed, a moment or two after your PUT returns — until then it is listed on the release with no download link, and downloading it answers 409.

Two consequences worth knowing before you meet them. The reservation is taken against the size you declared, and it is held whether or not you ever upload the bytes — deleting the file (or the whole release) is what gives it back. And because the size is signed into the URL, a PUT whose body is a different length is rejected by storage rather than quietly stored.

Every edit carries the version you last read

A release's version is a concurrency counter, not the version of your software — the software's version is the tag name. Read the release, send that number back with your edit, and an edit that lands on top of somebody else's is refused with 409 instead of silently overwriting their notes. Re-read the release and re-apply your change.

How to

Cut a release from a tag

Give the tag name and, when the tag does not exist, the branch or commit to create it from. The title defaults to the tag name, and the notes are markdown. Create it as a draft when you want to write the notes before anyone sees them; create it published when the version is already out.

Open the repository and choose Releases in the side navigation, then New release. Pick an existing tag from the tag box, or type a new tag name and choose the branch or commit it should be cut from. Fill in the title and the notes — the editor previews the markdown as you write. Save as draft keeps it invisible to everyone below Developer; Publish release makes it live immediately.

Terminalbash
gitmoap releases create --repo-id <id> --tag-name <tag name>
curlbash
curl -X POST https://api.gitmoap.example/repositories/{repo_id}/releases \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "tag_name": "<string>" }'

If the tag already exists, anything you pass as the target is ignored — the release attaches to the tag exactly where it points. If the tag does not exist and you name no target, the call is refused with 404 rather than inventing one.

When the call does create the tag, it creates the tag first and writes the release second. A refusal at that second step — a release already exists for the name, or the repository is at its release cap — therefore leaves the new tag behind. The tag is harmless and reusable on a retry, so delete it from the repository's tag list only if you do not want it.

Edit the notes, or publish a draft

Editing covers the title, the notes and both flags. Publishing a draft is the same call with the draft flag cleared, and it stamps the moment of publication — which is the moment that decides the latest-release ordering.

Open the release and choose Edit. Change the title or the notes and choose Save. On a draft, Publish release clears the draft flag in the same action; on a published release, the Pre-release toggle takes it out of the latest-release lookup without hiding it.

Terminalbash
gitmoap releases update --repo-id <id> --tag-name <id> --version <n>
curlbash
curl -X PATCH https://api.gitmoap.example/repositories/{repo_id}/releases/{tag_name} \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": "<int64>" }'

Send the version you got when you read the release. Omitting a field leaves it alone rather than clearing it. A release you have just created hands its version back in the create response, so a build that creates a draft and publishes it a moment later needs no extra read in between.

Attach a file to a release

Register the file first — its name, its media type and its exact size in bytes — then send the bytes to the URL you get back. Names are unique within a release, so checksums.txt on v1.4.0 and on v1.5.0 are two different files and neither collides.

Open the release and choose Edit, then drag the file onto the Assets area or choose Attach a file. The browser uploads it directly to storage and the progress bar is the real upload; the file appears in the list as soon as the upload is confirmed. Uploads continue while you keep editing the notes.

Terminalbash
gitmoap releases create-asset \
--repo-id <id> \
--tag-name <id> \
--content-type <content type> \
--name <name> \
--size <n>
curlbash
curl -X POST https://api.gitmoap.example/repositories/{repo_id}/releases/{tag_name}/assets \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "content_type": "<string>", "name": "<string>", "size": "<int64>" }'

An asset name is 1–255 characters of letters, digits, ., _, + and -. Slashes are not allowed, so upload linux-arm64.tar.gz, not dist/linux-arm64.tar.gz. Send the same content type on the PUT that you declared at registration, and let the client set the length from the file — a mismatch of either is refused by storage.

List a repository's releases

Newest-created first, paged. The list carries each release's title, notes, tag and commit, and omits its files: read a single release when you want its download links.

Open the repository and choose Releases. Published releases are listed newest first, each showing its title, its tag and when it was published, with the newest published one badged as Latest. Show drafts appears for Developer and above and folds unpublished releases into the same list, marked as drafts. Choose a title to open the release and see its downloads.

Terminalbash
gitmoap releases list --repo-id <id>
curlbash
curl -X GET https://api.gitmoap.example/repositories/{repo_id}/releases \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Asking for drafts as somebody below Developer is not an error — the flag is ignored and you get the published slice, which is the same answer you would get without it.

Look up one release by its tag

Open the repository, choose Releases, and choose the release's title. The release page carries the notes, the commit the tag pointed at when it was cut, every attached file with its size, and the source archive links.

Terminalbash
gitmoap releases get --repo-id <id> --tag-name <id>
curlbash
curl -X GET https://api.gitmoap.example/repositories/{repo_id}/releases/{tag_name} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

This is the read that includes the files, so it is the one to use when you want download links rather than a summary.

Point an installer at the newest version

One URL that always resolves to the newest published, non-pre-release version — the thing to put in an install script so it does not need updating every time you ship.

Open the repository and choose Releases. The newest published release sits at the top of the list with a Latest badge; drafts and pre-releases never carry it.

Terminalbash
gitmoap releases get-latest --repo-id <id>
curlbash
curl -X GET https://api.gitmoap.example/repositories/{repo_id}/releases/latest \
-H "Authorization: Bearer $GITMOAP_TOKEN"

A repository with no published, non-pre-release release answers 404 here, which is the correct answer for a project whose only releases so far are release candidates.

Download an attached file

The download answers with a redirect to a short-lived signed URL, which is what curl -L and a browser both follow without further thought. A single-page web app should not rely on following that redirect: it changes origin, and whether a browser follows it depends on which of the app's request headers survive the hop. Those clients ask for the URL as JSON instead, and navigate to it.

Open the release and choose a file in the Assets list. The download starts immediately, under the file's own name.

Terminalbash
gitmoap releases download-asset \
--repo-id <id> \
--tag-name <id> \
--asset-id <id>
curlbash
curl -X GET https://api.gitmoap.example/repositories/{repo_id}/releases/{tag_name}/assets/{asset_id} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

The signed URL it hands out lasts 5 minutes, so mint it at the point of download rather than storing it. A file whose upload has not been confirmed answers 409 rather than handing back an empty download.

Download the source at a release's tag

A .tar.gz or .zip of the repository at the release's tag, built for you — no clone, no history, just the tree. It is addressed through the release, so it exists for tags that have a release and not for bare tags.

Open the release and choose Source (tar.gz) or Source (zip) beneath the attached files.

Terminalbash
gitmoap releases get-archive --repo-id <id> --tag-name <id>
curlbash
curl -X GET https://api.gitmoap.example/repositories/{repo_id}/releases/{tag_name}/archive \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Ask for tar.gz or zip; leave the format out and you get tar.gz, and anything other than those two is refused with 400. The format is a query parameter — …/archive?format=zip — not an extension on the path, and the tarball_url and zipball_url a release carries are spelled that way, so following one works without rewriting it. Archives are built once per commit and reused, which makes the second request for a version much faster than the first — and means two tags on the same commit share one archive.

Remove a file from a release

Warning

Deleting an attached file destroys the bytes. Nothing keeps a copy, and the download links other people have already published — in an install script, a changelog, somebody's bookmark — start failing immediately. Its storage goes back to the namespace budget at once. Re-uploading a file with the same name gives it a new id, so any URL that named the old one stays broken.

Open the release, choose Edit, and choose the bin icon at the end of the file's row in the Assets list. The confirmation names the file and its size; Delete is immediate.

Terminalbash
gitmoap releases delete-asset --repo-id <id> --tag-name <id> --asset-id <id>
curlbash
curl -X DELETE https://api.gitmoap.example/repositories/{repo_id}/releases/{tag_name}/assets/{asset_id} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

This is also the way to clear a registration whose upload never happened: the file sits at uploading forever, holding its declared bytes against the budget, until you delete it.

Delete a release

Warning

Deleting a release destroys its notes and every file attached to it, in one irreversible step. The git tag survives, so clones and checkouts of that version keep working, but the notes and the downloads are gone and nothing restores them. Delete the release only when you intend to take its downloads off the internet; hide a release you merely want out of sight by turning it back into a draft instead.

Open the release and choose Edit, then Delete release. The confirmation names the tag and the number of files that go with it; Delete is immediate.

Terminalbash
gitmoap releases delete --repo-id <id> --tag-name <id>
curlbash
curl -X DELETE https://api.gitmoap.example/repositories/{repo_id}/releases/{tag_name} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Deleting the tag as well is a separate action in the repository's tag list, and it is worth thinking about separately: consumers may have pinned that tag.

Reference

10 endpoints

POST/repositories/{repo_id}/releasesrelease:create

Create a release for a tag, creating the tag from a target ref when absent

create_releasePOST /repositories/{repo_id}/releasesOpen in the API reference

Token scope

release:create

Counts against

releases_per_repo

Send

Path
  • repo_iduuidRepository ID
Body
  • bodyobjectoptional
  • draftbooleanoptional
  • nameobjectoptional
  • prereleasebooleanoptional
  • tag_namestring
  • targetobjectoptional

Refuses

  • 400Invalid request
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository or target not found
  • 409Release already exists
  • 429The repository's release limit is reached

Receive

  • assets[]object14 fields
    • asset_iduuid
    • content_typestring
    • created_atstring
    • download_urlobjectoptionalWhere to download the file.
    • namestring
    • repository_iduuid
    • sizeint64
    • stateavailableuploadingWhether a release file has been uploaded: uploading (an upload URL was issued, but the upload has not been confirmed yet) or available.
    • tag_namestring
    • updated_atstring
    • uploaderobject4 fields
      • avatar_urlobjectoptional
      • emailstring
      • idstring
      • usernamestring
  • authorobject4 fields
    • avatar_urlobjectoptional
    • emailstring
    • idstring
    • usernamestring
  • bodyobjectoptional
  • commit_shastring
  • created_atstring
  • draftbooleanoptionalWhether the release is an unpublished draft, visible only to people with Developer access or above.
  • namestring
  • prereleasebooleanoptionalWhether the release is marked as a pre-release. A pre-release is never returned as the latest release.
GET/repositories/{repo_id}/releases/{tag_name}release:read

Get a release by its tag

get_releaseGET /repositories/{repo_id}/releases/{tag_name}Open in the API reference

Token scope

release:read

Send

Path
  • repo_iduuidRepository ID
  • tag_namestringTag name

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Release not found

Receive

The same shape create_release returns — 8 fields. Go to it

Show it here
  • assets[]object14 fields
    • asset_iduuid
    • content_typestring
    • created_atstring
    • download_urlobjectoptionalWhere to download the file.
    • namestring
    • repository_iduuid
    • sizeint64
    • stateavailableuploadingWhether a release file has been uploaded: uploading (an upload URL was issued, but the upload has not been confirmed yet) or available.
    • tag_namestring
    • updated_atstring
    • uploaderobject4 fields
      • avatar_urlobjectoptional
      • emailstring
      • idstring
      • usernamestring
  • authorobject4 fields
    • avatar_urlobjectoptional
    • emailstring
    • idstring
    • usernamestring
  • bodyobjectoptional
  • commit_shastring
  • created_atstring
  • draftbooleanoptionalWhether the release is an unpublished draft, visible only to people with Developer access or above.
  • namestring
  • prereleasebooleanoptionalWhether the release is marked as a pre-release. A pre-release is never returned as the latest release.
GET/repositories/{repo_id}/releasesrelease:read

List a repository's releases, newest first

list_releasesGET /repositories/{repo_id}/releasesOpen in the API reference

Token scope

release:read

Send

Path
  • repo_iduuidRepository ID
Query
  • limitint32optionalPage size (1-100)
  • next_tokenstringoptionalPagination cursor
  • include_draftsbooleanoptionalInclude drafts (writers only)

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Repository not found

Receive

  • items[]object17 fields
    • authorobject4 fields
      • avatar_urlobjectoptional
      • emailstring
      • idstring
      • usernamestring
    • bodyobjectoptional
    • commit_shastring
    • created_atstring
    • draftbooleanoptionalWhether the release is an unpublished draft, visible only to people with Developer access or above.
    • namestring
    • prereleasebooleanoptionalWhether the release is marked as a pre-release. A pre-release is never returned as the latest release.
    • published_atobjectoptionalSet when the release leaves draft.
    • repository_iduuid
    • tag_namestring
    • tarball_urlobjectoptionalWhere to download the tagged source as a .tar.gz archive.
    • updated_atstring
    • versionint64optionalIncreases with every change. Send it with an update so that a change made since you read the release is not overwritten.
    • zipball_urlobjectoptionalWhere to download the tagged source as a .zip archive.
  • next_tokenobjectoptional
GET/repositories/{repo_id}/releases/latestrelease:read

Get the newest published, non-prerelease release

get_latest_releaseGET /repositories/{repo_id}/releases/latestOpen in the API reference

Token scope

release:read

Send

Path
  • repo_iduuidRepository ID

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404No published release

Receive

The same shape create_release returns — 8 fields. Go to it

Show it here
  • assets[]object14 fields
    • asset_iduuid
    • content_typestring
    • created_atstring
    • download_urlobjectoptionalWhere to download the file.
    • namestring
    • repository_iduuid
    • sizeint64
    • stateavailableuploadingWhether a release file has been uploaded: uploading (an upload URL was issued, but the upload has not been confirmed yet) or available.
    • tag_namestring
    • updated_atstring
    • uploaderobject4 fields
      • avatar_urlobjectoptional
      • emailstring
      • idstring
      • usernamestring
  • authorobject4 fields
    • avatar_urlobjectoptional
    • emailstring
    • idstring
    • usernamestring
  • bodyobjectoptional
  • commit_shastring
  • created_atstring
  • draftbooleanoptionalWhether the release is an unpublished draft, visible only to people with Developer access or above.
  • namestring
  • prereleasebooleanoptionalWhether the release is marked as a pre-release. A pre-release is never returned as the latest release.
PATCH/repositories/{repo_id}/releases/{tag_name}release:update

Update a release's title, notes, or draft/prerelease flags

update_releasePATCH /repositories/{repo_id}/releases/{tag_name}Open in the API reference

Token scope

release:update

Send

Path
  • repo_iduuidRepository ID
  • tag_namestringTag name
Body
  • bodyobjectoptional
  • draftobjectoptional
  • nameobjectoptional
  • prereleaseobjectoptional
  • versionint64The release's version as you last read it. The update is refused if the release has changed since.

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Release not found
  • 409Version conflict

Receive

The same shape create_release returns — 8 fields. Go to it

Show it here
  • assets[]object14 fields
    • asset_iduuid
    • content_typestring
    • created_atstring
    • download_urlobjectoptionalWhere to download the file.
    • namestring
    • repository_iduuid
    • sizeint64
    • stateavailableuploadingWhether a release file has been uploaded: uploading (an upload URL was issued, but the upload has not been confirmed yet) or available.
    • tag_namestring
    • updated_atstring
    • uploaderobject4 fields
      • avatar_urlobjectoptional
      • emailstring
      • idstring
      • usernamestring
  • authorobject4 fields
    • avatar_urlobjectoptional
    • emailstring
    • idstring
    • usernamestring
  • bodyobjectoptional
  • commit_shastring
  • created_atstring
  • draftbooleanoptionalWhether the release is an unpublished draft, visible only to people with Developer access or above.
  • namestring
  • prereleasebooleanoptionalWhether the release is marked as a pre-release. A pre-release is never returned as the latest release.
DELETE/repositories/{repo_id}/releases/{tag_name}release:delete

Delete a release and its assets (the git tag is left intact)

delete_releaseDELETE /repositories/{repo_id}/releases/{tag_name}Open in the API reference

Token scope

release:delete

Send

Path
  • repo_iduuidRepository ID
  • tag_namestringTag name

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Release not found
  • 409The request conflicts with the resource's current state; `reason` says how

Receive

No body.

POST/repositories/{repo_id}/releases/{tag_name}/assetsrelease:create

Register a release asset and get a presigned upload URL

create_release_assetPOST /repositories/{repo_id}/releases/{tag_name}/assetsOpen in the API reference

Token scope

release:create

Counts against

namespace_storage

Send

Path
  • repo_iduuidRepository ID
  • tag_namestringTag name
Body
  • content_typestring
  • namestring
  • sizeint64The file's exact size in bytes. An upload of any other size is rejected.

Refuses

  • 400Invalid request
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Not found — or found and private, which is masked as absent
  • 409Asset name already exists
  • 429Storage quota exceeded

Receive

  • asset_iduuid
  • content_typestring
  • created_atstring
  • namestring
  • sizeint64
  • stateavailableuploadingWhether a release file has been uploaded: uploading (an upload URL was issued, but the upload has not been confirmed yet) or available.
  • upload_urlstringA presigned URL to upload the file to with an HTTP PUT. It accepts only a file of the declared size, and expires shortly.
GET/repositories/{repo_id}/releases/{tag_name}/assets/{asset_id}release:read

Download a release asset.

download_release_assetGET /repositories/{repo_id}/releases/{tag_name}/assets/{asset_id}Open in the API reference

Token scope

release:read

Send

Path
  • repo_iduuidRepository ID
  • tag_namestringTag name
  • asset_iduuidAsset ID
Query
  • redirectbooleanoptionalWhen false, return 200 { url } instead of a 302

Refuses

  • 302Redirect to a short-lived download link for the asset
  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Asset not found
  • 409Asset upload has not completed yet

Receive

  • urlstringA presigned URL to download the file from. It expires shortly.
DELETE/repositories/{repo_id}/releases/{tag_name}/assets/{asset_id}release:delete

Delete a release asset and its uploaded file

delete_release_assetDELETE /repositories/{repo_id}/releases/{tag_name}/assets/{asset_id}Open in the API reference

Token scope

release:delete

Send

Path
  • repo_iduuidRepository ID
  • tag_namestringTag name
  • asset_iduuidAsset ID

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Asset not found
  • 409The request conflicts with the resource's current state; `reason` says how

Receive

No body.

GET/repositories/{repo_id}/releases/{tag_name}/archiverelease:read

Download a source archive of a release's tag

get_release_archiveGET /repositories/{repo_id}/releases/{tag_name}/archiveOpen in the API reference

Token scope

release:read

Send

Path
  • repo_iduuidRepository ID
  • tag_namestringTag name
Query
  • formatstringoptionalArchive format: tar.gz (default) or zip
  • redirectbooleanoptionalWhen false, return 200 { url } instead of a 302 (for clients that cannot follow a cross-origin redirect)

Refuses

  • 302Redirect to a signed download link for the archive
  • 400Unsupported archive format
  • 401Unauthenticated — no credential, or an invalid one
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Release or tag not found

Receive

The same shape download_release_asset returns — 1 field. Go to it

Show it here
  • urlstringA presigned URL to download the file from. It expires shortly.

Limits

Three ceilings apply, and all three are enforced before anything is stored.

PlanReleases per repositoryLargest single attached fileStored bytes across the namespace
Free1,0002 GiB5 GiB
Pro5,0005 GiB100 GiB
Team20,00010 GiB1 TiB
Enterprise50,00020 GiB10 TiB

The release count is per repository and counts drafts. Creating a release at the cap is refused with 429 TOO_MANY_REQUESTS and no release is written — but if the call was also going to create the tag, that tag has already been created and stays. Deleting a release gives the slot straight back.

The file-size ceiling is per file. Registering a file bigger than it is refused with 400 BAD_REQUEST, and the message names the byte limit so a script can report it. A declared size of zero is refused the same way.

The storage budget belongs to the namespace — the top-level group — and is shared with every repository's git objects and every package published to that group's registries. Registering a file that would push the namespace over is refused with 429 TOO_MANY_REQUESTS; the bytes are reserved before the upload URL is issued, so a refusal leaves nothing behind and there is no half-reservation to reclaim. The reservation is held against the size you declared from the moment you register, not from the moment the bytes land, and it is returned when you delete the file or its release.

Two more bounds you can hit while writing rather than while uploading: release notes are capped at 100,000 bytes of markdown — roughly 100,000 characters of plain English, fewer once accents and emoji are involved — and a title at 255 characters. Both are refused with 400 BAD_REQUEST above that. A listing's page size runs from 1 to 100 and is 50 when you leave it out; a value outside the range is clamped to it rather than refused.

Downloads — attached files and source archives alike — are charged against an outbound-transfer budget rather than the storage budget, and an exhausted budget answers 429 TOO_MANY_REQUESTS. The charge is taken before the signed URL is minted, so a refusal means nothing was sent and nothing was billed. Anonymous downloads draw on their own, smaller budget, so signing in raises the ceiling before anything else does.

To raise any of these, an Owner of the top-level group can request a limit increase; every limit in force, and how much of the storage budget is used, is on the group's usage view.

Troubleshooting

400 BAD_REQUESTnaming the tag name
Cause
The tag name is not a valid git ref — a space, a .., one of ~ ^ : ? *, a [ or \, a leading . or -, or a .lock ending
What to do
Rename it. Letters, digits, -, _ and . are all safe, and v1.4.0 is the shape most projects use
400 BAD_REQUESTnaming the file name
Cause
An asset name may only hold letters, digits, ., _, + and -, and may not contain .. or start with -
What to do
Upload it under its bare filename — linux-arm64.tar.gz, never a path with a / in it
400 BAD_REQUESTnaming a byte limit
Cause
The declared size is zero, or larger than the per-file ceiling on your plan
What to do
Split the artifact, or ask an Owner of the top-level group for a limit increase. The real numbers per plan are in Limits
400 BAD_REQUESTon the source archive
Cause
The requested format is neither tar.gz nor zip
What to do
Ask for one of those two. tgz is accepted as a spelling of tar.gz; nothing else is
400 BAD_REQUESTmentioning the repository or file id
Cause
An id in the URL is not a well-formed id — usually an owner/repo path pasted where the API wants an id
What to do
Use the repository's id in the API path. The CLI's --repo-id accepts either form
401 UNAUTHORIZED
Cause
No credential was sent, or the token is expired, revoked or mistyped
What to do
Send Authorization: Bearer <token>. If it was working yesterday, check whether the token has been revoked and mint a new one
403 FORBIDDENon create, edit or delete
Cause
You can read the repository but you are not Developer on it
What to do
Ask an Owner or Maintainer of the repository to raise your access level. Reading and downloading need only Reporter, which is why the read worked
403 INSUFFICIENT_SCOPE
Cause
The token is valid but was minted without the scope this call needs
What to do
Mint a new token carrying release:create, release:update or release:delete as appropriate. Scopes are fixed at mint time and cannot be added to an existing token
403 FORBIDDENsaying the token is not authorized for this resource, on a repository you can otherwise read
Cause
The token was bound to one repository or group when it was minted, and this repository sits outside it
What to do
Use a token bound to this repository, or an unbound one. The binding is fixed at mint time, and it applies even to a public repository
404 NOT_FOUNDon a repository you can see in the web app
Cause
The repository is private and you are not a member, so its existence is not confirmed
What to do
Ask for membership. A private repository answers 404 rather than 403 to a non-member on purpose
404 NOT_FOUNDon a release that is definitely there
Cause
It is a draft and you are below Developer, or you are not signed in
What to do
Sign in as somebody with Developer access, or publish the release. Drafts answer 404 even on a public repository
404 NOT_FOUNDon create, naming the tag or the target
Cause
The tag does not exist and no target was given, or the target branch or commit does not resolve
What to do
Pass the branch name or the commit hash to cut the tag from, and check it exists on this repository — a hash from a fork does not
404 NOT_FOUNDon the latest-release lookup
Cause
Every release is a draft or a pre-release, or the repository has none
What to do
Publish a release with the pre-release flag off. A project shipping only release candidates has no latest release, and this is the correct answer
404 NOT_FOUNDon a source archive while the release page works
Cause
The tag was deleted or renamed after the release was cut — the archive is built from the tag, the release is not
What to do
Recreate the tag at the commit shown on the release, or attach a built archive as a file instead
404 NOT_FOUNDon registering a file
Cause
There is no release on that tag yet — a file attaches to a release, not to a bare tag
What to do
Cut the release for that tag first, then register the file against it
404 NOT_FOUNDon a file you can see listed
Cause
The file id belongs to a different release, or the file has already been deleted
What to do
Re-read the release and take the id from its own file list. A file id is per file and does not survive a delete-and-re-upload, so a stored id can outlive the file it named
409 CONFLICTon create
Cause
A release already exists for that tag
What to do
Edit the existing release, or delete it and cut a new one. Nothing about the existing release was changed by the refused call. If the call also recreated a missing tag, that tag stays — remove it from the repository's tag list if it is unwanted
409 CONFLICTon edit
Cause
The version you sent is not the stored one — somebody edited the release between your read and your write
What to do
Re-read the release, re-apply your change to what you got back, and send the new version. Do not retry the same request unchanged
409 CONFLICTon registering a file
Cause
A file with that name is already on this release
What to do
Delete the existing file first if you are replacing it, or register under a different name. A replacement gets a new id, so update any link that named the old one
409 CONFLICTon download
Cause
The file is registered but its upload has not been confirmed
What to do
Wait a moment and retry — confirmation lands shortly after the upload finishes. If it never clears, the PUT did not complete: delete the file and register it again
429 TOO_MANY_REQUESTSon create
Cause
The repository is at its release cap, which counts drafts
What to do
Delete releases nobody consumes any more — drafts first — or ask an Owner of the top-level group for a limit increase. A tag the refused call had already created stays behind
429 TOO_MANY_REQUESTSon registering a file
Cause
The file's size would push the namespace past its storage budget
What to do
Delete files or packages nobody downloads, or ask for a limit increase. The group's usage view shows how much of the budget is left
429 TOO_MANY_REQUESTSon download
Cause
The outbound-transfer budget for you or for the namespace is exhausted for this window
What to do
Wait for the window to roll over, sign in if you were downloading anonymously, or ask an Owner for a higher budget. Nothing was transferred
501 NOT_IMPLEMENTEDon attaching, downloading or removing a file, or on deleting a release
Cause
Storage for release files is unavailable on Gitmoap's side, so there is nowhere to keep or read the bytes
What to do
Contact support@gitmoap.com. Cutting, editing, listing and reading releases keep working in the meantime
The upload URL is rejected with a signature or length error
Cause
The PUT sent a different byte count or a different content type than the registration declared
What to do
Send exactly the size and the media type you registered, and let your client set the length from the file rather than setting it by hand
The upload URL stopped working part-way through a slow upload
Cause
The signed upload URL lasts 15 minutes
What to do
Delete the file and register it again to get a fresh URL. From a slow link, register the file immediately before uploading rather than at the start of a long build
A file has been sitting at uploading for hours
Cause
The PUT never completed, so no confirmation ever arrived — and its declared bytes are still held against the budget
What to do
Delete the file and register it again. Deleting is what returns the reservation
A download link shared over chat gives an access error
Cause
The signed download URL lasts 5 minutes and had expired by the time it was opened
What to do
Share the release's own download link rather than the signed URL it redirects to — a signed URL is minted per download
A browser app fails the download with a redirect or preflight error
Cause
The redirect changes origin, and the browser would not follow it with the headers your app's request carried
What to do
Ask for redirect=false, read the URL out of the JSON, and navigate to it directly
The newest release is not the one served as latest
Cause
It is a draft, or its pre-release flag is set
What to do
Publish it, or clear the pre-release flag. Both are on the release's edit view
A release cut weeks ago is being served as the latest one
Cause
It was published today, and latest orders by publication time rather than by tag or by when the release was created
What to do
Publish the version you want served as newest, or clear its pre-release flag. The most recently published non-pre-release always wins
The downloaded source archive is not named after the tag you asked for
Cause
An archive is stored once per commit, so the file that arrives is named for the commit rather than the tag — and two tags on the same commit are served the same file
What to do
Name it at the point of download: curl -Lo tool-v1.4.0.tar.gz "…/releases/v1.4.0/archive". The contents are the tree at that commit, which is why one file serves both tags
A release survived deleting the tag
Cause
Releases and tags are separate — deleting one never deletes the other
What to do
Delete the release too if you meant to remove it entirely, or recreate the tag if you meant to keep it

Examples

Shipping a version from a build, with GITMOAP_API set to Gitmoap's API base, https://api.gitmoap.com, REPO_ID to the repository's id, and GITMOAP_TOKEN to a token carrying release:create and release:update. It creates the release as a draft, attaches one artifact, then publishes — so nothing is visible until the download is actually there.

shellscript
# 1. Cut the release from main as a draft, keeping the version it comes back with.
# The tag is created if it does not exist.
VERSION=$(curl -sS -X POST "$GITMOAP_API/repositories/$REPO_ID/releases" \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"tag_name":"v1.4.0","target":"main","name":"v1.4.0","body":"## Fixed\n\n- The thing that was broken.","draft":true}' \
| jq -r .version)
# 2. Register the artifact and read back its upload URL.
SIZE=$(wc -c < dist/tool-linux-arm64.tar.gz)
UPLOAD_URL=$(curl -sS -X POST "$GITMOAP_API/repositories/$REPO_ID/releases/v1.4.0/assets" \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"tool-linux-arm64.tar.gz\",\"content_type\":\"application/gzip\",\"size\":$SIZE}" \
| jq -r .upload_url)
# 3. Send the bytes straight to storage. The type and the length must match step 2.
curl -sS -X PUT "$UPLOAD_URL" \
-H 'Content-Type: application/gzip' \
--data-binary @dist/tool-linux-arm64.tar.gz
# 4. Publish. Attaching a file does not touch the release's version, so the one
# from step 1 is still current and no re-read is needed.
curl -sS -X PATCH "$GITMOAP_API/repositories/$REPO_ID/releases/v1.4.0" \
-H "Authorization: Bearer $GITMOAP_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"draft\":false,\"version\":$VERSION}"

The same four steps in the CLI, which resolves the repository from an owner/repo path so a build script does not have to carry an id:

shellscript
VERSION=$(gitmoap releases create --repo-id acme/tool --tag-name v1.4.0 --target main --draft true --output json | jq -r .version)
gitmoap releases create-asset --repo-id acme/tool --tag-name v1.4.0 \
--name tool-linux-arm64.tar.gz --content-type application/gzip \
--size "$(wc -c < dist/tool-linux-arm64.tar.gz)" --output json
# send the bytes to the upload_url it printed, then publish:
gitmoap releases update --repo-id acme/tool --tag-name v1.4.0 --draft false --version "$VERSION"

Fetching the newest version from an install script needs no token at all against a public repository. Each file's download path comes back relative to the same API base, so prefix it with $GITMOAP_API to fetch it — and a file whose upload has not been confirmed has no download path at all, which is the check an install script should make before it fetches:

shellscript
curl -sS "$GITMOAP_API/repositories/$REPO_ID/releases/latest" \
| jq -r '.tag_name, (.assets[] | "\(.name)\t\(.download_url)")'

FAQ

Does deleting a release delete the tag?

No. The tag is a git ref and survives; the release was the notes and files on top of it. Removing the tag is a separate deletion in the repository's tag list.

Can one tag have two releases?

No. The tag is the release's identity, and a second create for the same tag is refused with 409. To re-cut a version, delete the release and create it again — or, better, cut a new tag.

Do drafts count against the release cap?

Yes. A draft is a release that has not been published, and it occupies a slot until it is deleted.

Can I rename a release's tag?

No. The title, the notes and both flags are editable; the tag is not. Delete the release and cut it under the tag you want.

What is the version field?

A concurrency counter for editing, not your software's version number — that is the tag name. Send back the number you last read so an edit that would overwrite somebody else's is refused instead.

Why is the download link missing on a file I just registered?

The link appears once the upload is confirmed. A file that never gets its bytes stays without one, holds its declared storage, and needs deleting.

  • Repositoriescreating and deleting the tags releases are cut from, and browsing the commits behind them
  • Branchesthe branch a release is usually cut from, and how to create one without a clone
  • Package registriespublishing artifacts people install with npm, Cargo or pip, rather than download by hand
  • Pipelinesrunning the build that produces the files you attach, and cutting the release from it
  • Access tokensminting a token with release:create and nothing else, for a build that ships
  • Groupswhere the storage budget lives, what is using it, and how to ask for more
  • Billing and plansthe plan your release, file-size and storage ceilings come from, and how to move to a higher one