// Copyright 2022 Woodpecker Authors // Copyright 2018 Drone.IO Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package github import ( "bytes" "fmt" "io" "net/http" "strings" "github.com/google/go-github/v84/github" "go.woodpecker-ci.org/woodpecker/v3/server/forge/common" "go.woodpecker-ci.org/woodpecker/v3/server/forge/types" "go.woodpecker-ci.org/woodpecker/v3/server/model" ) const ( hookField = "payload" actionOpen = "opened" actionReopen = "reopened" actionClose = "closed" actionSync = "synchronize" actionReleased = "released" actionAssigned = "assigned" actionConvertedToDraft = "converted_to_draft" actionDemilestoned = "demilestoned" actionEdited = "edited" actionLabeled = "labeled" actionLocked = "locked" actionMilestoned = "milestoned" actionReadyForReview = "ready_for_review" actionUnassigned = "unassigned" actionUnlabeled = "unlabeled" actionUnlocked = "unlocked" labelCleared = "label_cleared" labelUpdated = "label_updated" ) // parseHook parses a GitHub hook from an http.Request request and returns // Repo and Pipeline detail. If a hook type is unsupported nil values are returned. func parseHook(r *http.Request, merge bool) (_ *github.PullRequest, _ *model.Repo, _ *model.Pipeline, currCommit, prevCommit string, _ error) { var reader io.Reader = r.Body if payload := r.FormValue(hookField); payload != "" { reader = bytes.NewBufferString(payload) } raw, err := io.ReadAll(reader) if err != nil { return nil, nil, nil, "", "", err } payload, err := github.ParseWebHook(github.WebHookType(r), raw) if err != nil { return nil, nil, nil, "", "", err } switch hook := payload.(type) { case *github.PushEvent: repo, pipeline, curr, prev := parsePushHook(hook) return nil, repo, pipeline, curr, prev, nil case *github.DeploymentEvent: repo, pipeline := parseDeployHook(hook) return nil, repo, pipeline, "", "", nil case *github.PullRequestEvent: pr, repo, pipeline, err := parsePullHook(hook, merge) return pr, repo, pipeline, "", "", err case *github.ReleaseEvent: repo, pipeline := parseReleaseHook(hook) return nil, repo, pipeline, "", "", nil default: return nil, nil, nil, "", "", &types.ErrIgnoreEvent{Event: github.Stringify(hook)} } } // parsePushHook parses a push hook and returns the Repo and Pipeline details. // If the commit type is unsupported nil values are returned. func parsePushHook(hook *github.PushEvent) (_ *model.Repo, _ *model.Pipeline, curr, prev string) { if hook.Deleted != nil && *hook.Deleted { return nil, nil, "", "" } pipeline := &model.Pipeline{ Event: model.EventPush, Commit: hook.GetHeadCommit().GetID(), Ref: hook.GetRef(), ForgeURL: hook.GetHeadCommit().GetURL(), Branch: strings.ReplaceAll(hook.GetRef(), "refs/heads/", ""), Message: hook.GetHeadCommit().GetMessage(), Email: hook.GetHeadCommit().GetAuthor().GetEmail(), Avatar: hook.GetSender().GetAvatarURL(), Author: hook.GetSender().GetLogin(), Sender: hook.GetSender().GetLogin(), } repo := convertRepoHook(hook.GetRepo()) if len(pipeline.Author) == 0 { pipeline.Author = hook.GetHeadCommit().GetAuthor().GetLogin() } if strings.HasPrefix(pipeline.Ref, "refs/tags/") { // just kidding, this is actually a tag event. Why did this come as a push // event we'll never know! pipeline.Event = model.EventTag // For tags, if the base_ref (tag's base branch) is set, we're using it // as pipeline's branch so that we can filter events base on it if strings.HasPrefix(hook.GetBaseRef(), "refs/heads/") { pipeline.Branch = strings.ReplaceAll(hook.GetBaseRef(), "refs/heads/", "") } return repo, pipeline, "", "" } return repo, pipeline, hook.GetHeadCommit().GetID(), hook.GetBefore() } // parseDeployHook parses a deployment and returns the Repo and Pipeline details. // If the commit type is unsupported nil values are returned. func parseDeployHook(hook *github.DeploymentEvent) (*model.Repo, *model.Pipeline) { pipeline := &model.Pipeline{ Event: model.EventDeploy, Commit: hook.GetDeployment().GetSHA(), ForgeURL: hook.GetDeployment().GetURL(), Message: hook.GetDeployment().GetDescription(), Ref: hook.GetDeployment().GetRef(), Branch: hook.GetDeployment().GetRef(), Avatar: hook.GetSender().GetAvatarURL(), Author: hook.GetSender().GetLogin(), Sender: hook.GetSender().GetLogin(), DeployTo: hook.GetDeployment().GetEnvironment(), DeployTask: hook.GetDeployment().GetTask(), } // if the ref is a sha or short sha we need to manually construct the ref. if strings.HasPrefix(pipeline.Commit, pipeline.Ref) || pipeline.Commit == pipeline.Ref { pipeline.Branch = hook.GetRepo().GetDefaultBranch() pipeline.Ref = fmt.Sprintf("refs/heads/%s", pipeline.Branch) } // if the ref is a branch we should make sure it has refs/heads prefix if !strings.HasPrefix(pipeline.Ref, "refs/") { // branch or tag pipeline.Ref = fmt.Sprintf("refs/heads/%s", pipeline.Branch) } return convertRepo(hook.GetRepo()), pipeline } // parsePullHook parses a pull request hook and returns the Repo and Pipeline // details. func parsePullHook(hook *github.PullRequestEvent, merge bool) (*github.PullRequest, *model.Repo, *model.Pipeline, error) { event := model.EventPull eventAction := "" switch hook.GetAction() { case actionOpen, actionReopen, actionSync: // default case nothing to do case actionClose: event = model.EventPullClosed case actionAssigned, actionConvertedToDraft, actionDemilestoned, actionEdited, actionLabeled, actionLocked, actionMilestoned, actionReadyForReview, actionUnassigned, actionUnlabeled, actionUnlocked: // metadata pull events event = model.EventPullMetadata eventAction = common.NormalizeEventReason(hook.GetAction()) default: return nil, nil, nil, &types.ErrIgnoreEvent{ Event: string(model.EventPullMetadata), Reason: fmt.Sprintf("action %s is not supported", hook.GetAction()), } } fromFork := hook.GetPullRequest().GetHead().GetRepo().GetID() != hook.GetPullRequest().GetBase().GetRepo().GetID() pipeline := &model.Pipeline{ Event: event, EventReason: []string{eventAction}, Commit: hook.GetPullRequest().GetHead().GetSHA(), ForgeURL: hook.GetPullRequest().GetHTMLURL(), Ref: fmt.Sprintf(headRefs, hook.GetPullRequest().GetNumber()), Branch: hook.GetPullRequest().GetBase().GetRef(), Message: hook.GetPullRequest().GetTitle(), Author: hook.GetPullRequest().GetUser().GetLogin(), Avatar: hook.GetPullRequest().GetUser().GetAvatarURL(), Title: hook.GetPullRequest().GetTitle(), Sender: hook.GetSender().GetLogin(), Refspec: fmt.Sprintf(refSpec, hook.GetPullRequest().GetHead().GetRef(), hook.GetPullRequest().GetBase().GetRef(), ), PullRequestLabels: convertLabels(hook.GetPullRequest().Labels), PullRequestMilestone: hook.GetPullRequest().GetMilestone().GetTitle(), FromFork: fromFork, } if merge { pipeline.Ref = fmt.Sprintf(mergeRefs, hook.GetPullRequest().GetNumber()) } // normalize label events to match other forges if eventAction == actionLabeled || eventAction == actionUnlabeled { if len(pipeline.PullRequestLabels) == 0 { pipeline.EventReason = []string{labelCleared} } else { pipeline.EventReason = []string{labelUpdated} } } return hook.GetPullRequest(), convertRepo(hook.GetRepo()), pipeline, nil } // parseReleaseHook parses a release hook and returns the Repo and Pipeline // details. func parseReleaseHook(hook *github.ReleaseEvent) (*model.Repo, *model.Pipeline) { if hook.GetAction() != actionReleased { return nil, nil } name := hook.GetRelease().GetName() if name == "" { name = hook.GetRelease().GetTagName() } pipeline := &model.Pipeline{ Event: model.EventRelease, ForgeURL: hook.GetRelease().GetHTMLURL(), Ref: fmt.Sprintf("refs/tags/%s", hook.GetRelease().GetTagName()), Branch: hook.GetRelease().GetTargetCommitish(), // cspell:disable-line Message: fmt.Sprintf("created release %s", name), Author: hook.GetRelease().GetAuthor().GetLogin(), Avatar: hook.GetRelease().GetAuthor().GetAvatarURL(), Sender: hook.GetSender().GetLogin(), IsPrerelease: hook.GetRelease().GetPrerelease(), } return convertRepo(hook.GetRepo()), pipeline }