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 dee29c7e..8788b68b 100644 --- a/service/hook/github/github.go +++ b/service/hook/github/github.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "slices" "strconv" "strings" @@ -38,6 +39,7 @@ type PushEventModel struct { Commits []CommitModel `json:"commits"` Repo RepoInfoModel `json:"repository"` Pusher PusherModel `json:"pusher"` + After string `json:"after"` } // UserModel ... @@ -209,6 +211,33 @@ func transformPushEvent(pushEvent PushEventModel) hookCommon.TransformResultMode // 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{ { @@ -219,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), }, @@ -248,6 +278,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|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) } @@ -297,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, @@ -480,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 @@ -496,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), } } @@ -549,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), } } @@ -557,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") @@ -565,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 diff --git a/service/hook/github/github_test.go b/service/hook/github/github_test.go index 6c9650f6..2bbe3622 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" @@ -667,6 +667,63 @@ 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 @@ -742,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) } @@ -753,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) } @@ -1835,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) @@ -1872,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{ @@ -1879,7 +1977,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) @@ -1913,7 +2011,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) @@ -1953,7 +2051,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) @@ -1999,7 +2097,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) @@ -2035,7 +2133,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) @@ -2073,7 +2171,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) @@ -2109,7 +2207,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)