From b8797aa52c78e7d44f508b0ac46579952d4f67ed Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Thu, 30 Jul 2026 11:49:04 -0400 Subject: [PATCH] feat: emit contributor billing after native code test --report --- go.mod | 16 ++-- go.sum | 30 ++++---- .../commands/code_workflow/native_workflow.go | 12 +++ internal/contributorbilling/emit.go | 62 ++++++++++++++++ internal/contributorbilling/emit_test.go | 73 +++++++++++++++++++ 5 files changed, 170 insertions(+), 23 deletions(-) create mode 100644 internal/contributorbilling/emit.go create mode 100644 internal/contributorbilling/emit_test.go diff --git a/go.mod b/go.mod index a41ffb6..a58921d 100644 --- a/go.mod +++ b/go.mod @@ -100,13 +100,13 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.52.0 // indirect - golang.org/x/mod v0.35.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.36.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.6 // indirect @@ -121,4 +121,6 @@ tool ( github.com/pact-foundation/pact-go/v2 ) -// replace github.com/snyk/go-application-framework => ../../go-application-framework +// Requires go-application-framework release with pkg/contributorbilling (IANDT-237). +// For local dev against ../go-application-framework: +// replace github.com/snyk/go-application-framework => ../go-application-framework diff --git a/go.sum b/go.sum index cc27d7e..290a4d3 100644 --- a/go.sum +++ b/go.sum @@ -246,8 +246,6 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/snyk/error-catalog-golang-public v0.0.0-20260205094614-116c03822905 h1:pUe6iOWHEOFY0t4u4ssXeTqpMmZBu1xq06VBFI9zUik= github.com/snyk/error-catalog-golang-public v0.0.0-20260205094614-116c03822905/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= -github.com/snyk/go-application-framework v0.0.0-20260504124159-e299fb4a44a1 h1:yiG0VOwT292qxs+g6HTnr6O8is/sKEI+9XTUGf2tFDU= -github.com/snyk/go-application-framework v0.0.0-20260504124159-e299fb4a44a1/go.mod h1:yTGCJKf6RmqdwrNs5B9zmukL9x1D8EhfSK8mzaPB1Rk= github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530 h1:s9PHNkL6ueYRiAKNfd8OVxlUOqU3qY0VDbgCD1f6WQY= github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -327,14 +325,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -354,8 +352,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -381,24 +379,24 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/commands/code_workflow/native_workflow.go b/internal/commands/code_workflow/native_workflow.go index a94c05a..29713fe 100644 --- a/internal/commands/code_workflow/native_workflow.go +++ b/internal/commands/code_workflow/native_workflow.go @@ -15,6 +15,7 @@ import ( codeclient "github.com/snyk/code-client-go" "github.com/snyk/code-client-go/bundle" codeclienthttp "github.com/snyk/code-client-go/http" + "github.com/snyk/code-client-go/internal/contributorbilling" "github.com/snyk/code-client-go/sarif" "github.com/snyk/code-client-go/scan" "github.com/snyk/error-catalog-golang-public/code" @@ -130,6 +131,17 @@ func EntryPointNative(invocationCtx workflow.InvocationContext, opts ...Optional return nil, err } + if err == nil { + reportMode, reportErr := GetReportMode(config) + if reportErr == nil && reportMode != noReport && resultMetaData != nil && resultMetaData.ProjectId != "" { + repoPath := config.GetString(configuration.INPUT_DIRECTORY) + if repoPath == "" { + repoPath = path + } + contributorbilling.EmitProject(invocationCtx.Context(), invocationCtx, resultMetaData.ProjectId, repoPath) + } + } + logger.Debug().Msgf("Result metadata: %+v", resultMetaData) resultAvailable := true diff --git a/internal/contributorbilling/emit.go b/internal/contributorbilling/emit.go new file mode 100644 index 0000000..c858314 --- /dev/null +++ b/internal/contributorbilling/emit.go @@ -0,0 +1,62 @@ +package contributorbilling + +import ( + "context" + "strings" + + "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/contributorbilling" + "github.com/snyk/go-application-framework/pkg/workflow" +) + +func defaultRepoPath(repoPath string) string { + if strings.TrimSpace(repoPath) == "" { + return "." + } + return repoPath +} + +func authHeader(config configuration.Configuration) string { + if token := strings.TrimSpace(config.GetString(configuration.AUTHENTICATION_TOKEN)); token != "" { + return "token " + token + } + if bearer := strings.TrimSpace(config.GetString(configuration.AUTHENTICATION_BEARER_TOKEN)); bearer != "" { + return "Bearer " + bearer + } + return "" +} + +// EmitProject fires contributor billing after a successful native code report test. +// It is fire-and-forget and must not affect command exit codes. +func EmitProject( + ctx context.Context, + ictx workflow.InvocationContext, + projectID string, + repoPath string, +) { + projectID = strings.TrimSpace(projectID) + if projectID == "" || ictx == nil { + return + } + + config := ictx.GetConfiguration() + scopeID := strings.TrimSpace(config.GetString(configuration.ORGANIZATION)) + if scopeID == "" { + return + } + + contributorbilling.EmitContributorBilling(ctx, contributorbilling.EmitOptions{ + HTTPClient: ictx.GetNetworkAccess().GetHttpClient(), + IngestURL: config.GetString(configuration.API_URL), + AuthHeader: authHeader(config), + Capability: contributorbilling.CapabilityCode, + ScopeID: scopeID, + RepoPath: defaultRepoPath(repoPath), + CollectContributors: true, + Timeout: contributorbilling.DefaultTimeout, + Logger: ictx.GetEnhancedLogger(), + Items: []contributorbilling.BillingItem{ + {EntityID: projectID}, + }, + }) +} diff --git a/internal/contributorbilling/emit_test.go b/internal/contributorbilling/emit_test.go new file mode 100644 index 0000000..a1f3e48 --- /dev/null +++ b/internal/contributorbilling/emit_test.go @@ -0,0 +1,73 @@ +package contributorbilling_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/rs/zerolog" + "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/contributorbilling" + "github.com/snyk/go-application-framework/pkg/mocks" + "github.com/stretchr/testify/require" + + billing "github.com/snyk/code-client-go/internal/contributorbilling" +) + +func TestEmitProject(t *testing.T) { + t.Parallel() + + var ( + mu sync.Mutex + requests []map[string]any + ) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + + var payload map[string]any + require.NoError(t, json.Unmarshal(body, &payload)) + + mu.Lock() + requests = append(requests, payload) + mu.Unlock() + + w.WriteHeader(http.StatusCreated) + })) + t.Cleanup(server.Close) + + ctrl := gomock.NewController(t) + logger := zerolog.Nop() + config := configuration.NewWithOpts() + config.Set(configuration.API_URL, server.URL) + config.Set(configuration.ORGANIZATION, "11111111-1111-1111-1111-111111111111") + config.Set(configuration.AUTHENTICATION_TOKEN, "test-token") + + networkAccess := mocks.NewMockNetworkAccess(ctrl) + networkAccess.EXPECT().GetHttpClient().Return(server.Client()).AnyTimes() + + invocation := mocks.NewMockInvocationContext(ctrl) + invocation.EXPECT().GetConfiguration().Return(config).AnyTimes() + invocation.EXPECT().GetEnhancedLogger().Return(&logger).AnyTimes() + invocation.EXPECT().GetNetworkAccess().Return(networkAccess).AnyTimes() + + billing.EmitProject( + context.Background(), + invocation, + "22222222-2222-2222-2222-222222222222", + "/tmp/repo", + ) + + require.True(t, contributorbilling.WaitWithTimeout(2*time.Second)) + + mu.Lock() + defer mu.Unlock() + require.Len(t, requests, 1) +}