From 978f9ae25c3f78d2bf802a990c25848911eb3adb Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Mon, 12 May 2025 22:33:02 +0200 Subject: [PATCH 1/9] merge queue push event added --- service/hook/github/github.go | 60 +++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/service/hook/github/github.go b/service/hook/github/github.go index dee29c7e..dd986680 100644 --- a/service/hook/github/github.go +++ b/service/hook/github/github.go @@ -8,6 +8,7 @@ import ( "slices" "strconv" "strings" + "regexp" "github.com/bitrise-io/bitrise-webhooks/bitriseapi" hookCommon "github.com/bitrise-io/bitrise-webhooks/service/hook/common" @@ -30,14 +31,26 @@ type CommitModel struct { CommitMessage string `json:"message"` } +// MergeQueueMeta ... +type MergeQueuePushModel struct { + PRNumber int `json:"pr_number,omitempty"` + BaseBranch string `json:"base_branch,omitempty"` + BaseSHA string `json:"base_sha,omitempty"` + SyntheticSHA string `json:"synthetic_sha,omitempty"` + Provider string `json:"provider,omitempty"` +} + // PushEventModel ... type PushEventModel struct { - Ref string `json:"ref"` - Deleted bool `json:"deleted"` - HeadCommit CommitModel `json:"head_commit"` - Commits []CommitModel `json:"commits"` - Repo RepoInfoModel `json:"repository"` - Pusher PusherModel `json:"pusher"` + Ref string `json:"ref"` + Deleted bool `json:"deleted"` + HeadCommit CommitModel `json:"head_commit"` + Commits []CommitModel `json:"commits"` + Repo RepoInfoModel `json:"repository"` + Pusher PusherModel `json:"pusher"` + IsMergeQueuePush bool `json:"is_merge_queue_push"` + After string `json:"after"` + MergeQueue *MergeQueuePushModel `json:"merge_queue"` } // UserModel ... @@ -205,6 +218,24 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode commitMessages = append(commitMessages, commit.CommitMessage) } + if strings.HasPrefix(pushEvent.Ref, "refs/heads/gh-readonly-queue/") { + // merge queue push + base, pr, sha, err := parseMergeQueueRef(pushEvent.Ref) + if err != nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("failed to parse merge queue ref: %w", err), + } + } + pushEvent.IsMergeQueuePush = true + pushEvent.MergeQueue = &MergeQueuePushModel{ + Provider: "github", + PRNumber: pr, + BaseBranch: base, + BaseSHA: sha, + SyntheticSHA: pushEvent.After, + } + } + if strings.HasPrefix(pushEvent.Ref, "refs/heads/") { // code push branch := strings.TrimPrefix(pushEvent.Ref, "refs/heads/") @@ -248,6 +279,23 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode return hookCommon.TransformResultModel{} } +// Example: refs/heads/gh-readonly-queue/main/pr-42-abc123 +var mergeQueueRefRegex = regexp.MustCompile(`^refs/heads/gh-readonly-queue/([^/]+)/pr-(\d+)-([a-f0-9]+)$`) + +func parseMergeQueueRef(ref string) (baseBranch string, prNumber int, baseSHA string, err error) { + matches := mergeQueueRefRegex.FindStringSubmatch(ref) + if matches == nil || len(matches) != 4 { + return "", 0, "", errors.New("ref does not match merge queue format") + } + + prNum, err := strconv.Atoi(matches[2]) + if err != nil { + return "", 0, "", fmt.Errorf("invalid PR number in ref: %w", err) + } + + return matches[1], prNum, matches[3], nil +} + func isAcceptPullRequestAction(prAction string) bool { return slices.Contains([]string{"opened", "reopened", "synchronize", "edited", "ready_for_review", "labeled"}, prAction) } From a790b42d304b36374dbac562195f66a609e3de2e Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Mon, 12 May 2025 22:58:04 +0200 Subject: [PATCH 2/9] merge queue push detection for graphite --- service/hook/github/github.go | 39 ++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/service/hook/github/github.go b/service/hook/github/github.go index dd986680..1574f86f 100644 --- a/service/hook/github/github.go +++ b/service/hook/github/github.go @@ -33,11 +33,11 @@ type CommitModel struct { // MergeQueueMeta ... type MergeQueuePushModel struct { - PRNumber int `json:"pr_number,omitempty"` - BaseBranch string `json:"base_branch,omitempty"` - BaseSHA string `json:"base_sha,omitempty"` - SyntheticSHA string `json:"synthetic_sha,omitempty"` - Provider string `json:"provider,omitempty"` + QueueProvider string `json:"queue_provider,omitempty"` + PRNumber int `json:"pr_number,omitempty"` + BaseBranch string `json:"base_branch,omitempty"` + BaseSHA string `json:"base_sha,omitempty"` + SyntheticSHA string `json:"synthetic_sha,omitempty"` } // PushEventModel ... @@ -218,9 +218,9 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode commitMessages = append(commitMessages, commit.CommitMessage) } - if strings.HasPrefix(pushEvent.Ref, "refs/heads/gh-readonly-queue/") { + if strings.HasPrefix(pushEvent.Ref, "refs/heads/gh-readonly-queue/") || strings.HasPrefix(pushEvent.Ref, "refs/heads/gt-queue/") { // merge queue push - base, pr, sha, err := parseMergeQueueRef(pushEvent.Ref) + provider, base, pr, sha, err := parseMergeQueueRef(pushEvent.Ref) if err != nil { return hookCommon.TransformResultModel{ Error: fmt.Errorf("failed to parse merge queue ref: %w", err), @@ -228,11 +228,11 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode } pushEvent.IsMergeQueuePush = true pushEvent.MergeQueue = &MergeQueuePushModel{ - Provider: "github", - PRNumber: pr, - BaseBranch: base, - BaseSHA: sha, - SyntheticSHA: pushEvent.After, + QueueProvider: provider, + PRNumber: pr, + BaseBranch: base, + BaseSHA: sha, + SyntheticSHA: pushEvent.After, } } @@ -280,20 +280,21 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode } // Example: refs/heads/gh-readonly-queue/main/pr-42-abc123 -var mergeQueueRefRegex = regexp.MustCompile(`^refs/heads/gh-readonly-queue/([^/]+)/pr-(\d+)-([a-f0-9]+)$`) +var mergeQueueRefRegex = regexp.MustCompile(`^refs/heads/(gh-readonly-queue|gt-queue)/([^/]+)/pr-(\d+)-([a-f0-9]+)$`) -func parseMergeQueueRef(ref string) (baseBranch string, prNumber int, baseSHA string, err error) { + +func parseMergeQueueRef(ref string) (provider string, baseBranch string, prNumber int, baseSHA string, err error) { matches := mergeQueueRefRegex.FindStringSubmatch(ref) - if matches == nil || len(matches) != 4 { - return "", 0, "", errors.New("ref does not match merge queue format") + if matches == nil || len(matches) != 5 { + return "", "", 0, "", errors.New("ref does not match merge queue format") } - prNum, err := strconv.Atoi(matches[2]) + prNum, err := strconv.Atoi(matches[3]) if err != nil { - return "", 0, "", fmt.Errorf("invalid PR number in ref: %w", err) + return "", "", 0, "", fmt.Errorf("invalid PR number in ref: %w", err) } - return matches[1], prNum, matches[3], nil + return matches[1], matches[2], prNum, matches[4], nil } func isAcceptPullRequestAction(prAction string) bool { From 140ccf04e3c21a37bcca1743e2cbd492b36bd7b5 Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Mon, 12 May 2025 23:05:22 +0200 Subject: [PATCH 3/9] format --- service/hook/github/github.go | 725 +++++++++++++++++----------------- 1 file changed, 363 insertions(+), 362 deletions(-) diff --git a/service/hook/github/github.go b/service/hook/github/github.go index 1574f86f..108521df 100644 --- a/service/hook/github/github.go +++ b/service/hook/github/github.go @@ -9,13 +9,13 @@ import ( "strconv" "strings" "regexp" - + "github.com/bitrise-io/bitrise-webhooks/bitriseapi" hookCommon "github.com/bitrise-io/bitrise-webhooks/service/hook/common" ) const ( - + // ProviderID ... ProviderID = "github" ) @@ -34,10 +34,10 @@ type CommitModel struct { // MergeQueueMeta ... type MergeQueuePushModel struct { QueueProvider string `json:"queue_provider,omitempty"` - PRNumber int `json:"pr_number,omitempty"` - BaseBranch string `json:"base_branch,omitempty"` - BaseSHA string `json:"base_sha,omitempty"` - SyntheticSHA string `json:"synthetic_sha,omitempty"` + PRNumber int `json:"pr_number,omitempty"` + BaseBranch string `json:"base_branch,omitempty"` + BaseSHA string `json:"base_sha,omitempty"` + SyntheticSHA string `json:"synthetic_sha,omitempty"` } // PushEventModel ... @@ -50,7 +50,7 @@ type PushEventModel struct { Pusher PusherModel `json:"pusher"` IsMergeQueuePush bool `json:"is_merge_queue_push"` After string `json:"after"` - MergeQueue *MergeQueuePushModel `json:"merge_queue"` + MergeQueue *MergeQueuePushModel `json:"merge_queue"` } // UserModel ... @@ -191,33 +191,33 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode ShouldSkip: true, } } - + if !strings.HasPrefix(pushEvent.Ref, "refs/heads/") && !strings.HasPrefix(pushEvent.Ref, "refs/tags/") { return hookCommon.TransformResultModel{ Error: fmt.Errorf("ref (%s) is not a head nor a tag ref", pushEvent.Ref), ShouldSkip: true, } } - + headCommit := pushEvent.HeadCommit if len(headCommit.CommitHash) == 0 { return hookCommon.TransformResultModel{ Error: fmt.Errorf("missing commit hash"), } } - + var commits = pushEvent.Commits if len(commits) == 0 { commits = []CommitModel{pushEvent.HeadCommit} } - + var commitPaths []bitriseapi.CommitPaths var commitMessages []string for _, commit := range commits { commitPaths = append(commitPaths, commit.CommitPaths) commitMessages = append(commitMessages, commit.CommitMessage) } - + if strings.HasPrefix(pushEvent.Ref, "refs/heads/gh-readonly-queue/") || strings.HasPrefix(pushEvent.Ref, "refs/heads/gt-queue/") { // merge queue push provider, base, pr, sha, err := parseMergeQueueRef(pushEvent.Ref) @@ -235,11 +235,11 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode SyntheticSHA: pushEvent.After, } } - + if strings.HasPrefix(pushEvent.Ref, "refs/heads/") { // code push branch := strings.TrimPrefix(pushEvent.Ref, "refs/heads/") - + return hookCommon.TransformResultModel{ TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ { @@ -255,381 +255,382 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode }, }, } - } else if strings.HasPrefix(pushEvent.Ref, "refs/tags/") { - // tag push - tag := strings.TrimPrefix(pushEvent.Ref, "refs/tags/") - - return hookCommon.TransformResultModel{ - TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ - { - BuildParams: bitriseapi.BuildParamsModel{ - Tag: tag, - CommitHash: headCommit.CommitHash, - CommitMessage: headCommit.CommitMessage, - CommitMessages: commitMessages, - PushCommitPaths: commitPaths, - BaseRepositoryURL: pushEvent.Repo.getRepositoryURL(), + } else if strings.HasPrefix(pushEvent.Ref, "refs/tags/") { + // tag push + tag := strings.TrimPrefix(pushEvent.Ref, "refs/tags/") + + return hookCommon.TransformResultModel{ + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + Tag: tag, + CommitHash: headCommit.CommitHash, + CommitMessage: headCommit.CommitMessage, + CommitMessages: commitMessages, + PushCommitPaths: commitPaths, + BaseRepositoryURL: pushEvent.Repo.getRepositoryURL(), + }, + TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pushEvent.Pusher.Name), }, - TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pushEvent.Pusher.Name), }, - }, + } } - } - - return hookCommon.TransformResultModel{} -} - -// Example: refs/heads/gh-readonly-queue/main/pr-42-abc123 -var mergeQueueRefRegex = regexp.MustCompile(`^refs/heads/(gh-readonly-queue|gt-queue)/([^/]+)/pr-(\d+)-([a-f0-9]+)$`) - - -func parseMergeQueueRef(ref string) (provider string, baseBranch string, prNumber int, baseSHA string, err error) { - matches := mergeQueueRefRegex.FindStringSubmatch(ref) - if matches == nil || len(matches) != 5 { - return "", "", 0, "", errors.New("ref does not match merge queue format") - } - - prNum, err := strconv.Atoi(matches[3]) - if err != nil { - return "", "", 0, "", fmt.Errorf("invalid PR number in ref: %w", err) - } - - return matches[1], matches[2], prNum, matches[4], nil -} - -func isAcceptPullRequestAction(prAction string) bool { - return slices.Contains([]string{"opened", "reopened", "synchronize", "edited", "ready_for_review", "labeled"}, prAction) -} - -func transformPullRequestEvent(pullRequest PullRequestEventModel) hookCommon.TransformResultModel { - if pullRequest.Action == "" { - return hookCommon.TransformResultModel{ - Error: errors.New("no Pull Request action specified"), - ShouldSkip: true, + + return hookCommon.TransformResultModel{} + } + + // Example: refs/heads/gh-readonly-queue/main/pr-42-abc123 + var mergeQueueRefRegex = regexp.MustCompile(`^refs/heads/(gh-readonly-queue|gt-queue)/([^/]+)/pr-(\d+)-([a-f0-9]+)$`) + + + func parseMergeQueueRef(ref string) (provider string, baseBranch string, prNumber int, baseSHA string, err error) { + matches := mergeQueueRefRegex.FindStringSubmatch(ref) + if matches == nil || len(matches) != 5 { + return "", "", 0, "", errors.New("ref does not match merge queue format") } - } - if !isAcceptPullRequestAction(pullRequest.Action) { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("pull Request action doesn't require a build: %s", pullRequest.Action), - ShouldSkip: true, + + prNum, err := strconv.Atoi(matches[3]) + if err != nil { + return "", "", 0, "", fmt.Errorf("invalid PR number in ref: %w", err) } + + return matches[1], matches[2], prNum, matches[4], nil } - if pullRequest.Action == "edited" { - // skip it if only title / description changed, and the previous pattern did not include a [skip ci] pattern - if pullRequest.Changes.Base == nil { - if !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Title.From) && !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Body.From) { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request edit doesn't require a build: only title and/or description was changed, and previous one was not skipped"), - ShouldSkip: true, + + func isAcceptPullRequestAction(prAction string) bool { + return slices.Contains([]string{"opened", "reopened", "synchronize", "edited", "ready_for_review", "labeled"}, prAction) + } + + func transformPullRequestEvent(pullRequest PullRequestEventModel) hookCommon.TransformResultModel { + if pullRequest.Action == "" { + return hookCommon.TransformResultModel{ + Error: errors.New("no Pull Request action specified"), + ShouldSkip: true, + } + } + if !isAcceptPullRequestAction(pullRequest.Action) { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("pull Request action doesn't require a build: %s", pullRequest.Action), + ShouldSkip: true, + } + } + if pullRequest.Action == "edited" { + // skip it if only title / description changed, and the previous pattern did not include a [skip ci] pattern + if pullRequest.Changes.Base == nil { + if !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Title.From) && !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Body.From) { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request edit doesn't require a build: only title and/or description was changed, and previous one was not skipped"), + ShouldSkip: true, + } } } } - } - if pullRequest.PullRequestInfo.Merged { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request already merged"), - ShouldSkip: true, + if pullRequest.PullRequestInfo.Merged { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request already merged"), + ShouldSkip: true, + } } - } - if pullRequest.Action == "labeled" && pullRequest.PullRequestInfo.Mergeable == nil { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request label added to PR that is not open yet"), - ShouldSkip: true, + if pullRequest.Action == "labeled" && pullRequest.PullRequestInfo.Mergeable == nil { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request label added to PR that is not open yet"), + ShouldSkip: true, + } } - } - - headRefBuildParam := fmt.Sprintf("pull/%d/head", pullRequest.PullRequestID) - unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", pullRequest.PullRequestID) - // If `mergeable` is nil, the merge ref is not up-to-date, it's not safe to use for checkouts. - mergeRefUpToDate := pullRequest.PullRequestInfo.Mergeable != nil - var mergeRefBuildParam string - if mergeRefUpToDate { - mergeRefBuildParam = unverifiedMergeRefBuildParam - } - if mergeRefUpToDate && *pullRequest.PullRequestInfo.Mergeable == false { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request is not mergeable"), - ShouldSkip: true, + + headRefBuildParam := fmt.Sprintf("pull/%d/head", pullRequest.PullRequestID) + unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", pullRequest.PullRequestID) + // If `mergeable` is nil, the merge ref is not up-to-date, it's not safe to use for checkouts. + mergeRefUpToDate := pullRequest.PullRequestInfo.Mergeable != nil + var mergeRefBuildParam string + if mergeRefUpToDate { + mergeRefBuildParam = unverifiedMergeRefBuildParam } - } - - commitMsg := pullRequest.PullRequestInfo.Title - if pullRequest.PullRequestInfo.Body != "" { - commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, pullRequest.PullRequestInfo.Body) - } - - buildEnvs := make([]bitriseapi.EnvironmentItem, 0) - if pullRequest.PullRequestInfo.Draft { - buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ - Name: "GITHUB_PR_IS_DRAFT", - Value: strconv.FormatBool(pullRequest.PullRequestInfo.Draft), - IsExpand: false, - }) - } - - var labels []string - for _, label := range pullRequest.PullRequestInfo.Labels { - labels = append(labels, label.Name) - } - - result := bitriseapi.TriggerAPIParamsModel{ - BuildParams: bitriseapi.BuildParamsModel{ - CommitMessage: commitMsg, - CommitHash: pullRequest.PullRequestInfo.HeadBranchInfo.CommitHash, - Branch: pullRequest.PullRequestInfo.HeadBranchInfo.Ref, - BranchRepoOwner: pullRequest.PullRequestInfo.HeadBranchInfo.Repo.Owner.Login, - BranchDest: pullRequest.PullRequestInfo.BaseBranchInfo.Ref, - BranchDestRepoOwner: pullRequest.PullRequestInfo.BaseBranchInfo.Repo.Owner.Login, - PullRequestID: &pullRequest.PullRequestID, - BaseRepositoryURL: pullRequest.PullRequestInfo.BaseBranchInfo.getRepositoryURL(), - HeadRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), - PullRequestRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), - PullRequestAuthor: pullRequest.PullRequestInfo.User.Login, - PullRequestHeadBranch: headRefBuildParam, - PullRequestMergeBranch: mergeRefBuildParam, - PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, - DiffURL: pullRequest.PullRequestInfo.DiffURL, - Environments: buildEnvs, - PullRequestReadyState: pullRequestReadyState(pullRequest), - PullRequestLabels: labels, - }, - TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pullRequest.Sender.Login), - } - - if pullRequest.Label != nil { - result.BuildParams.PullRequestLabelsAdded = []string{pullRequest.Label.Name} - } - - return hookCommon.TransformResultModel{ - TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ - result, - }, - SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Title) && - hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Body), - } -} - -func pullRequestReadyState(pullRequest PullRequestEventModel) bitriseapi.PullRequestReadyState { - switch { - case pullRequest.Action == "ready_for_review": - return bitriseapi.PullRequestReadyStateConvertedToReadyForReview - case pullRequest.PullRequestInfo.Draft: - return bitriseapi.PullRequestReadyStateDraft - default: - return bitriseapi.PullRequestReadyStateReadyForReview - } -} - -func isAcceptIssueCommentAction(action string) bool { - return slices.Contains([]string{"created", "edited"}, action) -} - -func transformIssueCommentEvent(eventModel IssueCommentEventModel) hookCommon.TransformResultModel { - if eventModel.Action == "" { - return hookCommon.TransformResultModel{ - Error: errors.New("no issue comment action specified"), - ShouldSkip: true, + if mergeRefUpToDate && *pullRequest.PullRequestInfo.Mergeable == false { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request is not mergeable"), + ShouldSkip: true, + } } - } - if !isAcceptIssueCommentAction(eventModel.Action) { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("issue comment action doesn't require a build: %s", eventModel.Action), - ShouldSkip: true, + + commitMsg := pullRequest.PullRequestInfo.Title + if pullRequest.PullRequestInfo.Body != "" { + commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, pullRequest.PullRequestInfo.Body) } - } - - issue := eventModel.Issue - if issue.PullRequest == nil { - return hookCommon.TransformResultModel{ - Error: errors.New("issue comment is not for a pull request"), - ShouldSkip: true, + + buildEnvs := make([]bitriseapi.EnvironmentItem, 0) + if pullRequest.PullRequestInfo.Draft { + buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ + Name: "GITHUB_PR_IS_DRAFT", + Value: strconv.FormatBool(pullRequest.PullRequestInfo.Draft), + IsExpand: false, + }) } - } - if issue.State == "" { - return hookCommon.TransformResultModel{ - Error: errors.New("issue comment is for a pull request that has an unknown state"), - ShouldSkip: true, + + var labels []string + for _, label := range pullRequest.PullRequestInfo.Labels { + labels = append(labels, label.Name) } - } - if issue.State != "open" { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("issue comment is for a pull request that is not open: %s", issue.State), - ShouldSkip: true, + + result := bitriseapi.TriggerAPIParamsModel{ + BuildParams: bitriseapi.BuildParamsModel{ + CommitMessage: commitMsg, + CommitHash: pullRequest.PullRequestInfo.HeadBranchInfo.CommitHash, + Branch: pullRequest.PullRequestInfo.HeadBranchInfo.Ref, + BranchRepoOwner: pullRequest.PullRequestInfo.HeadBranchInfo.Repo.Owner.Login, + BranchDest: pullRequest.PullRequestInfo.BaseBranchInfo.Ref, + BranchDestRepoOwner: pullRequest.PullRequestInfo.BaseBranchInfo.Repo.Owner.Login, + PullRequestID: &pullRequest.PullRequestID, + BaseRepositoryURL: pullRequest.PullRequestInfo.BaseBranchInfo.getRepositoryURL(), + HeadRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), + PullRequestRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), + PullRequestAuthor: pullRequest.PullRequestInfo.User.Login, + PullRequestHeadBranch: headRefBuildParam, + PullRequestMergeBranch: mergeRefBuildParam, + PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, + DiffURL: pullRequest.PullRequestInfo.DiffURL, + Environments: buildEnvs, + PullRequestReadyState: pullRequestReadyState(pullRequest), + PullRequestLabels: labels, + }, + TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pullRequest.Sender.Login), } - } - - pullRequest := issue.PullRequest - if pullRequest.MergedAt != "" { + + if pullRequest.Label != nil { + result.BuildParams.PullRequestLabelsAdded = []string{pullRequest.Label.Name} + } + return hookCommon.TransformResultModel{ - Error: errors.New("issue comment is for a pull request that is already merged"), - ShouldSkip: true, + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + result, + }, + SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Title) && + hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Body), } } - - // NOTE: we cannot do the other PR checks (see transformPullRequestEvent mergeability conditions) because the payload doesn't have enough data - - headRefBuildParam := fmt.Sprintf("pull/%d/head", issue.PullRequestID) - unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", issue.PullRequestID) - - commitMsg := issue.Title - if issue.Body != "" { - commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, issue.Body) - } - - buildEnvs := make([]bitriseapi.EnvironmentItem, 0) - if issue.Draft { - buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ - Name: "GITHUB_PR_IS_DRAFT", - Value: strconv.FormatBool(issue.Draft), - IsExpand: false, - }) - } - - var readyState bitriseapi.PullRequestReadyState - if issue.Draft { - readyState = bitriseapi.PullRequestReadyStateDraft - } else { - readyState = bitriseapi.PullRequestReadyStateReadyForReview - } - - var labels []string - for _, label := range issue.Labels { - labels = append(labels, label.Name) - } - - result := bitriseapi.TriggerAPIParamsModel{ - BuildParams: bitriseapi.BuildParamsModel{ - CommitMessage: commitMsg, - BranchDestRepoOwner: eventModel.Repo.Owner.Login, - PullRequestID: &issue.PullRequestID, - HeadRepositoryURL: eventModel.Repo.getRepositoryURL(), - PullRequestRepositoryURL: eventModel.Repo.getRepositoryURL(), - PullRequestAuthor: issue.User.Login, - PullRequestHeadBranch: headRefBuildParam, - PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, - DiffURL: pullRequest.DiffURL, - Environments: buildEnvs, - PullRequestReadyState: readyState, - PullRequestLabels: labels, - PullRequestComment: eventModel.Comment.Body, - PullRequestCommentID: strconv.FormatInt(eventModel.Comment.ID, 10), - }, - TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, eventModel.Sender.Login), - } - - return hookCommon.TransformResultModel{ - TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ - result, - }, - SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(issue.Title) && - hookCommon.IsSkipBuildByCommitMessage(issue.Body), - } -} - -func detectContentTypeAndEventID(header http.Header) (string, string, error) { - contentType := header.Get("Content-Type") - if contentType == "" { - return "", "", errors.New("No Content-Type Header found") + + func pullRequestReadyState(pullRequest PullRequestEventModel) bitriseapi.PullRequestReadyState { + switch { + case pullRequest.Action == "ready_for_review": + return bitriseapi.PullRequestReadyStateConvertedToReadyForReview + case pullRequest.PullRequestInfo.Draft: + return bitriseapi.PullRequestReadyStateDraft + default: + return bitriseapi.PullRequestReadyStateReadyForReview + } } - - ghEvent := header.Get("X-Github-Event") - if ghEvent == "" { - return "", "", errors.New("No X-Github-Event Header found") + + func isAcceptIssueCommentAction(action string) bool { + return slices.Contains([]string{"created", "edited"}, action) } - - return contentType, ghEvent, nil -} - -// TransformRequest ... -func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformResultModel { - contentType, ghEvent, err := detectContentTypeAndEventID(r.Header) - if err != nil { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Issue with Headers: %s", err), + + func transformIssueCommentEvent(eventModel IssueCommentEventModel) hookCommon.TransformResultModel { + if eventModel.Action == "" { + return hookCommon.TransformResultModel{ + Error: errors.New("no issue comment action specified"), + ShouldSkip: true, + } } - } - - if contentType != hookCommon.ContentTypeApplicationJSON && contentType != hookCommon.ContentTypeApplicationXWWWFormURLEncoded { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Content-Type is not supported: %s", contentType), + if !isAcceptIssueCommentAction(eventModel.Action) { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("issue comment action doesn't require a build: %s", eventModel.Action), + ShouldSkip: true, + } } - } - - if ghEvent == "ping" { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("ping event received"), - ShouldSkip: true, + + issue := eventModel.Issue + if issue.PullRequest == nil { + return hookCommon.TransformResultModel{ + Error: errors.New("issue comment is not for a pull request"), + ShouldSkip: true, + } } - } - if ghEvent != "push" && ghEvent != "pull_request" && ghEvent != "issue_comment" { - // Unsupported GitHub Event - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("unsupported GitHub Webhook event: %s", ghEvent), + if issue.State == "" { + return hookCommon.TransformResultModel{ + Error: errors.New("issue comment is for a pull request that has an unknown state"), + ShouldSkip: true, + } } - } - - if r.Body == nil { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("failed to read content of request body: no or empty request body"), + if issue.State != "open" { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("issue comment is for a pull request that is not open: %s", issue.State), + ShouldSkip: true, + } } - } - - if ghEvent == "push" { - eventModel, err := decodeEventPayload[PushEventModel](r, contentType) - if err != nil { - return hookCommon.TransformResultModel{Error: err} + + pullRequest := issue.PullRequest + if pullRequest.MergedAt != "" { + return hookCommon.TransformResultModel{ + Error: errors.New("issue comment is for a pull request that is already merged"), + ShouldSkip: true, + } } - - return transformPushEvent(*eventModel) - } else if ghEvent == "pull_request" { - eventModel, err := decodeEventPayload[PullRequestEventModel](r, contentType) - if err != nil { - return hookCommon.TransformResultModel{Error: err} + + // NOTE: we cannot do the other PR checks (see transformPullRequestEvent mergeability conditions) because the payload doesn't have enough data + + headRefBuildParam := fmt.Sprintf("pull/%d/head", issue.PullRequestID) + unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", issue.PullRequestID) + + commitMsg := issue.Title + if issue.Body != "" { + commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, issue.Body) } - - return transformPullRequestEvent(*eventModel) - } else if ghEvent == "issue_comment" { - eventModel, err := decodeEventPayload[IssueCommentEventModel](r, contentType) - if err != nil { - return hookCommon.TransformResultModel{Error: err} + + buildEnvs := make([]bitriseapi.EnvironmentItem, 0) + if issue.Draft { + buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ + Name: "GITHUB_PR_IS_DRAFT", + Value: strconv.FormatBool(issue.Draft), + IsExpand: false, + }) } - - return transformIssueCommentEvent(*eventModel) - } - - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Unsupported GitHub event type: %s", ghEvent), - } -} - -func decodeEventPayload[T interface{}](r *http.Request, contentType string) (*T, error) { - var eventModel T - if contentType == hookCommon.ContentTypeApplicationJSON { - if err := json.NewDecoder(r.Body).Decode(&eventModel); err != nil { - return nil, fmt.Errorf("Failed to parse request body as JSON: %s", err) - } - } else if contentType == hookCommon.ContentTypeApplicationXWWWFormURLEncoded { - payloadValue := r.PostFormValue("payload") - if payloadValue == "" { - return nil, fmt.Errorf("failed to parse request body: empty payload") - } - if err := json.NewDecoder(strings.NewReader(payloadValue)).Decode(&eventModel); err != nil { - return nil, fmt.Errorf("Failed to parse payload: %s", err) - } - } else { - return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) - } - - return &eventModel, nil -} - -func (branchInfoModel BranchInfoModel) getRepositoryURL() string { - return branchInfoModel.Repo.getRepositoryURL() -} - -func (repoInfoModel RepoInfoModel) getRepositoryURL() string { - if repoInfoModel.Private { - return repoInfoModel.SSHURL - } - return repoInfoModel.CloneURL -} + + var readyState bitriseapi.PullRequestReadyState + if issue.Draft { + readyState = bitriseapi.PullRequestReadyStateDraft + } else { + readyState = bitriseapi.PullRequestReadyStateReadyForReview + } + + var labels []string + for _, label := range issue.Labels { + labels = append(labels, label.Name) + } + + result := bitriseapi.TriggerAPIParamsModel{ + BuildParams: bitriseapi.BuildParamsModel{ + CommitMessage: commitMsg, + BranchDestRepoOwner: eventModel.Repo.Owner.Login, + PullRequestID: &issue.PullRequestID, + HeadRepositoryURL: eventModel.Repo.getRepositoryURL(), + PullRequestRepositoryURL: eventModel.Repo.getRepositoryURL(), + PullRequestAuthor: issue.User.Login, + PullRequestHeadBranch: headRefBuildParam, + PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, + DiffURL: pullRequest.DiffURL, + Environments: buildEnvs, + PullRequestReadyState: readyState, + PullRequestLabels: labels, + PullRequestComment: eventModel.Comment.Body, + PullRequestCommentID: strconv.FormatInt(eventModel.Comment.ID, 10), + }, + TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, eventModel.Sender.Login), + } + + return hookCommon.TransformResultModel{ + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + result, + }, + SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(issue.Title) && + hookCommon.IsSkipBuildByCommitMessage(issue.Body), + } + } + + func detectContentTypeAndEventID(header http.Header) (string, string, error) { + contentType := header.Get("Content-Type") + if contentType == "" { + return "", "", errors.New("No Content-Type Header found") + } + + ghEvent := header.Get("X-Github-Event") + if ghEvent == "" { + return "", "", errors.New("No X-Github-Event Header found") + } + + return contentType, ghEvent, nil + } + + // TransformRequest ... + func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformResultModel { + contentType, ghEvent, err := detectContentTypeAndEventID(r.Header) + if err != nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Issue with Headers: %s", err), + } + } + + if contentType != hookCommon.ContentTypeApplicationJSON && contentType != hookCommon.ContentTypeApplicationXWWWFormURLEncoded { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Content-Type is not supported: %s", contentType), + } + } + + if ghEvent == "ping" { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("ping event received"), + ShouldSkip: true, + } + } + if ghEvent != "push" && ghEvent != "pull_request" && ghEvent != "issue_comment" { + // Unsupported GitHub Event + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("unsupported GitHub Webhook event: %s", ghEvent), + } + } + + if r.Body == nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("failed to read content of request body: no or empty request body"), + } + } + + if ghEvent == "push" { + eventModel, err := decodeEventPayload[PushEventModel](r, contentType) + if err != nil { + return hookCommon.TransformResultModel{Error: err} + } + + return transformPushEvent(*eventModel) + } else if ghEvent == "pull_request" { + eventModel, err := decodeEventPayload[PullRequestEventModel](r, contentType) + if err != nil { + return hookCommon.TransformResultModel{Error: err} + } + + return transformPullRequestEvent(*eventModel) + } else if ghEvent == "issue_comment" { + eventModel, err := decodeEventPayload[IssueCommentEventModel](r, contentType) + if err != nil { + return hookCommon.TransformResultModel{Error: err} + } + + return transformIssueCommentEvent(*eventModel) + } + + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Unsupported GitHub event type: %s", ghEvent), + } + } + + func decodeEventPayload[T interface{}](r *http.Request, contentType string) (*T, error) { + var eventModel T + if contentType == hookCommon.ContentTypeApplicationJSON { + if err := json.NewDecoder(r.Body).Decode(&eventModel); err != nil { + return nil, fmt.Errorf("Failed to parse request body as JSON: %s", err) + } + } else if contentType == hookCommon.ContentTypeApplicationXWWWFormURLEncoded { + payloadValue := r.PostFormValue("payload") + if payloadValue == "" { + return nil, fmt.Errorf("failed to parse request body: empty payload") + } + if err := json.NewDecoder(strings.NewReader(payloadValue)).Decode(&eventModel); err != nil { + return nil, fmt.Errorf("Failed to parse payload: %s", err) + } + } else { + return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) + } + + return &eventModel, nil + } + + func (branchInfoModel BranchInfoModel) getRepositoryURL() string { + return branchInfoModel.Repo.getRepositoryURL() + } + + func (repoInfoModel RepoInfoModel) getRepositoryURL() string { + if repoInfoModel.Private { + return repoInfoModel.SSHURL + } + return repoInfoModel.CloneURL + } + \ No newline at end of file From 95c68d919dea841eb87b79359261c53c4167ed32 Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Tue, 13 May 2025 10:16:57 +0200 Subject: [PATCH 4/9] sampleMergeQueuePushData added for testing --- service/hook/github/github.go | 752 ++++++++++++++--------------- service/hook/github/github_test.go | 57 ++- 2 files changed, 431 insertions(+), 378 deletions(-) diff --git a/service/hook/github/github.go b/service/hook/github/github.go index 108521df..8f1389d3 100644 --- a/service/hook/github/github.go +++ b/service/hook/github/github.go @@ -5,17 +5,17 @@ import ( "errors" "fmt" "net/http" + "regexp" "slices" "strconv" "strings" - "regexp" - + "github.com/bitrise-io/bitrise-webhooks/bitriseapi" hookCommon "github.com/bitrise-io/bitrise-webhooks/service/hook/common" ) const ( - + // ProviderID ... ProviderID = "github" ) @@ -33,24 +33,24 @@ type CommitModel struct { // MergeQueueMeta ... type MergeQueuePushModel struct { - QueueProvider string `json:"queue_provider,omitempty"` - PRNumber int `json:"pr_number,omitempty"` - BaseBranch string `json:"base_branch,omitempty"` - BaseSHA string `json:"base_sha,omitempty"` - SyntheticSHA string `json:"synthetic_sha,omitempty"` + QueueProvider string `json:"queue_provider,omitempty"` + PRNumber int `json:"pr_number,omitempty"` + BaseBranch string `json:"base_branch,omitempty"` + BaseSHA string `json:"base_sha,omitempty"` + SyntheticSHA string `json:"synthetic_sha,omitempty"` } // PushEventModel ... type PushEventModel struct { - Ref string `json:"ref"` - Deleted bool `json:"deleted"` - HeadCommit CommitModel `json:"head_commit"` - Commits []CommitModel `json:"commits"` - Repo RepoInfoModel `json:"repository"` - Pusher PusherModel `json:"pusher"` - IsMergeQueuePush bool `json:"is_merge_queue_push"` - After string `json:"after"` - MergeQueue *MergeQueuePushModel `json:"merge_queue"` + Ref string `json:"ref"` + Deleted bool `json:"deleted"` + HeadCommit CommitModel `json:"head_commit"` + Commits []CommitModel `json:"commits"` + Repo RepoInfoModel `json:"repository"` + Pusher PusherModel `json:"pusher"` + IsMergeQueuePush bool `json:"is_merge_queue_push"` + After string `json:"after"` + MergeQueue *MergeQueuePushModel `json:"merge_queue"` } // UserModel ... @@ -191,33 +191,33 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode ShouldSkip: true, } } - + if !strings.HasPrefix(pushEvent.Ref, "refs/heads/") && !strings.HasPrefix(pushEvent.Ref, "refs/tags/") { return hookCommon.TransformResultModel{ Error: fmt.Errorf("ref (%s) is not a head nor a tag ref", pushEvent.Ref), ShouldSkip: true, } } - + headCommit := pushEvent.HeadCommit if len(headCommit.CommitHash) == 0 { return hookCommon.TransformResultModel{ Error: fmt.Errorf("missing commit hash"), } } - + var commits = pushEvent.Commits if len(commits) == 0 { commits = []CommitModel{pushEvent.HeadCommit} } - + var commitPaths []bitriseapi.CommitPaths var commitMessages []string for _, commit := range commits { commitPaths = append(commitPaths, commit.CommitPaths) commitMessages = append(commitMessages, commit.CommitMessage) } - + if strings.HasPrefix(pushEvent.Ref, "refs/heads/gh-readonly-queue/") || strings.HasPrefix(pushEvent.Ref, "refs/heads/gt-queue/") { // merge queue push provider, base, pr, sha, err := parseMergeQueueRef(pushEvent.Ref) @@ -228,18 +228,18 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode } pushEvent.IsMergeQueuePush = true pushEvent.MergeQueue = &MergeQueuePushModel{ - QueueProvider: provider, - PRNumber: pr, - BaseBranch: base, - BaseSHA: sha, - SyntheticSHA: pushEvent.After, + QueueProvider: provider, + PRNumber: pr, + BaseBranch: base, + BaseSHA: sha, + SyntheticSHA: pushEvent.After, } } - + if strings.HasPrefix(pushEvent.Ref, "refs/heads/") { // code push branch := strings.TrimPrefix(pushEvent.Ref, "refs/heads/") - + return hookCommon.TransformResultModel{ TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ { @@ -255,382 +255,380 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode }, }, } - } else if strings.HasPrefix(pushEvent.Ref, "refs/tags/") { - // tag push - tag := strings.TrimPrefix(pushEvent.Ref, "refs/tags/") - - return hookCommon.TransformResultModel{ - TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ - { - BuildParams: bitriseapi.BuildParamsModel{ - Tag: tag, - CommitHash: headCommit.CommitHash, - CommitMessage: headCommit.CommitMessage, - CommitMessages: commitMessages, - PushCommitPaths: commitPaths, - BaseRepositoryURL: pushEvent.Repo.getRepositoryURL(), - }, - TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pushEvent.Pusher.Name), + } else if strings.HasPrefix(pushEvent.Ref, "refs/tags/") { + // tag push + tag := strings.TrimPrefix(pushEvent.Ref, "refs/tags/") + + return hookCommon.TransformResultModel{ + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + Tag: tag, + CommitHash: headCommit.CommitHash, + CommitMessage: headCommit.CommitMessage, + CommitMessages: commitMessages, + PushCommitPaths: commitPaths, + BaseRepositoryURL: pushEvent.Repo.getRepositoryURL(), }, + TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pushEvent.Pusher.Name), }, - } - } - - return hookCommon.TransformResultModel{} - } - - // Example: refs/heads/gh-readonly-queue/main/pr-42-abc123 - var mergeQueueRefRegex = regexp.MustCompile(`^refs/heads/(gh-readonly-queue|gt-queue)/([^/]+)/pr-(\d+)-([a-f0-9]+)$`) - - - func parseMergeQueueRef(ref string) (provider string, baseBranch string, prNumber int, baseSHA string, err error) { - matches := mergeQueueRefRegex.FindStringSubmatch(ref) - if matches == nil || len(matches) != 5 { - return "", "", 0, "", errors.New("ref does not match merge queue format") - } - - prNum, err := strconv.Atoi(matches[3]) - if err != nil { - return "", "", 0, "", fmt.Errorf("invalid PR number in ref: %w", err) + }, } - - return matches[1], matches[2], prNum, matches[4], nil } - - func isAcceptPullRequestAction(prAction string) bool { - return slices.Contains([]string{"opened", "reopened", "synchronize", "edited", "ready_for_review", "labeled"}, prAction) + + return hookCommon.TransformResultModel{} +} + +// Example: refs/heads/gh-readonly-queue/main/pr-42-abc123 +var mergeQueueRefRegex = regexp.MustCompile(`^refs/heads/(gh-readonly-queue|gt-queue)/([^/]+)/pr-(\d+)-([a-f0-9]+)$`) + +func parseMergeQueueRef(ref string) (provider string, baseBranch string, prNumber int, baseSHA string, err error) { + matches := mergeQueueRefRegex.FindStringSubmatch(ref) + if matches == nil || len(matches) != 5 { + return "", "", 0, "", errors.New("ref does not match merge queue format") } - - func transformPullRequestEvent(pullRequest PullRequestEventModel) hookCommon.TransformResultModel { - if pullRequest.Action == "" { - return hookCommon.TransformResultModel{ - Error: errors.New("no Pull Request action specified"), - ShouldSkip: true, - } + + prNum, err := strconv.Atoi(matches[3]) + if err != nil { + return "", "", 0, "", fmt.Errorf("invalid PR number in ref: %w", err) + } + + return matches[1], matches[2], prNum, matches[4], nil +} + +func isAcceptPullRequestAction(prAction string) bool { + return slices.Contains([]string{"opened", "reopened", "synchronize", "edited", "ready_for_review", "labeled"}, prAction) +} + +func transformPullRequestEvent(pullRequest PullRequestEventModel) hookCommon.TransformResultModel { + if pullRequest.Action == "" { + return hookCommon.TransformResultModel{ + Error: errors.New("no Pull Request action specified"), + ShouldSkip: true, } - if !isAcceptPullRequestAction(pullRequest.Action) { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("pull Request action doesn't require a build: %s", pullRequest.Action), - ShouldSkip: true, - } + } + if !isAcceptPullRequestAction(pullRequest.Action) { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("pull Request action doesn't require a build: %s", pullRequest.Action), + ShouldSkip: true, } - if pullRequest.Action == "edited" { - // skip it if only title / description changed, and the previous pattern did not include a [skip ci] pattern - if pullRequest.Changes.Base == nil { - if !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Title.From) && !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Body.From) { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request edit doesn't require a build: only title and/or description was changed, and previous one was not skipped"), - ShouldSkip: true, - } + } + if pullRequest.Action == "edited" { + // skip it if only title / description changed, and the previous pattern did not include a [skip ci] pattern + if pullRequest.Changes.Base == nil { + if !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Title.From) && !hookCommon.IsSkipBuildByCommitMessage(pullRequest.Changes.Body.From) { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request edit doesn't require a build: only title and/or description was changed, and previous one was not skipped"), + ShouldSkip: true, } } } - if pullRequest.PullRequestInfo.Merged { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request already merged"), - ShouldSkip: true, - } - } - if pullRequest.Action == "labeled" && pullRequest.PullRequestInfo.Mergeable == nil { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request label added to PR that is not open yet"), - ShouldSkip: true, - } - } - - headRefBuildParam := fmt.Sprintf("pull/%d/head", pullRequest.PullRequestID) - unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", pullRequest.PullRequestID) - // If `mergeable` is nil, the merge ref is not up-to-date, it's not safe to use for checkouts. - mergeRefUpToDate := pullRequest.PullRequestInfo.Mergeable != nil - var mergeRefBuildParam string - if mergeRefUpToDate { - mergeRefBuildParam = unverifiedMergeRefBuildParam + } + if pullRequest.PullRequestInfo.Merged { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request already merged"), + ShouldSkip: true, } - if mergeRefUpToDate && *pullRequest.PullRequestInfo.Mergeable == false { - return hookCommon.TransformResultModel{ - Error: errors.New("pull Request is not mergeable"), - ShouldSkip: true, - } + } + if pullRequest.Action == "labeled" && pullRequest.PullRequestInfo.Mergeable == nil { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request label added to PR that is not open yet"), + ShouldSkip: true, } - - commitMsg := pullRequest.PullRequestInfo.Title - if pullRequest.PullRequestInfo.Body != "" { - commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, pullRequest.PullRequestInfo.Body) + } + + headRefBuildParam := fmt.Sprintf("pull/%d/head", pullRequest.PullRequestID) + unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", pullRequest.PullRequestID) + // If `mergeable` is nil, the merge ref is not up-to-date, it's not safe to use for checkouts. + mergeRefUpToDate := pullRequest.PullRequestInfo.Mergeable != nil + var mergeRefBuildParam string + if mergeRefUpToDate { + mergeRefBuildParam = unverifiedMergeRefBuildParam + } + if mergeRefUpToDate && *pullRequest.PullRequestInfo.Mergeable == false { + return hookCommon.TransformResultModel{ + Error: errors.New("pull Request is not mergeable"), + ShouldSkip: true, } - - buildEnvs := make([]bitriseapi.EnvironmentItem, 0) - if pullRequest.PullRequestInfo.Draft { - buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ - Name: "GITHUB_PR_IS_DRAFT", - Value: strconv.FormatBool(pullRequest.PullRequestInfo.Draft), - IsExpand: false, - }) + } + + commitMsg := pullRequest.PullRequestInfo.Title + if pullRequest.PullRequestInfo.Body != "" { + commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, pullRequest.PullRequestInfo.Body) + } + + buildEnvs := make([]bitriseapi.EnvironmentItem, 0) + if pullRequest.PullRequestInfo.Draft { + buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ + Name: "GITHUB_PR_IS_DRAFT", + Value: strconv.FormatBool(pullRequest.PullRequestInfo.Draft), + IsExpand: false, + }) + } + + var labels []string + for _, label := range pullRequest.PullRequestInfo.Labels { + labels = append(labels, label.Name) + } + + result := bitriseapi.TriggerAPIParamsModel{ + BuildParams: bitriseapi.BuildParamsModel{ + CommitMessage: commitMsg, + CommitHash: pullRequest.PullRequestInfo.HeadBranchInfo.CommitHash, + Branch: pullRequest.PullRequestInfo.HeadBranchInfo.Ref, + BranchRepoOwner: pullRequest.PullRequestInfo.HeadBranchInfo.Repo.Owner.Login, + BranchDest: pullRequest.PullRequestInfo.BaseBranchInfo.Ref, + BranchDestRepoOwner: pullRequest.PullRequestInfo.BaseBranchInfo.Repo.Owner.Login, + PullRequestID: &pullRequest.PullRequestID, + BaseRepositoryURL: pullRequest.PullRequestInfo.BaseBranchInfo.getRepositoryURL(), + HeadRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), + PullRequestRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), + PullRequestAuthor: pullRequest.PullRequestInfo.User.Login, + PullRequestHeadBranch: headRefBuildParam, + PullRequestMergeBranch: mergeRefBuildParam, + PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, + DiffURL: pullRequest.PullRequestInfo.DiffURL, + Environments: buildEnvs, + PullRequestReadyState: pullRequestReadyState(pullRequest), + PullRequestLabels: labels, + }, + TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pullRequest.Sender.Login), + } + + if pullRequest.Label != nil { + result.BuildParams.PullRequestLabelsAdded = []string{pullRequest.Label.Name} + } + + return hookCommon.TransformResultModel{ + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + result, + }, + SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Title) && + hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Body), + } +} + +func pullRequestReadyState(pullRequest PullRequestEventModel) bitriseapi.PullRequestReadyState { + switch { + case pullRequest.Action == "ready_for_review": + return bitriseapi.PullRequestReadyStateConvertedToReadyForReview + case pullRequest.PullRequestInfo.Draft: + return bitriseapi.PullRequestReadyStateDraft + default: + return bitriseapi.PullRequestReadyStateReadyForReview + } +} + +func isAcceptIssueCommentAction(action string) bool { + return slices.Contains([]string{"created", "edited"}, action) +} + +func transformIssueCommentEvent(eventModel IssueCommentEventModel) hookCommon.TransformResultModel { + if eventModel.Action == "" { + return hookCommon.TransformResultModel{ + Error: errors.New("no issue comment action specified"), + ShouldSkip: true, } - - var labels []string - for _, label := range pullRequest.PullRequestInfo.Labels { - labels = append(labels, label.Name) + } + if !isAcceptIssueCommentAction(eventModel.Action) { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("issue comment action doesn't require a build: %s", eventModel.Action), + ShouldSkip: true, } - - result := bitriseapi.TriggerAPIParamsModel{ - BuildParams: bitriseapi.BuildParamsModel{ - CommitMessage: commitMsg, - CommitHash: pullRequest.PullRequestInfo.HeadBranchInfo.CommitHash, - Branch: pullRequest.PullRequestInfo.HeadBranchInfo.Ref, - BranchRepoOwner: pullRequest.PullRequestInfo.HeadBranchInfo.Repo.Owner.Login, - BranchDest: pullRequest.PullRequestInfo.BaseBranchInfo.Ref, - BranchDestRepoOwner: pullRequest.PullRequestInfo.BaseBranchInfo.Repo.Owner.Login, - PullRequestID: &pullRequest.PullRequestID, - BaseRepositoryURL: pullRequest.PullRequestInfo.BaseBranchInfo.getRepositoryURL(), - HeadRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), - PullRequestRepositoryURL: pullRequest.PullRequestInfo.HeadBranchInfo.getRepositoryURL(), - PullRequestAuthor: pullRequest.PullRequestInfo.User.Login, - PullRequestHeadBranch: headRefBuildParam, - PullRequestMergeBranch: mergeRefBuildParam, - PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, - DiffURL: pullRequest.PullRequestInfo.DiffURL, - Environments: buildEnvs, - PullRequestReadyState: pullRequestReadyState(pullRequest), - PullRequestLabels: labels, - }, - TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pullRequest.Sender.Login), + } + + issue := eventModel.Issue + if issue.PullRequest == nil { + return hookCommon.TransformResultModel{ + Error: errors.New("issue comment is not for a pull request"), + ShouldSkip: true, } - - if pullRequest.Label != nil { - result.BuildParams.PullRequestLabelsAdded = []string{pullRequest.Label.Name} + } + if issue.State == "" { + return hookCommon.TransformResultModel{ + Error: errors.New("issue comment is for a pull request that has an unknown state"), + ShouldSkip: true, } - + } + if issue.State != "open" { return hookCommon.TransformResultModel{ - TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ - result, - }, - SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Title) && - hookCommon.IsSkipBuildByCommitMessage(pullRequest.PullRequestInfo.Body), + Error: fmt.Errorf("issue comment is for a pull request that is not open: %s", issue.State), + ShouldSkip: true, } } - - func pullRequestReadyState(pullRequest PullRequestEventModel) bitriseapi.PullRequestReadyState { - switch { - case pullRequest.Action == "ready_for_review": - return bitriseapi.PullRequestReadyStateConvertedToReadyForReview - case pullRequest.PullRequestInfo.Draft: - return bitriseapi.PullRequestReadyStateDraft - default: - return bitriseapi.PullRequestReadyStateReadyForReview + + pullRequest := issue.PullRequest + if pullRequest.MergedAt != "" { + return hookCommon.TransformResultModel{ + Error: errors.New("issue comment is for a pull request that is already merged"), + ShouldSkip: true, } } - - func isAcceptIssueCommentAction(action string) bool { - return slices.Contains([]string{"created", "edited"}, action) + + // NOTE: we cannot do the other PR checks (see transformPullRequestEvent mergeability conditions) because the payload doesn't have enough data + + headRefBuildParam := fmt.Sprintf("pull/%d/head", issue.PullRequestID) + unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", issue.PullRequestID) + + commitMsg := issue.Title + if issue.Body != "" { + commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, issue.Body) } - - func transformIssueCommentEvent(eventModel IssueCommentEventModel) hookCommon.TransformResultModel { - if eventModel.Action == "" { - return hookCommon.TransformResultModel{ - Error: errors.New("no issue comment action specified"), - ShouldSkip: true, - } - } - if !isAcceptIssueCommentAction(eventModel.Action) { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("issue comment action doesn't require a build: %s", eventModel.Action), - ShouldSkip: true, - } - } - - issue := eventModel.Issue - if issue.PullRequest == nil { - return hookCommon.TransformResultModel{ - Error: errors.New("issue comment is not for a pull request"), - ShouldSkip: true, - } + + buildEnvs := make([]bitriseapi.EnvironmentItem, 0) + if issue.Draft { + buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ + Name: "GITHUB_PR_IS_DRAFT", + Value: strconv.FormatBool(issue.Draft), + IsExpand: false, + }) + } + + var readyState bitriseapi.PullRequestReadyState + if issue.Draft { + readyState = bitriseapi.PullRequestReadyStateDraft + } else { + readyState = bitriseapi.PullRequestReadyStateReadyForReview + } + + var labels []string + for _, label := range issue.Labels { + labels = append(labels, label.Name) + } + + result := bitriseapi.TriggerAPIParamsModel{ + BuildParams: bitriseapi.BuildParamsModel{ + CommitMessage: commitMsg, + BranchDestRepoOwner: eventModel.Repo.Owner.Login, + PullRequestID: &issue.PullRequestID, + HeadRepositoryURL: eventModel.Repo.getRepositoryURL(), + PullRequestRepositoryURL: eventModel.Repo.getRepositoryURL(), + PullRequestAuthor: issue.User.Login, + PullRequestHeadBranch: headRefBuildParam, + PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, + DiffURL: pullRequest.DiffURL, + Environments: buildEnvs, + PullRequestReadyState: readyState, + PullRequestLabels: labels, + PullRequestComment: eventModel.Comment.Body, + PullRequestCommentID: strconv.FormatInt(eventModel.Comment.ID, 10), + }, + TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, eventModel.Sender.Login), + } + + return hookCommon.TransformResultModel{ + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + result, + }, + SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(issue.Title) && + hookCommon.IsSkipBuildByCommitMessage(issue.Body), + } +} + +func detectContentTypeAndEventID(header http.Header) (string, string, error) { + contentType := header.Get("Content-Type") + if contentType == "" { + return "", "", errors.New("No Content-Type Header found") + } + + ghEvent := header.Get("X-Github-Event") + if ghEvent == "" { + return "", "", errors.New("No X-Github-Event Header found") + } + + return contentType, ghEvent, nil +} + +// TransformRequest ... +func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformResultModel { + contentType, ghEvent, err := detectContentTypeAndEventID(r.Header) + if err != nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Issue with Headers: %s", err), } - if issue.State == "" { - return hookCommon.TransformResultModel{ - Error: errors.New("issue comment is for a pull request that has an unknown state"), - ShouldSkip: true, - } + } + + if contentType != hookCommon.ContentTypeApplicationJSON && contentType != hookCommon.ContentTypeApplicationXWWWFormURLEncoded { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Content-Type is not supported: %s", contentType), } - if issue.State != "open" { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("issue comment is for a pull request that is not open: %s", issue.State), - ShouldSkip: true, - } + } + + if ghEvent == "ping" { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("ping event received"), + ShouldSkip: true, } - - pullRequest := issue.PullRequest - if pullRequest.MergedAt != "" { - return hookCommon.TransformResultModel{ - Error: errors.New("issue comment is for a pull request that is already merged"), - ShouldSkip: true, - } + } + if ghEvent != "push" && ghEvent != "pull_request" && ghEvent != "issue_comment" { + // Unsupported GitHub Event + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("unsupported GitHub Webhook event: %s", ghEvent), } - - // NOTE: we cannot do the other PR checks (see transformPullRequestEvent mergeability conditions) because the payload doesn't have enough data - - headRefBuildParam := fmt.Sprintf("pull/%d/head", issue.PullRequestID) - unverifiedMergeRefBuildParam := fmt.Sprintf("pull/%d/merge", issue.PullRequestID) - - commitMsg := issue.Title - if issue.Body != "" { - commitMsg = fmt.Sprintf("%s\n\n%s", commitMsg, issue.Body) + } + + if r.Body == nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("failed to read content of request body: no or empty request body"), } - - buildEnvs := make([]bitriseapi.EnvironmentItem, 0) - if issue.Draft { - buildEnvs = append(buildEnvs, bitriseapi.EnvironmentItem{ - Name: "GITHUB_PR_IS_DRAFT", - Value: strconv.FormatBool(issue.Draft), - IsExpand: false, - }) + } + + if ghEvent == "push" { + eventModel, err := decodeEventPayload[PushEventModel](r, contentType) + if err != nil { + return hookCommon.TransformResultModel{Error: err} } - - var readyState bitriseapi.PullRequestReadyState - if issue.Draft { - readyState = bitriseapi.PullRequestReadyStateDraft - } else { - readyState = bitriseapi.PullRequestReadyStateReadyForReview - } - - var labels []string - for _, label := range issue.Labels { - labels = append(labels, label.Name) - } - - result := bitriseapi.TriggerAPIParamsModel{ - BuildParams: bitriseapi.BuildParamsModel{ - CommitMessage: commitMsg, - BranchDestRepoOwner: eventModel.Repo.Owner.Login, - PullRequestID: &issue.PullRequestID, - HeadRepositoryURL: eventModel.Repo.getRepositoryURL(), - PullRequestRepositoryURL: eventModel.Repo.getRepositoryURL(), - PullRequestAuthor: issue.User.Login, - PullRequestHeadBranch: headRefBuildParam, - PullRequestUnverifiedMergeBranch: unverifiedMergeRefBuildParam, - DiffURL: pullRequest.DiffURL, - Environments: buildEnvs, - PullRequestReadyState: readyState, - PullRequestLabels: labels, - PullRequestComment: eventModel.Comment.Body, - PullRequestCommentID: strconv.FormatInt(eventModel.Comment.ID, 10), - }, - TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, eventModel.Sender.Login), - } - - return hookCommon.TransformResultModel{ - TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ - result, - }, - SkippedByPrDescription: !hookCommon.IsSkipBuildByCommitMessage(issue.Title) && - hookCommon.IsSkipBuildByCommitMessage(issue.Body), - } + + return transformPushEvent(*eventModel) + } else if ghEvent == "pull_request" { + eventModel, err := decodeEventPayload[PullRequestEventModel](r, contentType) + if err != nil { + return hookCommon.TransformResultModel{Error: err} } - - func detectContentTypeAndEventID(header http.Header) (string, string, error) { - contentType := header.Get("Content-Type") - if contentType == "" { - return "", "", errors.New("No Content-Type Header found") - } - - ghEvent := header.Get("X-Github-Event") - if ghEvent == "" { - return "", "", errors.New("No X-Github-Event Header found") - } - - return contentType, ghEvent, nil + + return transformPullRequestEvent(*eventModel) + } else if ghEvent == "issue_comment" { + eventModel, err := decodeEventPayload[IssueCommentEventModel](r, contentType) + if err != nil { + return hookCommon.TransformResultModel{Error: err} } - - // TransformRequest ... - func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformResultModel { - contentType, ghEvent, err := detectContentTypeAndEventID(r.Header) - if err != nil { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Issue with Headers: %s", err), - } - } - - if contentType != hookCommon.ContentTypeApplicationJSON && contentType != hookCommon.ContentTypeApplicationXWWWFormURLEncoded { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Content-Type is not supported: %s", contentType), - } - } - - if ghEvent == "ping" { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("ping event received"), - ShouldSkip: true, - } - } - if ghEvent != "push" && ghEvent != "pull_request" && ghEvent != "issue_comment" { - // Unsupported GitHub Event - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("unsupported GitHub Webhook event: %s", ghEvent), - } - } - - if r.Body == nil { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("failed to read content of request body: no or empty request body"), - } - } - - if ghEvent == "push" { - eventModel, err := decodeEventPayload[PushEventModel](r, contentType) - if err != nil { - return hookCommon.TransformResultModel{Error: err} - } - - return transformPushEvent(*eventModel) - } else if ghEvent == "pull_request" { - eventModel, err := decodeEventPayload[PullRequestEventModel](r, contentType) - if err != nil { - return hookCommon.TransformResultModel{Error: err} - } - - return transformPullRequestEvent(*eventModel) - } else if ghEvent == "issue_comment" { - eventModel, err := decodeEventPayload[IssueCommentEventModel](r, contentType) - if err != nil { - return hookCommon.TransformResultModel{Error: err} - } - - return transformIssueCommentEvent(*eventModel) - } - - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Unsupported GitHub event type: %s", ghEvent), - } - } - - func decodeEventPayload[T interface{}](r *http.Request, contentType string) (*T, error) { - var eventModel T - if contentType == hookCommon.ContentTypeApplicationJSON { - if err := json.NewDecoder(r.Body).Decode(&eventModel); err != nil { - return nil, fmt.Errorf("Failed to parse request body as JSON: %s", err) - } - } else if contentType == hookCommon.ContentTypeApplicationXWWWFormURLEncoded { - payloadValue := r.PostFormValue("payload") - if payloadValue == "" { - return nil, fmt.Errorf("failed to parse request body: empty payload") - } - if err := json.NewDecoder(strings.NewReader(payloadValue)).Decode(&eventModel); err != nil { - return nil, fmt.Errorf("Failed to parse payload: %s", err) - } - } else { - return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) - } - - return &eventModel, nil - } - - func (branchInfoModel BranchInfoModel) getRepositoryURL() string { - return branchInfoModel.Repo.getRepositoryURL() - } - - func (repoInfoModel RepoInfoModel) getRepositoryURL() string { - if repoInfoModel.Private { - return repoInfoModel.SSHURL - } - return repoInfoModel.CloneURL - } - \ No newline at end of file + + return transformIssueCommentEvent(*eventModel) + } + + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("Unsupported GitHub event type: %s", ghEvent), + } +} + +func decodeEventPayload[T interface{}](r *http.Request, contentType string) (*T, error) { + var eventModel T + if contentType == hookCommon.ContentTypeApplicationJSON { + if err := json.NewDecoder(r.Body).Decode(&eventModel); err != nil { + return nil, fmt.Errorf("Failed to parse request body as JSON: %s", err) + } + } else if contentType == hookCommon.ContentTypeApplicationXWWWFormURLEncoded { + payloadValue := r.PostFormValue("payload") + if payloadValue == "" { + return nil, fmt.Errorf("failed to parse request body: empty payload") + } + if err := json.NewDecoder(strings.NewReader(payloadValue)).Decode(&eventModel); err != nil { + return nil, fmt.Errorf("Failed to parse payload: %s", err) + } + } else { + return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) + } + + return &eventModel, nil +} + +func (branchInfoModel BranchInfoModel) getRepositoryURL() string { + return branchInfoModel.Repo.getRepositoryURL() +} + +func (repoInfoModel RepoInfoModel) getRepositoryURL() string { + if repoInfoModel.Private { + return repoInfoModel.SSHURL + } + return repoInfoModel.CloneURL +} diff --git a/service/hook/github/github_test.go b/service/hook/github/github_test.go index 6c9650f6..08a72144 100644 --- a/service/hook/github/github_test.go +++ b/service/hook/github/github_test.go @@ -667,7 +667,62 @@ const ( "id": 517812 } }` -) + sampleMergeQueuePushData = `{ + "ref": "refs/heads/gh-readonly-queue/main/pr-1-7ed40c455464eaa0c5c4a0aeaefb9ffb16bd2c64", + "before": "0000000000000000000000000000000000000000", + "after": "cc76bc3a5ffd4836ca30d0eeb224967b7127ab50", + "repository": { + "name": "birmacher-test", + "full_name": "bitrise-io/birmacher-test", + "private": true, + "html_url": "https://github.com/bitrise-io/birmacher-test", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/bitrise-io/birmacher-test", + "ssh_url": "git@github.com:bitrise-io/birmacher-test.git", + "clone_url": "https://github.com/bitrise-io/birmacher-test.git", + }, + "pusher": { + "name": "github-merge-queue[bot]", + "email": null + }, + "sender": { + }, + "created": true, + "deleted": false, + "forced": false, + "base_ref": "refs/heads/main", + "compare": "https://github.com/bitrise-io/birmacher-test/compare/gh-readonly-queue/main/pr-1-7ed40c455464eaa0c5c4a0aeaefb9ffb16bd2c64", + "commits": [ + ], + "head_commit": { + "id": "cc76bc3a5ffd4836ca30d0eeb224967b7127ab50", + "tree_id": "ca78a46cdb752ae92599844f4fe30983eacc27de", + "distinct": true, + "message": "Merge pull request #1 from bitrise-io/birmacher-patch-1\n\nUpdate README.md", + "timestamp": "2025-05-12T16:04:25Z", + "url": "https://github.com/bitrise-io/birmacher-test/commit/cc76bc3a5ffd4836ca30d0eeb224967b7127ab50", + "author": { + "name": "Barnabas Birmacher", + "email": "birmacher@gmail.com", + "username": "birmacher" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "username": "web-flow" + }, + "added": [ + + ], + "removed": [ + + ], + "modified": [ + "README.md" + ] + } +}` var boolFalse = false var boolTrue = true From 57c591126e04e43315c51b01c4dcffbc77976b29 Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Tue, 13 May 2025 11:26:03 +0200 Subject: [PATCH 5/9] fix --- service/hook/github/github_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/service/hook/github/github_test.go b/service/hook/github/github_test.go index 08a72144..f937d802 100644 --- a/service/hook/github/github_test.go +++ b/service/hook/github/github_test.go @@ -667,6 +667,7 @@ const ( "id": 517812 } }` + sampleMergeQueuePushData = `{ "ref": "refs/heads/gh-readonly-queue/main/pr-1-7ed40c455464eaa0c5c4a0aeaefb9ffb16bd2c64", "before": "0000000000000000000000000000000000000000", @@ -723,6 +724,7 @@ const ( ] } }` +) var boolFalse = false var boolTrue = true From 7a2fdb1ad8e56e8cf70423e2d32fb83fa8d5f050 Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Tue, 13 May 2025 11:28:48 +0200 Subject: [PATCH 6/9] ioutil.NopCloser changed to io.NopCloser --- service/hook/github/github_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/service/hook/github/github_test.go b/service/hook/github/github_test.go index f937d802..01acac95 100644 --- a/service/hook/github/github_test.go +++ b/service/hook/github/github_test.go @@ -1,7 +1,7 @@ package github import ( - "io/ioutil" + "io" "net/http" "strings" "testing" @@ -1892,7 +1892,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"push"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(sampleCodePushData)), + Body: io.NopCloser(strings.NewReader(sampleCodePushData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) @@ -1936,7 +1936,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"push"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(sampleTagPushData)), + Body: io.NopCloser(strings.NewReader(sampleTagPushData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) @@ -1970,7 +1970,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"pull_request"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(samplePullRequestData)), + Body: io.NopCloser(strings.NewReader(samplePullRequestData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) @@ -2010,7 +2010,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"pull_request"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(sampleDraftPullRequestData)), + Body: io.NopCloser(strings.NewReader(sampleDraftPullRequestData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) @@ -2056,7 +2056,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"pull_request"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(samplePullRequestEditedData)), + Body: io.NopCloser(strings.NewReader(samplePullRequestEditedData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) @@ -2092,7 +2092,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"pull_request"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(samplePullRequestLabeledData)), + Body: io.NopCloser(strings.NewReader(samplePullRequestLabeledData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) @@ -2130,7 +2130,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"issue_comment"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(sampleIssueCommentCreatedData)), + Body: io.NopCloser(strings.NewReader(sampleIssueCommentCreatedData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) @@ -2166,7 +2166,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { "X-Github-Event": {"issue_comment"}, "Content-Type": {"application/json"}, }, - Body: ioutil.NopCloser(strings.NewReader(sampleIssueCommentEditedData)), + Body: io.NopCloser(strings.NewReader(sampleIssueCommentEditedData)), } hookTransformResult := provider.TransformRequest(&request) require.NoError(t, hookTransformResult.Error) From d558458533d5bd5159144f9536353a22e47384f6 Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Tue, 13 May 2025 14:33:07 +0200 Subject: [PATCH 7/9] MergeQueue added to common push handler and tests --- bitriseapi/bitriseapi.go | 11 +++++ service/hook/github/github.go | 71 +++++++++++++++--------------- service/hook/github/github_test.go | 43 +++++++++++++++++- 3 files changed, 88 insertions(+), 37 deletions(-) diff --git a/bitriseapi/bitriseapi.go b/bitriseapi/bitriseapi.go index 4da4e98b..222ba40f 100644 --- a/bitriseapi/bitriseapi.go +++ b/bitriseapi/bitriseapi.go @@ -41,6 +41,15 @@ const ( PullRequestReadyStateConvertedToReadyForReview PullRequestReadyState = "converted_to_ready_for_review" ) +// MergeQueue ... +type MergeQueueModel struct { + QueueProvider string `json:"queue_provider"` + PullRequestID int `json:"pull_request_id"` + BaseBranch string `json:"base_branch"` + BaseSHA string `json:"base_sha"` + SyntheticSHA string `json:"synthetic_sha"` +} + // BuildParamsModel ... type BuildParamsModel struct { // git commit hash @@ -97,6 +106,8 @@ type BuildParamsModel struct { PullRequestComment string `json:"pull_request_comment,omitempty"` // newly added pull request comment's ID PullRequestCommentID string `json:"pull_request_comment_id,omitempty"` + // merge queue + MergeQueue MergeQueueModel `json:"merge_queue,omitempty"` } // TriggerAPIParamsModel ... diff --git a/service/hook/github/github.go b/service/hook/github/github.go index 8f1389d3..ac72ec7b 100644 --- a/service/hook/github/github.go +++ b/service/hook/github/github.go @@ -31,26 +31,15 @@ type CommitModel struct { CommitMessage string `json:"message"` } -// MergeQueueMeta ... -type MergeQueuePushModel struct { - QueueProvider string `json:"queue_provider,omitempty"` - PRNumber int `json:"pr_number,omitempty"` - BaseBranch string `json:"base_branch,omitempty"` - BaseSHA string `json:"base_sha,omitempty"` - SyntheticSHA string `json:"synthetic_sha,omitempty"` -} - // PushEventModel ... type PushEventModel struct { - Ref string `json:"ref"` - Deleted bool `json:"deleted"` - HeadCommit CommitModel `json:"head_commit"` - Commits []CommitModel `json:"commits"` - Repo RepoInfoModel `json:"repository"` - Pusher PusherModel `json:"pusher"` - IsMergeQueuePush bool `json:"is_merge_queue_push"` - After string `json:"after"` - MergeQueue *MergeQueuePushModel `json:"merge_queue"` + Ref string `json:"ref"` + Deleted bool `json:"deleted"` + HeadCommit CommitModel `json:"head_commit"` + Commits []CommitModel `json:"commits"` + Repo RepoInfoModel `json:"repository"` + Pusher PusherModel `json:"pusher"` + After string `json:"after"` } // UserModel ... @@ -218,28 +207,37 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode commitMessages = append(commitMessages, commit.CommitMessage) } - if strings.HasPrefix(pushEvent.Ref, "refs/heads/gh-readonly-queue/") || strings.HasPrefix(pushEvent.Ref, "refs/heads/gt-queue/") { - // merge queue push - provider, base, pr, sha, err := parseMergeQueueRef(pushEvent.Ref) - if err != nil { - return hookCommon.TransformResultModel{ - Error: fmt.Errorf("failed to parse merge queue ref: %w", err), - } - } - pushEvent.IsMergeQueuePush = true - pushEvent.MergeQueue = &MergeQueuePushModel{ - QueueProvider: provider, - PRNumber: pr, - BaseBranch: base, - BaseSHA: sha, - SyntheticSHA: pushEvent.After, - } - } - if strings.HasPrefix(pushEvent.Ref, "refs/heads/") { // code push branch := strings.TrimPrefix(pushEvent.Ref, "refs/heads/") + // merge queue push + var mergeQueue bitriseapi.MergeQueueModel + if strings.HasPrefix(pushEvent.Ref, "refs/heads/gh-readonly-queue/") || strings.HasPrefix(pushEvent.Ref, "refs/heads/gt-queue/") { + providerRef, base, pr, sha, err := parseMergeQueueRef(pushEvent.Ref) + if err != nil { + return hookCommon.TransformResultModel{ + Error: fmt.Errorf("failed to parse merge queue ref: %w", err), + } + } + + var provider string + switch providerRef { + case "gh-readonly-queue": + provider = "github" + case "gt-queue": + provider = "gitlab" + } + + mergeQueue = bitriseapi.MergeQueueModel{ + QueueProvider: provider, + PullRequestID: pr, + BaseBranch: base, + BaseSHA: sha, + SyntheticSHA: pushEvent.After, + } + } + return hookCommon.TransformResultModel{ TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ { @@ -250,6 +248,7 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode CommitMessages: commitMessages, PushCommitPaths: commitPaths, BaseRepositoryURL: pushEvent.Repo.getRepositoryURL(), + MergeQueue: mergeQueue, }, TriggeredBy: hookCommon.GenerateTriggeredBy(ProviderID, pushEvent.Pusher.Name), }, diff --git a/service/hook/github/github_test.go b/service/hook/github/github_test.go index 01acac95..9ca40d64 100644 --- a/service/hook/github/github_test.go +++ b/service/hook/github/github_test.go @@ -681,7 +681,7 @@ const ( "fork": false, "url": "https://api.github.com/repos/bitrise-io/birmacher-test", "ssh_url": "git@github.com:bitrise-io/birmacher-test.git", - "clone_url": "https://github.com/bitrise-io/birmacher-test.git", + "clone_url": "https://github.com/bitrise-io/birmacher-test.git" }, "pusher": { "name": "github-merge-queue[bot]", @@ -1929,6 +1929,47 @@ func Test_HookProvider_TransformRequest(t *testing.T) { require.Equal(t, false, hookTransformResult.DontWaitForTriggerResponse) } + t.Log("Merge Queue Push - should be handled") + { + request := http.Request{ + Header: http.Header{ + "X-Github-Event": {"push"}, + "Content-Type": {"application/json"}, + }, + Body: io.NopCloser(strings.NewReader(sampleMergeQueuePushData)), + } + hookTransformResult := provider.TransformRequest(&request) + require.NoError(t, hookTransformResult.Error) + require.False(t, hookTransformResult.ShouldSkip) + require.Equal(t, []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + CommitHash: "cc76bc3a5ffd4836ca30d0eeb224967b7127ab50", + CommitMessage: "Merge pull request #1 from bitrise-io/birmacher-patch-1\n\nUpdate README.md", + CommitMessages: []string{"Merge pull request #1 from bitrise-io/birmacher-patch-1\n\nUpdate README.md"}, + Branch: "gh-readonly-queue/main/pr-1-7ed40c455464eaa0c5c4a0aeaefb9ffb16bd2c64", + PushCommitPaths: []bitriseapi.CommitPaths{ + { + Added: []string{}, + Removed: []string{}, + Modified: []string{"README.md"}, + }, + }, + BaseRepositoryURL: "git@github.com:bitrise-io/birmacher-test.git", + MergeQueue: bitriseapi.MergeQueueModel{ + QueueProvider: "github", + PullRequestID: 1, + BaseBranch: "main", + BaseSHA: "7ed40c455464eaa0c5c4a0aeaefb9ffb16bd2c64", + SyntheticSHA: "cc76bc3a5ffd4836ca30d0eeb224967b7127ab50", + }, + }, + TriggeredBy: "webhook-github/github-merge-queue[bot]", + }, + }, hookTransformResult.TriggerAPIParams) + require.Equal(t, false, hookTransformResult.DontWaitForTriggerResponse) + } + t.Log("Tag Push - should be handled") { request := http.Request{ From 8ef14bdda9b1a0e0d70ad2da0599e48ab19de553 Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Tue, 13 May 2025 14:40:09 +0200 Subject: [PATCH 8/9] styling fixes --- service/hook/github/github.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/service/hook/github/github.go b/service/hook/github/github.go index ac72ec7b..8788b68b 100644 --- a/service/hook/github/github.go +++ b/service/hook/github/github.go @@ -344,7 +344,7 @@ func transformPullRequestEvent(pullRequest PullRequestEventModel) hookCommon.Tra if mergeRefUpToDate { mergeRefBuildParam = unverifiedMergeRefBuildParam } - if mergeRefUpToDate && *pullRequest.PullRequestInfo.Mergeable == false { + if mergeRefUpToDate && !*pullRequest.PullRequestInfo.Mergeable { return hookCommon.TransformResultModel{ Error: errors.New("pull Request is not mergeable"), ShouldSkip: true, @@ -527,12 +527,12 @@ func transformIssueCommentEvent(eventModel IssueCommentEventModel) hookCommon.Tr func detectContentTypeAndEventID(header http.Header) (string, string, error) { contentType := header.Get("Content-Type") if contentType == "" { - return "", "", errors.New("No Content-Type Header found") + return "", "", errors.New("no Content-Type Header found") } ghEvent := header.Get("X-Github-Event") if ghEvent == "" { - return "", "", errors.New("No X-Github-Event Header found") + return "", "", errors.New("no X-Github-Event Header found") } return contentType, ghEvent, nil @@ -543,7 +543,7 @@ func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformRes contentType, ghEvent, err := detectContentTypeAndEventID(r.Header) if err != nil { return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Issue with Headers: %s", err), + Error: fmt.Errorf("issue with Headers: %s", err), } } @@ -596,7 +596,7 @@ func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformRes } return hookCommon.TransformResultModel{ - Error: fmt.Errorf("Unsupported GitHub event type: %s", ghEvent), + Error: fmt.Errorf("unsupported GitHub event type: %s", ghEvent), } } @@ -604,7 +604,7 @@ func decodeEventPayload[T interface{}](r *http.Request, contentType string) (*T, var eventModel T if contentType == hookCommon.ContentTypeApplicationJSON { if err := json.NewDecoder(r.Body).Decode(&eventModel); err != nil { - return nil, fmt.Errorf("Failed to parse request body as JSON: %s", err) + return nil, fmt.Errorf("failed to parse request body as JSON: %s", err) } } else if contentType == hookCommon.ContentTypeApplicationXWWWFormURLEncoded { payloadValue := r.PostFormValue("payload") @@ -612,10 +612,10 @@ func decodeEventPayload[T interface{}](r *http.Request, contentType string) (*T, return nil, fmt.Errorf("failed to parse request body: empty payload") } if err := json.NewDecoder(strings.NewReader(payloadValue)).Decode(&eventModel); err != nil { - return nil, fmt.Errorf("Failed to parse payload: %s", err) + return nil, fmt.Errorf("failed to parse payload: %s", err) } } else { - return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) + return nil, fmt.Errorf("unsupported Content-Type: %s", contentType) } return &eventModel, nil From e0e1248e8da5ead1660af576f8112a25c3223bed Mon Sep 17 00:00:00 2001 From: Barnabas Birmacher Date: Tue, 13 May 2025 14:41:29 +0200 Subject: [PATCH 9/9] test fixes --- service/hook/github/github_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/hook/github/github_test.go b/service/hook/github/github_test.go index 9ca40d64..2bbe3622 100644 --- a/service/hook/github/github_test.go +++ b/service/hook/github/github_test.go @@ -799,7 +799,7 @@ func Test_detectContentTypeAndEventID(t *testing.T) { "Content-Type": {"application/json"}, } contentType, ghEvent, err := detectContentTypeAndEventID(header) - require.EqualError(t, err, "No X-Github-Event Header found") + require.EqualError(t, err, "no X-Github-Event Header found") require.Equal(t, "", contentType) require.Equal(t, "", ghEvent) } @@ -810,7 +810,7 @@ func Test_detectContentTypeAndEventID(t *testing.T) { "X-Github-Event": {"push"}, } contentType, ghEvent, err := detectContentTypeAndEventID(header) - require.EqualError(t, err, "No Content-Type Header found") + require.EqualError(t, err, "no Content-Type Header found") require.Equal(t, "", contentType) require.Equal(t, "", ghEvent) }