feat(api): add REST API for repository project boards#36831
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
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
- Rename response timestamps to created_at / updated_at / closed_at
- Replace is_closed bool with state ("open" / "closed") via api.StateType
- Switch template_type / card_type / type to string enums with input validation
- Embed creator User object on Project and ProjectColumn (batched lookup)
- Add absolute html_url; drop relative url
- Add POST /repos/.../projects/{id}/issues/{issue_id}/move with optional sorting
- Validate column hex color and reject writes to closed projects
- Document issue-only project scope in swagger
- Push project-issue existence check into project_model.IsIssueInColumn
- Add project_service.ErrIssueNotInProject sentinel for the move endpoint
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
|
0783004 adds some compat against https://docs.github.com/en/enterprise-server@3.16/rest/projects-classic?apiVersion=2022-11-28, notably, replacing int enums with strings. |
…o-gitea#37406) Make the watch, star, and fork buttons in the repo header consistent for logged-out users: - Apply the same look to all three buttons (number labels included), instead of only the action button being grayed. - Clicking any of them while logged out now leads to the login page (with a redirect back) instead of being inert. - Split the per-button markup out of `header.tmpl` into a dedicated `templates/repo/header/` folder (`fork.tmpl`, `star.tmpl`, `watch.tmpl`). --------- Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
…nism01/gitea into hanism01-fix/project-board-api-review-feedback
|
Actually after reading more context, I would withdraw my review : migrate int8 column. It was designed to only support no more than 127 columns. So the database design can be kept as is. No need to use migration. |
|
And this PR should also wait for the issue multiple project support, since it touches that part. |
Per review feedback, the 127-column cap is intentional (maxProjectColumns is 20), so the DB schema is left as-is and no migration is needed. Reverts the Column.Sorting widening to match. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
|
Migration dropped in 49b0df0. |
@hanism01 May I know what exactly is needed to take get this PR merged? |
Hi @beardev-in, here's where things stand:
This comment was generated with the assistance of AI (Claude, Anthropic). |
wxiaoguang
left a comment
There was a problem hiding this comment.
Allow multiple projects per issue and pull requests #36784 has been merged.
The API design should follow the "multiple projects support" and need new changes.
Feel free to mark "ready for review" when it is really ready.
Rebased and updated in #37518, adapted for the multi-project model from #36784. All maintainer feedback from this thread has been addressed. |
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 a column's 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
Issues raised by @lunny in the original review:
1. Duplicate permission checks removed
The
/projectsroute group is wrapped withreqRepoReader(unit.TypeProjects)inapi.go, and individual write routes carryreqRepoWriter. The inlineCanRead/CanWritechecks at the top of all 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, and setsX-Total-CountandLinkheaders per API contribution guidelines. Two new model functions (CountProjectColumns,GetProjectColumns) are added with unit tests.Further review feedback addressed
Review rounds after the initial three items above:
routers/common.ParseIssueFilterStateIsClosedfor thestatequery parameter; validatestateinEditProjectand return 422 on invalid values.optional.Option[T]/optional.FromPtrpattern forEditProject, via a newservices/projects.UpdateProject+UpdateProjectOptions(mirrorsuser_service.UpdateOptions). The service helper wraps field updates andChangeProjectStatusindb.WithTxso a PATCH applies atomically.Column.Sortingfromint8toint. New migrationv332widensproject_board.sortingtoINT. This drops the 127-column ceiling and lets the API expose a normalintwithout truncation.issues_model.SortTypeProjectColumnSortingand replace the magic-string callsites inmodels/issues,services/projects, and the new API handler.issues_model.GetIssueByRepoIDinstead ofGetIssueByID+ manualRepoIDcheck.assignIssueToProjectColumn(ctx, add bool)helper.LoadRepo/LoadOwnerfromconvert.ToProject; callers preloadproject.Repoto avoid N+1 queries when listing.RemoveIssueFromProjectColumnnow verifies theproject_issuerow matches the URL-specified column before clearing the assignment, so a DELETE on column A cannot detach an issue that lives in column B of the same project.Testing
Built from source and tested against a local Gitea instance with SQLite. All endpoints verified end-to-end.
Unit tests:
go test -tags sqlite,sqlite_unlock_notify ./models/project/...Integration tests:
go test -tags sqlite,sqlite_unlock_notify -run '^TestAPIProjects$' ./tests/integration/cover all project and column endpoints including pagination, the invalid-staterejection, and the cross-column DELETE guard.AI disclosure
This PR was prepared with the assistance of Claude (Anthropic). The original implementation in #36008 was prepared with Claude Sonnet 4.5; subsequent review-feedback rounds in this PR were prepared with Claude Opus 4.6 and Claude Opus 4.7. The contributor (hanism01) owns the review dialogue, manually tested all endpoints, and reviewed all generated code before submission.