gitea4s

A Scala 3 client for the Gitea API

Typed endpoints for Gitea, built on ZIO 2, sttp client4 and zio-json. Every endpoint carries metadata that is audited against the vendored Gitea OpenAPI contract, so the client and the server description cannot drift apart unnoticed.

Scala 3 Java 21 baseline ZIO 2 sttp client4 Gitea API 1.26.2 Apache-2.0

Install

Most applications only need gitea4s-backend-zio — it pulls in client and core.

"io.worxbend" %% "gitea4s-core"           % "1.0.0"
"io.worxbend" %% "gitea4s-client"         % "1.0.0"
"io.worxbend" %% "gitea4s-backend-zio"    % "1.0.0"
"io.worxbend" %% "gitea4s-backend-okhttp" % "1.0.0"

Maven Central is the canonical channel. GitHub Packages, JitPack and prebuilt jars attached to each release are also available — see distribution channels for resolvers and authentication.

Quickstart

import io.worxbend.gitea4s.GiteaClient
import io.worxbend.gitea4s.backend.zio.ZioGiteaBackend
import io.worxbend.gitea4s.http.RepoListParams
import sttp.client4.*
import zio.{Console, ZIO, ZIOAppDefault}

object Main extends ZIOAppDefault:
  private val layer =
    ZioGiteaBackend.withToken(uri"https://gitea.example", sys.env("GITEA_TOKEN"))

  def run =
    ZIO.serviceWithZIO[GiteaClient] { client =>
      for
        me    <- client.users.me
        login <- ZIO.fromOption(me.login).orElseFail(new RuntimeException("missing login"))
        repos <- client.repos.list(login, RepoListParams(limit = Some(25))).take(25).runCollect
        _     <- Console.printLine(s"Repositories for $login")
        _     <- ZIO.foreachDiscard(repos)(r =>
                   Console.printLine(s"- ${r.fullName.orElse(r.name).getOrElse("<unknown>")}"))
      yield ()
    }.provideLayer(layer)

Note. GITEA_URL is the server root, not the API root: use https://gitea.example, not https://gitea.example/api/v1. The /api/v1 prefix is added for you, so including it yields /api/v1/api/v1/… and a NotFound on every call.

Namespaces

The client is organized into resource namespaces; every call goes through one.

NamespaceCovers
client.reposrepositories, Git data, contents, collaborators, statuses
client.issuesissues, comments, labels, reactions, tracked time
client.pullspull requests, reviews, merges, diffs
client.releasesreleases and release assets
client.notificationsnotification threads and counts
client.usersthe current user (users.me), lookup, search, followers
client.orgsorganizations and their members/repositories

What the design buys you

Audited against the contract

Every endpoint's method, path, parameters and response labels are checked against the vendored plugin-redoc-2.yaml, and the URI a builder produces is checked against the path template it declares.

Bounded failure

Retries cover only GET and HEAD, so they cannot duplicate a write. Attempts, rate-limit waits, response bodies and stalled downloads all have explicit ceilings.

Survives server drift

An enum value a newer Gitea introduces decodes as None rather than failing the page it arrived in — one unreadable field instead of a lost collection.

Credentials stay put

toString redacts tokens, passwords and one-time passwords; config errors name the setting and never the value; URL userinfo is stripped.

Modules

core
client          -> core
backend-zio     -> client
backend-okhttp  -> client

OkHttp is confined to backend-okhttp; core, client and backend-zio do not depend on it. backend-zio is the default and the only one that can stream binary downloads, because that needs a ZioStreams-capable backend.

The public API of all four published modules is pinned by a snapshot in api-snapshot/ and guarded by ./mill compatibility.check, so a change to a published signature cannot land unnoticed.