feat(api): add REST API for repository project boards#36831
feat(api): add REST API for repository project boards#36831hanism01 wants to merge 31 commits intogo-gitea:mainfrom
Conversation
This adds a complete REST API implementation for managing repository
project boards, including projects, columns, and adding issues to columns.
API Endpoints:
- GET /repos/{owner}/{repo}/projects - List projects
- POST /repos/{owner}/{repo}/projects - Create project
- GET /repos/{owner}/{repo}/projects/{id} - Get project
- PATCH /repos/{owner}/{repo}/projects/{id} - Update project
- DELETE /repos/{owner}/{repo}/projects/{id} - Delete project
- GET /repos/{owner}/{repo}/projects/{id}/columns - List columns
- POST /repos/{owner}/{repo}/projects/{id}/columns - Create column
- PATCH /repos/{owner}/{repo}/projects/columns/{id} - Update column
- DELETE /repos/{owner}/{repo}/projects/columns/{id} - Delete column
- POST /repos/{owner}/{repo}/projects/columns/{id}/issues - Add issue
Features:
- Full Swagger/OpenAPI documentation
- Proper permission checks
- Pagination support for list endpoints
- State filtering (open/closed/all)
- Comprehensive error handling
- Token-based authentication with scope validation
- Archive repository protection
New Files:
- modules/structs/project.go: API data structures
- routers/api/v1/repo/project.go: API handlers
- routers/api/v1/swagger/project.go: Swagger responses
- services/convert/project.go: Model converters
- tests/integration/api_repo_project_test.go: Integration tests
Modified Files:
- models/project/issue.go: Added AddOrUpdateIssueToColumn function
- routers/api/v1/api.go: Registered project API routes
- routers/api/v1/swagger/options.go: Added project option types
- templates/swagger/v1_json.tmpl: Regenerated swagger spec
fix(api): remove duplicated permission checks in project handlers
Route middleware reqRepoReader(unit.TypeProjects) wraps the entire
/projects route group, and reqRepoWriter(unit.TypeProjects) is applied
to each mutating route individually in api.go. These middleware run
before any handler fires and already gate access correctly.
The inline CanRead/CanWrite checks at the top of all 10 handlers were
therefore unreachable dead code — removed from ListProjects, GetProject,
CreateProject, EditProject, DeleteProject, ListProjectColumns,
CreateProjectColumn, EditProjectColumn, DeleteProjectColumn, and
AddIssueToProjectColumn.
The now-unused "code.gitea.io/gitea/models/unit" import is also removed.
Addresses review feedback on: go-gitea#36008
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
fix(api): replace AddOrUpdateIssueToColumn with IssueAssignOrRemoveProject
The custom AddOrUpdateIssueToColumn function introduced by this PR was
missing three things that the existing IssueAssignOrRemoveProject provides:
1. db.WithTx transaction wrapper — raw DB updates without a transaction
can leave the database in a partial state on error.
2. CreateComment(CommentTypeProject) — assigning an issue to a project
column via the UI creates a comment on the issue timeline. The API
doing the same action silently was an inconsistency.
3. CanBeAccessedByOwnerRepo ownership check — IssueAssignOrRemoveProject
validates that the issue is accessible within the repo/org context
before mutating state.
AddOrUpdateIssueToColumn is removed entirely. AddIssueToProjectColumn
now delegates to issues_model.IssueAssignOrRemoveProject, which already
has the issue object loaded earlier in the handler.
Addresses review feedback on: go-gitea#36008
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
fix(api): remove unnecessary pagination from ListProjectColumns
Project columns are few in number by design (typically 3-8 per board).
The previous implementation fetched all columns from the DB then sliced
the result in memory — adding complexity and a misleading Link header
without any practical benefit.
ListProjectColumns now returns all columns directly. The page/limit
query parameters and associated swagger docs are removed.
Addresses review feedback on: go-gitea#36008
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
fix(api): regenerate swagger spec after removing ListProjectColumns pagination
Removes the page and limit parameters from the generated swagger spec
for the ListProjectColumns endpoint, matching the handler change that
dropped in-memory pagination.
Co-authored-by: Claude <noreply@anthropic.com>
test(api): remove pagination assertion from TestAPIListProjectColumns
ListProjectColumns no longer supports pagination — it returns all columns
directly. Remove the page/limit test case that expected 2 of 3 columns.
Co-authored-by: Claude <noreply@anthropic.com>
fix(api): implement proper pagination for ListProjectColumns
Per contribution guidelines, list endpoints must support page/limit
query params and set X-Total-Count header.
- Add CountColumns and GetColumnsPaginated to project model (DB-level,
not in-memory slicing)
- ListProjectColumns uses utils.GetListOptions, calls paginated model
functions, and sets X-Total-Count via ctx.SetTotalCountHeader
- Restore page/limit swagger doc params on the endpoint
- Regenerate swagger spec
- Integration test covers: full list with X-Total-Count, page 1 of 2,
page 2 of 2, and 404 for non-existent project
Co-authored-by: Claude <noreply@anthropic.com>
Three issues raised by @lunny in review of go-gitea#36008 are addressed: 1. Duplicate permission checks removed The /projects route group is already wrapped with reqRepoReader and reqRepoWriter in api.go. The inline CanRead/CanWrite checks at the top of all 10 handlers were unreachable dead code. 2. AddOrUpdateIssueToColumn replaced with IssueAssignOrRemoveProject The custom function introduced in go-gitea#36008 was missing a db.WithTx transaction wrapper, the CommentTypeProject audit comment written by the UI, and the CanBeAccessedByOwnerRepo cross-repo ownership guard. AddIssueToProjectColumn now delegates to the existing IssueAssignOrRemoveProject which provides all three. 3. ListProjectColumns pagination implemented correctly Added CountColumns and GetColumnsPaginated (using db.SetSessionPagination) to the project model. The handler uses utils.GetListOptions and sets X-Total-Count via ctx.SetTotalCountHeader per API contribution guidelines. Integration tests cover full list, page 1, page 2, and 404. Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
silverwind
left a comment
There was a problem hiding this comment.
Review by Claude
-
Bug:
EditProjectdrops field updates whenIsClosedis set (routers/api/v1/repo/project.go:273-282)
Whenform.IsClosed != nil, onlyChangeProjectStatusis called —UpdateProjectis in theelsebranch. A PATCH with{"title": "new title", "is_closed": true}silently discards the title change.UpdateProjectshould be called unconditionally. -
ListProjectspagination inconsistent with other endpoints (routers/api/v1/repo/project.go:73-82)
ListProjectsmanually parsespage/limitviactx.FormIntwith asetting.UI.IssuePagingNumfallback, whileListProjectColumns(and all other Gitea list endpoints) useutils.GetListOptions(ctx). Should use the standard helper for consistency and to respectDEFAULT_PAGING_NUM. -
AddIssueToProjectColumnswagger body defined inline (routers/api/v1/repo/project.go:635)
All other endpoints reference their body schema via"$ref": "#/definitions/...", but this one defines it inline.AddIssueToProjectColumnOptionalready exists inmodules/structs/project.goand is registered inswagger/options.go— the swagger comment should reference it via$ref. -
MoveProjectColumnOptionis unused (modules/structs/project.go:125-131)
Defined but no endpoint references it. Should be removed or deferred to a future PR that adds column reordering. -
Minor:
Sortingtruncation (routers/api/v1/repo/project.go:539)
column.Sorting = int8(*form.Sorting)silently truncates — the API accepts anyintbut the model isint8.
|
CI failures are from #36858, |
|
Excited for this API to land! Anything that would be helpful in getting the CI failures resolved? |
|
feel free to contribute. currently swamped myself with my own projects. |
…nism01/gitea into hanism01-fix/project-board-api-review-feedback
…nism01/gitea into hanism01-fix/project-board-api-review-feedback
There was a problem hiding this comment.
Pull request overview
Adds first-class REST API support for repository project boards, including CRUD for projects/columns and issue assignment, plus model support for paginated column listing and updated Swagger + tests.
Changes:
- Introduces
/repos/{owner}/{repo}/projectsREST endpoints (projects, columns, column issues + assign/unassign). - Adds DB-level pagination helpers for project columns (
CountProjectColumns,GetProjectColumns) with unit tests. - Updates web/integration code to use the new column listing helper and adds integration coverage for the new API.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/integration/project_test.go | Switches project column loading to the new paginated helper. |
| tests/integration/api_repo_project_test.go | Adds integration coverage for the new repo project board REST API endpoints. |
| templates/swagger/v1_json.tmpl | Adds Swagger paths/definitions for the new project board endpoints. |
| services/convert/project.go | Adds converters from project models to API structs (Project/ProjectColumn). |
| routers/web/repo/projects.go | Uses new GetProjectColumns helper in repo project view. |
| routers/web/repo/issue_page_meta.go | Uses new GetProjectColumns helper when building issue page project metadata. |
| routers/web/org/projects.go | Uses new GetProjectColumns helper in org project view. |
| routers/api/v1/swagger/project.go | Adds Swagger response wrappers for Project/ProjectColumn list + single responses. |
| routers/api/v1/swagger/options.go | Registers project-related request body option types for Swagger generation. |
| routers/api/v1/repo/project.go | Implements the new REST API handlers for repo project boards. |
| routers/api/v1/api.go | Registers the new /projects API routes under repo scope. |
| modules/structs/project.go | Introduces API structs/options for projects and project columns. |
| models/project/column.go | Removes the old Project.GetColumns and moves list helpers elsewhere. |
| models/project/column_test.go | Updates tests to use new GetProjectColumns helper. |
| models/project/column_list.go | Adds paginated column listing + counting and restores GetColumnsByIDs. |
| models/project/column_list_test.go | Adds unit tests for count/pagination and GetColumnsByIDs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nism01/gitea into hanism01-fix/project-board-api-review-feedback
| isClosed = optional.None[bool]() | ||
| default: | ||
| isClosed = optional.Some(false) | ||
| } |
There was a problem hiding this comment.
ParseIssueFilterStateIsClosed
| form := web.GetForm(ctx).(*api.EditProjectOption) | ||
|
|
||
| if form.Title != nil { | ||
| project.Title = *form.Title |
| column.Color = *form.Color | ||
| } | ||
| if form.Sorting != nil { | ||
| sorting := int8(*form.Sorting) |
There was a problem hiding this comment.
I think it is a bug, Sorting int8 xorm:"NOT NULL DEFAULT 0"` should be migrated to a normal int.
| RepoIDs: []int64{ctx.Repo.Repository.ID}, | ||
| ProjectID: column.ProjectID, | ||
| ProjectColumnID: column.ID, | ||
| SortType: "project-column-sorting", |
| return | ||
| } | ||
|
|
||
| issue, err := issues_model.GetIssueByID(ctx, ctx.PathParamInt64("issue_id")) |
There was a problem hiding this comment.
Why not GetRepoIssueByID(repoID, issueID)?
| issue, err := issues_model.GetIssueByID(ctx, ctx.PathParamInt64("issue_id")) | ||
| if err != nil { | ||
| if issues_model.IsErrIssueNotExist(err) { | ||
| ctx.APIError(http.StatusUnprocessableEntity, "issue not found") | ||
| } else { | ||
| ctx.APIErrorInternal(err) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| if issue.RepoID != ctx.Repo.Repository.ID { | ||
| ctx.APIError(http.StatusUnprocessableEntity, "issue does not belong to this repository") | ||
| return | ||
| } |
| defer func() { | ||
| _ = project_model.DeleteProjectByID(t.Context(), project.ID) | ||
| }() |
29f4615 to
4fea94c
Compare
Is this design really easy to use for end users? GitHub supports using issue id OR index(number) directly. |



Summary
Picks up the work from #36008 by @SupenBysz, which added comprehensive REST API endpoints for Gitea repository project boards. That PR has been idle since December 2025 with review feedback from @lunny unaddressed. Full credit for the original implementation goes to @SupenBysz.
This PR tracks issue #36824.
Closes #36824.
Endpoints
GET /repos/{owner}/{repo}/projects— list projects (paginated)POST /repos/{owner}/{repo}/projects— create projectGET /repos/{owner}/{repo}/projects/{id}— get projectPATCH /repos/{owner}/{repo}/projects/{id}— update projectDELETE /repos/{owner}/{repo}/projects/{id}— delete projectGET /repos/{owner}/{repo}/projects/{id}/columns— list columns (paginated)POST /repos/{owner}/{repo}/projects/{id}/columns— create columnPATCH /repos/{owner}/{repo}/projects/{id}/columns/{column_id}— update columnDELETE /repos/{owner}/{repo}/projects/{id}/columns/{column_id}— delete columnGET /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues-- list columns' issuesPOST /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues/{issue_id}— assign issue to columnDELETE /repos/{owner}/{repo}/projects/{id}columns/{column_id}/issues/{issue_id}-- remove issue from columnChanges from #36008
Three issues raised by @lunny in review:
1. Duplicate permission checks removed
The
/projectsroute group is already wrapped withreqRepoReader(unit.TypeProjects)inapi.go, and individual write routes carryreqRepoWriter. The inlineCanRead/CanWritechecks at the top of all 10 handlers were unreachable dead code.2.
AddOrUpdateIssueToColumnreplacedThe custom function introduced in #36008 was missing a
db.WithTxtransaction wrapper, theCommentTypeProjectaudit comment written by the UI, and theCanBeAccessedByOwnerRepocross-repo ownership guard.AddIssueToProjectColumnnow delegates to the existingissues_model.IssueAssignOrRemoveProject, which provides all three. The custom function is deleted entirely.3.
ListProjectColumnspagination implemented correctlyThe original implementation fetched all columns then sliced in memory.
ListProjectColumnsnow uses DB-level pagination viadb.SetSessionPagination, setsX-Total-CountandLinkheaders per API contribution guidelines. Two new model functions (CountColumns,GetColumnsPaginated) are added with unit tests.Testing
Built from source and tested against a local Gitea instance with SQLite. All 10 endpoints verified end-to-end.
Unit tests:
go test -tags sqlite,sqlite_unlock_notify ./models/project/...Integration tests cover all project and column endpoints including pagination.
AI disclosure
This PR was prepared with the assistance of Claude Sonnet 4.5 (Anthropic). The contributor (hanism01) owns the review dialogue, manually tested all endpoints, and reviewed all generated code before submission.