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
Publish a release
Start at Releases in the side navigation.
Choose New release.
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.
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:readto list, read, download and fetch source archives;release:createto create a release and register a file on it;release:updateto edit or publish;release:deleteto 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 carryrelease:createandrelease:updateand nothing else. - The repository's id.The API paths are addressed by id. The CLI's
--repo-idalso accepts anowner/repopath, so you can hand it something a human typed. - An HTTP client that can
PUTa 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 withcurlor 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:
| State | Who can see it | In the repository's release list | Answers the latest-release lookup |
|---|---|---|---|
| Draft | Developer and above only | only for Developer and above, and only when drafts are explicitly requested | no |
| Published, pre-release | everyone the repository's visibility allows | yes | no |
| Published | everyone the repository's visibility allows | yes | yes — 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 workingRegistering 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.
gitmoap releases create --repo-id <id> --tag-name <tag name>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.
gitmoap releases update --repo-id <id> --tag-name <id> --version <n>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.
gitmoap releases create-asset \ --repo-id <id> \ --tag-name <id> \ --content-type <content type> \ --name <name> \ --size <n>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.
gitmoap releases list --repo-id <id>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.
gitmoap releases get --repo-id <id> --tag-name <id>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.
gitmoap releases get-latest --repo-id <id>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.
gitmoap releases download-asset \ --repo-id <id> \ --tag-name <id> \ --asset-id <id>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.
gitmoap releases get-archive --repo-id <id> --tag-name <id>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.
gitmoap releases delete-asset --repo-id <id> --tag-name <id> --asset-id <id>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.
gitmoap releases delete --repo-id <id> --tag-name <id>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
Token scope
release:create
Counts against
releases_per_repo
Send
Path
- repo_iduuidRepository ID
Body
- bodyobjectoptional
- draftbooleanoptional
- nameobjectoptional
- prereleasebooleanoptional
- tag_namestring
- targetobjectoptional
Refuses
400Invalid request401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository or target not found409Release already exists429The 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) oravailable. - 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
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 type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Release 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) oravailable. - 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
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 type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Repository 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.gzarchive. - 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
.ziparchive.
- next_tokenobjectoptional
GET/repositories/{repo_id}/releases/latestrelease:read
Get the newest published, non-prerelease release
Token scope
release:read
Send
Path
- repo_iduuidRepository ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404No 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) oravailable. - 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
Token scope
release:update
Send
Path
- repo_iduuidRepository ID
- tag_namestringTag name
Body
- bodyobjectoptional
- draftobjectoptional
- nameobjectoptional
- prereleaseobjectoptional
- versionint64The release's
versionas 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 type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Release not found409Version 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) oravailable. - 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)
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 type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Release not found409The 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 referenceToken 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 request401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent409Asset name already exists429Storage 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) oravailable. - 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 referenceToken scope
release:read
Send
Path
- repo_iduuidRepository ID
- tag_namestringTag name
- asset_iduuidAsset ID
Query
- redirectbooleanoptionalWhen
false, return200 { url }instead of a302
Refuses
302Redirect to a short-lived download link for the asset400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Asset not found409Asset 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 referenceToken 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 type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Asset not found409The 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
Token scope
release:read
Send
Path
- repo_iduuidRepository ID
- tag_namestringTag name
Query
- formatstringoptionalArchive format: tar.gz (default) or zip
- redirectbooleanoptionalWhen
false, return200 { url }instead of a302(for clients that cannot follow a cross-origin redirect)
Refuses
302Redirect to a signed download link for the archive400Unsupported archive format401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Release 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.
| Plan | Releases per repository | Largest single attached file | Stored bytes across the namespace |
|---|---|---|---|
| Free | 1,000 | 2 GiB | 5 GiB |
| Pro | 5,000 | 5 GiB | 100 GiB |
| Team | 20,000 | 10 GiB | 1 TiB |
| Enterprise | 50,000 | 20 GiB | 10 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.lockending - What to do
- Rename it. Letters, digits,
-,_and.are all safe, andv1.4.0is 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.gznorzip - What to do
- Ask for one of those two.
tgzis accepted as a spelling oftar.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/repopath pasted where the API wants an id - What to do
- Use the repository's id in the API path. The CLI's
--repo-idaccepts 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:updateorrelease:deleteas 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
404rather than403to 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
404even 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
PUTdid 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
PUTsent 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
PUTnever 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.
# 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:
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:
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.
Related
- 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:createand 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