Skip to content

Conversation

@begedin
Copy link
Contributor

@begedin begedin commented Jul 13, 2017

Closes #828

This PR started out as a handler for the Issues "opened" webhook exclusively, but I managed to find a generalized solution for all 4 basic subtypes of this webhook.

I also believe we will be able to extend this generalised solution for other subtypes in the future.

This is still not done, but there is an uncertainty we need to discuss, so I'm creating a PR for that purpose.

What the handler does

  • First, the handler tries to find a GithubRepo for the issue
  • If no repo, {:error, :unmatched_repo} is returned, meaning the event itself will be marked as errored
  • If a repo is found, it then for each ProjectGithubRepo child of the GithubRepo (found by github_id, project_id), it will
    • find or create a Task
    • associate that task with a User (also found by github id)

What I'm unsure of is the following:

  • in cases other than the "opened" event, should we be creating a Task in case one was not found. Presumably, as the Project was connected to a GithubRepo, a sync was performed, so all tasks should be present, meaning this should not actually be occurring, but to code allows it, so I'm not enthusiastic about just ignoring it. Yes, we should be creating a task
  • if we do not find a User what should we do? This is a realistic option and something we've discussed in the past, but we never agreed on a solution fully. Do we hold a CodeCorps user locally, to assign to the Task? Do we store the user_github_id as a Task field, to be joined later? Do we create a user, which can later be merged with a newly registered/connected user? We should be creating a user

TODO

  • Write tests for StateManager
  • Write tests for Validator
  • Update/write tests for Handler
  • Write tests for ChangesetBuilder
  • Deal with the unmatched user case
  • Deal with any changes for unmatched task

@joshsmith
Copy link
Contributor

in cases other than the "opened" event, should we be creating a Task in case one was not found? Presumably, as the Project was connected to a GithubRepo, a sync was performed, so all tasks should be present, meaning this should not actually be occurring, but to code allows it, so I'm not enthusiastic about just ignoring it.

Yes, we should be creating tasks if they're not found. Ideally, of course, we have some mechanism to prevent actually ingesting these events if the sync is still being performed. Bear in mind that a particularly large repository may take several hours of processing time. Even our own repositories are at this stage. We should probably design with this expectation in mind. Luckily we have good test cases available to us in order to get this right.

if we do not find a User what should we do? This is a realistic option and something we've discussed in the past, but we never agreed on a solution fully. Do we hold a CodeCorps user locally, to assign to the Task? Do we store the user_github_id as a Task field, to be joined later? Do we create a user, which can later be merged with a newly registered/connected user?

We create a user which can later be merged with a new user or a newly connected-to-GitHub user. There are some tasks hanging around related to this, including the one to remove the database-level reliance on a user's email being present when signing up (GitHub does not necessarily provide us with this information; more often than not, it will not).

@begedin begedin force-pushed the 828-issues-opened-webhook branch from 571a18e to 7021416 Compare July 17, 2017 12:05
@@ -0,0 +1,86 @@
defmodule CodeCorps.Adapter.MapTransformer do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were using this module as a Stripe util module, but the approach it uses is generic and can be reused for the purposes of GitHub adapters, so I moved it to outside of Stripe's scope.

I also did some renaming and added documentation to make it clearer what the functions in it actually do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested this still works with Stripe locally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some testing, but generally, the stripe adapter layer is already well-tested.

I went through it one more time and the only thing that was missing was a from_params_test for the StripeConnectAccount adapter, so I added that one.

|> Changeset.validate_required([
:github_id, :name, :github_account_id,
:github_account_avatar_url, :github_account_login, :github_account_type
])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having the adapter implicitly doing validation of the repo payload, I switched to simply having the process (and the transaction) erroring out if there are any validation errors on any of the repos.

{:error, :repo, :unmatched_project, _steps} -> {:ok, []}
{:error, _errored_step, error_response, _steps} -> {:error, error_response}
end
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding support for other subtypes of the Issues event, should be relatively simple from here. Most likely, it will be a matter of extending the behavior of the ChangesetBuilder module below.


case Repo.transaction(multi) do
{:ok, %{tasks: tasks}} -> {:ok, tasks}
{:error, :repo, :unmatched_project, _steps} -> {:ok, []}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the transaction to fail if there are no tasks to create/update due to unmatched projects. If it does not fail, we will end up creating a user for no reason.

However, this transaction failure should not be interpreted as a failure to handle the webhook, so we translate it into a success with no data updated.

|> Changeset.put_change(:user_id, user_id)
|> Changeset.validate_required([:project_id, :user_id, :markdown, :body, :title])
|> Changeset.assoc_constraint(:project)
|> Changeset.assoc_constraint(:user)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely, as we add new event subtypes for Issues, the actual changes will happen here. We may be creating TaskUser records alongside the main record, etc.

%User{}
|> Changeset.change(user_attrs |> UserAdapter.from_github_user())
|> Repo.insert
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to introduce the concept of something like an IdentityProvider record linked to a user record, which is something we've mentioned in the past, that would likely affect the UserLinker module here.

"id" => _, "title" => _, "body" => _, "state" => _,
"user" => %{"id" => _}
},
"repository" => %{"id" => _}}), do: true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function returns true if the payload possesses the minimum required data to handle the Issues webhook.

Basically this is our "first line of defense", to make sure nothing has changed with the Github API spec and the structure of the payload is as we expect it to be.

def up do
alter table(:users) do
modify :email, :string, null: true
modify :username, :string, null: true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could technically leave this one out and set the username to be the same as github_username, but I feel it just confuses things as we later merge a new/newly-connected user with this one.

@begedin begedin force-pushed the 828-issues-opened-webhook branch 2 times, most recently from 5ed2a9a to 48f27fd Compare July 17, 2017 13:47
@begedin
Copy link
Contributor Author

begedin commented Jul 17, 2017

@joshsmith This should be read for a review and, hopefully, merge

Final process:

  • We recieve an issues payload.
  • If the subtype is any of ~w(opened closed edited reopened) we handle it. The event will eventually be processed
  • If the subtype is any of ~w(assigned unassigned labeled unlabeled milestoned demilestoned) we do not handle it. The event will error out
  • If the subtype is any other, we do not handle it, but the return value is different from previous. Not really important right now, but might be. Wanted to make it explicit. The event will error out.
  • If the payload data does not contain the minimum needed to handle the event, the event will error out.

When handling

  • we try and find a github repo. If none is found, we error out, the event will error out
  • we also find or create a user
  • for the found github repo, if there are any ProjectGithubRepo children, for each of those we create or update a Task assigned to the found User and the associated Project
  • the whole process is in a transaction, so if anything goes wrong, the event errors out and none of the records are created/updated

Mapping of issue -> task

  • Issue state is open or closed. This becomes task status
  • Task state can be published or edited. Issue openedbecomespublished. closed, reopened, editedall becomeedited`
  • Issue body is task markdown
  • Task body gets generated using the same facilities we use when creating the task directly on cc

Mapping of github user --> user

  • id -> github_id
  • avatar_url -> github_avatar_url
  • login -> github_username

Not other mapping is performed. User is created without username or email.

Copy link
Contributor

@joshsmith joshsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to submit this review until way late in the evening. Everything looks generally fine. Some small comments/questions here, but nothing preventing merge if addressed.

We do need to loop back again in #849, and I'm sure I just need to see this in action to understand all the possible edge cases here, but it looks like much of the main path is figured out here.

@@ -0,0 +1,86 @@
defmodule CodeCorps.Adapter.MapTransformer do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested this still works with Stripe locally?

{:github_account_id, ["owner", "id"]},
{:github_account_avatar_url, ["owner", "avatar_url"]},
{:github_account_login, ["owner", "login"]},
{:github_account_type, ["owner", "type"]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for consistency's sake, would clean up alpha ordering here (and wherever possible).

{:error, :unmatched_repository}

@unimplemented_actions ~w(assigned unassigned milestoned demilestoned labeled unlabeled)
@implemented_actions ~w(opened closed edited reopened)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might change ordering of these.

@begedin begedin force-pushed the 828-issues-opened-webhook branch from 48f27fd to 26b10ff Compare July 18, 2017 09:32
@begedin begedin force-pushed the 828-issues-opened-webhook branch from 26b10ff to 5f48b41 Compare July 18, 2017 09:36
@begedin begedin merged commit 307eef0 into develop Jul 18, 2017
@begedin begedin deleted the 828-issues-opened-webhook branch July 18, 2017 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants