Update ListWorkItems to return only CE fields by default. ListWorkItemOptions now includes ReturnedFields which allows users to opt in to EE fields
Summary
Splits work-item field selection so the default ListWorkItems query no longer requests EE-only widget fields (color, healthStatus, iteration, status, weight). Callers that need those fields opt in via a new ReturnedFields []string on ListWorkItemsOptions. Field-rendering logic is factored into internal/graphqlfields so future GraphQL services can adopt the same pattern with only a Field registry.
Follows the direction converged on in !2931 (closed): CE-safe default, caller-controlled ReturnedFields, no metadata detection or sync.Once cache inside client-go.
Motivation
Client-go's GraphQL templates hard-code field selection. The WorkItem fragment includes fields that only exist in the EE schema. On Community Edition those widget types are not present in the schema (WorkItemWidgetType enum is built dynamically and EE widgets are added by an EE module via prepend_mod that never loads on CE), so requesting them is a hard GraphQL validation error before any resolver runs. That's the root cause of gitlab-org/cli#8368; it also blocks the ongoing glab work-items list refactor to use client-go instead of a hand-rolled query.
API surface
// Default: CE-safe fields only. Works on both CE and EE instances.
wi, _, err := client.WorkItems.ListWorkItems(path, nil)
// Explicit subset: only iid, title, state, webUrl populated.
wi, _, err := client.WorkItems.ListWorkItems(path, &gitlab.ListWorkItemsOptions{
ReturnedFields: []string{"iid", "title", "state", "webUrl"},
})
// Opt in to EE fields (caller must determine the edition first,
// typically via client.Metadata.GetMetadata().Enterprise):
wi, _, err := client.WorkItems.ListWorkItems(path, &gitlab.ListWorkItemsOptions{
ReturnedFields: append(gitlab.WorkItemDefaultListFields(), "weight", "healthStatus"),
})New public surface:
WorkItemDefaultListFields() []stringreturns the CE-safe default slice.ListWorkItemsOptions.ReturnedFields []string.
Field names are passed as plain strings. The unexported workItemCEFields and workItemEEFields slices are the source of truth for what's valid; unknown names return an error before the request is sent. Typed constants were considered but dropped: their values collided with pre-existing GraphQL variable names in this package and elsewhere (goconst false positives), and WorkItemDefaultListFields() already gives IDE-discoverable defaults without leaking a WorkItemField* namespace.
Internal helper: internal/graphqlfields
Renders a GraphQL fragment body from a per-service Field registry and a caller-supplied selection. Handles both top-level fields and container-grouped fields (work-item widgets sit under features { ... }) generically, so future GraphQL services adopt field selection with just:
var myFields = []graphqlfields.Field{
{Name: "id", Fragment: "id"},
{Name: "labels", Fragment: `labels { ... }`, Container: "features"},
// ...
}
// One-liner per service:
fragment, err := graphqlfields.BuildFragment(myFields, selected)Registry order is preserved for stable golden-file assertions. Unknown names in the selection return an error before any request is sent. Verbatim fragment text is copied through, so nested template references (e.g. {{ template "UserCoreBasic" }}) bind against the outer template tree. Multi-line fragments are indented at the target depth per line, so hand-written GraphQL block style is preserved in the rendered query.
The helper is internal/ so we can iterate on the shape while only work items uses it.
Performance
buildListWorkItemsTemplate caches parsed templates in a sync.Map keyed on the sorted field-set signature. Paginated callers (ScanAndCollect etc.) pay the parse cost once per unique selection, not per page.
Scope
Applied to ListWorkItems only. The same pattern extends naturally to GetWorkItem, CreateWorkItem, and UpdateWorkItem; deliberately leaving those for follow-up MRs so this one stays reviewable.
Field classification correctness
Verified against the Rails monolith (GitLab 19.2.0-pre). The five widgets moved to opt-in match EE::WorkItems::TypesFramework::SystemDefined::WidgetDefinition::WIDGETS_WITH_LICENSE exactly:
| Widget | EE license |
|---|---|
color |
epic_colors |
healthStatus |
issuable_health_status |
iteration |
iterations |
status |
work_item_status |
weight |
issue_weights |
The remaining 8 EE-gated widgets in Rails (verification_status, requirement_legacy, test_reports, progress, custom_fields, vulnerabilities, ai_session, agent_plan) aren't in the client-go template at all, so no opt-in is needed for them.
The registry is partitioned at declaration: workItemCEFields holds CE-safe fields, workItemEEFields holds the five EE fields, and workItemFields = append(workItemCEFields, workItemEEFields...) is what graphqlfields.BuildFragment validates against. WorkItemDefaultListFields() iterates just the CE partition, so the CE/EE split is structural — no separate lookup table to keep in sync.
Behavior change
The work-items API is marked experimental, so the breaking change is deliberate:
- Callers running against EE that previously received populated
Color,HealthStatus,IterationID,Status,Weightfields without opting in will now see zero values until they passReturnedFields. - Callers running against CE that previously received GraphQL validation errors will now succeed.
Test plan
-
TestListWorkItems_DefaultQueryMatchesGoldenFile— full default query is validated againsttestdata/query_list_work_items.graphql, guarding against shell-level regressions -
TestListWorkItems_DefaultReturnedFieldsAreCESafe— default query contains no EE fragments -
TestListWorkItems_ReturnedFieldsControlsQueryShape— explicit list narrows the query -
TestListWorkItems_EnterpriseFieldsCanBeOptedIn— appending EE fields renders them -
TestListWorkItems_UnknownFieldReturnsError— typo returns an error before the request -
TestWorkItemCEAndEEFieldsAreDisjoint— invariant check that the two partitions don't overlap -
internal/graphqlfieldsunit tests cover empty registry (with and without selection), empty selection, top-level-only, container grouping, mixed order preservation, multiple containers, unknown field, and verbatim fragment pass-through - Full test suite passes;
golangci-lint run(full, not--new-from-rev) is clean;gofumptclean