fix(gitea-releases): restore the prerelease imperative to the create path

The retrofit left only a descriptive sentence on the loaded path — "Gitea
never infers a prerelease from a -beta/-rc tag name" — and moved the
imperative into references/conventions.md behind a trigger listing semver
naming, release-notes sourcing and release-to-tag relationships. Draft and
prerelease are not in that list, and "cut a v2.0.0-beta.1" is exactly the
request where the caller does not raise the topic, so the rule was
unreachable from the flow that needs it.

Severity comes from the repair path: the MCP surface has create, get,
get_latest, list and delete only — there is no update or edit tool. A
release published without is_pre_release can only be corrected by
delete_release plus a fresh create, and get_latest_release points
consumers at the beta meanwhile. Neither SKILL.md nor call-signatures.md
said so anywhere.

The flags are now set explicitly on every create, the missing update tool
and its delete-and-recreate consequence are stated in the body, and the
semver pass-through rule stranded behind the same trigger is promoted
alongside it. call-signatures.md now cites the deployed tool description
as direct evidence that get_latest_release excludes drafts, while keeping
the prerelease half hedged — that remains unconfirmed.

Verified live against gitea-mcp v1.7.0, read-only calls.

Refs #99
This commit is contained in:
2026-08-30 20:51:22 +00:00
parent 8982ac58b7
commit d2da45f78c
6 changed files with 26 additions and 8 deletions

View File

@@ -18,7 +18,7 @@ metadata:
## Gotchas ## Gotchas
- **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives. - **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives.
- **`is_draft`/`is_pre_release` are booleans the caller sets — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected. - **Set `is_draft`/`is_pre_release` explicitly on every `create_release` — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** A tag named `v2.0.0-beta.1` publishes as a full release, and becomes the repo's latest, unless `is_pre_release: true` is passed in the same call. The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected.
- **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs. - **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs.
## Dispatch table ## Dispatch table
@@ -37,11 +37,13 @@ metadata:
`target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from. `target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from.
Pass a caller-supplied `tag_name` through verbatim. Semver with a `v` prefix is a tooling convention, not a Gitea constraint — the API accepts any string — so never validate or rewrite it.
## Workflow ## Workflow
- [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, and `title`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it. - [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. **There is no update or edit tool on this surface**: `create_release`, `get_release`, `get_latest_release`, `list_releases` and `delete_release` are the whole set. A release published with the wrong flag therefore has no non-destructive repair — the only fix is `delete_release` plus a fresh `create_release`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it.
- [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first. - [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first.
- [ ] **Deleting a tag along with its release:** Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own. - [ ] **Deleting a tag along with its release:** Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own.
- [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. - [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates.
If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`. If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, draft/prerelease semantics, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`.

View File

@@ -32,7 +32,7 @@ for brevity.
**`get_latest_release`** **`get_latest_release`**
- No parameters beyond `owner`/`repo`. - No parameters beyond `owner`/`repo`.
- Returns a single release object for the most recently published release. It is assumed (by analogy with typical "latest release" semantics) that this excludes drafts and prereleases, but that exclusion is not directly confirmed by any of the research docs — verify with `list_releases` if the caller depends on this. - Returns a single release object for the most recently published release. The deployed tool's own description reads "the most recent published (non-draft) release" — it names drafts as excluded and is silent on prereleases, so whether prereleases are also excluded is *unconfirmed*; verify with `list_releases` if the caller depends on it. Either way a release created without `is_pre_release: true` is eligible, which is why that flag has to be set in the create call (see `conventions.md`).
**`create_release`** **`create_release`**
- Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string) - Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string)

View File

@@ -33,6 +33,13 @@ commonly used to signal a prerelease to humans. When a user asks to "cut a beta"
release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on
the tag string to carry that meaning. the tag string to carry that meaning.
Getting this wrong is not cheaply repairable. The gitea-mcp release surface is `create_release`,
`get_release`, `get_latest_release`, `list_releases` and `delete_release` — there is **no update or
edit tool**, so a published release's flags cannot be corrected in place. The only remedy is
`delete_release` plus a fresh `create_release`, a destructive round trip; meanwhile a beta published
without `is_pre_release` is the release `get_latest_release` returns. Set the flags in the create
call.
Note the input/output naming mismatch: the input param is `is_draft`, but the release object Note the input/output naming mismatch: the input param is `is_draft`, but the release object
returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid
input key — passing `draft: true` to `create_release` is silently ignored rather than erroring. input key — passing `draft: true` to `create_release` is silently ignored rather than erroring.

View File

@@ -18,7 +18,7 @@ metadata:
## Gotchas ## Gotchas
- **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives. - **Deleting a release never deletes its tag.** A release is a metadata wrapper around a tag, so removing both takes two independent destructive calls. The reverse — whether deleting a tag deletes its release — is *unconfirmed*; verify with `list_releases`/`get_release` after `delete_tag` rather than assume it survives.
- **`is_draft`/`is_pre_release` are booleans the caller sets — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected. - **Set `is_draft`/`is_pre_release` explicitly on every `create_release` — Gitea never infers a prerelease from a `-beta`/`-rc` tag name.** A tag named `v2.0.0-beta.1` publishes as a full release, and becomes the repo's latest, unless `is_pre_release: true` is passed in the same call. The response object names them `draft`/`prerelease`; passing `draft` as an input key is silently ignored, not rejected.
- **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs. - **`list_releases`/`list_tags` default `per_page` to 20**, where most other gitea-mcp list tools default to 30 — a caller assuming 30 under-counts the pages a full sweep needs.
## Dispatch table ## Dispatch table
@@ -37,11 +37,13 @@ metadata:
`target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from. `target` (on `create_release`/`create_tag`) is a commitish — a branch name, existing tag, or commit SHA — the point the new tag is cut from.
Pass a caller-supplied `tag_name` through verbatim. Semver with a `v` prefix is a tooling convention, not a Gitea constraint — the API accepts any string — so never validate or rewrite it.
## Workflow ## Workflow
- [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, and `title`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it. - [ ] **Creating a release:** Call `create_release` with `tag_name`, `target`, `title`, and `is_draft`/`is_pre_release` set explicitly — never left to default. **There is no update or edit tool on this surface**: `create_release`, `get_release`, `get_latest_release`, `list_releases` and `delete_release` are the whole set. A release published with the wrong flag therefore has no non-destructive repair — the only fix is `delete_release` plus a fresh `create_release`. Gitea is assumed to create the tag from `target` when `tag_name` does not yet exist — plausible from the API shape, not confirmed in the research docs — so a separate `create_tag` is only needed to tag a commit without wrapping it in a release. Verify with `get_tag` afterward if the caller depends on it.
- [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first. - [ ] **Deleting a release:** `delete_release` takes the numeric `id` and never a `tag_name`; `delete_tag` is the mirror opposite and never takes an id. With only a tag name in hand, resolve the id through `list_releases` (paginating if needed) or `get_release` first.
- [ ] **Deleting a tag along with its release:** Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own. - [ ] **Deleting a tag along with its release:** Delete the release first, then call `delete_tag` — confirm both are intended before proceeding, since each is irreversible on its own.
- [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates. - [ ] **Listing every page:** Loop `page: 1, 2, 3...` until a response returns fewer than `per_page` entries. Nothing here auto-paginates.
If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`. If exact input params or response field shapes are needed, read `references/call-signatures.md`. If the caller raises semver tag naming, draft/prerelease semantics, release-notes sourcing, or how a release relates to its tag, read `references/conventions.md`.

View File

@@ -32,7 +32,7 @@ for brevity.
**`get_latest_release`** **`get_latest_release`**
- No parameters beyond `owner`/`repo`. - No parameters beyond `owner`/`repo`.
- Returns a single release object for the most recently published release. It is assumed (by analogy with typical "latest release" semantics) that this excludes drafts and prereleases, but that exclusion is not directly confirmed by any of the research docs — verify with `list_releases` if the caller depends on this. - Returns a single release object for the most recently published release. The deployed tool's own description reads "the most recent published (non-draft) release" — it names drafts as excluded and is silent on prereleases, so whether prereleases are also excluded is *unconfirmed*; verify with `list_releases` if the caller depends on it. Either way a release created without `is_pre_release: true` is eligible, which is why that flag has to be set in the create call (see `conventions.md`).
**`create_release`** **`create_release`**
- Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string) - Required: `tag_name` (string), `target` (string — branch, tag, or commit SHA to cut the tag from), `title` (string)

View File

@@ -33,6 +33,13 @@ commonly used to signal a prerelease to humans. When a user asks to "cut a beta"
release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on release candidate," set `is_pre_release: true` explicitly in the same call rather than relying on
the tag string to carry that meaning. the tag string to carry that meaning.
Getting this wrong is not cheaply repairable. The gitea-mcp release surface is `create_release`,
`get_release`, `get_latest_release`, `list_releases` and `delete_release` — there is **no update or
edit tool**, so a published release's flags cannot be corrected in place. The only remedy is
`delete_release` plus a fresh `create_release`, a destructive round trip; meanwhile a beta published
without `is_pre_release` is the release `get_latest_release` returns. Set the flags in the create
call.
Note the input/output naming mismatch: the input param is `is_draft`, but the release object Note the input/output naming mismatch: the input param is `is_draft`, but the release object
returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid returned by the API uses `draft` (and `prerelease`) as the field names. `draft` is never a valid
input key — passing `draft: true` to `create_release` is silently ignored rather than erroring. input key — passing `draft: true` to `create_release` is silently ignored rather than erroring.