11defmodule CodeCorps.GitHub.Event.InstallationRepositories do
2- @ moduledoc """
3- In charge of dealing with "InstallationRepositories" GitHub Webhook events
2+ @ moduledoc ~S"""
3+ In charge of handling a GitHub Webhook payload for the
4+ InstallationRepositories event type.
5+
6+ [https://developer.github.com/v3/activity/events/types/#installationrepositoriesevent](https://developer.github.com/v3/activity/events/types/#installationrepositoriesevent)
47 """
58
9+ @ behaviour CodeCorps.GitHub.Event.Handler
10+
611 alias CodeCorps . {
712 GithubAppInstallation ,
813 GithubEvent ,
914 GithubRepo ,
15+ GitHub.Event.Common.ResultAggregator ,
16+ GitHub.Event.InstallationRepositories ,
1017 Repo
1118 }
1219
13- alias Ecto.Changeset
20+ alias Ecto . { Changeset , Multi }
1421
15- @ typep outcome :: { :ok , [ GithubRepo . t ] } |
16- { :error , :no_installation } |
17- { :error , :unexpected_action_or_payload } |
18- { :error , :unexpected_installation_payload } |
19- { :error , :unexpected_repo_payload }
22+ @ type outcome :: { :ok , list ( GithubRepo . t ) } |
23+ { :error , :unmatched_installation } |
24+ { :error , :unexpected_action } |
25+ { :error , :unexpected_payload } |
26+ { :error , :validation_error_on_syncing_repos } |
27+ { :error , :unexpected_transaction_outcome }
2028
2129 @ doc """
2230 Handles an "InstallationRepositories" GitHub Webhook event. The event could be
2331 of subtype "added" or "removed" and is handled differently based on that.
2432
25- `InstallationRepositories::added` will
26- - find `GithubAppInstallation`
27- - marks event as errored if not found
28- - marks event as errored if payload does not have keys it needs, meaning
29- there's something wrong with our code and we need to updated
30- - find or create `GithubRepo` records affected
33+ - the process of handling the "added" subtype is as follows
34+ - try to match with `CodeCorps.GithubAppInstallation` record
35+ - sync affected `CodeCorps.GithubRepo` records (update, create)
3136
32- `InstallationRepositories::removed` will
33- - find `GithubAppInstallation`
34- - marks event as errored if not found
35- - marks event as errored if payload does not have keys it needs, meaning
36- there's something wrong with our code and we need to updated
37- - find `GithubRepo` records affected and delete them
38- - if there is no `GithubRepo` to delete, it just skips it
39- - `ProjectGithubRepo` are deleted automatically, since they are set to
40- `on_delete: :delete_all`
37+ - the process of handling the "removed" subtype is as follows
38+ - try to match with a `CodeCorps.GithubAppInstallation` record
39+ - delete affected `CodeCorps.GithubRepo` records, respecting the rules
40+ - if the GitHub payload for a repo is not matched with a record in our
41+ database, just skip deleting it
42+ - if the deleted `CodeCorps.GithubRepo` record is associated with
43+ `CodeCorps.ProjectGithubRepo` records, they are deleted automatically,
44+ due to `on_delete: :delete_all` set at the database level.
4145 """
4246 @ spec handle ( GithubEvent . t , map ) :: outcome
43- def handle ( % GithubEvent { action: action } , payload ) , do: do_handle ( action , payload )
44-
45- @ spec do_handle ( String . t , map ) :: outcome
46- defp do_handle ( "added" , % { "installation" => installation_attrs , "repositories_added" => repositories_attr_list } ) do
47- case installation_attrs |> find_installation ( ) do
48- nil -> { :error , :no_installation }
49- :unexpected_installation_payload -> { :error , :unexpected_installation_payload }
50- % GithubAppInstallation { } = installation ->
51- case repositories_attr_list |> Enum . all? ( & valid? / 1 ) do
52- true -> find_or_create_all ( installation , repositories_attr_list )
53- false -> { :error , :unexpected_repo_payload }
54- end
55- end
47+ def handle ( % GithubEvent { } , payload ) do
48+ Multi . new
49+ |> Multi . run ( :payload , fn _ -> payload |> validate_payload ( ) end )
50+ |> Multi . run ( :action , fn _ -> payload |> validate_action ( ) end )
51+ |> Multi . run ( :installation , fn _ -> payload |> match_installation ( ) end )
52+ |> Multi . run ( :repos , fn % { installation: installation } -> installation |> sync_repos ( payload ) end )
53+ |> Repo . transaction
54+ |> marshall_result ( )
5655 end
57- defp do_handle ( "removed" , % { "installation" => installation_attrs , "repositories_removed" => repositories_attr_list } ) do
58- case installation_attrs |> find_installation ( ) do
59- nil -> { :error , :no_installation }
60- :unexpected_installation_payload -> { :error , :unexpected_installation_payload }
61- % GithubAppInstallation { } = installation ->
62- case repositories_attr_list |> Enum . all? ( & valid? / 1 ) do
63- true -> delete_all ( installation , repositories_attr_list )
64- false -> { :error , :unexpected_repo_payload }
65- end
56+
57+ @ spec validate_payload ( map ) :: { :ok , :valid } | { :error , :invalid }
58+ defp validate_payload ( % { } = payload ) do
59+ case payload |> InstallationRepositories.Validator . valid? do
60+ true -> { :ok , :valid }
61+ false -> { :error , :invalid }
6662 end
6763 end
68- defp do_handle ( _action , _payload ) , do: { :error , :unexpected_action_or_payload }
6964
70- @ spec find_installation ( any ) :: GithubAppInstallation . t | nil | :unexpected_installation_payload
71- defp find_installation ( % { "id" => github_id } ) , do: GithubAppInstallation |> Repo . get_by ( github_id: github_id )
72- defp find_installation ( _payload ) , do: :unexpected_installation_payload
65+ @ valid_actions ~w( added removed)
66+ @ spec validate_action ( map ) :: { :ok , :implemented } | { :error , :unexpected_action }
67+ defp validate_action ( % { "action" => action } ) when action in @ valid_actions , do: { :ok , :implemented }
68+ defp validate_action ( % { } ) , do: { :error , :unexpected_action }
7369
74- # should return true if the payload is a map and has the expected keys
75- @ spec valid? ( any ) :: boolean
76- defp valid? ( % { "id" => _ , "name" => _ } ) , do: true
77- defp valid? ( _ ) , do: false
70+ @ spec match_installation ( map ) :: { :ok , GithubAppInstallation . t } |
71+ { :error , :unmatched_installation }
72+ defp match_installation ( % { "installation" => % { "id" => github_id } } ) do
73+ case GithubAppInstallation |> Repo . get_by ( github_id: github_id ) do
74+ nil -> { :error , :unmatched_installation }
75+ % GithubAppInstallation { } = installation -> { :ok , installation }
76+ end
77+ end
78+ defp match_installation ( % { } ) , do: { :error , :unmatched_installation }
7879
79- @ spec find_or_create_all ( GithubAppInstallation . t , list ( map ) ) :: { :ok , list ( GithubRepo . t ) }
80- defp find_or_create_all ( % GithubAppInstallation { } = installation , repositories_attr_list ) when is_list ( repositories_attr_list ) do
81- repositories_attr_list
80+ @ spec sync_repos ( GithubAppInstallation . t , map ) :: { :ok , list ( GithubRepo . t ) } | { :error , { list ( GithubRepo . t ) , list ( Changeset . t ) } }
81+ defp sync_repos ( % GithubAppInstallation { } = installation , % { "action" => "added" , "repositories_added" => repositories } ) do
82+ repositories
8283 |> Enum . map ( & find_or_create ( installation , & 1 ) )
83- |> aggregate ( )
84+ |> ResultAggregator . aggregate
85+ end
86+ defp sync_repos ( % GithubAppInstallation { } = installation , % { "action" => "removed" , "repositories_removed" => repositories } ) do
87+ repositories
88+ |> Enum . map ( & find_repo ( installation , & 1 ) )
89+ |> Enum . reject ( & is_nil / 1 )
90+ |> Enum . map ( & Repo . delete / 1 )
91+ |> ResultAggregator . aggregate
8492 end
8593
86- @ spec find_or_create ( GithubAppInstallation . t , map ) :: { :ok , GithubRepo . t }
94+ @ spec find_or_create ( GithubAppInstallation . t , map ) :: { :ok , GithubRepo . t } | { :error , Changeset . t }
8795 defp find_or_create ( % GithubAppInstallation { } = installation , % { "id" => github_id , "name" => name } = attrs ) do
8896 case find_repo ( installation , attrs ) do
8997 nil ->
@@ -92,32 +100,23 @@ defmodule CodeCorps.GitHub.Event.InstallationRepositories do
92100 |> Changeset . put_assoc ( :github_app_installation , installation )
93101 |> Repo . insert ( )
94102 % GithubRepo { } = github_repo ->
95- { :ok , github_repo }
103+ github_repo
104+ |> Changeset . change ( % { name: name } )
105+ |> Repo . update ( )
96106 end
97107 end
98108
99- @ spec delete_all ( GithubAppInstallation . t , list ( map ) ) :: { :ok , [ GithubRepo . t ] }
100- defp delete_all ( % GithubAppInstallation { } = installation , repositories_attr_list ) when is_list ( repositories_attr_list ) do
101- repositories_attr_list
102- |> Enum . map ( & find_repo ( installation , & 1 ) )
103- |> Enum . reject ( & is_nil / 1 )
104- |> Enum . map ( & Repo . delete / 1 )
105- |> aggregate ( )
106- end
107-
108109 @ spec find_repo ( GithubAppInstallation . t , map ) :: GithubRepo . t | nil
109110 defp find_repo ( % GithubAppInstallation { id: installation_id } , % { "id" => github_id } ) do
110111 GithubRepo
111112 |> Repo . get_by ( github_app_installation_id: installation_id , github_id: github_id )
112113 end
113114
114- # [{:ok, repo_1}, {:ok, repo_2}, {:ok, repo_3}] -> {:ok, [repo_1, repo_2, repo_3]}
115- @ spec aggregate ( list ( { :ok , GithubRepo . t } ) ) :: { :ok , list ( GithubRepo . t ) }
116- defp aggregate ( results ) do
117- repositories =
118- results
119- |> Enum . map ( fn { :ok , % GithubRepo { } = github_repo } -> github_repo end )
120-
121- { :ok , repositories }
122- end
115+ @ spec marshall_result ( tuple ) :: tuple
116+ defp marshall_result ( { :ok , % { repos: repos } } ) , do: { :ok , repos }
117+ defp marshall_result ( { :error , :payload , :invalid , _steps } ) , do: { :error , :unexpected_payload }
118+ defp marshall_result ( { :error , :action , :unexpected_action , _steps } ) , do: { :error , :unexpected_action }
119+ defp marshall_result ( { :error , :installation , :unmatched_installation , _steps } ) , do: { :error , :unmatched_installation }
120+ defp marshall_result ( { :error , :repos , { _repos , _changesets } , _steps } ) , do: { :error , :validation_error_on_syncing_repos }
121+ defp marshall_result ( { :error , _errored_step , _error_response , _steps } ) , do: { :error , :unexpected_transaction_outcome }
123122end
0 commit comments