-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement handling of "InstallationRepositories" "added" and "removed…
…" events
- Loading branch information
Showing
23 changed files
with
662 additions
and
91 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
defmodule CodeCorps.GitHub.Event do | ||
@moduledoc ~S""" | ||
In charge of marking `GithubEvent` records as "processing", "processed" or | ||
"errored", based on the outcome of processing a webhook event payload. | ||
""" | ||
|
||
alias CodeCorps.{GithubEvent, Repo} | ||
alias Ecto.Changeset | ||
|
||
@type error :: atom | Changeset.t | ||
@type processing_result :: {:ok, any} | {:error, error} | ||
|
||
@doc ~S""" | ||
Sets record status to "processing", marking it as being processed at this | ||
moment. Our webhook handling should skip processing payloads for events which | ||
are already being processed. | ||
""" | ||
@spec start_processing(GithubEvent.t) :: {:ok, GithubEvent.t} | ||
def start_processing(%GithubEvent{} = event) do | ||
event |> Changeset.change(%{status: "processing"}) |> Repo.update() | ||
end | ||
|
||
@doc ~S""" | ||
Sets record status to "processed" or "errored" based on the first element of | ||
first argument, which is the result tuple. The result tuple should always be | ||
either `{:ok, data}` if the the processing of the event payload went as | ||
expected, or `{:error, reason}` if something went wrong. | ||
""" | ||
@spec stop_processing(processing_result, GithubEvent.t) :: {:ok, GithubEvent.t} | ||
def stop_processing({:ok, _data}, %GithubEvent{} = event) do | ||
event |> Changeset.change(%{status: "processed"}) |> Repo.update | ||
end | ||
def stop_processing({:error, _reason}, %GithubEvent{} = event) do | ||
event |> Changeset.change(%{status: "errored"}) |> Repo.update | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
lib/code_corps/github/event/installation_repositories.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
defmodule CodeCorps.GitHub.Event.InstallationRepositories do | ||
@moduledoc """ | ||
In charge of dealing with "InstallationRepositories" GitHub Webhook events | ||
""" | ||
|
||
alias CodeCorps.{ | ||
GithubAppInstallation, | ||
GithubEvent, | ||
GithubRepo, | ||
GitHub.Event, | ||
Repo | ||
} | ||
|
||
alias Ecto.Changeset | ||
|
||
@doc """ | ||
Handles an "InstallationRepositories" GitHub Webhook event. The event could be | ||
of subtype "added" or "removed" and is handled differently based on that. | ||
`InstallationRepositories::added` will | ||
- find `GithubAppInstallation` | ||
- marks event as errored if not found | ||
- marks event as errored if payload does not have keys it needs, meaning | ||
there's something wrong with our code and we need to updated | ||
- find or create `GithubRepo` records affected | ||
`InstallationRepositories::removed` will | ||
- find `GithubAppInstallation` | ||
- marks event as errored if not found | ||
- marks event as errored if payload does not have keys it needs, meaning | ||
there's something wrong with our code and we need to updated | ||
- find `GithubRepo` records affected and delete them | ||
- if there is no `GithubRepo` to delete, it just skips it | ||
- `ProjectGithubRepo` are deleted automatically, since they are set to | ||
`on_delete: :delete_all` | ||
""" | ||
@spec handle(GithubEvent.t, map) :: {:ok, GithubEvent.t} | ||
def handle(%GithubEvent{action: action} = event, payload) do | ||
event | ||
|> Event.start_processing() | ||
|> do_handle(action, payload) | ||
|> Event.stop_processing(event) | ||
end | ||
|
||
@typep outcome :: {:ok, [GithubRepo.t]} | | ||
{:error, :no_installation} | | ||
{:error, :unexpected_action_or_payload} | | ||
{:error, :unexpected_installation_payload} | | ||
{:error, :unexpected_repo_payload} | ||
|
||
@spec do_handle({:ok, GithubEvent.t}, String.t, map) :: outcome | ||
defp do_handle({:ok, %GithubEvent{}}, "added", %{"installation" => installation_attrs, "repositories_added" => repositories_attr_list}) do | ||
case installation_attrs |> find_installation() do | ||
nil -> {:error, :no_installation} | ||
:unexpected_installation_payload -> {:error, :unexpected_installation_payload} | ||
%GithubAppInstallation{} = installation -> | ||
case repositories_attr_list |> Enum.all?(&valid?/1) do | ||
true -> find_or_create_all(installation, repositories_attr_list) | ||
false -> {:error, :unexpected_repo_payload} | ||
end | ||
end | ||
end | ||
defp do_handle({:ok, %GithubEvent{}}, "removed", %{"installation" => installation_attrs, "repositories_removed" => repositories_attr_list}) do | ||
case installation_attrs |> find_installation() do | ||
nil -> {:error, :no_installation} | ||
:unexpected_installation_payload -> {:error, :unexpected_installation_payload} | ||
%GithubAppInstallation{} = installation -> | ||
case repositories_attr_list |> Enum.all?(&valid?/1) do | ||
true -> delete_all(installation, repositories_attr_list) | ||
false -> {:error, :unexpected_repo_payload} | ||
end | ||
end | ||
end | ||
defp do_handle({:ok, %GithubEvent{}}, _action, _payload), do: {:error, :unexpected_action_or_payload} | ||
|
||
@spec find_installation(any) :: GithubAppInstallation.t | nil | :unexpected_installation_payload | ||
defp find_installation(%{"id" => github_id}), do: GithubAppInstallation |> Repo.get_by(github_id: github_id) | ||
defp find_installation(_payload), do: :unexpected_installation_payload | ||
|
||
# should return true if the payload is a map and has the expected keys | ||
@spec valid?(any) :: boolean | ||
defp valid?(%{"id" => _, "name" => _}), do: true | ||
defp valid?(_), do: false | ||
|
||
@spec find_or_create_all(GithubAppInstallation.t, list(map)) :: {:ok, list(GithubRepo.t)} | ||
defp find_or_create_all(%GithubAppInstallation{} = installation, repositories_attr_list) when is_list(repositories_attr_list) do | ||
repositories_attr_list | ||
|> Enum.map(&find_or_create(installation, &1)) | ||
|> aggregate() | ||
end | ||
|
||
@spec find_or_create(GithubAppInstallation.t, map) :: {:ok, GithubRepo.t} | ||
defp find_or_create(%GithubAppInstallation{} = installation, %{"id" => github_id, "name" => name} = attrs) do | ||
case find_repo(installation, attrs) do | ||
nil -> | ||
%GithubRepo{} | ||
|> Changeset.change(%{github_id: github_id, name: name}) | ||
|> Changeset.put_assoc(:github_app_installation, installation) | ||
|> Repo.insert() | ||
%GithubRepo{} = github_repo -> | ||
{:ok, github_repo} | ||
end | ||
end | ||
|
||
@spec delete_all(GithubAppInstallation.t, list(map)) :: {:ok, [GithubRepo.t]} | ||
defp delete_all(%GithubAppInstallation{} = installation, repositories_attr_list) when is_list(repositories_attr_list) do | ||
repositories_attr_list | ||
|> Enum.map(&find_repo(installation, &1)) | ||
|> Enum.reject(&is_nil/1) | ||
|> Enum.map(&Repo.delete/1) | ||
|> aggregate() | ||
end | ||
|
||
@spec find_repo(GithubAppInstallation.t, map) :: GithubRepo.t | nil | ||
defp find_repo(%GithubAppInstallation{id: installation_id}, %{"id" => github_id}) do | ||
GithubRepo | ||
|> Repo.get_by(github_app_installation_id: installation_id, github_id: github_id) | ||
end | ||
|
||
# [{:ok, repo_1}, {:ok, repo_2}, {:ok, repo_3}] -> {:ok, [repo_1, repo_2, repo_3]} | ||
@spec aggregate(list({:ok, GithubRepo.t})) :: {:ok, list(GithubRepo.t)} | ||
defp aggregate(results) do | ||
repositories = | ||
results | ||
|> Enum.map(fn {:ok, %GithubRepo{} = github_repo} -> github_repo end) | ||
|
||
{:ok, repositories} | ||
end | ||
end |
2 changes: 1 addition & 1 deletion
2
...code_corps/github/events/issue_comment.ex → lib/code_corps/github/event/issue_comment.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
lib/code_corps/github/events/issues.ex → lib/code_corps/github/event/issues.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
priv/repo/migrations/20170630140136_create_project_github_repo.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
defmodule CodeCorps.Repo.Migrations.CreateProjectGithubRepo do | ||
use Ecto.Migration | ||
|
||
def change do | ||
create table(:project_github_repos) do | ||
add :project_id, references(:projects, on_delete: :delete_all) | ||
add :github_repo_id, references(:github_repos, on_delete: :delete_all) | ||
|
||
timestamps() | ||
end | ||
|
||
create unique_index(:project_github_repos, [:project_id, :github_repo_id]) | ||
end | ||
end |
Oops, something went wrong.