diff --git a/lib/algora/admin/admin.ex b/lib/algora/admin/admin.ex index 73ed62b17..0d405532d 100644 --- a/lib/algora/admin/admin.ex +++ b/lib/algora/admin/admin.ex @@ -21,6 +21,42 @@ defmodule Algora.Admin do require Logger + def add_label(url, amount) do + %{owner: owner, repo: repo, number: number} = parse_ticket_url(url) + + with installation_id when not is_nil(installation_id) <- Workspace.get_installation_id_by_owner(owner), + {:ok, token} <- Github.get_installation_token(installation_id) do + Workspace.add_amount_label(token, owner, repo, number, Money.parse(amount)) + end + end + + def backfill_labels(org_handle, opts \\ []) do + with org when not is_nil(org) <- Repo.get_by(User, handle: org_handle), + installation_id when not is_nil(installation_id) <- Workspace.get_installation_id_by_owner(org.provider_login), + {:ok, token} <- Github.get_installation_token(installation_id) do + bounties = + Bounties.list_bounties( + owner_id: org.id, + limit: :infinity, + status: :open + ) + + Enum.each(bounties, fn bounty -> + if opts[:dry_run] do + Logger.info("#{org.provider_login} - #{bounty.repository.name} - #{bounty.ticket.number} - #{bounty.amount}") + else + Workspace.add_amount_label( + token, + org.provider_login, + bounty.repository.name, + bounty.ticket.number, + bounty.amount + ) + end + end) + end + end + def init_contributors(repo_owner, repo_name) do with {:ok, repo} <- Workspace.ensure_repository(token(), repo_owner, repo_name) do Workspace.ensure_contributors(token(), repo) diff --git a/lib/algora/bounties/jobs/notify_bounty.ex b/lib/algora/bounties/jobs/notify_bounty.ex index a1344010f..934bad8ff 100644 --- a/lib/algora/bounties/jobs/notify_bounty.ex +++ b/lib/algora/bounties/jobs/notify_bounty.ex @@ -68,7 +68,7 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do @impl Oban.Worker def perform(%Oban.Job{ args: %{ - "amount" => _amount, + "amount" => amount, "ticket_ref" => ticket_ref, "installation_id" => installation_id, "command_id" => command_id, @@ -85,7 +85,9 @@ defmodule Algora.Bounties.Jobs.NotifyBounty do {:ok, ticket} <- Workspace.ensure_ticket(token, ticket_ref.owner, ticket_ref.repo, ticket_ref.number), bounties when bounties != [] <- Bounties.list_bounties(ticket_id: ticket.id), - {:ok, _} <- Github.add_labels(token, ticket_ref.owner, ticket_ref.repo, ticket_ref.number, ["💎 Bounty"]) do + {:ok, _} <- Github.add_labels(token, ticket_ref.owner, ticket_ref.repo, ticket_ref.number, ["💎 Bounty"]), + :ok <- + Workspace.add_amount_label(token, ticket_ref.owner, ticket_ref.repo, ticket_ref.number, Money.parse(amount)) do attempts = Bounties.list_attempts_for_ticket(ticket.id) claims = Bounties.list_claims([ticket.id]) diff --git a/lib/algora/integrations/github/behaviour.ex b/lib/algora/integrations/github/behaviour.ex index 3beff859c..c10ac8eb0 100644 --- a/lib/algora/integrations/github/behaviour.ex +++ b/lib/algora/integrations/github/behaviour.ex @@ -30,4 +30,6 @@ defmodule Algora.Github.Behaviour do @callback list_repository_languages(token(), String.t(), String.t()) :: {:ok, [map()]} | {:error, String.t()} @callback list_repository_contributors(token(), String.t(), String.t()) :: {:ok, [map()]} | {:error, String.t()} @callback add_labels(token(), String.t(), String.t(), integer(), [String.t()]) :: {:ok, [map()]} | {:error, String.t()} + @callback create_label(token(), String.t(), String.t(), map()) :: {:ok, map()} | {:error, String.t()} + @callback get_label(token(), String.t(), String.t(), String.t()) :: {:ok, map()} | {:error, String.t()} end diff --git a/lib/algora/integrations/github/client.ex b/lib/algora/integrations/github/client.ex index ad002b175..c788f9f46 100644 --- a/lib/algora/integrations/github/client.ex +++ b/lib/algora/integrations/github/client.ex @@ -272,4 +272,14 @@ defmodule Algora.Github.Client do labels: labels }) end + + @impl true + def create_label(access_token, owner, repo, label) do + fetch(access_token, "/repos/#{owner}/#{repo}/labels", "POST", label) + end + + @impl true + def get_label(access_token, owner, repo, label) do + fetch(access_token, "/repos/#{owner}/#{repo}/labels/#{label}") + end end diff --git a/lib/algora/integrations/github/github.ex b/lib/algora/integrations/github/github.ex index a5e52e9d8..3e9792dde 100644 --- a/lib/algora/integrations/github/github.ex +++ b/lib/algora/integrations/github/github.ex @@ -144,4 +144,10 @@ defmodule Algora.Github do @impl true def add_labels(token, owner, repo, number, labels), do: client().add_labels(token, owner, repo, number, labels) + + @impl true + def create_label(token, owner, repo, label), do: client().create_label(token, owner, repo, label) + + @impl true + def get_label(token, owner, repo, label), do: client().get_label(token, owner, repo, label) end diff --git a/lib/algora/shared/util.ex b/lib/algora/shared/util.ex index 59ad63497..688129099 100644 --- a/lib/algora/shared/util.ex +++ b/lib/algora/shared/util.ex @@ -28,6 +28,33 @@ defmodule Algora.Util do |> :erlang.binary_to_term() end + def format_number_compact(number) when is_struct(number, Decimal) do + number + |> Decimal.to_float() + |> format_number_compact() + end + + def format_number_compact(number) do + n = trunc(number) + + case n do + n when n >= 1_000_000 -> + "#{(n / 1_000_000) |> Float.round(1) |> trim_trailing_zero()}M" + + n when n >= 1_000 -> + "#{(n / 1_000) |> Float.round(1) |> trim_trailing_zero()}K" + + n -> + to_string(n) + end + end + + defp trim_trailing_zero(number) do + number + |> Float.to_string() + |> String.replace(~r/\.0+$/, "") + end + def time_ago(datetime) do now = NaiveDateTime.utc_now() diff = NaiveDateTime.diff(now, datetime, :second) diff --git a/lib/algora/workspace/workspace.ex b/lib/algora/workspace/workspace.ex index 8050a56e8..caa13373c 100644 --- a/lib/algora/workspace/workspace.ex +++ b/lib/algora/workspace/workspace.ex @@ -554,4 +554,25 @@ defmodule Algora.Workspace do def fetch_contributor(repository_id, user_id) do Repo.fetch_by(Contributor, repository_id: repository_id, user_id: user_id) end + + def get_or_create_label(token, owner, repo, name, color) do + case Github.get_label(token, owner, repo, name) do + {:ok, _} -> {:ok, name} + {:error, _reason} -> Github.create_label(token, owner, repo, %{"name" => name, "color" => color}) + end + end + + def add_amount_label(token, owner, repo, number, amount) do + label = "$#{amount |> Money.to_decimal() |> Util.format_number_compact()}" + + with {:ok, _} <- get_or_create_label(token, owner, repo, label, "34d399"), + {:ok, _} <- Github.add_labels(token, owner, repo, number, [label]) do + :ok + else + {:error, reason} -> + Logger.error("Failed to add label #{label} to #{owner}/#{repo}##{number}: #{inspect(reason)}") + + :error + end + end end diff --git a/test/support/github_mock.ex b/test/support/github_mock.ex index 834cde246..83b65c856 100644 --- a/test/support/github_mock.ex +++ b/test/support/github_mock.ex @@ -170,4 +170,14 @@ defmodule Algora.Support.GithubMock do def add_labels(_access_token, _owner, _repo, _number, _labels) do {:ok, []} end + + @impl true + def create_label(_access_token, _owner, _repo, _label) do + {:ok, %{"id" => random_id()}} + end + + @impl true + def get_label(_access_token, _owner, _repo, _label) do + {:ok, %{"id" => random_id()}} + end end