Skip to content

Add read-only support for the Group epic boards API (/groups/:id/epic_boards) #3460

Open

Description

@hidingmyname

Description of the problem, including code/CLI snippet

Location (python-gitlab/python-gitlab @ default branch 8daa940):

    badges: GroupBadgeManager
    billable_members: GroupBillableMemberManager
    boards: GroupBoardManager
    clusters: GroupClusterManager
    customattributes: GroupCustomAttributeManager
    deploytokens: GroupDeployTokenManager
    descendant_groups: GroupDescendantGroupManager
    epics: GroupEpicManager
    exports: GroupExportManager
    hooks: GroupHookManager
    imports: GroupImportManager

Problem

GitLab documents a Group epic boards API (Premium/Ultimate) with four read endpoints:

  • GET /groups/:id/epic_boards
  • GET /groups/:id/epic_boards/:board_id
  • GET /groups/:id/epic_boards/:board_id/lists
  • GET /groups/:id/epic_boards/:board_id/lists/:list_id

python-gitlab exposes group issue boards (group.boards, with board.lists), but there is no manager for epic boards: the Group class has boards and epics but no epic-board attribute, and boards.py only defines issue-board managers. Listing epic boards and their columns (e.g. for board inventory or migration scripts) therefore requires raw gl.http_get(...) calls.

Reproduction

group = gl.groups.get(5, lazy=True)
group.epicboards.list()
# AttributeError: 'Group' object has no attribute 'epicboards'

Environment: main at 8daa94041261 (8.6.0); also absent at 51eb17e6e67a.

Expected Behavior

Typed managers analogous to the existing group board managers, e.g.:

boards = group.epicboards.list()          # GET /groups/5/epic_boards
board = group.epicboards.get(1)           # GET /groups/5/epic_boards/1
lists = board.lists.list()                # GET /groups/5/epic_boards/1/lists
lst = board.lists.get(2)                  # GET /groups/5/epic_boards/1/lists/2

Proposed scope: list/get for boards and board lists only, i.e. exactly what the REST page documents today. No create/update/delete (not documented for this resource), and nothing related to the deprecated Epics REST API or Work Items. The attribute name (epicboards, epic_boards, ...) is up to the maintainers.

Actual Behavior

AttributeError: 'Group' object has no attribute 'epicboards'; the only workaround is gl.http_get(f"/groups/{gid}/epic_boards") / gl.http_list(...), which returns plain dicts.

Already implemented in xanzy/go-gitlab

PR: xanzy/go-gitlab#1764 — "Epic Boards" (merged 2023-07-30T16:08:48Z, merge commit fd3ac6371e6). It adds a GroupEpicBoardsService with ListGroupEpicBoards and GetGroupEpicBoard, covering the board-level endpoints; the /lists endpoints are not part of this PR. go-gitlab is a separate client for the same GitLab REST API, so this is precedent, not an obligation.

How go-gitlab fixed it:

--- a/group_epic_boards.go
+++ b/group_epic_boards.go
@@ -0,0 +1,104 @@
...
+// GroupEpicBoard represents a GitLab group epic board.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_epic_boards.html
+type GroupEpicBoard struct {
+	ID     int             `json:"id"`
+	Name   string          `json:"name"`
+	Group  *Group          `json:"group"`
+	Labels []*LabelDetails `json:"labels"`
+	Lists  []*BoardList    `json:"lists"`
+}
...
+func (s *GroupEpicBoardsService) ListGroupEpicBoards(gid interface{}, opt *ListGroupEpicBoardsOptions, options ...RequestOptionFunc) ([]*GroupEpicBoard, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/epic_boards", PathEscape(group))
...
+func (s *GroupEpicBoardsService) GetGroupEpicBoard(gid interface{}, board int, options ...RequestOptionFunc) (*GroupEpicBoard, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/epic_boards/%d", PathEscape(group), board)

Suggested fix

Add read-only (RetrieveMixin) group epic board and board list managers next to the existing group board managers in boards.py, attach the board manager to Group as epicboards, and document them; this covers the two board endpoints go-gitlab has plus the two documented /lists endpoints:

diff --git a/docs/gl_objects/boards.rst b/docs/gl_objects/boards.rst
--- a/docs/gl_objects/boards.rst
+++ b/docs/gl_objects/boards.rst
@@ -102,3 +102,39 @@ set it at the given position and move the following lists up a position::
 Delete a list::
 
     b_list.delete()
+
+Group epic boards
+=================
+
+Epic boards show the epics of a group, organized in lists. Only read access
+is available through the API.
+
+Reference
+---------
+
+* v4 API:
+
+  + :class:`gitlab.v4.objects.GroupEpicBoard`
+  + :class:`gitlab.v4.objects.GroupEpicBoardManager`
+  + :attr:`gitlab.v4.objects.Group.epicboards`
+  + :class:`gitlab.v4.objects.GroupEpicBoardList`
+  + :class:`gitlab.v4.objects.GroupEpicBoardListManager`
+  + :attr:`gitlab.v4.objects.GroupEpicBoard.lists`
+
+* GitLab API: https://docs.gitlab.com/api/group_epic_boards (EE feature)
+
+Examples
+--------
+
+List the epic boards of a group::
+
+    boards = group.epicboards.list(get_all=True)
+
+Get a single epic board::
+
+    board = group.epicboards.get(board_id)
+
+List the lists of an epic board, or get a single list::
+
+    b_lists = board.lists.list(get_all=True)
+    b_list = board.lists.get(list_id)
diff --git a/gitlab/v4/objects/boards.py b/gitlab/v4/objects/boards.py
--- a/gitlab/v4/objects/boards.py
+++ b/gitlab/v4/objects/boards.py
@@ -1,5 +1,5 @@
 from gitlab.base import RESTObject
-from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
+from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin
 from gitlab.types import RequiredOptional
 
 __all__ = [
@@ -7,6 +7,10 @@ __all__ = [
     "GroupBoardListManager",
     "GroupBoard",
     "GroupBoardManager",
+    "GroupEpicBoardList",
+    "GroupEpicBoardListManager",
+    "GroupEpicBoard",
+    "GroupEpicBoardManager",
     "ProjectBoardList",
     "ProjectBoardListManager",
     "ProjectBoard",
@@ -39,6 +43,26 @@ class GroupBoardManager(CRUDMixin[GroupBoard]):
     _create_attrs = RequiredOptional(required=("name",))
 
 
+class GroupEpicBoardList(RESTObject):
+    pass
+
+
+class GroupEpicBoardListManager(RetrieveMixin[GroupEpicBoardList]):
+    _path = "/groups/{group_id}/epic_boards/{board_id}/lists"
+    _obj_cls = GroupEpicBoardList
+    _from_parent_attrs = {"group_id": "group_id", "board_id": "id"}
+
+
+class GroupEpicBoard(RESTObject):
+    lists: GroupEpicBoardListManager
+
+
+class GroupEpicBoardManager(RetrieveMixin[GroupEpicBoard]):
+    _path = "/groups/{group_id}/epic_boards"
+    _obj_cls = GroupEpicBoard
+    _from_parent_attrs = {"group_id": "id"}
+
+
 class ProjectBoardList(SaveMixin, ObjectDeleteMixin, RESTObject):
     pass
 
diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py
--- a/gitlab/v4/objects/groups.py
+++ b/gitlab/v4/objects/groups.py
@@ -23,7 +23,7 @@ from gitlab.types import RequiredOptional
 from .access_requests import GroupAccessRequestManager  # noqa: F401
 from .audit_events import GroupAuditEventManager  # noqa: F401
 from .badges import GroupBadgeManager  # noqa: F401
-from .boards import GroupBoardManager  # noqa: F401
+from .boards import GroupBoardManager, GroupEpicBoardManager  # noqa: F401
 from .branches import GroupProtectedBranchManager  # noqa: F401
 from .clusters import GroupClusterManager  # noqa: F401
 from .container_registry import GroupRegistryRepositoryManager  # noqa: F401
@@ -84,6 +84,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
     customattributes: GroupCustomAttributeManager
     deploytokens: GroupDeployTokenManager
     descendant_groups: GroupDescendantGroupManager
+    epicboards: GroupEpicBoardManager
     epics: GroupEpicManager
     exports: GroupExportManager
     hooks: GroupHookManager

Verified on 51eb17e (Python 3.14.10, pytest 9.1.1): a responses-based unit test for the four endpoints (tests/unit/objects/test_group_epic_boards.py, which would go into the PR) fails without this patch with the AttributeError above and passes with it; no other test results change.

I can submit a PR for this.

Checked: searches for epic board, epic_boards, epicboards, "epic boards" in this repository found no existing issue or PR.

Specifications

  • python-gitlab version: main at 8daa94041261 (8.6.0); also absent at 51eb17e6e67a
  • Gitlab server version (or gitlab.com): API documented for GitLab Premium/Ultimate (GitLab.com, Self-Managed, Dedicated)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions