Skip to content

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

Closed
hanism01 wants to merge 44 commits into
go-gitea:mainfrom
hanism01:fix/project-board-api-review-feedback
Closed

feat(api): add REST API for repository project boards#36831
hanism01 wants to merge 44 commits into
go-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 a column's 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

Issues raised by @lunny in the original review:

1. Duplicate permission checks removed
The /projects route group is wrapped with reqRepoReader(unit.TypeProjects) in api.go, and individual write routes carry reqRepoWriter. The inline CanRead/CanWrite checks at the top of all 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, and sets X-Total-Count and Link headers 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:

  • Use routers/common.ParseIssueFilterStateIsClosed for the state query parameter; validate state in EditProject and return 422 on invalid values.
  • Adopt the optional.Option[T] / optional.FromPtr pattern for EditProject, via a new services/projects.UpdateProject + UpdateProjectOptions (mirrors user_service.UpdateOptions). The service helper wraps field updates and ChangeProjectStatus in db.WithTx so a PATCH applies atomically.
  • Migrate Column.Sorting from int8 to int. New migration v332 widens project_board.sorting to INT. This drops the 127-column ceiling and lets the API expose a normal int without truncation.
  • Add issues_model.SortTypeProjectColumnSorting and replace the magic-string callsites in models/issues, services/projects, and the new API handler.
  • Use issues_model.GetIssueByRepoID instead of GetIssueByID + manual RepoID check.
  • Dedupe the add and remove issue handlers via a shared assignIssueToProjectColumn(ctx, add bool) helper.
  • Drop lazy LoadRepo / LoadOwner from convert.ToProject; callers preload project.Repo to avoid N+1 queries when listing.
  • RemoveIssueFromProjectColumn now verifies the project_issue row 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-state rejection, 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.

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
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.

@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 27, 2026
silverwind and others added 3 commits April 27, 2026 13:03
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>
@silverwind
Copy link
Copy Markdown
Member

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>
@lunny lunny added this to the 1.27.0 milestone Apr 27, 2026
…nism01/gitea into hanism01-fix/project-board-api-review-feedback
Comment thread models/migrations/v1_27/v332.go Outdated
@GiteaBot GiteaBot added lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. and removed lgtm/need 1 This PR needs approval from one additional maintainer to be merged. labels Apr 27, 2026
@lunny lunny dismissed their stale review April 27, 2026 18:45

need some adjustments

@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. labels Apr 27, 2026
@wxiaoguang
Copy link
Copy Markdown
Contributor

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.

@wxiaoguang wxiaoguang marked this pull request as draft April 27, 2026 18:48
@wxiaoguang
Copy link
Copy Markdown
Contributor

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>
@silverwind
Copy link
Copy Markdown
Member

Migration dropped in 49b0df0.

@beardev-in
Copy link
Copy Markdown

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

@hanism01 May I know what exactly is needed to take get this PR merged?

@hanism01
Copy link
Copy Markdown
Author

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

@hanism01 May I know what exactly is needed to take get this PR merged?

Hi @beardev-in, here's where things stand:

  1. Blocked on upstream work. @wxiaoguang marked this PR as draft and noted it should wait for the multiple-project-support work to land first, since this PR touches that area (comment).
  2. Maintainer approval dismissed. @lunny dismissed their earlier approval, requesting adjustments (event).
  3. Review feedback from @silverwind. Five items were flagged in this review that may still need addressing.
    This is blocked until the multiple-project-support issue is resolved upstream. After that, it needs a rebase, remaining review feedback addressed, and one more maintainer approval.
    I'm currently too swamped with other projects to tackle this in the near future. If anyone wants to pick it up, feel free.

This comment was generated with the assistance of AI (Claude, Anthropic).

Copy link
Copy Markdown
Contributor

@wxiaoguang wxiaoguang left a comment

Choose a reason for hiding this comment

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

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.

@GiteaBot GiteaBot added lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged and removed lgtm/need 1 This PR needs approval from one additional maintainer to be merged. labels Apr 30, 2026
@beardev-in
Copy link
Copy Markdown

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.

@wxiaoguang wxiaoguang closed this May 3, 2026
@GiteaBot GiteaBot removed this from the 1.27.0 milestone May 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged 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