action/command/vcs_user_test.go
Rama main · réplica de lectura del repositorio original.
package command
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"github.com/bytebase/bytebase/action/bitbucket"
"github.com/bytebase/bytebase/action/world"
v1pb "github.com/bytebase/bytebase/backend/generated-go/v1"
)
func TestGetVCSUserFromGitHubPullRequest(t *testing.T) {
eventPath := filepath.Join(t.TempDir(), "event.json")
require.NoError(t, os.WriteFile(eventPath, []byte(`{"pull_request":{"user":{"id":1001,"login":"alice","name":"Alice","type":"User"}}}`), 0600))
t.Setenv("GITHUB_EVENT_NAME", "pull_request")
t.Setenv("GITHUB_EVENT_PATH", eventPath)
user := getVCSUser(world.GitHub)
require.NotNil(t, user)
require.Equal(t, v1pb.VCSType_GITHUB, user.VcsType)
require.Equal(t, "1001", user.UserId)
require.Equal(t, "alice", user.UserName)
require.Equal(t, "Alice", user.DisplayName)
}
func TestGetVCSUserSkipsGitHubBot(t *testing.T) {
eventPath := filepath.Join(t.TempDir(), "event.json")
require.NoError(t, os.WriteFile(eventPath, []byte(`{"pull_request":{"user":{"id":41898282,"login":"github-actions[bot]","type":"Bot"}}}`), 0600))
t.Setenv("GITHUB_EVENT_NAME", "pull_request")
t.Setenv("GITHUB_EVENT_PATH", eventPath)
require.Nil(t, getVCSUser(world.GitHub))
}
func TestGetVCSUserFromGitHubPullRequestTarget(t *testing.T) {
eventPath := filepath.Join(t.TempDir(), "event.json")
require.NoError(t, os.WriteFile(eventPath, []byte(`{"pull_request":{"user":{"id":1001,"login":"alice","name":"Alice","type":"User"}}}`), 0600))
t.Setenv("GITHUB_EVENT_NAME", "pull_request_target")
t.Setenv("GITHUB_EVENT_PATH", eventPath)
user := getVCSUser(world.GitHub)
require.NotNil(t, user)
require.Equal(t, v1pb.VCSType_GITHUB, user.VcsType)
require.Equal(t, "1001", user.UserId)
}
func TestGetVCSUserSkipsGitLabWhenAuthorUnavailable(t *testing.T) {
t.Setenv("CI_PIPELINE_SOURCE", "merge_request_event")
t.Setenv("GITLAB_USER_ID", "2002")
t.Setenv("GITLAB_USER_LOGIN", "bob")
t.Setenv("GITLAB_USER_NAME", "Bob")
require.Nil(t, getVCSUser(world.GitLab))
}
func TestGetVCSUserFromGitLabMergeRequestAuthor(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/api/v4/projects/987/merge_requests/42", r.URL.Path)
require.Equal(t, "job-token", r.Header.Get("JOB-TOKEN"))
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"author":{"id":3003,"username":"alice","name":"Alice"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("CI_MERGE_REQUEST_PROJECT_ID", "987")
t.Setenv("CI_MERGE_REQUEST_IID", "42")
t.Setenv("CI_API_V4_URL", server.URL+"/api/v4")
t.Setenv("CI_JOB_TOKEN", "job-token")
t.Setenv("GITLAB_USER_ID", "2002")
t.Setenv("GITLAB_USER_LOGIN", "triggerer")
t.Setenv("GITLAB_USER_NAME", "Pipeline Triggerer")
user := getVCSUser(world.GitLab)
require.NotNil(t, user)
require.Equal(t, v1pb.VCSType_GITLAB, user.VcsType)
require.Equal(t, "3003", user.UserId)
require.Equal(t, "alice", user.UserName)
require.Equal(t, "Alice", user.DisplayName)
}
func TestGetVCSUserSkipsGitLabBotAuthor(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"author":{"id":4004,"username":"project_987_bot_a1b2c3","name":"Project bot"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("CI_MERGE_REQUEST_PROJECT_ID", "987")
t.Setenv("CI_MERGE_REQUEST_IID", "42")
t.Setenv("CI_API_V4_URL", server.URL+"/api/v4")
t.Setenv("CI_JOB_TOKEN", "job-token")
require.Nil(t, getVCSUser(world.GitLab))
}
func TestGetVCSUserSkipsBitbucketWhenAuthorUnavailable(t *testing.T) {
t.Setenv("BITBUCKET_PR_ID", "10")
t.Setenv("BITBUCKET_STEP_TRIGGERER_UUID", "{3003}")
t.Setenv("BITBUCKET_STEP_TRIGGERER_USERNAME", "carol")
require.Nil(t, getVCSUser(world.Bitbucket))
}
func TestGetVCSUserFromBitbucketPullRequestAuthor(t *testing.T) {
var gotPath string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "application/json", r.Header.Get("Accept"))
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"author":{"account_id":"557058:author-account","uuid":"{author-uuid}","nickname":"alice","display_name":"Alice","type":"user"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("BITBUCKET_PR_ID", "10")
t.Setenv("BITBUCKET_REPO_FULL_NAME", "bytebase/example")
t.Setenv("BITBUCKET_STEP_TRIGGERER_UUID", "{triggerer-uuid}")
t.Setenv("BITBUCKET_STEP_TRIGGERER_USERNAME", "triggerer")
t.Setenv("BYTEBASE_BITBUCKET_API_BASE_URL", server.URL)
user := getVCSUser(world.Bitbucket)
require.NotNil(t, user)
require.Equal(t, "/repositories/bytebase/example/pullrequests/10", gotPath)
require.Equal(t, v1pb.VCSType_BITBUCKET, user.VcsType)
require.Equal(t, "557058:author-account", user.UserId)
require.Equal(t, "alice", user.UserName)
require.Equal(t, "Alice", user.DisplayName)
}
func TestGetVCSUserFromAzureDevOpsPullRequestCreator(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "/collection/_apis/git/pullrequests/42", r.URL.Path)
require.Equal(t, "7.1", r.URL.Query().Get("api-version"))
require.Equal(t, "Bearer access-token", r.Header.Get("Authorization"))
require.Equal(t, "application/json", r.Header.Get("Accept"))
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"createdBy":{"id":"author-id","uniqueName":"alice@example.com","displayName":"Alice"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", server.URL+"/collection/")
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
user := getVCSUser(world.AzureDevOps)
require.NotNil(t, user)
require.Equal(t, v1pb.VCSType_AZURE_DEVOPS, user.VcsType)
require.Equal(t, "author-id", user.UserId)
require.Equal(t, "alice@example.com", user.UserName)
require.Equal(t, "Alice", user.DisplayName)
}
func TestGetVCSUserFromAzureDevOpsPullRequestCreatorWithTeamFoundationCollectionURI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/collection/_apis/git/pullrequests/42", r.URL.Path)
_, err := w.Write([]byte(`{"createdBy":{"id":"author-id","uniqueName":"alice@example.com","displayName":"Alice"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", server.URL+"/collection/")
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
require.NotNil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsWhenPullRequestMetadataUnavailable(t *testing.T) {
t.Setenv("SYSTEM_COLLECTIONURI", "https://dev.azure.com/bytebase/")
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
t.Setenv("BUILD_REQUESTEDFORID", "requester-id")
t.Setenv("BUILD_REQUESTEDFOR", "Requester")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsWhenAccessTokenUnavailable(t *testing.T) {
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", "https://dev.azure.com/bytebase/")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsServicePrincipalCreator(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`{"createdBy":{"id":"bot-id","uniqueName":"build-service","displayName":"Build Service","descriptor":"aadsp.service-principal"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", server.URL)
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsBuildServiceCreator(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`{"createdBy":{"id":"bot-id","uniqueName":"build-service","displayName":"Build Service","descriptor":"svc.build-service"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", server.URL)
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsBotCreator(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`{"createdBy":{"id":"bot-id","uniqueName":"release[bot]","displayName":"Release Bot"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", server.URL)
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsCreatorWithoutID(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`{"createdBy":{"uniqueName":"alice@example.com","displayName":"Alice"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", server.URL)
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsInvalidPullRequestResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte(`invalid`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", server.URL)
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestGetVCSUserSkipsAzureDevOpsNonOKPullRequestResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
t.Cleanup(server.Close)
t.Setenv("SYSTEM_PULLREQUEST_PULLREQUESTID", "42")
t.Setenv("SYSTEM_COLLECTIONURI", server.URL)
t.Setenv("SYSTEM_ACCESSTOKEN", "access-token")
require.Nil(t, getVCSUser(world.AzureDevOps))
}
func TestBitbucketDefaultAPIUsesPipelinesProxy(t *testing.T) {
requestURL, err := buildBitbucketPullRequestURL(getBitbucketAPIBaseURL(), "bytebase", "example", "10")
require.NoError(t, err)
require.Equal(t, "http://api.bitbucket.org/2.0/repositories/bytebase/example/pullrequests/10", requestURL)
client := bitbucket.NewHTTPClient(getBitbucketAPIBaseURL())
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
proxyURL, err := transport.Proxy(&http.Request{URL: mustParseURL(t, requestURL)})
require.NoError(t, err)
require.Equal(t, "http://localhost:29418", proxyURL.String())
}
func TestGetVCSUserSkipsBitbucketBotAuthor(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"author":{"account_id":"557058:bot-account","nickname":"release[bot]","type":"bot"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("BITBUCKET_PR_ID", "10")
t.Setenv("BITBUCKET_REPO_FULL_NAME", "bytebase/example")
t.Setenv("BYTEBASE_BITBUCKET_API_BASE_URL", server.URL)
require.Nil(t, getVCSUser(world.Bitbucket))
}
func TestGetVCSUserSkipsBitbucketAppUserAuthor(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(`{"author":{"account_id":"557058:app-account","nickname":"release-app","type":"app_user"}}`))
require.NoError(t, err)
}))
t.Cleanup(server.Close)
t.Setenv("BITBUCKET_PR_ID", "10")
t.Setenv("BITBUCKET_REPO_FULL_NAME", "bytebase/example")
t.Setenv("BYTEBASE_BITBUCKET_API_BASE_URL", server.URL)
require.Nil(t, getVCSUser(world.Bitbucket))
}
func TestGetVCSUserSkipsUnsupportedPlatform(t *testing.T) {
require.Nil(t, getVCSUser(world.LocalPlatform))
}
func mustParseURL(t *testing.T, rawURL string) *url.URL {
t.Helper()
parsedURL, err := url.Parse(rawURL)
require.NoError(t, err)
return parsedURL
}
func TestGetVCSSource(t *testing.T) {
githubPush := map[string]string{
"GITHUB_SERVER_URL": "https://github.com",
"GITHUB_REPOSITORY": "example/app",
"GITHUB_SHA": "0a1b2c3d",
"GITHUB_EVENT_NAME": "push",
"GITHUB_REF_NAME": "main",
"GITHUB_REF_TYPE": "branch",
githubEventPayload: "",
}
with := func(base map[string]string, extra map[string]string) map[string]string {
out := map[string]string{}
for k, v := range base {
out[k] = v
}
for k, v := range extra {
out[k] = v
}
return out
}
tests := []struct {
name string
platform world.JobPlatform
env map[string]string
flags func(w *world.World)
want *v1pb.Release_VCSSource
}{
{
name: "GitHub pull request run uses the source branch and the PR",
platform: world.GitHub,
env: with(githubPush, map[string]string{
"GITHUB_EVENT_NAME": "pull_request",
"GITHUB_REF_NAME": "42/merge",
"GITHUB_HEAD_REF": "feature/orders",
githubEventPayload: `{"pull_request":{"number":42,"title":"Add the orders table","html_url":"https://github.com/example/app/pull/42","user":{"login":"alexdoe"}}}`,
}),
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/0a1b2c3d",
CommitSha: "0a1b2c3d", Branch: "feature/orders", AuthorLogin: "alexdoe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://github.com/example/app/pull/42", Number: 42, Title: "Add the orders table"},
},
},
{
name: "GitHub squash-merge push takes the PR number from the title once",
platform: world.GitHub,
env: with(githubPush, map[string]string{
githubEventPayload: `{"head_commit":{"message":"Add the orders table (#42)\n\n* first commit","author":{"name":"Alex Doe","username":"alexdoe"}}}`,
}),
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/0a1b2c3d",
CommitSha: "0a1b2c3d", Branch: "main", CommitTitle: "Add the orders table",
AuthorName: "Alex Doe", AuthorLogin: "alexdoe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://github.com/example/app/pull/42", Number: 42},
},
},
{
name: "GitHub merge-commit push takes the PR title from the message body",
platform: world.GitHub,
env: with(githubPush, map[string]string{
githubEventPayload: `{"head_commit":{"message":"Merge pull request #42 from example/feature/orders\n\nAdd the orders table","author":{"name":"Alex Doe","username":"alexdoe"}}}`,
}),
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/0a1b2c3d",
CommitSha: "0a1b2c3d", Branch: "main", CommitTitle: "Merge pull request #42 from example/feature/orders",
AuthorName: "Alex Doe", AuthorLogin: "alexdoe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://github.com/example/app/pull/42", Number: 42, Title: "Add the orders table"},
},
},
{
name: "GitHub rebase push has a commit title and no PR",
platform: world.GitHub,
env: with(githubPush, map[string]string{
githubEventPayload: `{"head_commit":{"message":"Add the orders index\n\nbody","author":{"name":"Alex Doe","username":"alexdoe"}}}`,
}),
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/0a1b2c3d",
CommitSha: "0a1b2c3d", Branch: "main", CommitTitle: "Add the orders index",
AuthorName: "Alex Doe", AuthorLogin: "alexdoe",
},
},
{
name: "GitHub tag push has no branch",
platform: world.GitHub,
env: with(githubPush, map[string]string{
"GITHUB_REF_NAME": "v1.2.0",
"GITHUB_REF_TYPE": "tag",
}),
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/0a1b2c3d", CommitSha: "0a1b2c3d",
},
},
{
name: "GitHub merge queue run has no branch",
platform: world.GitHub,
env: with(githubPush, map[string]string{
"GITHUB_EVENT_NAME": "merge_group",
"GITHUB_REF_NAME": "gh-readonly-queue/main/pr-42-0a1b2c3d",
}),
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/0a1b2c3d", CommitSha: "0a1b2c3d",
},
},
{
name: "GitLab tag pipeline has no branch",
platform: world.GitLab,
env: map[string]string{
"CI_PROJECT_URL": "https://gitlab.example.com/group/app",
"CI_COMMIT_SHA": "9f8e7d6c",
"CI_COMMIT_REF_NAME": "v1.2.0",
"CI_COMMIT_TITLE": "Release 1.2.0",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITLAB, Url: "https://gitlab.example.com/group/app/-/commit/9f8e7d6c",
CommitSha: "9f8e7d6c", CommitTitle: "Release 1.2.0",
},
},
{
name: "Azure tag build has no branch",
platform: world.AzureDevOps,
env: map[string]string{
"SYSTEM_COLLECTIONURI": "https://dev.azure.com/example/",
"SYSTEM_TEAMPROJECT": "shop",
"BUILD_REPOSITORY_NAME": "app",
"BUILD_SOURCEVERSION": "1c2d3e4f",
"BUILD_SOURCEBRANCH": "refs/tags/v1.2.0",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_AZURE_DEVOPS, Url: "https://dev.azure.com/example/shop/_git/app/commit/1c2d3e4f", CommitSha: "1c2d3e4f",
},
},
{
name: "GitLab merge request pipeline uses the source branch and the MR",
platform: world.GitLab,
env: map[string]string{
"CI_PROJECT_URL": "https://gitlab.example.com/group/app",
"CI_COMMIT_SHA": "9f8e7d6c",
"CI_COMMIT_REF_NAME": "refs/merge-requests/7/head",
"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME": "feature/orders",
"CI_COMMIT_TITLE": "Add the orders table",
"CI_COMMIT_MESSAGE": "Add the orders table",
"CI_COMMIT_AUTHOR": "Alex Doe <alex@example.com>",
"CI_MERGE_REQUEST_IID": "7",
"CI_MERGE_REQUEST_TITLE": "Orders table",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITLAB, Url: "https://gitlab.example.com/group/app/-/commit/9f8e7d6c",
CommitSha: "9f8e7d6c", Branch: "feature/orders", CommitTitle: "Add the orders table", AuthorName: "Alex Doe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://gitlab.example.com/group/app/-/merge_requests/7", Number: 7, Title: "Orders table"},
},
},
{
name: "GitLab push takes the MR number from the merge commit message",
platform: world.GitLab,
env: map[string]string{
"CI_PROJECT_URL": "https://gitlab.example.com/group/app",
"CI_COMMIT_SHA": "9f8e7d6c",
"CI_COMMIT_REF_NAME": "main",
"CI_COMMIT_BRANCH": "main",
"CI_COMMIT_TITLE": "Merge branch 'feature/orders' into 'main'",
"CI_COMMIT_MESSAGE": "Merge branch 'feature/orders' into 'main'\n\nOrders table\n\nSee merge request group/app!7",
"CI_COMMIT_AUTHOR": "Alex Doe <alex@example.com>",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITLAB, Url: "https://gitlab.example.com/group/app/-/commit/9f8e7d6c",
CommitSha: "9f8e7d6c", Branch: "main", CommitTitle: "Merge branch 'feature/orders' into 'main'", AuthorName: "Alex Doe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://gitlab.example.com/group/app/-/merge_requests/7", Number: 7},
},
},
{
name: "Bitbucket push has no title or author",
platform: world.Bitbucket,
env: map[string]string{
"BITBUCKET_GIT_HTTP_ORIGIN": "https://bitbucket.org/example/app",
"BITBUCKET_COMMIT": "5e6f7a8b",
"BITBUCKET_BRANCH": "main",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_BITBUCKET, Url: "https://bitbucket.org/example/app/commits/5e6f7a8b",
CommitSha: "5e6f7a8b", Branch: "main",
},
},
{
name: "Bitbucket pull request pipeline has the PR number",
platform: world.Bitbucket,
env: map[string]string{
"BITBUCKET_GIT_HTTP_ORIGIN": "https://bitbucket.org/example/app",
"BITBUCKET_COMMIT": "5e6f7a8b",
"BITBUCKET_BRANCH": "feature/orders",
"BITBUCKET_PR_ID": "12",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_BITBUCKET, Url: "https://bitbucket.org/example/app/commits/5e6f7a8b",
CommitSha: "5e6f7a8b", Branch: "feature/orders",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://bitbucket.org/example/app/pull-requests/12", Number: 12},
},
},
{
name: "Azure PR build keeps a slashed source branch whole",
platform: world.AzureDevOps,
env: map[string]string{
"SYSTEM_COLLECTIONURI": "https://dev.azure.com/example/",
"SYSTEM_TEAMPROJECT": "shop",
"BUILD_REPOSITORY_NAME": "app",
"BUILD_SOURCEVERSION": "1c2d3e4f",
"BUILD_SOURCEBRANCH": "refs/pull/31/merge",
"BUILD_SOURCEBRANCHNAME": "merge",
"SYSTEM_PULLREQUEST_SOURCEBRANCH": "refs/heads/feature/orders",
"SYSTEM_PULLREQUEST_PULLREQUESTID": "31",
"BUILD_SOURCEVERSIONMESSAGE": "Merge pull request 31 from feature/orders into main",
"BUILD_REQUESTEDFOR": "Alex Doe",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_AZURE_DEVOPS, Url: "https://dev.azure.com/example/shop/_git/app/commit/1c2d3e4f",
CommitSha: "1c2d3e4f", Branch: "feature/orders", CommitTitle: "Merge pull request 31 from feature/orders into main", AuthorName: "Alex Doe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://dev.azure.com/example/shop/_git/app/pullrequest/31", Number: 31},
},
},
{
name: "Azure push takes the PR number from the completion message",
platform: world.AzureDevOps,
env: map[string]string{
"SYSTEM_COLLECTIONURI": "https://dev.azure.com/example/",
"SYSTEM_TEAMPROJECT": "shop",
"BUILD_REPOSITORY_NAME": "app",
"BUILD_SOURCEVERSION": "1c2d3e4f",
"BUILD_SOURCEBRANCH": "refs/heads/main",
"BUILD_SOURCEVERSIONMESSAGE": "Merged PR 31: Add the orders table\n\nbody",
"BUILD_REQUESTEDFOR": "Alex Doe",
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_AZURE_DEVOPS, Url: "https://dev.azure.com/example/shop/_git/app/commit/1c2d3e4f",
CommitSha: "1c2d3e4f", Branch: "main", CommitTitle: "Add the orders table", AuthorName: "Alex Doe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://dev.azure.com/example/shop/_git/app/pullrequest/31", Number: 31},
},
},
{
name: "undetected platform without flags has no source",
platform: world.LocalPlatform,
want: nil,
},
{
name: "undetected platform takes what the flags pass",
platform: world.LocalPlatform,
flags: func(w *world.World) {
w.SourceTitle = "Add the orders table"
w.SourceURL = "https://git.example.com/app/commit/77aa"
w.SourceAuthor = "Alex Doe"
w.SourceRef = "main"
},
want: &v1pb.Release_VCSSource{
Url: "https://git.example.com/app/commit/77aa", CommitTitle: "Add the orders table", AuthorName: "Alex Doe", Branch: "main",
},
},
{
name: "flags win over detection",
platform: world.GitHub,
env: with(githubPush, map[string]string{
githubEventPayload: `{"head_commit":{"message":"Add the orders index","author":{"name":"Alex Doe","username":"alexdoe"}}}`,
}),
flags: func(w *world.World) {
w.SourceTitle = "Release train 12"
w.SourceRef = "release/12"
w.SourceAuthor = "Release Bot"
w.SourceURL = "https://github.com/example/app/commit/ffff"
},
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/ffff",
Branch: "release/12", CommitTitle: "Release train 12", AuthorName: "Release Bot",
},
},
{
name: "a URL flag drops the detected pull request, title, author and branch",
platform: world.GitHub,
env: with(githubPush, map[string]string{
"GITHUB_EVENT_NAME": "pull_request",
githubEventPayload: `{"pull_request":{"number":42,"title":"Add the orders table","html_url":"https://github.com/example/app/pull/42","user":{"login":"alexdoe"}}}`,
}),
flags: func(w *world.World) { w.SourceURL = "https://github.com/example/app/pull/99" },
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/pull/99",
},
},
{
name: "a title flag replaces a detected pull request's title",
platform: world.GitHub,
env: with(githubPush, map[string]string{
"GITHUB_EVENT_NAME": "pull_request",
githubEventPayload: `{"pull_request":{"number":42,"title":"Add the orders table","html_url":"https://github.com/example/app/pull/42","user":{"login":"alexdoe"}}}`,
}),
flags: func(w *world.World) { w.SourceTitle = "Release train 12" },
want: &v1pb.Release_VCSSource{
VcsType: v1pb.VCSType_GITHUB, Url: "https://github.com/example/app/commit/0a1b2c3d",
CommitSha: "0a1b2c3d", Branch: "main", CommitTitle: "Release train 12", AuthorLogin: "alexdoe",
PullRequest: &v1pb.Release_VCSSource_PullRequest{Url: "https://github.com/example/app/pull/42", Number: 42, Title: "Release train 12"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
for _, key := range vcsSourceEnvKeys {
t.Setenv(key, "")
}
for key, value := range tc.env {
if key == githubEventPayload {
if value != "" {
eventPath := filepath.Join(t.TempDir(), "event.json")
require.NoError(t, os.WriteFile(eventPath, []byte(value), 0600))
t.Setenv("GITHUB_EVENT_PATH", eventPath)
}
continue
}
t.Setenv(key, value)
}
w := world.NewWorld()
w.Platform = tc.platform
if tc.flags != nil {
tc.flags(w)
}
got := getVCSSource(w)
require.True(t, proto.Equal(tc.want, got), "want %v\ngot %v", tc.want, got)
})
}
}
// A case's GitHub event payload, written to the file GITHUB_EVENT_PATH names.
const githubEventPayload = "github event payload"
// Every variable getVCSSource reads, cleared per case so the runner's own CI
// environment cannot leak in.
var vcsSourceEnvKeys = []string{
"GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_SHA", "GITHUB_EVENT_NAME", "GITHUB_EVENT_PATH", "GITHUB_REF_NAME", "GITHUB_REF_TYPE", "GITHUB_HEAD_REF",
"CI_PROJECT_URL", "CI_COMMIT_SHA", "CI_COMMIT_REF_NAME", "CI_COMMIT_BRANCH", "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", "CI_COMMIT_TITLE", "CI_COMMIT_MESSAGE", "CI_COMMIT_AUTHOR", "CI_MERGE_REQUEST_IID", "CI_MERGE_REQUEST_TITLE",
"BITBUCKET_GIT_HTTP_ORIGIN", "BITBUCKET_COMMIT", "BITBUCKET_BRANCH", "BITBUCKET_PR_ID",
"SYSTEM_COLLECTIONURI", "SYSTEM_TEAMPROJECT", "BUILD_REPOSITORY_NAME", "BUILD_SOURCEVERSION", "BUILD_SOURCEBRANCH", "BUILD_SOURCEBRANCHNAME", "SYSTEM_PULLREQUEST_SOURCEBRANCH", "SYSTEM_PULLREQUEST_PULLREQUESTID", "BUILD_SOURCEVERSIONMESSAGE", "BUILD_REQUESTEDFOR",
}