From ebf478653127b90c1d32433ad374b6d9fcc45480 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 9 Sep 2026 15:32:25 +0300 Subject: [PATCH 1/4] Add Bitbucket Cloud integration tests (WIP, pre-merge) Co-Authored-By: Claude Sonnet 5 --- .github/workflows/test.yml | 76 +++++++++++++++++++++++++++++++++++++ bitbucket_cloud_test.go | 58 ++++++++++++++++++++++++++++ integrationutils.go | 78 ++++++++++++++++++++++++++++++++------ 3 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 bitbucket_cloud_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4c4caee9..af39915ce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -319,6 +319,82 @@ jobs: JF_ACCESS_TOKEN: ${{ secrets.PLATFORM_ADMIN_TOKEN }} FROGBOT_V3_TESTS_GITLAB_TOKEN: ${{ secrets.FROGBOT_V3_TESTS_GITLAB_TOKEN }} + bitbucket-cloud-integration: + name: Bitbucket Cloud Integration Tests + needs: Pretest + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: [ ubuntu, windows, macos ] + env: + JFROG_CLI_LOG_LEVEL: "DEBUG" + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + allow-unsafe-pr-checkout: true + persist-credentials: false + + - uses: jfrog/boost@v0 + with: + accept_terms: yes + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: false + - name: Go Cache Build & Tests + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~\AppData\Local\go-build + ~/Library/Caches/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/*.go') }} + restore-keys: | + ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}- + ${{ runner.os }}-go- + + - name: Run Tests + run: go test -tags integration bitbucket_cloud_test.go integrationutils.go commands.go -v -race -timeout 30m -cover + env: + JF_URL: ${{ secrets.PLATFORM_URL }} + JF_ACCESS_TOKEN: ${{ secrets.PLATFORM_ADMIN_TOKEN }} + FROGBOT_V3_TESTS_BITBUCKET_CLOUD_TOKEN: ${{ secrets.FROGBOT_V3_TESTS_BITBUCKET_CLOUD_TOKEN }} + + bitbucket-cloud-integration-cleanup: + name: Cleanup Bitbucket Cloud Integration Test Artifacts + needs: bitbucket-cloud-integration + if: always() + runs-on: ubuntu-latest + env: + JFROG_CLI_LOG_LEVEL: "DEBUG" + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + allow-unsafe-pr-checkout: true + persist-credentials: false + + - uses: jfrog/boost@v0 + with: + accept_terms: yes + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: false + + - name: Run Cleanup + run: go test -tags integration -run TestHelper_CleanupIntegrationTestsArtifactsBitbucketCloud bitbucket_cloud_test.go integrationutils.go commands.go -v + env: + JF_URL: ${{ secrets.PLATFORM_URL }} + JF_ACCESS_TOKEN: ${{ secrets.PLATFORM_ADMIN_TOKEN }} + FROGBOT_V3_TESTS_BITBUCKET_CLOUD_TOKEN: ${{ secrets.FROGBOT_V3_TESTS_BITBUCKET_CLOUD_TOKEN }} + bitbucket-server-integration: name: Bitbucket Server Integration Tests needs: Pretest diff --git a/bitbucket_cloud_test.go b/bitbucket_cloud_test.go new file mode 100644 index 000000000..4e670705d --- /dev/null +++ b/bitbucket_cloud_test.go @@ -0,0 +1,58 @@ +//go:build integration + +package main + +import ( + "github.com/jfrog/frogbot/v3/utils" + "github.com/jfrog/froggit-go/vcsclient" + "github.com/jfrog/froggit-go/vcsutils" + "github.com/stretchr/testify/assert" + "testing" +) + +const ( + //#nosec G101 -- False positive - no hardcoded credentials. + bitbucketCloudIntegrationTokenEnv = "FROGBOT_V3_TESTS_BITBUCKET_CLOUD_TOKEN" + bitbucketCloudGitCloneUrl = "https://bitbucket.org/frogbot-e2e-test/frogbot-test.git" + bitbucketCloudRepoOwner = "frogbot-e2e-test" + // Atlassian API tokens with Bitbucket scopes require the literal username "x-bitbucket-api-token-auth" + // for git's HTTP transport (clone/push), but that username is rejected by Bitbucket Cloud's REST API, + // which instead expects Bearer auth (no username at all). The two credentials can't be unified, so the + // git-only username is kept separate from the (empty) REST/env-var username via GitPushUsername. + bitbucketCloudGitPushUsername = "x-bitbucket-api-token-auth" +) + +func buildBitbucketCloudClient(t *testing.T, bitbucketCloudToken string) vcsclient.VcsClient { + bbClient, err := vcsclient.NewClientBuilder(vcsutils.BitbucketCloud).Token(bitbucketCloudToken).Build() + assert.NoError(t, err) + return bbClient +} + +func buildBitbucketCloudIntegrationTestDetails(t *testing.T) *IntegrationTestDetails { + integrationRepoToken := getIntegrationToken(t, bitbucketCloudIntegrationTokenEnv) + testDetails := NewIntegrationTestDetails(integrationRepoToken, string(utils.BitbucketCloud), bitbucketCloudGitCloneUrl, bitbucketCloudRepoOwner) + testDetails.GitUsername = "" + testDetails.GitPushUsername = bitbucketCloudGitPushUsername + return testDetails +} + +func bitbucketCloudTestsInit(t *testing.T) (vcsclient.VcsClient, *IntegrationTestDetails) { + testDetails := buildBitbucketCloudIntegrationTestDetails(t) + bbClient := buildBitbucketCloudClient(t, testDetails.GitToken) + return bbClient, testDetails +} + +func TestBitbucketCloud_ScanPullRequestIntegration(t *testing.T) { + bbClient, testDetails := bitbucketCloudTestsInit(t) + runScanPullRequestCmd(t, bbClient, testDetails) +} + +func TestBitbucketCloud_ScanRepositoryIntegration(t *testing.T) { + bbClient, testDetails := bitbucketCloudTestsInit(t) + runScanRepositoryCmd(t, bbClient, testDetails) +} + +func TestHelper_CleanupIntegrationTestsArtifactsBitbucketCloud(t *testing.T) { + bbClient, testDetails := bitbucketCloudTestsInit(t) + cleanupIntegrationArtifacts(t, bbClient, testDetails) +} diff --git a/integrationutils.go b/integrationutils.go index 588f25a9f..4ab177947 100644 --- a/integrationutils.go +++ b/integrationutils.go @@ -43,11 +43,19 @@ type IntegrationTestDetails struct { GitProvider string GitProject string GitUsername string + GitPushUsername string ApiEndpoint string PullRequestID string CustomBranchName string } +func (d *IntegrationTestDetails) gitPushUsername() string { + if d.GitPushUsername != "" { + return d.GitPushUsername + } + return d.GitUsername +} + func NewIntegrationTestDetails(token, gitProvider, gitCloneUrl, repoOwner string) *IntegrationTestDetails { return &IntegrationTestDetails{ GitProject: repoName, @@ -62,7 +70,7 @@ func NewIntegrationTestDetails(token, gitProvider, gitCloneUrl, repoOwner string func buildGitManager(t *testing.T, testDetails *IntegrationTestDetails) *utils.GitManager { gitManager, err := utils.NewGitManager(). - SetAuth(testDetails.GitUsername, testDetails.GitToken). + SetAuth(testDetails.gitPushUsername(), testDetails.GitToken). SetRemoteGitUrl(testDetails.GitCloneURL) assert.NoError(t, err) return gitManager @@ -135,6 +143,15 @@ func findRelevantPrID(pullRequests []vcsclient.PullRequestInfo, branch string) ( return } +func findRelevantPrIDByBranchPrefix(pullRequests []vcsclient.PullRequestInfo, branchPrefix string) (prId int, branchName string) { + for _, pr := range pullRequests { + if strings.HasPrefix(pr.Source.Name, branchPrefix) && pr.Target.Name == mainBranch { + return int(pr.ID), pr.Source.Name + } + } + return 0, "" +} + func getOpenPullRequests(t *testing.T, client vcsclient.VcsClient, testDetails *IntegrationTestDetails) []vcsclient.PullRequestInfo { ctx := context.Background() pullRequests, err := client.ListOpenPullRequests(ctx, testDetails.RepoOwner, testDetails.RepoName) @@ -190,7 +207,7 @@ func runScanRepositoryCmd(t *testing.T, client vcsclient.VcsClient, testDetails cloneOptions := &git.CloneOptions{ URL: testDetails.GitCloneURL, Auth: &githttp.BasicAuth{ - Username: testDetails.GitUsername, + Username: testDetails.gitPushUsername(), Password: testDetails.GitToken, }, RemoteName: "origin", @@ -212,17 +229,14 @@ func runScanRepositoryCmd(t *testing.T, client vcsclient.VcsClient, testDetails gitManager := buildGitManager(t, testDetails) pullRequests := getOpenPullRequests(t, client, testDetails) - expectedBranches := []string{ - "frogbot-snyk-5aaa88cc32aaaf2d8d893decd0a1b284", - "frogbot-lodash-36ab76ead8f9cace70988ea19d280c93", - "frogbot-minimist-e6e68f7e53c2b59c6bd946e00af797f7", - } - for _, expectedBranch := range expectedBranches { - prId := findRelevantPrID(pullRequests, expectedBranch) - assert.NotZero(t, prId, "Expected to find PR for branch %s", expectedBranch) + expectedFixPackages := []string{"snyk", "lodash", "minimist"} + for _, pkg := range expectedFixPackages { + branchPrefix := scanRepoTestBranchNamePrefix + pkg + "-" + prId, branchName := findRelevantPrIDByBranchPrefix(pullRequests, branchPrefix) + assert.NotZero(t, prId, "Expected to find PR for package %s", pkg) if prId != 0 { closePullRequest(t, client, testDetails, prId) - assert.NoError(t, gitManager.RemoveRemoteBranch(expectedBranch)) + assert.NoError(t, gitManager.RemoveRemoteBranch(branchName)) } } cleanupLeftoverFrogbotPRs(t, client, testDetails, gitManager) @@ -241,6 +255,31 @@ func cleanupLeftoverFrogbotPRs(t *testing.T, client vcsclient.VcsClient, testDet } } +func cleanupIntegrationArtifacts(t *testing.T, client vcsclient.VcsClient, testDetails *IntegrationTestDetails) { + ctx := context.Background() + gitManager := buildGitManager(t, testDetails) + + branches, err := client.ListBranches(ctx, testDetails.RepoOwner, testDetails.RepoName) + require.NoError(t, err) + for _, branch := range branches { + if branch == mainBranch || branch == issuesBranch { + continue + } + t.Logf("Cleanup: removing leftover branch %s", branch) + if err := gitManager.RemoveRemoteBranch(branch); err != nil { + t.Logf("Warning: failed to remove leftover branch %s: %v", branch, err) + } + } + + for _, pr := range getOpenPullRequests(t, client, testDetails) { + if pr.Source.Name == issuesBranch { + continue + } + t.Logf("Cleanup: closing leftover PR %s (ID: %d)", pr.Source.Name, pr.ID) + closePullRequest(t, client, testDetails, int(pr.ID)) + } +} + func validateResults(t *testing.T, ctx context.Context, client vcsclient.VcsClient, testDetails *IntegrationTestDetails, prID int) { comments, err := client.ListPullRequestComments(ctx, testDetails.RepoOwner, testDetails.RepoName, prID) require.NoError(t, err) @@ -252,6 +291,8 @@ func validateResults(t *testing.T, ctx context.Context, client vcsclient.VcsClie validateAzureComments(t, comments) case *vcsclient.BitbucketServerClient: validateBitbucketServerComments(t, comments) + case *vcsclient.BitbucketCloudClient: + validateBitbucketCloudComments(t, comments) case *vcsclient.GitLabClient: validateGitLabComments(t, comments) } @@ -285,6 +326,15 @@ func validateBitbucketServerComments(t *testing.T, comments []vcsclient.CommentI assertBannerExists(t, comments, outputwriter.GetSimplifiedTitle(outputwriter.VulnerabilitiesPrBannerSource)) } +func validateBitbucketCloudComments(t *testing.T, comments []vcsclient.CommentInfo) { + assert.True(t, containsCommentMentioning(comments, outputwriter.GetSimplifiedTitle(outputwriter.VulnerabilitiesPrBannerSource)), + "expected a PR comment containing the Frogbot banner") + assert.True(t, containsCommentMentioning(comments, scanPrTestAddedVulnDependency), + "expected a PR comment mentioning the vulnerable dependency "+scanPrTestAddedVulnDependency) + assert.True(t, containsCommentMentioning(comments, cveCommentPrefix), + "expected a PR comment with CVE findings") +} + func validateGitLabComments(t *testing.T, comments []vcsclient.CommentInfo) { assert.True(t, containsCommentMentioning(comments, string(outputwriter.VulnerabilitiesMrBannerSource)), "expected a MR comment containing the Frogbot banner") @@ -343,5 +393,11 @@ func closePullRequest(t *testing.T, client vcsclient.VcsClient, testDetails *Int targetBranch = "" } err := client.UpdatePullRequest(context.Background(), testDetails.RepoOwner, testDetails.RepoName, "integration test finished", "", targetBranch, prID, vcsutils.Closed) + if _, isBitbucketCloudClient := client.(*vcsclient.BitbucketCloudClient); isBitbucketCloudClient { + if err != nil { + t.Logf("Warning: failed to close PR %d on Bitbucket Cloud (known froggit-go limitation): %v", prID, err) + } + return + } assert.NoError(t, err) } From 816d275a8905fd6c4783d045ab0bb2dd510b03a5 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 9 Sep 2026 15:44:02 +0300 Subject: [PATCH 2/4] Apply concurrency/race-condition safeguards to Bitbucket Cloud integration jobs Mirrors the pattern from #1408 (github/azure/gitlab): shared concurrency group between the test job and its cleanup job, max-parallel:1 across the OS matrix, a -run filter so the cleanup helper test doesn't also run inside the main job, and a stricter cleanup if-condition. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/test.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71228d70b..6e6689490 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -438,8 +438,12 @@ jobs: runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false + max-parallel: 1 matrix: os: [ ubuntu, windows, macos ] + concurrency: + group: bitbucket-cloud-integration-fixture + cancel-in-progress: false env: JFROG_CLI_LOG_LEVEL: "DEBUG" steps: @@ -471,7 +475,7 @@ jobs: ${{ runner.os }}-go- - name: Run Tests - run: go test -tags integration bitbucket_cloud_test.go integrationutils.go commands.go -v -race -timeout 30m -cover + run: go test -tags integration -run 'TestBitbucketCloud_' bitbucket_cloud_test.go integrationutils.go commands.go -v -race -timeout 30m -cover env: JF_URL: ${{ secrets.PLATFORM_URL }} JF_ACCESS_TOKEN: ${{ secrets.PLATFORM_ADMIN_TOKEN }} @@ -479,9 +483,12 @@ jobs: bitbucket-cloud-integration-cleanup: name: Cleanup Bitbucket Cloud Integration Test Artifacts - needs: bitbucket-cloud-integration - if: always() + needs: [ Pretest, bitbucket-cloud-integration ] + if: ${{ always() && !cancelled() && needs.Pretest.result != 'skipped' && needs.bitbucket-cloud-integration.result != 'skipped' }} runs-on: ubuntu-latest + concurrency: + group: bitbucket-cloud-integration-fixture + cancel-in-progress: false env: JFROG_CLI_LOG_LEVEL: "DEBUG" steps: From 62cce6508c2e57cea70a8cdb4cf9f3250d5a4a50 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 9 Sep 2026 19:17:02 +0300 Subject: [PATCH 3/4] fix CR notes --- bitbucket_cloud_test.go | 8 +++----- integrationutils.go | 6 ------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/bitbucket_cloud_test.go b/bitbucket_cloud_test.go index 4e670705d..ad7c3948b 100644 --- a/bitbucket_cloud_test.go +++ b/bitbucket_cloud_test.go @@ -15,11 +15,9 @@ const ( bitbucketCloudIntegrationTokenEnv = "FROGBOT_V3_TESTS_BITBUCKET_CLOUD_TOKEN" bitbucketCloudGitCloneUrl = "https://bitbucket.org/frogbot-e2e-test/frogbot-test.git" bitbucketCloudRepoOwner = "frogbot-e2e-test" - // Atlassian API tokens with Bitbucket scopes require the literal username "x-bitbucket-api-token-auth" - // for git's HTTP transport (clone/push), but that username is rejected by Bitbucket Cloud's REST API, - // which instead expects Bearer auth (no username at all). The two credentials can't be unified, so the - // git-only username is kept separate from the (empty) REST/env-var username via GitPushUsername. - bitbucketCloudGitPushUsername = "x-bitbucket-api-token-auth" + // Matches utils.toBasicAuth's own default for git operations. Bitbucket Cloud's REST API rejects + // this username though, hence GitPushUsername being kept separate from the (empty) REST username. + bitbucketCloudGitPushUsername = "x-token-auth" ) func buildBitbucketCloudClient(t *testing.T, bitbucketCloudToken string) vcsclient.VcsClient { diff --git a/integrationutils.go b/integrationutils.go index f3ce893f9..775031d01 100644 --- a/integrationutils.go +++ b/integrationutils.go @@ -426,11 +426,5 @@ func closePullRequest(t *testing.T, client vcsclient.VcsClient, testDetails *Int targetBranch = "" } err := client.UpdatePullRequest(context.Background(), testDetails.RepoOwner, testDetails.RepoName, "integration test finished", "", targetBranch, prID, vcsutils.Closed) - if _, isBitbucketCloudClient := client.(*vcsclient.BitbucketCloudClient); isBitbucketCloudClient { - if err != nil { - t.Logf("Warning: failed to close PR %d on Bitbucket Cloud (known froggit-go limitation): %v", prID, err) - } - return - } assert.NoError(t, err) } From 7b0febe8e63d36ad46f08e89875bf02be14988ea Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 9 Sep 2026 19:17:18 +0300 Subject: [PATCH 4/4] update deps to include fix made in froggit-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9a584b681..3a6a768a4 100644 --- a/go.mod +++ b/go.mod @@ -140,4 +140,4 @@ require ( // replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go master -// replace github.com/jfrog/froggit-go => github.com/jfrog/froggit-go master +replace github.com/jfrog/froggit-go => github.com/jfrog/froggit-go v1.23.2-0.20260909160929-7d982ce8ac90 diff --git a/go.sum b/go.sum index 3d34ac674..7364123e1 100644 --- a/go.sum +++ b/go.sum @@ -144,8 +144,8 @@ github.com/jfrog/archiver/v3 v3.6.4 h1:qHAWCLKwo3+ocHNNoWzGZ8ESl8QQk/lR3W09Pt+RO github.com/jfrog/archiver/v3 v3.6.4/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= github.com/jfrog/build-info-go v1.13.1-0.20260818195724-23e528d30b96 h1:aVT0+x1sn1PW7piZhqErCHnikcwdBQqDyzvvgmin7Bc= github.com/jfrog/build-info-go v1.13.1-0.20260818195724-23e528d30b96/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= -github.com/jfrog/froggit-go v1.23.1 h1:4wmaHeuptxVINbovMaeITzVhi3+VQoc/FFIjF4axzu0= -github.com/jfrog/froggit-go v1.23.1/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= +github.com/jfrog/froggit-go v1.23.2-0.20260909160929-7d982ce8ac90 h1:PKe1Qo+/leup348YO8UXCK5wZmfgW8mSy2vE+XM4Dzw= +github.com/jfrog/froggit-go v1.23.2-0.20260909160929-7d982ce8ac90/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY=