Forgejo: our own project-board API + board automation patch (until upstream ships) #404

Closed
opened 2026-10-05 01:33:49 +00:00 by sky_line · 1 comment
Owner

Forgejo has no API for project boards and no board automation. We want both for the Slipstream board: moving cards when status/* labels change, moving closed tickets to Done, adding new tickets automatically, and setting the label when a card is dragged. This ticket covers running our own small patch on our Forgejo instance until upstream ships an API. It can be based on the existing upstream patches.

Research was done on 2026-10-04 against our instance (Forgejo 15.0.9, base gitea-1.22), reading the v15.0.9 and v16.0.5 source. Nothing has been changed yet.

Upstream status

  • Forgejo has no project API. GET /api/v1/repos/{owner}/{repo}/projects returns 404, and boards are web-only. There's also no server-side plugin system, so any API or automation means patching Forgejo.
  • The Forgejo API is in progress, done by Telekom contributors (discussions #466):
    • #13066 (consistent project assignment) and #13700 (project API refactorings, groundwork only) are merged for v17.
    • #13714 "Project API Full" is open and WIP, and is being split into smaller PRs. It covers full CRUD for projects, columns and issues-in-columns, including move. It adds new token scopes read:project / write:project and no migration.
    • Earliest release: v18 (14 Jan 2027). More realistically v19 LTS (15 Apr 2027).
  • Automation (label ↔ column) isn't planned upstream. The issues #4462, #6733 and #10612 have no code. Gitea's #30205 (project workflows) is open and stalled; it also adds a project_workflow table, i.e. a migration.
  • Gitea merged a project API in #38691 (Gitea v28). It can't be applied to Forgejo as a patch (different module path, error API and service layout), but it's a good reference.
  • Upstreaming our own patch isn't realistic. Forgejo's AI Agreement rejects AI-generated code (an earlier full API, #9384, was dropped over it), and the API work already has an owner.

Options

Option Effort Risk Upkeep
A. Our own minimal patch on v15.0.9 (recommended) ~1.5–3 days + ~½ day build setup low: no DB migration, no new token scope, stock binary can be swapped back any time ~15–30 min rebase per 15.0.x release; delete when upstream ships
B. Backport #13066 + #13700 + #13714 onto v15.0.9 1–2 days of mechanical conflict resolution medium: #13714 is WIP and keeps moving; write:project tokens won't parse on stock high until upstream lands
B2. Same stack onto v16.0.5 (applies almost cleanly) ~½ day medium: moving to v16 is one-way, and v16 is end-of-life 29 Oct 2026 high
C. Wait for upstream 0 0 API in v18/v19; automation not planned

Option A design (paths are in v15.0.9)

All the primitives exist already, so no schema change is needed:

  • models/issues/issue_project.go:103 IssueAssignOrRemoveProject(ctx, issue, doer, projectID, columnID) adds an issue to a project or moves it to a column, and writes a project comment.
  • models/project/issue.go:70 MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs) reorders or moves cards.
  • models/project/column.go: GetColumns (238), GetDefaultColumn (248), GetColumn (205). project_model.GetProjectByID loads a project.

API: a new routers/api/v1/repo/project.go (~300–400 lines), registered in routers/api/v1/api.go under /repos/{username}/{reponame}. Gate it with reqRepoReader/Writer(unit.TypeProjects), reqToken(), and the existing tokenRequiresScopes(AccessTokenScopeCategoryIssue) (the same scope Gitea uses), so no new scope. Endpoints:

  • GET projects
  • GET projects/{id}/columns
  • GET projects/{id}/columns/{cid}/issues
  • POST projects/{id}/issues (bulk add)
  • POST projects/{id}/columns/{cid}/issues/{index} (add/move)

Mirror #13714's route shapes so the later switch to upstream is cheap.

Automation: a new services/projectautomation/notifier.go (~200 lines). Embed notify_service.NullNotifier (services/notify/null.go:19) and override:

  • IssueChangeLabels (services/notify/notifier.go:41, fired from services/issue/label.go): status/* label → matching column.
  • IssueChangeStatus (notifier.go:32): closed → Done; reopened → the column for its status label.
  • NewIssue (notifier.go:31): auto-add to the board.

Register it with notify_service.RegisterNotifier from an Init() in routers/init.go, next to mustInit(automerge.Init) (~line 153). Configure it by convention (column title ↔ the label's exclusive-scope value) or with a custom [project_automation] app.ini section, so nothing goes in the DB.

Card → label (reverse direction): v15 fires no notifier, webhook or comment when a card moves. Add ~30 lines in routers/web/repo/projects.go MoveIssues (598, after ~683) that call issue_service.AddLabel with the column's label. Exclusive scopes drop the old status/* label. There's no loop: the follow-up move lands in the same column, which is a no-op.

Alternative for the forward direction: patch only the API, and run label→column / closed→Done / auto-add as a Forgejo Actions workflow (on: issues: [opened, closed, labeled]) that calls the API with a token. That fits the applejack runner and survives the switch to upstream. The reverse direction still needs the MoveIssues hook.

Total: about 600–900 lines of Go.

Database migration risk (why A avoids schema changes)

Forgejo records each migration in the forgejo_migration table. If a custom build adds even one, every stock release refuses to start (models/forgejo_migrations/migrate.go:181-195). That blocks rolling back to stock 15.0.x and upgrading to stock v16/v17 alike. Adding struct fields without a migration is silent schema drift, so don't do that either. Option A has neither.

Building and running

  • Toolchain: Go ≥ 1.26 (toolchain go1.26.7) and Node ≥ 20.
  • Build: git checkout v15.0.9, apply the patch, then TAGS="bindata timetzdata sqlite sqlite_unlock_notify" make build. The output binary is named gitea. This could run on applejack CI.
  • Deploy: swap the binary on starswirl and restart the systemd unit, keeping the stock binary for rollback.
  • Upgrades: rebase on each 15.0.x security release (they come every couple of weeks). v15 LTS is supported to 15 Jul 2027, which outlasts the upstream timeline.
  • Trade-off: we lose the officially signed binary and must rebuild promptly after security releases.

Interim (no code needed)

  • Bulk-add every open issue: the web route POST /sky_line/Slipstream/issues/projects (issue_ids = internal ids, comma-separated; id = project id) has no cap on how many ids it takes, and v15 uses same-origin protection rather than a _csrf token. A same-origin fetch from a logged-in browser console can therefore add all issues in one call (collect ids via /api/v1/repos/sky_line/Slipstream/issues?state=open&type=issues&limit=50&page=N).
  • Or: set [ui] ISSUE_PAGING_NUM = 300 (it has no maximum; the issue-list handler ignores URL page sizes) and restart, then select all → Projects.
  • Until automation exists: keep the status/* labels updated and drag cards by hand.

Done when

  • Board API endpoints work with an issue-scoped token (list projects/columns, bulk add, move).
  • Label change, close, reopen and new issue each update the board automatically.
  • Dragging a card sets the matching status/* label.
  • No DB migration, no new token scope; the stock binary still starts on the same DB.
  • Build + deploy + rollback steps documented; patch rebased on the current 15.0.x.
  • Plan to drop the patch once upstream's project API (#13714 successors) ships.
Forgejo has no API for project boards and no board automation. We want both for the **Slipstream** board: moving cards when `status/*` labels change, moving closed tickets to Done, adding new tickets automatically, and setting the label when a card is dragged. This ticket covers running our own small patch on our Forgejo instance until upstream ships an API. It can be based on the existing upstream patches. Research was done on 2026-10-04 against our instance (**Forgejo 15.0.9**, base gitea-1.22), reading the v15.0.9 and v16.0.5 source. Nothing has been changed yet. ## Upstream status - **Forgejo has no project API.** `GET /api/v1/repos/{owner}/{repo}/projects` returns 404, and boards are web-only. There's also no server-side plugin system, so any API or automation means patching Forgejo. - **The Forgejo API is in progress, done by Telekom contributors** ([discussions #466](https://codeberg.org/forgejo/discussions/issues/466)): - [#13066](https://codeberg.org/forgejo/forgejo/pulls/13066) (consistent project assignment) and [#13700](https://codeberg.org/forgejo/forgejo/pulls/13700) (project API refactorings, groundwork only) are merged for v17. - [#13714](https://codeberg.org/forgejo/forgejo/pulls/13714) "Project API Full" is **open and WIP**, and is being split into smaller PRs. It covers full CRUD for projects, columns and issues-in-columns, including move. It adds new token scopes `read:project` / `write:project` and no migration. - **Earliest release:** v18 (14 Jan 2027). More realistically v19 LTS (15 Apr 2027). - **Automation (label ↔ column) isn't planned upstream.** The issues [#4462](https://codeberg.org/forgejo/forgejo/issues/4462), [#6733](https://codeberg.org/forgejo/forgejo/issues/6733) and [#10612](https://codeberg.org/forgejo/forgejo/issues/10612) have no code. Gitea's [#30205](https://github.com/go-gitea/gitea/pull/30205) (project workflows) is open and stalled; it also adds a `project_workflow` table, i.e. a migration. - **Gitea merged a project API** in [#38691](https://github.com/go-gitea/gitea/pull/38691) (Gitea v28). It can't be applied to Forgejo as a patch (different module path, error API and service layout), but it's a good reference. - **Upstreaming our own patch isn't realistic.** Forgejo's AI Agreement rejects AI-generated code (an earlier full API, [#9384](https://codeberg.org/forgejo/forgejo/pulls/9384), was dropped over it), and the API work already has an owner. ## Options | Option | Effort | Risk | Upkeep | |---|---|---|---| | **A. Our own minimal patch on v15.0.9** (recommended) | ~1.5–3 days + ~½ day build setup | low: no DB migration, no new token scope, stock binary can be swapped back any time | ~15–30 min rebase per 15.0.x release; delete when upstream ships | | B. Backport #13066 + #13700 + #13714 onto v15.0.9 | 1–2 days of mechanical conflict resolution | medium: #13714 is WIP and keeps moving; `write:project` tokens won't parse on stock | high until upstream lands | | B2. Same stack onto v16.0.5 (applies almost cleanly) | ~½ day | medium: moving to v16 is one-way, and v16 is end-of-life 29 Oct 2026 | high | | C. Wait for upstream | 0 | 0 | API in v18/v19; automation not planned | ## Option A design (paths are in v15.0.9) All the primitives exist already, so **no schema change is needed**: - `models/issues/issue_project.go:103` `IssueAssignOrRemoveProject(ctx, issue, doer, projectID, columnID)` adds an issue to a project or moves it to a column, and writes a project comment. - `models/project/issue.go:70` `MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs)` reorders or moves cards. - `models/project/column.go`: `GetColumns` (238), `GetDefaultColumn` (248), `GetColumn` (205). `project_model.GetProjectByID` loads a project. **API:** a new `routers/api/v1/repo/project.go` (~300–400 lines), registered in `routers/api/v1/api.go` under `/repos/{username}/{reponame}`. Gate it with `reqRepoReader/Writer(unit.TypeProjects)`, `reqToken()`, and the **existing** `tokenRequiresScopes(AccessTokenScopeCategoryIssue)` (the same scope Gitea uses), so no new scope. Endpoints: - `GET projects` - `GET projects/{id}/columns` - `GET projects/{id}/columns/{cid}/issues` - `POST projects/{id}/issues` (bulk add) - `POST projects/{id}/columns/{cid}/issues/{index}` (add/move) Mirror #13714's route shapes so the later switch to upstream is cheap. **Automation:** a new `services/projectautomation/notifier.go` (~200 lines). Embed `notify_service.NullNotifier` (`services/notify/null.go:19`) and override: - `IssueChangeLabels` (`services/notify/notifier.go:41`, fired from `services/issue/label.go`): `status/*` label → matching column. - `IssueChangeStatus` (`notifier.go:32`): closed → Done; reopened → the column for its status label. - `NewIssue` (`notifier.go:31`): auto-add to the board. Register it with `notify_service.RegisterNotifier` from an `Init()` in `routers/init.go`, next to `mustInit(automerge.Init)` (~line 153). Configure it by convention (column title ↔ the label's exclusive-scope value) or with a custom `[project_automation]` app.ini section, so nothing goes in the DB. **Card → label (reverse direction):** v15 fires no notifier, webhook or comment when a card moves. Add ~30 lines in `routers/web/repo/projects.go` `MoveIssues` (598, after ~683) that call `issue_service.AddLabel` with the column's label. Exclusive scopes drop the old `status/*` label. There's no loop: the follow-up move lands in the same column, which is a no-op. **Alternative for the forward direction:** patch only the API, and run label→column / closed→Done / auto-add as a Forgejo Actions workflow (`on: issues: [opened, closed, labeled]`) that calls the API with a token. That fits the applejack runner and survives the switch to upstream. The reverse direction still needs the `MoveIssues` hook. **Total:** about 600–900 lines of Go. ## Database migration risk (why A avoids schema changes) Forgejo records each migration in the `forgejo_migration` table. If a custom build adds even one, **every stock release refuses to start** (`models/forgejo_migrations/migrate.go:181-195`). That blocks rolling back to stock 15.0.x and upgrading to stock v16/v17 alike. Adding struct fields without a migration is silent schema drift, so don't do that either. Option A has neither. ## Building and running - **Toolchain:** Go ≥ 1.26 (toolchain go1.26.7) and Node ≥ 20. - **Build:** `git checkout v15.0.9`, apply the patch, then `TAGS="bindata timetzdata sqlite sqlite_unlock_notify" make build`. The output binary is named `gitea`. This could run on applejack CI. - **Deploy:** swap the binary on starswirl and restart the systemd unit, keeping the stock binary for rollback. - **Upgrades:** rebase on each 15.0.x security release (they come every couple of weeks). v15 LTS is supported to 15 Jul 2027, which outlasts the upstream timeline. - **Trade-off:** we lose the officially signed binary and must rebuild promptly after security releases. ## Interim (no code needed) - **Bulk-add every open issue:** the web route `POST /sky_line/Slipstream/issues/projects` (`issue_ids` = internal ids, comma-separated; `id` = project id) has no cap on how many ids it takes, and v15 uses same-origin protection rather than a `_csrf` token. A same-origin `fetch` from a logged-in browser console can therefore add all issues in one call (collect ids via `/api/v1/repos/sky_line/Slipstream/issues?state=open&type=issues&limit=50&page=N`). - **Or:** set `[ui] ISSUE_PAGING_NUM = 300` (it has no maximum; the issue-list handler ignores URL page sizes) and restart, then select all → Projects. - **Until automation exists:** keep the `status/*` labels updated and drag cards by hand. ## Done when - [ ] Board API endpoints work with an issue-scoped token (list projects/columns, bulk add, move). - [ ] Label change, close, reopen and new issue each update the board automatically. - [ ] Dragging a card sets the matching `status/*` label. - [ ] No DB migration, no new token scope; the stock binary still starts on the same DB. - [ ] Build + deploy + rollback steps documented; patch rebased on the current 15.0.x. - [ ] Plan to drop the patch once upstream's project API (#13714 successors) ships.
sky_line added this to the Slipstream project 2026-10-06 15:04:13 +00:00
Author
Owner

Deployed 2026-10-06 15:03 UTC: starswirl now runs Forgejo 15.0.9-slipstream1 (patch branch slipstream/15.0.9 in the local forgejo-slipstream repo; runbook DEPLOY.md). No schema change; the stock binary is kept on the host for instant rollback.

  • Board API live at /api/v1/repos/sky_line/Slipstream/projects/...
  • All 289 open issues imported into Backlog
  • Automation enabled on the Slipstream board (status/up-next → To Do, status/in-progress → In Progress, status/testing → Testing, closed → Done, new issues → Backlog)
  • Smoke test on #492: new → Backlog, +up-next → To Do, card dragged → label became in-progress, +testing → Testing, closed → Done. All passed.

Ongoing cost: rebuild and redeploy on each 15.0.x security release (DEPLOY.md §7).

Deployed 2026-10-06 15:03 UTC: starswirl now runs Forgejo `15.0.9-slipstream1` (patch branch `slipstream/15.0.9` in the local forgejo-slipstream repo; runbook DEPLOY.md). No schema change; the stock binary is kept on the host for instant rollback. - Board API live at `/api/v1/repos/sky_line/Slipstream/projects/...` - All 289 open issues imported into Backlog - Automation enabled on the Slipstream board (status/up-next → To Do, status/in-progress → In Progress, status/testing → Testing, closed → Done, new issues → Backlog) - Smoke test on #492: new → Backlog, +up-next → To Do, card dragged → label became in-progress, +testing → Testing, closed → Done. All passed. Ongoing cost: rebuild and redeploy on each 15.0.x security release (DEPLOY.md §7).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sky_line/Slipstream#404
No description provided.