elixir-github

GitHub API wrapper for Elixir (Noizu.Github).

The full REST surface is generated from the vendored OpenAPI description in docs/github-api/api.github.com.json. Generated code lives under lib/api/:

Setup

Requirements

Elixir 1.20 / OTP 29 (see .tool-versions; works with recent 1.14+ releases). Dependencies are managed with Mix: Finch for HTTP and Jason for JSON.

Add the dependency

In your app's mix.exs:

defp deps do
[
{:noizu_github, github: "noizu-labs/elixir-github", branch: "main"}
# or pin a tag/release:
# {:noizu_github, github: "noizu-labs/elixir-github", tag: "v0.1.0"}
]
end
mix deps.get

noizu_github is an OTP application (Noizu.Github.Application) that starts a supervised Finch pool named Noizu.Github.Finch. It starts automatically when your application starts its dependencies — no manual children entry needed.

Provide a GitHub token

Every request sends Authorization: Bearer <token> and the pinned X-GitHub-Api-Version: 2022-11-28 header. Use a personal access token (or app token) with scopes appropriate to the endpoints you call.

Configuration

# config/runtime.exs (recommended for secrets)
config :noizu_github, NoizuLabs.Github.Config,
api_key: System.get_env("GITHUB_TOKEN"),
owner: "your-org",
repo: "your-repo"
KeyRequiredDescription
:api_keyyesGitHub personal access token (or app token). Sent as Authorization: Bearer <token>.
:ownernoDefault repository owner (org or user).
:reponoDefault repository name.

All three can be overridden per call via the options keyword list (:token, :owner, :repo), so you can target multiple repositories or accounts from one application without reconfiguring:

# override for a single call
{:ok, repo} = Noizu.Github.Api.Repos.get(owner: "elixir-lang", repo: "elixir")

Note the config namespace is NoizuLabs.Github.Config under the :noizu_github app, not Noizu.Github.

Usage

Each operation function takes:

  1. any leading path parameters positionally (owner/repo excepted — those come from config/options),
  2. a body map for write operations (POST/PUT/PATCH, and DELETE with a body),
  3. an options keyword list for query params, per-call overrides, and request options.

Fetch a pull request

alias Noizu.Github.Api.Pulls
# uses owner/repo from config
{:ok, pr} = Pulls.get(42)
pr.title # => "Add pagination helpers"
pr.state # => "open"
pr.head.ref # => "feature/pagination"
pr.base.ref # => "main"
pr.html_url # => "https://github.com/your-org/your-repo/pull/42"
pr.user # => %Noizu.Github.SimpleUser{login: "octocat", ...}
# or target a different repo
{:ok, pr} = Pulls.get(123, owner: "elixir-lang", repo: "elixir")

Create a branch

Branches are created via the Git refs API. You need the SHA of the commit to branch from (typically the tip of main):

alias Noizu.Github.Api.Git
# get the current HEAD of main
{:ok, ref} = Git.get_ref("heads/main")
sha = ref.object.sha # => "abc123..."
# create a new branch pointing at that SHA
{:ok, _} = Git.create_ref(%{
ref: "refs/heads/my-new-branch",
sha: sha
})

Comment on a pull request

Pull request comments go through the Issues API (GitHub treats PRs as issues for comments). Use Issues.create_comment/3 with the PR number:

alias Noizu.Github.Api.Issues
{:ok, comment} = Issues.create_comment(42, %{
body: "Looks good! :shipit:"
})
comment.id # => 123456789
comment.html_url # => "https://github.com/your-org/your-repo/issues/42#issuecomment-..."

For inline review comments on specific lines of a diff, use the Pulls API instead:

alias Noizu.Github.Api.Pulls
# comment on a specific line in the diff
{:ok, _} = Pulls.create_review_comment(42, %{
body: "Consider using `Enum.map` here.",
commit_id: "abc123...",
path: "lib/my_module.ex",
line: 15
})
# or submit a full review (approve / request changes / comment)
{:ok, _} = Pulls.create_review(42, %{
event: "APPROVE",
body: "LGTM!"
})

More examples

alias Noizu.Github.Api.{Issues, Repos, Search, Users}
# issues
{:ok, issues} = Issues.list_for_repo(state: "open", per_page: 50)
{:ok, issue} = Issues.create(%{title: "Bug", body: "..."})
{:ok, _} = Issues.update(42, %{state: "closed"})
# repos
{:ok, repo} = Repos.get()
{:ok, _} = Repos.create_release(%{tag_name: "v1.0.0", name: "v1.0.0", body: "..."})
# search
{:ok, results} = Search.repos(q: "language:elixir stars:>1000", sort: "stars")
# users
{:ok, user} = Users.get_by_username("octocat")

Pagination

Two built-in helpers walk paginated endpoints automatically. Both accept any generated list function (as a capture) and your initial options:

paginate/2 — eagerly fetches all pages and returns {:ok, all_items}:

{:ok, all_issues} = Noizu.Github.paginate(
&Noizu.Github.Api.Issues.list_for_repo/1,
state: "open", per_page: 100
)
# all_issues => [%Noizu.Github.Issue{}, %Noizu.Github.Issue{}, ...]

stream_pages/2 — returns a lazy Stream yielding one {:ok, page_result} per page, so you can process or short-circuit without fetching everything:

Noizu.Github.stream_pages(
&Noizu.Github.Api.Issues.list_for_repo/1,
state: "open", per_page: 100
)
|> Enum.flat_map(fn
{:ok, %{items: items}} -> items
{:error, _} -> []
end)

Both helpers increment the :page option and follow links[:next] until the API signals no more pages. Errors propagate: paginate/2 returns the first {:error, _} immediately; stream_pages/2 yields it as the final element.

Each page result also carries links (%{first:, prev:, next:, last:} URLs parsed from the Link header via Noizu.Github.extract_links/1) if you need manual control.

Curated views

Generated structs are full and spec-faithful. For compact display, the hand-maintained Noizu.Github.Format module (lib/format.ex, not generated) exposes curated :basic projections:

{:ok, issues} = Noizu.Github.Api.Issues.list_for_repo(state: "open")
Noizu.Github.Format.format(issues, :basic)
# => [%{id: 1, title: "...", state: "open", user: %{login: ...}, labels: [...]}, ...]

Unknown shapes pass through unchanged, so format/2 is always safe to call.

Response shapes & errors

Generated structs are permissive: extra and missing keys are tolerated, so the client is robust to minor API drift.

Timeouts

Requests use generous timeouts (pool_timeout and receive_timeout of 600_000 ms / 10 min) to accommodate long-running endpoints and rate limiting. Adjust at the transport level if you need stricter limits.

Testing

The decode path is exercised with Mimic, stubbing Finch.request/3 with canned responses — no network access required (test/api/issues_test.exs). In your own app, the same seam lets you stub Finch to assert on the URLs and bodies your code produces.

Run the suite:

mix test

config/test.secret.exs (gitignored) supplies test defaults; copy the pattern and set GITHUB_TOKEN for any integration-style tests you add.

Regenerating

After updating docs/github-api/api.github.com.json, regenerate the client:

mix github.gen
mix compile
mix test

mix github.gen wipes and rewrites lib/api/. The runtime core in lib/noizu_github.ex (api_call/5, headers/1, extract_links/1, get_field/3, put_field/4) is hand-maintained and not generated.

Further reading