Forgejo: our own project-board API + board automation patch (until upstream ships) #404
Labels
No labels
amd
avatars
bug
builder
cannot-reproduce
crash
epic
feature
from-viewer
gl-removal
known-limitation
monitoring
parity
perf
platform
priority
critical
priority
high
priority
low
priority
medium
provenance
rendering
rt
status
in-progress
status
testing
status
up-next
triage
ui
upstream
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
sky_line/Slipstream#404
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
GET /api/v1/repos/{owner}/{repo}/projectsreturns 404, and boards are web-only. There's also no server-side plugin system, so any API or automation means patching Forgejo.read:project/write:projectand no migration.project_workflowtable, i.e. a migration.Options
write:projecttokens won't parse on stockOption A design (paths are in v15.0.9)
All the primitives exist already, so no schema change is needed:
models/issues/issue_project.go:103IssueAssignOrRemoveProject(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:70MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs)reorders or moves cards.models/project/column.go:GetColumns(238),GetDefaultColumn(248),GetColumn(205).project_model.GetProjectByIDloads a project.API: a new
routers/api/v1/repo/project.go(~300–400 lines), registered inrouters/api/v1/api.gounder/repos/{username}/{reponame}. Gate it withreqRepoReader/Writer(unit.TypeProjects),reqToken(), and the existingtokenRequiresScopes(AccessTokenScopeCategoryIssue)(the same scope Gitea uses), so no new scope. Endpoints:GET projectsGET projects/{id}/columnsGET projects/{id}/columns/{cid}/issuesPOST 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). Embednotify_service.NullNotifier(services/notify/null.go:19) and override:IssueChangeLabels(services/notify/notifier.go:41, fired fromservices/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.RegisterNotifierfrom anInit()inrouters/init.go, next tomustInit(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.goMoveIssues(598, after ~683) that callissue_service.AddLabelwith the column's label. Exclusive scopes drop the oldstatus/*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 theMoveIssueshook.Total: about 600–900 lines of Go.
Database migration risk (why A avoids schema changes)
Forgejo records each migration in the
forgejo_migrationtable. 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
git checkout v15.0.9, apply the patch, thenTAGS="bindata timetzdata sqlite sqlite_unlock_notify" make build. The output binary is namedgitea. This could run on applejack CI.Interim (no code needed)
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_csrftoken. A same-originfetchfrom 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).[ui] ISSUE_PAGING_NUM = 300(it has no maximum; the issue-list handler ignores URL page sizes) and restart, then select all → Projects.status/*labels updated and drag cards by hand.Done when
status/*label.Deployed 2026-10-06 15:03 UTC: starswirl now runs Forgejo
15.0.9-slipstream1(patch branchslipstream/15.0.9in the local forgejo-slipstream repo; runbook DEPLOY.md). No schema change; the stock binary is kept on the host for instant rollback./api/v1/repos/sky_line/Slipstream/projects/...Ongoing cost: rebuild and redeploy on each 15.0.x security release (DEPLOY.md §7).