API: project-card create/move endpoints unusable for adding issues to project boards #1
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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}/cardsEither 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-clusterissue tracker — the agent there had to report "manual UI step required" after attempting this endpoint.)What's needed
Repro
POST /repos/{owner}/{repo}/projectsPOST /repos/{owner}/{repo}/projects/{project_id}/columnsPOST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cardswith various payload shapes ({"issue_id": ...},{"type": "Issue", "content_id": ...}, etc.)Suggested labels
kind/bugortype/bug,kind/apiif such a label exists, or whatever the repo's convention is.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}/cardswith{"issue_id": 1}→ 404POST /repos/{owner}/{repo}/projects/{project_id}/columns/{column_id}/cardswith{"type": "Issue", "content_id": 106}→ 404POST /repos/{owner}/{repo}/projects/{project_id}/cardswith{"issue_id": 1}→ 404{"content_id": 106}, bare1,{"external_tracker_id": "1"}) also returned 404UI 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).
New evidence (2026-05-15)
Confirmed during a bulk-triage session against project 7 on this instance:
POST /{owner}/-/projects/{id}/movewithissue_id+column_idform 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./api/v1/projects/...write endpoints exist for column operations.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:
/-/projects/{id}/moveweb route, OR/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
reference_gitea_project_column_move.mdScope 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+MoveProjectIssuerouters/api/v1/api.go:1578-1597underreqRepoReader/reqRepoWriterwithreqToken(). 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-agnosticOwnerIDfield, butrouters/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 reusesproject_model.GetProjectByIDAndOwner(already used byrouters/web/org/projects.go).Also:
MoveProjectIssue(tests/integration/api_repo_project_test.go).make generate-swaggerafter.No DB migrations needed.