Skip to content

feat(api): add REST API for repository project boards#36831

Draft
hanism01 wants to merge 31 commits intogo-gitea:mainfrom
hanism01:fix/project-board-api-review-feedback
Draft

feat(api): add REST API for repository project boards#36831
hanism01 wants to merge 31 commits intogo-gitea:mainfrom
hanism01:fix/project-board-api-review-feedback

Conversation

@hanism01
Copy link
Copy Markdown

@hanism01 hanism01 commented Mar 4, 2026

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 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 (paginated)
  • POST /repos/{owner}/{repo}/projects/{id}/columns — create column
  • PATCH /repos/{owner}/{repo}/projects/{id}/columns/{column_id} — update column
  • DELETE /repos/{owner}/{repo}/projects/{id}/columns/{column_id} — delete column
  • GET /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues -- list columns' issues
  • POST /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues/{issue_id} — assign issue to column
  • DELETE /repos/{owner}/{repo}/projects/{id}columns/{column_id}/issues/{issue_id} -- remove issue from column

Changes from #36008

Three issues raised by @lunny in review:

1. Duplicate permission checks removed
The /projects route group is already wrapped with reqRepoReader(unit.TypeProjects) in api.go, and individual write routes carry reqRepoWriter. The inline CanRead/CanWrite checks at the top of all 10 handlers were unreachable dead code.

2. AddOrUpdateIssueToColumn replaced
The custom function introduced in #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 issues_model.IssueAssignOrRemoveProject, which provides all three. The custom function is deleted entirely.

3. ListProjectColumns pagination implemented correctly
The original implementation fetched all columns then sliced in memory. ListProjectColumns now uses DB-level pagination via db.SetSessionPagination, sets X-Total-Count and Link headers 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.

SupenBysz and others added 2 commits March 4, 2026 08:12
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>
@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Mar 4, 2026
@github-actions github-actions bot added modifies/api This PR adds API routes or modifies them modifies/go Pull requests that update Go code labels Mar 4, 2026
lunny
lunny previously approved these changes Mar 4, 2026
@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Mar 4, 2026
@lunny lunny added type/enhancement An improvement of existing functionality topic/api Concerns mainly the API labels Mar 4, 2026
@lunny lunny added this to the 1.26.0 milestone Mar 4, 2026
Copy link
Copy Markdown
Member

@silverwind silverwind left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by Claude

  1. Bug: EditProject drops field updates when IsClosed is set (routers/api/v1/repo/project.go:273-282)
    When form.IsClosed != nil, only ChangeProjectStatus is called — UpdateProject is in the else branch. A PATCH with {"title": "new title", "is_closed": true} silently discards the title change. UpdateProject should be called unconditionally.

  2. ListProjects pagination inconsistent with other endpoints (routers/api/v1/repo/project.go:73-82)
    ListProjects manually parses page/limit via ctx.FormInt with a setting.UI.IssuePagingNum fallback, while ListProjectColumns (and all other Gitea list endpoints) use utils.GetListOptions(ctx). Should use the standard helper for consistency and to respect DEFAULT_PAGING_NUM.

  3. AddIssueToProjectColumn swagger 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. AddIssueToProjectColumnOption already exists in modules/structs/project.go and is registered in swagger/options.go — the swagger comment should reference it via $ref.

  4. MoveProjectColumnOption is 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.

  5. Minor: Sorting truncation (routers/api/v1/repo/project.go:539)
    column.Sorting = int8(*form.Sorting) silently truncates — the API accepts any int but the model is int8.

@silverwind
Copy link
Copy Markdown
Member

CI failures are from #36858, SetLinkHeader signature has changed.

@Rutledge
Copy link
Copy Markdown

Excited for this API to land! Anything that would be helpful in getting the CI failures resolved?

@hanism01
Copy link
Copy Markdown
Author

feel free to contribute. currently swamped myself with my own projects.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}/projects REST 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.

@lunny lunny mentioned this pull request Apr 4, 2026
6 tasks
@github-actions github-actions bot added modifies/cli PR changes something on the CLI, i.e. gitea doctor or gitea admin modifies/templates This PR modifies the template files modifies/internal modifies/frontend labels Apr 4, 2026
@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Apr 4, 2026
@github-actions github-actions bot removed modifies/cli PR changes something on the CLI, i.e. gitea doctor or gitea admin modifies/templates This PR modifies the template files modifies/internal modifies/frontend labels Apr 5, 2026
isClosed = optional.None[bool]()
default:
isClosed = optional.Some(false)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParseIssueFilterStateIsClosed

form := web.GetForm(ctx).(*api.EditProjectOption)

if form.Title != nil {
project.Title = *form.Title
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional.FromPtr

column.Color = *form.Color
}
if form.Sorting != nil {
sorting := int8(*form.Sorting)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int8 ?????

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No const for it?

image

return
}

issue, err := issues_model.GetIssueByID(ctx, ctx.PathParamInt64("issue_id"))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not GetRepoIssueByID(repoID, issueID)?

Comment on lines +797 to +810
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate code

Comment on lines +687 to +689
defer func() {
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
}()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% AI slop

@wxiaoguang wxiaoguang force-pushed the fix/project-board-api-review-feedback branch from 29f4615 to 4fea94c Compare April 5, 2026 02:56
@wxiaoguang
Copy link
Copy Markdown
Contributor

wxiaoguang commented Apr 5, 2026

  • POST /repos/{owner}/{repo}/projects/columns/{id}/issues/{issue_id} — assign issue to column
  • DELETE /repos/{owner}/{repo}/projects/columns/{id}/issues/{issue_id} -- remove issue from column

Is this design really easy to use for end users? GitHub supports using issue id OR index(number) directly.

Details image image

@lunny lunny marked this pull request as draft April 5, 2026 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/need 1 This PR needs approval from one additional maintainer to be merged. modifies/api This PR adds API routes or modifies them modifies/go Pull requests that update Go code topic/api Concerns mainly the API type/enhancement An improvement of existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: REST API for repository project boards

8 participants