Skip to content

Commit

Permalink
enhance(webhook): handle repository transfer events (#883)
Browse files Browse the repository at this point in the history
* enhance(webhook): handle repsository transfer events

* use constant for transferred action

* increase log level for renaming repo

---------

Co-authored-by: David May <[email protected]>
  • Loading branch information
ecrupper and wass3rw3rk authored Jun 20, 2023
1 parent ef7d338 commit 3b439fc
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 40 deletions.
61 changes: 32 additions & 29 deletions api/webhook/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ func handleRepositoryEvent(c *gin.Context, m *types.Metadata, h *library.Hook, r

switch h.GetEventAction() {
// if action is rename, go through rename routine
case constants.ActionRenamed:
case constants.ActionRenamed, constants.ActionTransferred:
r, err := renameRepository(h, r, c, m)
if err != nil {
h.SetStatus(constants.StatusFailure)
Expand Down Expand Up @@ -779,34 +779,16 @@ func handleRepositoryEvent(c *gin.Context, m *types.Metadata, h *library.Hook, r
// renameRepository is a helper function that takes the old name of the repo,
// queries the database for the repo that matches that name and org, and updates
// that repo to its new name in order to preserve it. It also updates the secrets
// associated with that repo.
// associated with that repo as well as build links for the UI.
func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types.Metadata) (*library.Repo, error) {
logrus.Debugf("renaming repository from %s to %s", r.GetPreviousName(), r.GetName())
logrus.Infof("renaming repository from %s to %s", r.GetPreviousName(), r.GetName())
// get the old name of the repo
previousName := r.GetPreviousName()
// get the repo from the database that matches the old name
dbR, err := database.FromContext(c).GetRepoForOrg(r.GetOrg(), previousName)
if err != nil {
retErr := fmt.Errorf("%s: failed to get repo %s/%s from database", baseErr, r.GetOrg(), previousName)
util.HandleError(c, http.StatusBadRequest, retErr)

h.SetStatus(constants.StatusFailure)
h.SetError(retErr.Error())

return nil, retErr
}
prevOrg, prevRepo := util.SplitFullName(r.GetPreviousName())

// update the repo name information
dbR.SetName(r.GetName())
dbR.SetFullName(r.GetFullName())
dbR.SetClone(r.GetClone())
dbR.SetLink(r.GetLink())
dbR.SetPreviousName(previousName)

// update the repo in the database
err = database.FromContext(c).UpdateRepo(dbR)
// get the repo from the database that matches the old name
dbR, err := database.FromContext(c).GetRepoForOrg(prevOrg, prevRepo)
if err != nil {
retErr := fmt.Errorf("%s: failed to update repo %s/%s in database", baseErr, r.GetOrg(), previousName)
retErr := fmt.Errorf("%s: failed to get repo %s/%s from database", baseErr, prevOrg, prevRepo)
util.HandleError(c, http.StatusBadRequest, retErr)

h.SetStatus(constants.StatusFailure)
Expand Down Expand Up @@ -840,7 +822,7 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types
// get total number of secrets associated with repository
t, err := database.FromContext(c).CountSecretsForRepo(dbR, map[string]interface{}{})
if err != nil {
return nil, fmt.Errorf("unable to get secret count for repo %s/%s: %w", r.GetOrg(), previousName, err)
return nil, fmt.Errorf("unable to get secret count for repo %s/%s: %w", prevOrg, prevRepo, err)
}

secrets := []*library.Secret{}
Expand All @@ -849,7 +831,7 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types
for repoSecrets := int64(0); repoSecrets < t; repoSecrets += 100 {
s, _, err := database.FromContext(c).ListSecretsForRepo(dbR, map[string]interface{}{}, page, 100)
if err != nil {
return nil, fmt.Errorf("unable to get secret list for repo %s/%s: %w", r.GetOrg(), previousName, err)
return nil, fmt.Errorf("unable to get secret list for repo %s/%s: %w", prevOrg, prevRepo, err)
}

secrets = append(secrets, s...)
Expand All @@ -859,11 +841,12 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types

// update secrets to point to the new repository name
for _, secret := range secrets {
secret.SetOrg(r.GetOrg())
secret.SetRepo(r.GetName())

err = database.FromContext(c).UpdateSecret(secret)
if err != nil {
return nil, fmt.Errorf("unable to update secret for repo %s/%s: %w", r.GetOrg(), previousName, err)
return nil, fmt.Errorf("unable to update secret for repo %s/%s: %w", prevOrg, prevRepo, err)
}
}

Expand All @@ -890,7 +873,7 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types
// update build link to route to proper repo name
for _, build := range builds {
build.SetLink(
fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, dbR.GetFullName(), build.GetNumber()),
fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), build.GetNumber()),
)

_, err = database.FromContext(c).UpdateBuild(build)
Expand All @@ -899,5 +882,25 @@ func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context, m *types
}
}

// update the repo name information
dbR.SetName(r.GetName())
dbR.SetOrg(r.GetOrg())
dbR.SetFullName(r.GetFullName())
dbR.SetClone(r.GetClone())
dbR.SetLink(r.GetLink())
dbR.SetPreviousName(r.GetPreviousName())

// update the repo in the database
err = database.FromContext(c).UpdateRepo(dbR)
if err != nil {
retErr := fmt.Errorf("%s: failed to update repo %s/%s in database", baseErr, prevOrg, prevRepo)
util.HandleError(c, http.StatusBadRequest, retErr)

h.SetStatus(constants.StatusFailure)
h.SetError(retErr.Error())

return nil, retErr
}

return dbR, nil
}
9 changes: 1 addition & 8 deletions router/middleware/perm/perm.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,7 @@ func MustSecretAdmin() gin.HandlerFunc {

// if caller is worker with build token, verify it has access to requested secret
if strings.EqualFold(cl.TokenType, constants.WorkerBuildTokenType) {
// split repo full name into org and repo
repoSlice := strings.Split(cl.Repo, "/")
if len(repoSlice) != 2 {
logger.Errorf("unable to parse repo claim in build token")
}

org := repoSlice[0]
repo := repoSlice[1]
org, repo := util.SplitFullName(cl.Repo)

switch t {
case constants.SecretShared:
Expand Down
159 changes: 159 additions & 0 deletions scm/github/testdata/hooks/repository_transferred.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
{
"action": "transferred",
"changes": {
"owner": {
"from": {
"user": {
"login": "Old-Codertocat",
"id": 4,
"node_id": "MDQ6VXNlcjQ=",
"avatar_url": "https://octocoders.github.io/avatars/u/4?",
"gravatar_id": "",
"url": "https://octocoders.github.io/api/v3/users/Codertocat",
"html_url": "https://octocoders.github.io/Codertocat",
"followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers",
"following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}",
"gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}",
"starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions",
"organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs",
"repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos",
"events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}",
"received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events",
"type": "User",
"site_admin": false
}
}
}
},
"repository": {
"id": 118,
"node_id": "MDEwOlJlcG9zaXRvcnkxMTg=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"login": "Codertocat",
"id": 4,
"node_id": "MDQ6VXNlcjQ=",
"avatar_url": "https://octocoders.github.io/avatars/u/4?",
"gravatar_id": "",
"url": "https://octocoders.github.io/api/v3/users/Codertocat",
"html_url": "https://octocoders.github.io/Codertocat",
"followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers",
"following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}",
"gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}",
"starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions",
"organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs",
"repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos",
"events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}",
"received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://octocoders.github.io/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World",
"forks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/forks",
"keys_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/events",
"assignees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/merges",
"archive_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/deployments",
"created_at": "2019-05-15T19:37:07Z",
"updated_at": "2019-05-15T19:38:25Z",
"pushed_at": "2019-05-15T19:38:23Z",
"git_url": "git://octocoders.github.io/Codertocat/Hello-World.git",
"ssh_url": "[email protected]:Codertocat/Hello-World.git",
"clone_url": "https://octocoders.github.io/Codertocat/Hello-World.git",
"svn_url": "https://octocoders.github.io/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Ruby",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master"
},
"enterprise": {
"id": 1,
"slug": "github",
"name": "GitHub",
"node_id": "MDg6QnVzaW5lc3Mx",
"avatar_url": "https://octocoders.github.io/avatars/b/1?",
"description": null,
"website_url": null,
"html_url": "https://octocoders.github.io/businesses/github",
"created_at": "2019-05-14T19:31:12Z",
"updated_at": "2019-05-14T19:31:12Z"
},
"sender": {
"login": "Codertocat",
"id": 4,
"node_id": "MDQ6VXNlcjQ=",
"avatar_url": "https://octocoders.github.io/avatars/u/4?",
"gravatar_id": "",
"url": "https://octocoders.github.io/api/v3/users/Codertocat",
"html_url": "https://octocoders.github.io/Codertocat",
"followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers",
"following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}",
"gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}",
"starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions",
"organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs",
"repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos",
"events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}",
"received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"installation": {
"id": 5,
"node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNQ=="
}
}
15 changes: 13 additions & 2 deletions scm/github/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,19 @@ func (c *client) processRepositoryEvent(h *library.Hook, payload *github.Reposit
r.SetTopics(repo.Topics)

// if action is renamed, then get the previous name from payload
if payload.GetAction() == "renamed" {
r.SetPreviousName(payload.GetChanges().GetRepo().GetName().GetFrom())
if payload.GetAction() == constants.ActionRenamed {
r.SetPreviousName(repo.GetOwner().GetLogin() + "/" + payload.GetChanges().GetRepo().GetName().GetFrom())
}

// if action is transferred, then get the previous owner from payload
// could be a user or an org, but both are User structs
if payload.GetAction() == constants.ActionTransferred {
org := payload.GetChanges().GetOwner().GetOwnerInfo().GetOrg()
if org == nil {
org = payload.GetChanges().GetOwner().GetOwnerInfo().GetUser()
}

r.SetPreviousName(org.GetLogin() + "/" + repo.GetName())
}

h.SetEvent(constants.EventRepository)
Expand Down
67 changes: 66 additions & 1 deletion scm/github/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,72 @@ func TestGitHub_ProcessWebhook_RepositoryRename(t *testing.T) {
wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git")
wantRepo.SetBranch("master")
wantRepo.SetPrivate(false)
wantRepo.SetPreviousName("Hello-Old-World")
wantRepo.SetPreviousName("Codertocat/Hello-Old-World")
wantRepo.SetTopics(nil)

want := &types.Webhook{
Comment: "",
Hook: wantHook,
Repo: wantRepo,
}

got, err := client.ProcessWebhook(request)

if err != nil {
t.Errorf("ProcessWebhook returned err: %v", err)
}

if !reflect.DeepEqual(got, want) {
t.Errorf("ProcessWebhook is %v, want %v", got, want)
}
}

func TestGitHub_ProcessWebhook_RepositoryTransfer(t *testing.T) {
// setup router
s := httptest.NewServer(http.NotFoundHandler())
defer s.Close()

// setup request
body, err := os.Open("testdata/hooks/repository_transferred.json")
if err != nil {
t.Errorf("unable to open file: %v", err)
}

defer body.Close()

request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a")
request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e")
request.Header.Set("X-GitHub-Hook-ID", "123456")
request.Header.Set("X-GitHub-Event", "repository")

// setup client
client, _ := NewTest(s.URL)

// run test
wantHook := new(library.Hook)
wantHook.SetNumber(1)
wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e")
wantHook.SetWebhookID(123456)
wantHook.SetCreated(time.Now().UTC().Unix())
wantHook.SetHost("github.com")
wantHook.SetEvent(constants.EventRepository)
wantHook.SetEventAction(constants.ActionTransferred)
wantHook.SetBranch("master")
wantHook.SetStatus(constants.StatusSuccess)
wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks")

wantRepo := new(library.Repo)
wantRepo.SetActive(true)
wantRepo.SetOrg("Codertocat")
wantRepo.SetName("Hello-World")
wantRepo.SetFullName("Codertocat/Hello-World")
wantRepo.SetLink("https://octocoders.github.io/Codertocat/Hello-World")
wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git")
wantRepo.SetBranch("master")
wantRepo.SetPrivate(false)
wantRepo.SetPreviousName("Old-Codertocat/Hello-World")
wantRepo.SetTopics(nil)

want := &types.Webhook{
Expand Down
Loading

0 comments on commit 3b439fc

Please sign in to comment.