feat(labels): repo label CRUD across CLI, MCP, and OpenCode #42

Merged
ses merged 2 commits from feat/labels into main 2026-09-07 13:23:36 +00:00
Owner

Summary

  • Five new core operations: listLabels, getLabel, createLabel, updateLabel, deleteLabel (plus the resolveLabelId helper).
  • CLI: forgejoctl label {list,view,create,update,delete}.
  • MCP server: list_labels, get_label, create_label, update_label, delete_label.
  • OpenCode plugin: matching tool({}) entries.
  • LabelView { id, name, color, description } — upstream exclusive, is_archived, url deliberately omitted.

Why

  • Symptom: the only label-touching surface today is create_issue.labels (and the list_issues filter); there is no way to manage the repo's label catalogue itself.
  • Root cause: the core never had label CRUD operations.
  • Policy (per review choice): identify labels by name end-to-end, mirroring the reference CLIs (fj, fgj). The new resolveLabelId helper does the name → id lookup once per call so the user never has to know the internal id.
  • Devil's Advocate — rejected alternatives:
    • Numeric-id only on write paths: cheaper (no list round-trip) but worse UX, breaks parity with both references.
    • Two-view split (LabelSummaryView / LabelDetailView): the four fields we surface fit one view; YAGNI.
    • Case-insensitive name fallback: deferrable. YAGNI.
    • --force upsert, random color palette: deferrable. YAGNI.
    • Pre-validating --color against ^#[0-9a-fA-F]{6}$: rejected. The API returns a clear 422; client-side validation duplicates policy.

Verification

Scenario Expected Result
bun run lint clean passes
bun run typecheck clean passes
bun run test 645 pass, 0 fail passes
bun run build cli + plugin + mcp bundles passes
./dist/forgejoctl help shows the new label subcommands passes
./dist/forgejoctl label list (live) hits GET /repos/{owner}/{repo}/labels reachable (token scope 403 from existing token, not a wiring issue)

CI runs the same gate plus e2e-plugin-load (per .forgejo/workflows/ci.yaml).

Out of scope

  • Issue-label attachment (already covered by create_issue.labels, resolved via the existing resolveLabelIds helper).
  • Label-template endpoints (/repos/{owner}/{repo}/label/templates).
  • Color pre-validation — delegated to the upstream 422.
  • --force upsert, random color palette, case-insensitive name fallback — all trivial follow-ups if a real caller asks.
  • LabelSummaryView / LabelDetailView split, exclusive / isArchived / url fields — YAGNI; the four-field view matches both reference CLIs.
## Summary - Five new core operations: `listLabels`, `getLabel`, `createLabel`, `updateLabel`, `deleteLabel` (plus the `resolveLabelId` helper). - CLI: `forgejoctl label {list,view,create,update,delete}`. - MCP server: `list_labels`, `get_label`, `create_label`, `update_label`, `delete_label`. - OpenCode plugin: matching `tool({})` entries. - `LabelView { id, name, color, description }` — upstream `exclusive`, `is_archived`, `url` deliberately omitted. ## Why - Symptom: the only label-touching surface today is `create_issue.labels` (and the `list_issues` filter); there is no way to manage the repo's label catalogue itself. - Root cause: the core never had label CRUD operations. - Policy (per review choice): identify labels by name end-to-end, mirroring the reference CLIs (`fj`, `fgj`). The new `resolveLabelId` helper does the name → id lookup once per call so the user never has to know the internal id. - Devil's Advocate — rejected alternatives: - Numeric-id only on write paths: cheaper (no list round-trip) but worse UX, breaks parity with both references. - Two-view split (`LabelSummaryView` / `LabelDetailView`): the four fields we surface fit one view; YAGNI. - Case-insensitive name fallback: deferrable. YAGNI. - `--force` upsert, random color palette: deferrable. YAGNI. - Pre-validating `--color` against `^#[0-9a-fA-F]{6}$`: rejected. The API returns a clear 422; client-side validation duplicates policy. ## Verification | Scenario | Expected | Result | |---|---|---| | `bun run lint` | clean | passes | | `bun run typecheck` | clean | passes | | `bun run test` | 645 pass, 0 fail | passes | | `bun run build` | cli + plugin + mcp bundles | passes | | `./dist/forgejoctl help` | shows the new `label` subcommands | passes | | `./dist/forgejoctl label list` (live) | hits `GET /repos/{owner}/{repo}/labels` | reachable (token scope 403 from existing token, not a wiring issue) | CI runs the same gate plus `e2e-plugin-load` (per `.forgejo/workflows/ci.yaml`). ## Out of scope - Issue-label attachment (already covered by `create_issue.labels`, resolved via the existing `resolveLabelIds` helper). - Label-template endpoints (`/repos/{owner}/{repo}/label/templates`). - Color pre-validation — delegated to the upstream 422. - `--force` upsert, random color palette, case-insensitive name fallback — all trivial follow-ups if a real caller asks. - `LabelSummaryView` / `LabelDetailView` split, `exclusive` / `isArchived` / `url` fields — YAGNI; the four-field view matches both reference CLIs.
Five new core operations plus a name→id resolver, all identified by
label name (mirrors the reference CLIs 'fj' and 'fgj'):

- listLabels: GET /repos/{owner}/{repo}/labels with optional sort,
  page, limit query params.
- getLabel: name → resolveLabelId → GET /labels/{id}, returns
  LabelView.
- createLabel: POST /labels with name, color (required) and optional
  description.
- updateLabel: name → resolveLabelId → PATCH /labels/{id} with any
  subset of new_name, color, description.
- deleteLabel: name → resolveLabelId → DELETE /labels/{id}.

LabelView exposes id, name, color, description only; the upstream
exclusive/is_archived/url fields are dropped to match the reference
CLIs and to honour YAGNI. resolveLabelId lives alongside the existing
resolveLabelIds helper for issue-side multi-name resolution.

Issue-side label handling (list_issues filter, create_issue.labels) is
unchanged.
feat(labels): expose label CRUD across CLI, MCP, and OpenCode adapters
All checks were successful
ci / test (pull_request) Successful in 1m21s
aa2c461bad
Wires the five new core operations through every adapter:

- CLI: forgejoctl label {list,view,create,update,delete} with
  positional name for view/delete and --name/--color/--new-name/
  --description flags for create/update. New argv branches in
  parseFlags (--color, --description, --new-name); new dispatch
  block mirroring the tag/release subcommand shape.
- MCP server: list_labels, get_label, create_label, update_label,
  delete_label tools with JSON Schema inputs that mirror the
  existing issue/release tool contracts. Name is the primary
  identifier everywhere; color is the 6-char hex without '#' (no
  client-side validation, per the decision in the plan).
- OpenCode plugin: matching tool({}) entries with the established
  description template ("Use when … — don't use when …"). The
  default-export test enumerates the new tools alphabetically.

docs/GLOSSARY.md gains a Label entry documenting the name-first
identifier convention and noting that issue-side label attach is
unchanged.
ses merged commit 5ef69a8d41 into main 2026-09-07 13:23:36 +00:00
ses deleted branch feat/labels 2026-09-07 13:23:36 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
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
sebatec-eu/forgejoctl!42
No description provided.