Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bitriseapi/bitriseapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ...
Expand Down
63 changes: 55 additions & 8 deletions service/hook/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -38,6 +39,7 @@ type PushEventModel struct {
Commits []CommitModel `json:"commits"`
Repo RepoInfoModel `json:"repository"`
Pusher PusherModel `json:"pusher"`
After string `json:"after"`
}

// UserModel ...
Expand Down Expand Up @@ -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{
{
Expand All @@ -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),
},
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -549,26 +596,26 @@ 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),
}
}

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)
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)
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
Expand Down
120 changes: 109 additions & 11 deletions service/hook/github/github_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package github

import (
"io/ioutil"
"io"
"net/http"
"strings"
"testing"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1872,14 +1929,55 @@ 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{
Header: http.Header{
"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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down