Create the forgejo-client gem for the Forgejo REST API #855

Closed
opened 2026-09-30 18:14:36 +00:00 by jared · 4 comments
Owner

Problem

ADR-0256 moves the Forgejo tracker backend from the tea CLI to the Forgejo REST API. os-backlog and os-sdlc both need the same small client, and neither plugin may depend on the other. No such client exists yet.

Reproduce

n/a: new component.

Approach

Create a new gem forgejo-client in its own repository, ~/dev/forgejo-client, with a private Forgejo remote. It uses Net::HTTP from the standard library and has no runtime dependencies. It covers only the endpoints that os-backlog and os-sdlc use today (all under /api/v1):

Client method Endpoint
issue(slug, number) GET /repos/{o}/{r}/issues/{index}
issues(slug, state: "open") GET /repos/{o}/{r}/issues?state=&type=issues (all pages)
create_issue(slug, title:, body:, labels: []) POST /repos/{o}/{r}/issues (label names resolved to IDs)
update_issue(slug, number, **fields) PATCH /repos/{o}/{r}/issues/{index} (title, body, state, assignees)
labels(slug) GET /repos/{o}/{r}/labels
comments(slug, number) GET /repos/{o}/{r}/issues/{index}/comments
add_comment(slug, number, body) POST /repos/{o}/{r}/issues/{index}/comments
add_labels(slug, number, names) POST /repos/{o}/{r}/issues/{index}/labels (names resolved to IDs)
remove_label(slug, number, name) DELETE /repos/{o}/{r}/issues/{index}/labels/{id}

Each method returns parsed JSON (Hash or Array). A non-2xx response raises Forgejo::Error with the HTTP status and the response message. The client takes host: and token:. It never reads the environment; the caller passes ENV.fetch("FORGEJO_TOKEN"). Tests use webmock; no test calls a real server.

Required work:

  1. Create the repository with a gemspec, Minitest suite, and README usage example.

Rejected: generate a full client from /swagger.v1.json with api-wrapper (ADR-0256: large and mostly unused).

Acceptance criteria

  1. Given a stubbed server that returns an issue as JSON, when issue("jared/cc-os", 845) runs, then it returns that issue's number, title, body, state, and labels.
  2. Given a body that starts with --label, when create_issue runs, then the request body is JSON and carries the text unchanged, and the method returns the new issue's number and html_url.
  3. Given label names that exist on the repository, when create_issue or add_labels runs, then the request carries the matching label IDs.
  4. Given a label name that does not exist, when create_issue runs, then it raises an error that names the missing label and sends no create request.
  5. Given more open issues than one page holds, when issues runs, then it returns the issues from every page.
  6. Given a 401 or 404 response, when any method runs, then it raises Forgejo::Error with the status and the server message.
  7. Given any request, when it is sent, then it carries Authorization: token <token> and goes to https://<host>/api/v1/....

Owner sketch

module Forgejo
  class Client
    def initialize(host:, token:)
    def issue(slug, number)
    def issues(slug, state: "open")
    def create_issue(slug, title:, body:, labels: [])
    def update_issue(slug, number, **fields)
    def labels(slug)
    def comments(slug, number)
    def add_comment(slug, number, body)
    def add_labels(slug, number, names)
    def remove_label(slug, number, name)
  end

  class Error < StandardError
    attr_reader :status
  end
end

Non-goals

  • Adoption in os-backlog and os-sdlc. Each plugin has its own ticket.
  • GitHub. ADR-0256 uses octokit for GitHub.

Origin

  • Trigger: #845 audit; ADR-0256 decided 2026-09-30.
  • Improvised this session: none
  • Chain: ticket text passes through argv and create output needs a regex ← backends shell out to tea (tea 0.13.0 issues create has no --output json) ← DESIGN: ADR-0123, now amended by ADR-0256.
  • Root candidate: this ticket
  • Where: new repository ~/dev/forgejo-client
  • Session: f090dd39-8ad6-4f4e-8743-7bbedb8f08d4
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f090dd39-8ad6-4f4e-8743-7bbedb8f08d4.jsonl
## Problem ADR-0256 moves the Forgejo tracker backend from the `tea` CLI to the Forgejo REST API. os-backlog and os-sdlc both need the same small client, and neither plugin may depend on the other. No such client exists yet. ## Reproduce n/a: new component. ## Approach Create a new gem `forgejo-client` in its own repository, `~/dev/forgejo-client`, with a private Forgejo remote. It uses `Net::HTTP` from the standard library and has no runtime dependencies. It covers only the endpoints that os-backlog and os-sdlc use today (all under `/api/v1`): | Client method | Endpoint | |---|---| | `issue(slug, number)` | `GET /repos/{o}/{r}/issues/{index}` | | `issues(slug, state: "open")` | `GET /repos/{o}/{r}/issues?state=&type=issues` (all pages) | | `create_issue(slug, title:, body:, labels: [])` | `POST /repos/{o}/{r}/issues` (label names resolved to IDs) | | `update_issue(slug, number, **fields)` | `PATCH /repos/{o}/{r}/issues/{index}` (`title`, `body`, `state`, `assignees`) | | `labels(slug)` | `GET /repos/{o}/{r}/labels` | | `comments(slug, number)` | `GET /repos/{o}/{r}/issues/{index}/comments` | | `add_comment(slug, number, body)` | `POST /repos/{o}/{r}/issues/{index}/comments` | | `add_labels(slug, number, names)` | `POST /repos/{o}/{r}/issues/{index}/labels` (names resolved to IDs) | | `remove_label(slug, number, name)` | `DELETE /repos/{o}/{r}/issues/{index}/labels/{id}` | Each method returns parsed JSON (Hash or Array). A non-2xx response raises `Forgejo::Error` with the HTTP status and the response message. The client takes `host:` and `token:`. It never reads the environment; the caller passes `ENV.fetch("FORGEJO_TOKEN")`. Tests use webmock; no test calls a real server. Required work: 1. Create the repository with a gemspec, Minitest suite, and README usage example. Rejected: generate a full client from `/swagger.v1.json` with api-wrapper (ADR-0256: large and mostly unused). ## Acceptance criteria 1. Given a stubbed server that returns an issue as JSON, when `issue("jared/cc-os", 845)` runs, then it returns that issue's number, title, body, state, and labels. 2. Given a body that starts with `--label`, when `create_issue` runs, then the request body is JSON and carries the text unchanged, and the method returns the new issue's `number` and `html_url`. 3. Given label names that exist on the repository, when `create_issue` or `add_labels` runs, then the request carries the matching label IDs. 4. Given a label name that does not exist, when `create_issue` runs, then it raises an error that names the missing label and sends no create request. 5. Given more open issues than one page holds, when `issues` runs, then it returns the issues from every page. 6. Given a 401 or 404 response, when any method runs, then it raises `Forgejo::Error` with the status and the server message. 7. Given any request, when it is sent, then it carries `Authorization: token <token>` and goes to `https://<host>/api/v1/...`. ## Owner sketch ```ruby module Forgejo class Client def initialize(host:, token:) def issue(slug, number) def issues(slug, state: "open") def create_issue(slug, title:, body:, labels: []) def update_issue(slug, number, **fields) def labels(slug) def comments(slug, number) def add_comment(slug, number, body) def add_labels(slug, number, names) def remove_label(slug, number, name) end class Error < StandardError attr_reader :status end end ``` ## Non-goals - Adoption in os-backlog and os-sdlc. Each plugin has its own ticket. - GitHub. ADR-0256 uses octokit for GitHub. ## Origin - Trigger: #845 audit; ADR-0256 decided 2026-09-30. - Improvised this session: none - Chain: ticket text passes through argv and create output needs a regex ← backends shell out to tea (tea 0.13.0 `issues create` has no `--output json`) ← DESIGN: ADR-0123, now amended by ADR-0256. - Root candidate: this ticket - Where: new repository ~/dev/forgejo-client - Session: f090dd39-8ad6-4f4e-8743-7bbedb8f08d4 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/f090dd39-8ad6-4f4e-8743-7bbedb8f08d4.jsonl
Author
Owner

Work started: building the gem locally at ~/dev/forgejo-client (TDD). No remote yet.

Work started: building the gem locally at ~/dev/forgejo-client (TDD). No remote yet.
Author
Owner

Status 2026-09-30: gem built locally at ~/dev/forgejo-client, commit 162daf7 (no remote). 7 runs, 18 assertions, 0 failures; rubocop clean. Live read test returned HTTP 401 (the FORGEJO_TOKEN value is not a valid token on forgejo.swansoncloud.com); waiting for a new token.

Defect found in the live test, must be fixed before any plugin uses the gem: on 401 Forgejo puts token material in the response body ("task with token ..."), and Connection#failure_message copies that body into the Forgejo::Error message. For 401 and 403, the error message must carry only the status and a fixed text, never the server body. Also still open: tests for update_issue and remove_label, and Net::HTTP timeouts.

Status 2026-09-30: gem built locally at ~/dev/forgejo-client, commit 162daf7 (no remote). 7 runs, 18 assertions, 0 failures; rubocop clean. Live read test returned HTTP 401 (the FORGEJO_TOKEN value is not a valid token on forgejo.swansoncloud.com); waiting for a new token. Defect found in the live test, must be fixed before any plugin uses the gem: on 401 Forgejo puts token material in the response body ("task with token ..."), and `Connection#failure_message` copies that body into the `Forgejo::Error` message. For 401 and 403, the error message must carry only the status and a fixed text, never the server body. Also still open: tests for `update_issue` and `remove_label`, and Net::HTTP timeouts.
Author
Owner

--label test: this comment starts with a flag-like word and was posted by forgejo-client (POST /issues/855/comments). Live checks 2026-09-30: issue, labels (41), issues (131, paginated), comments, add_comment all passed with the new FORGEJO_TOKEN. Gem commits 162daf7, 3ec5a02 (401/403 bodies never echoed; 10 s timeouts). Open before plugins adopt it: a Forgejo remote for the gem, and how plugins load it.

--label test: this comment starts with a flag-like word and was posted by forgejo-client (POST /issues/855/comments). Live checks 2026-09-30: issue, labels (41), issues (131, paginated), comments, add_comment all passed with the new FORGEJO_TOKEN. Gem commits 162daf7, 3ec5a02 (401/403 bodies never echoed; 10 s timeouts). Open before plugins adopt it: a Forgejo remote for the gem, and how plugins load it.
Author
Owner

Resolution

Done: The Forgejo REST client exists as the generated forgejo-api gem (485 Forgejo 16.0.5 operations) in the private repo jared/forgejo-api, replacing the hand-written forgejo-client. 401/403 exception messages carry no server text. ADR-0257 records the change.

Evidence: forgejo-api commit a9ad21d pushed to ssh://git@forgejo.swansoncloud.com:2222/jared/forgejo-api.git (ls-remote main = a9ad21d); 731 runs, 0 failures, 7 skips (integration, no cassettes); rubocop 0 offenses; live: repo created and read back through the gem (me.create_repo, repo.get private=true, 404 mapped to NotFoundError); cc-os ADR-0257 commit b68750db.

Follow-ups: Record forgejo-api integration cassettes: filed on jared/forgejo-api #1. ~/dev/forgejo-client retirement: left to the user (local-only repo, deletion is irreversible). Adoption: #856, #858 (existing).

## Resolution **Done:** The Forgejo REST client exists as the generated forgejo-api gem (485 Forgejo 16.0.5 operations) in the private repo jared/forgejo-api, replacing the hand-written forgejo-client. 401/403 exception messages carry no server text. ADR-0257 records the change. **Evidence:** forgejo-api commit a9ad21d pushed to ssh://git@forgejo.swansoncloud.com:2222/jared/forgejo-api.git (ls-remote main = a9ad21d); 731 runs, 0 failures, 7 skips (integration, no cassettes); rubocop 0 offenses; live: repo created and read back through the gem (me.create_repo, repo.get private=true, 404 mapped to NotFoundError); cc-os ADR-0257 commit b68750db. **Follow-ups:** Record forgejo-api integration cassettes: filed on jared/forgejo-api #1. ~/dev/forgejo-client retirement: left to the user (local-only repo, deletion is irreversible). Adoption: #856, #858 (existing).
jared closed this issue 2026-09-30 19:59:59 +00:00
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
jared/cc-os#855
No description provided.