API: project-card create/move endpoints unusable for adding issues to project boards #1

Closed
opened 2026-05-13 00:32:32 +03:00 by oleks · 3 comments
Owner

Context

While automating project-board card creation against a self-hosted Gitea via the API, the documented endpoint for adding cards to a project column is unusable:

  • POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards

Either the route returns 404 / not-implemented on the current Gitea version, or the request schema isn't documented in a way that a client can produce a working call. As a result, we created a project + columns + a milestone and assigned issues to the milestone via API, but had to drop into the UI to drag issues onto the project board. (See related friction context: oleks/claude-plugin-cluster issue tracker — the agent there had to report "manual UI step required" after attempting this endpoint.)

What's needed

  • Confirm the canonical endpoint for adding an issue (or arbitrary card) to a project column on a given Gitea version
  • Update the API docs / Swagger to reflect the actual route + request schema
  • If the endpoint is genuinely not implemented in this Gitea release, prioritize implementing it — automating project boards from CI/agents is a common need

Repro

  1. Create a repo project via API: POST /repos/{owner}/{repo}/projects
  2. Create columns via API: POST /repos/{owner}/{repo}/projects/{project_id}/columns
  3. Try to add an existing issue to a column: POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards with various payload shapes ({"issue_id": ...}, {"type": "Issue", "content_id": ...}, etc.)
  4. Observe: no shape produces a working card. UI drag-and-drop is the only path.

Suggested labels

kind/bug or type/bug, kind/api if such a label exists, or whatever the repo's convention is.

## Context While automating project-board card creation against a self-hosted Gitea via the API, the documented endpoint for adding cards to a project column is unusable: - `POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards` Either the route returns 404 / not-implemented on the current Gitea version, or the request schema isn't documented in a way that a client can produce a working call. As a result, we created a project + columns + a milestone and assigned issues to the milestone via API, but had to drop into the UI to drag issues onto the project board. (See related friction context: `oleks/claude-plugin-cluster` issue tracker — the agent there had to report "manual UI step required" after attempting this endpoint.) ## What's needed - Confirm the canonical endpoint for adding an issue (or arbitrary card) to a project column on a given Gitea version - Update the API docs / Swagger to reflect the actual route + request schema - If the endpoint is genuinely not implemented in this Gitea release, prioritize implementing it — automating project boards from CI/agents is a common need ## Repro 1. Create a repo project via API: `POST /repos/{owner}/{repo}/projects` 2. Create columns via API: `POST /repos/{owner}/{repo}/projects/{project_id}/columns` 3. Try to add an existing issue to a column: `POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards` with various payload shapes (`{"issue_id": ...}`, `{"type": "Issue", "content_id": ...}`, etc.) 4. Observe: no shape produces a working card. UI drag-and-drop is the only path. ## Suggested labels `kind/bug` or `type/bug`, `kind/api` if such a label exists, or whatever the repo's convention is.
Author
Owner

Confirmed self-applicable: tried to add this issue to the new Roadmap project board via the API; all card-creation shapes failed with 404:

  • POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards with {"issue_id": 1} → 404
  • POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards with {"type": "Issue", "content_id": 106} → 404
  • POST /repos/{owner}/{repo}/projects/{project_id}/cards with {"issue_id": 1} → 404
  • Additional payload shapes ({"content_id": 106}, bare 1, {"external_tracker_id": "1"}) also returned 404

UI drag-and-drop is the only path to add issues to project cards on the current Gitea build (1.26.0-unstable-2026-05-12).

Confirmed self-applicable: tried to add this issue to the new Roadmap project board via the API; all card-creation shapes failed with 404: - `POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards` with `{"issue_id": 1}` → 404 - `POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cards` with `{"type": "Issue", "content_id": 106}` → 404 - `POST /repos/{owner}/{repo}/projects/{project_id}/cards` with `{"issue_id": 1}` → 404 - Additional payload shapes (`{"content_id": 106}`, bare `1`, `{"external_tracker_id": "1"}`) also returned 404 UI drag-and-drop is the only path to add issues to project cards on the current Gitea build (1.26.0-unstable-2026-05-12).
Author
Owner

New evidence (2026-05-15)

Confirmed during a bulk-triage session against project 7 on this instance:

  • POST /{owner}/-/projects/{id}/move with issue_id + column_id form params returns HTTP 303 even with a valid PAT, but the 303 is a redirect to the login page — not success. The route requires a browser session cookie + CSRF token. This is a footgun: callers may report "done" based on the 2xx-class response.
  • No /api/v1/projects/... write endpoints exist for column operations.
  • No GET endpoint enumerates "issues in project N" or "issues in column N" — column membership can only be inspected by parsing the rendered project HTML.
  • PATCH /api/v1/repos/{owner}/{repo}/issues/{n} with {"projects":[<id>]} does work for linking an issue to a project — just not for column placement within it.

Suggested fix shape

Either:

  • (a) Accept token auth on the existing /-/projects/{id}/move web route, OR
  • (b) Add proper REST endpoints under /api/v1/projects/{id}/columns/{col_id}/issues (POST to add, GET to list, DELETE to remove) plus a /api/v1/projects/{id} GET that includes column membership.

(b) is cleaner and unblocks downstream tools (gitea-mcp, tea CLI, automation scripts).

Cross-refs

  • Tracking issue in cluster repo: oleks/cluster#14
  • Local memory note: reference_gitea_project_column_move.md
## New evidence (2026-05-15) Confirmed during a bulk-triage session against project 7 on this instance: - `POST /{owner}/-/projects/{id}/move` with `issue_id` + `column_id` form params returns **HTTP 303 even with a valid PAT**, but the 303 is a redirect to the login page — not success. The route requires a browser session cookie + CSRF token. This is a footgun: callers may report "done" based on the 2xx-class response. - No `/api/v1/projects/...` write endpoints exist for column operations. - No GET endpoint enumerates "issues in project N" or "issues in column N" — column membership can only be inspected by parsing the rendered project HTML. - `PATCH /api/v1/repos/{owner}/{repo}/issues/{n}` with `{"projects":[<id>]}` *does* work for linking an issue to a project — just not for column placement within it. ### Suggested fix shape Either: - (a) Accept token auth on the existing `/-/projects/{id}/move` web route, OR - (b) Add proper REST endpoints under `/api/v1/projects/{id}/columns/{col_id}/issues` (POST to add, GET to list, DELETE to remove) plus a `/api/v1/projects/{id}` GET that includes column membership. (b) is cleaner and unblocks downstream tools (gitea-mcp, tea CLI, automation scripts). ### Cross-refs - Tracking issue in cluster repo: oleks/cluster#14 - Local memory note: `reference_gitea_project_column_move.md`
oleks added this to the (deleted) project 2026-05-15 14:46:04 +03:00
Author
Owner

Scope correction after reading the fork (2026-05-15)

The repo-scope project API is already implemented in this fork's main:

  • routers/api/v1/repo/project.go — all CRUD + columns + AddIssueToProjectColumn + RemoveIssueFromProjectColumn + MoveProjectIssue
  • Routes registered in routers/api/v1/api.go:1578-1597 under reqRepoReader/reqRepoWriter with reqToken(). PAT auth works.

The actual gap is no user-scope or org-scope project REST API. The web layer (routers/web/org/projects.go) handles both via the owner-agnostic OwnerID field, but routers/api/v1/ has no mirror.

Plan

Add two new route groups, copy-pasting the repo handlers as a starting point (per-scope subdirs, matches Gitea's existing pattern):

  • routers/api/v1/user/project.go/api/v1/users/{username}/projects/...
  • routers/api/v1/org/project.go/api/v1/orgs/{org}/projects/...

Permissions: user-scope requires owner==authenticated user (or admin); org-scope reuses reqOrgMembership() style. Model layer reuses project_model.GetProjectByIDAndOwner (already used by routers/web/org/projects.go).

Also:

  • Add the missing integration test for the existing repo-scope MoveProjectIssue (tests/integration/api_repo_project_test.go).
  • Decide DELETE-from-column semantics (currently fully detaches issue from project; the alternative is "move to default column"). Apply consistently across repo/user/org.
  • make generate-swagger after.

No DB migrations needed.

## Scope correction after reading the fork (2026-05-15) The repo-scope project API is **already implemented** in this fork's `main`: - `routers/api/v1/repo/project.go` — all CRUD + columns + `AddIssueToProjectColumn` + `RemoveIssueFromProjectColumn` + `MoveProjectIssue` - Routes registered in `routers/api/v1/api.go:1578-1597` under `reqRepoReader`/`reqRepoWriter` with `reqToken()`. PAT auth works. The actual gap is **no user-scope or org-scope project REST API**. The web layer (`routers/web/org/projects.go`) handles both via the owner-agnostic `OwnerID` field, but `routers/api/v1/` has no mirror. ### Plan Add two new route groups, copy-pasting the repo handlers as a starting point (per-scope subdirs, matches Gitea's existing pattern): - `routers/api/v1/user/project.go` → `/api/v1/users/{username}/projects/...` - `routers/api/v1/org/project.go` → `/api/v1/orgs/{org}/projects/...` Permissions: user-scope requires owner==authenticated user (or admin); org-scope reuses `reqOrgMembership()` style. Model layer reuses `project_model.GetProjectByIDAndOwner` (already used by `routers/web/org/projects.go`). Also: - Add the missing integration test for the existing repo-scope `MoveProjectIssue` (`tests/integration/api_repo_project_test.go`). - Decide DELETE-from-column semantics (currently fully detaches issue from project; the alternative is "move to default column"). Apply consistently across repo/user/org. - `make generate-swagger` after. No DB migrations needed.
oleks closed this issue 2026-05-15 15:53:12 +03:00
oleks moved this to Closed in My issues on 2026-05-15 20:52:15 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: oleks/gitea#1