Skip to content

Proposal: Support looking up GitHub users keeping their email address private #77

@carlgleisner

Description

@carlgleisner

Current behaviour

Users are fetched using GitHub's /search/users endpoint with the parameter q as contributor@example.com in:email.

case Req.get("#{GitOps.Config.github_api_base_url()}/search/users",
headers: github_headers(),
params: [q: "#{email} in:email", per_page: 2]
) do

This works fine for user exposing their email addresses. However, the search endpoint will not return any results for the @users.noreply.github.com addresses on commits made by users using the "Keep my email address private" feature.

As a result of the endpoint not returning any records, the lookup for users using this feature fails.

Proposal

Since the username is in included in the email address – such as: carlgleisner@users.noreply.github.com – it is possible to extract that username and call the /users/<username> endpoint instead in those cases.

@doc """
Find a GitHub user by their email address.
Returns {:ok, user} if found, where user contains :username, :id, and :url.
Returns {:error, reason} if not found or if there's an error.
"""
def fetch_user_from_api(nil) do
  {:error, "Error making GitHub API request: No email address"}
end

The above would be a new case to catch no email provided since the (proposed) if email do statement would be replaced with an if statement on whether the provided email address is of the "keep my email address private" variety.

def fetch_user_from_api(email) do
  Application.ensure_all_started(:req)

  if String.match?(email, ~r/@users.noreply.github.com$/) do
    case Req.get("#{GitOps.Config.github_api_base_url()}/users/#{username_from_email(email)}") do
      {:ok, %Req.Response{status: 200, body: user}} ->
        {:ok,
         %{
           username: user["login"],
           id: user["id"],
           url: user["html_url"]
         }}

      {:ok, %Req.Response{status: status, body: body}} ->
        {:error, "GitHub API request failed with status #{status}: #{inspect(body)}"}

      {:error, reason} ->
        {:error, "Error making GitHub API request: #{inspect(reason)}"}
    end

That's the proposed new part, then the rest is as before.

  else
    case Req.get("#{GitOps.Config.github_api_base_url()}/search/users",
           headers: github_headers(),
           params: [q: "#{email} in:email", per_page: 2]
         ) do
      {:ok, %Req.Response{status: 200, body: %{"items" => [first_user | _]}}} ->
        {:ok,
         %{
           username: first_user["login"],
           id: first_user["id"],
           url: first_user["html_url"]
         }}

      {:ok, %Req.Response{status: 200, body: %{"items" => []}}} ->
        {:error, "No user found with email #{email}"}

      {:ok, %Req.Response{status: status, body: body}} ->
        {:error, "GitHub API request failed with status #{status}: #{inspect(body)}"}

      {:error, reason} ->
        {:error, "Error making GitHub API request: #{inspect(reason)}"}
    end
  end
rescue
  error ->
    {:error, "Error making GitHub API request: #{inspect(error)}"}
end

Since the email addresses can have the format 1234567+username@ I propose a private helper function to extract the username:

defp username_from_email(email) do
  Regex.named_captures(~r/^(\d+\+){0,1}(?<username>\w+)@users.noreply.github.com$/, email)
  |> Map.get("username")
end

I've got a working branch of this in my own fork, for what it's worth.

This proposal is provided most humbly of course! 😇

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions