From 390cf9b669a58255b18ca14093d0db8d83a050a0 Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Thu, 30 Jul 2026 04:37:56 -0400 Subject: [PATCH 1/7] feat: emit contributor billing for legacy CLI commands [IANDT-240] --- cliv2/go.mod | 2 + .../proxy/interceptor/networkinjector.go | 13 +- .../proxy/interceptor/networkinjector_test.go | 46 ++++++- .../legacy_contributor_billing.go | 109 +++++++++++++++++ .../legacy_contributor_billing_test.go | 112 ++++++++++++++++++ cliv2/pkg/basic_workflows/legacycli.go | 22 +++- cliv2/pkg/basic_workflows/legacycli_test.go | 2 +- cliv2/pkg/core/main.go | 3 + 8 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 cliv2/pkg/basic_workflows/legacy_contributor_billing.go create mode 100644 cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go diff --git a/cliv2/go.mod b/cliv2/go.mod index 7c52dd1f4f..e7d0c9ebf6 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -270,6 +270,8 @@ require ( // version 2491eb6c1c75 contains a valid license replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 +// Requires go-application-framework release with IANDT-237 (contributorbilling) and +// IANDT-238 (contributorcapture middleware). For local dev against ../../go-application-framework: // replace github.com/snyk/go-application-framework => ../../go-application-framework // replace github.com/snyk/snyk-ls => ../../snyk-ls diff --git a/cliv2/internal/proxy/interceptor/networkinjector.go b/cliv2/internal/proxy/interceptor/networkinjector.go index a1151ad375..eef41c797d 100644 --- a/cliv2/internal/proxy/interceptor/networkinjector.go +++ b/cliv2/internal/proxy/interceptor/networkinjector.go @@ -1,15 +1,18 @@ package interceptor import ( - "github.com/elazarl/goproxy" - "github.com/snyk/go-application-framework/pkg/workflow" + "context" "net/http" "regexp" + + "github.com/elazarl/goproxy" + "github.com/snyk/go-application-framework/pkg/workflow" ) type networkInjector struct { requestCondition goproxy.ReqCondition invocationCtx workflow.InvocationContext + requestContext context.Context } func (ni networkInjector) GetCondition() goproxy.ReqCondition { @@ -21,6 +24,9 @@ func (ni networkInjector) GetCondition() goproxy.ReqCondition { // and the gocli in two different places. func (ni networkInjector) GetHandler() goproxy.FuncReqHandler { return func(req *http.Request, proxyCtx *goproxy.ProxyCtx) (*http.Request, *http.Response) { + if ni.requestContext != nil { + req = req.WithContext(ni.requestContext) + } resp, err := ni.invocationCtx.GetNetworkAccess().GetRoundTripper().RoundTrip(req) if err != nil { ni.invocationCtx.GetEnhancedLogger().Trace().Msgf("intercepting call failed with error: %v", err) @@ -35,10 +41,11 @@ func (ni networkInjector) GetHandler() goproxy.FuncReqHandler { } } -func NewNetworkInjector(invocationCtx workflow.InvocationContext) Interceptor { +func NewNetworkInjector(invocationCtx workflow.InvocationContext, requestContext context.Context) Interceptor { i := networkInjector{ requestCondition: goproxy.UrlMatches(regexp.MustCompile(".*")), invocationCtx: invocationCtx, + requestContext: requestContext, } return i } diff --git a/cliv2/internal/proxy/interceptor/networkinjector_test.go b/cliv2/internal/proxy/interceptor/networkinjector_test.go index 9a9633c0ed..27cc4b4178 100644 --- a/cliv2/internal/proxy/interceptor/networkinjector_test.go +++ b/cliv2/internal/proxy/interceptor/networkinjector_test.go @@ -1,6 +1,7 @@ package interceptor import ( + "context" "errors" "net/http" "testing" @@ -8,6 +9,7 @@ import ( "github.com/golang/mock/gomock" "github.com/rs/zerolog" "github.com/snyk/go-application-framework/pkg/mocks" + "github.com/snyk/go-application-framework/pkg/networking/contributorcapture" "github.com/elazarl/goproxy" "github.com/stretchr/testify/assert" @@ -37,7 +39,7 @@ func TestNetworkInjector_ErrorHandling(t *testing.T) { invocationCtxMock.EXPECT().GetNetworkAccess().Return(networkAccessMock).AnyTimes() invocationCtxMock.EXPECT().GetEnhancedLogger().Return(&logger).AnyTimes() - ni := NewNetworkInjector(invocationCtxMock) + ni := NewNetworkInjector(invocationCtxMock, context.Background()) handler := ni.GetHandler() req := &http.Request{} @@ -56,3 +58,45 @@ func TestNetworkInjector_ErrorHandling(t *testing.T) { // Goproxy will send the request again if the response is nil, why it's imperative this does not happen. assert.Nil(t, resp, "response should not be nil when RoundTrip returns an error") } + +func TestNetworkInjector_AttachesRequestContext(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logger := zerolog.Nop() + capture := contributorcapture.NewCapture() + requestContext := contributorcapture.WithCapture(context.Background(), capture) + + var capturedContext context.Context + roundTripperMock := mockRoundTripperWithContext{onRoundTrip: func(req *http.Request) { + capturedContext = req.Context() + }} + + networkAccessMock := mocks.NewMockNetworkAccess(ctrl) + networkAccessMock.EXPECT().GetRoundTripper().Return(roundTripperMock) + + invocationCtxMock := mocks.NewMockInvocationContext(ctrl) + invocationCtxMock.EXPECT().GetNetworkAccess().Return(networkAccessMock).AnyTimes() + invocationCtxMock.EXPECT().GetEnhancedLogger().Return(&logger).AnyTimes() + + ni := NewNetworkInjector(invocationCtxMock, requestContext) + handler := ni.GetHandler() + + req := &http.Request{} + proxyCtx := &goproxy.ProxyCtx{} + _, _ = handler(req, proxyCtx) + + assert.Equal(t, requestContext, capturedContext) + assert.Equal(t, capture, contributorcapture.FromContext(capturedContext)) +} + +type mockRoundTripperWithContext struct { + onRoundTrip func(*http.Request) +} + +func (m mockRoundTripperWithContext) RoundTrip(req *http.Request) (*http.Response, error) { + if m.onRoundTrip != nil { + m.onRoundTrip(req) + } + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} diff --git a/cliv2/pkg/basic_workflows/legacy_contributor_billing.go b/cliv2/pkg/basic_workflows/legacy_contributor_billing.go new file mode 100644 index 0000000000..19379639fa --- /dev/null +++ b/cliv2/pkg/basic_workflows/legacy_contributor_billing.go @@ -0,0 +1,109 @@ +package basic_workflows + +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/networking/contributorcapture" + "github.com/snyk/go-application-framework/pkg/workflow" +) + +func defaultRepoPath(workingDirectory string) string { + if strings.TrimSpace(workingDirectory) == "" { + return "." + } + return workingDirectory +} + +func contributorBillingAuthHeader(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 "" +} + +func billingCapability(capability contributorcapture.Capability) string { + switch capability { + case contributorcapture.CapabilityOSS: + return contributorbilling.CapabilityOSS + case contributorcapture.CapabilityIaC: + return contributorbilling.CapabilityIaC + case contributorcapture.CapabilityCode: + return contributorbilling.CapabilityCode + default: + return "" + } +} + +// emitLegacyContributorBilling posts contributor billing for project IDs captured during a +// legacy CLI invocation. It is fire-and-forget and must not affect command exit codes. +// +// TODO(IANDT-238): delegate to contributorcapture.EmitCapturedRecords once GAF wires HTTP +// client fields on EmitOptions (depends on IANDT-237 + IANDT-238 landing). +func emitLegacyContributorBilling( + ctx context.Context, + invocation workflow.InvocationContext, + capture *contributorcapture.Capture, + workingDirectory string, +) { + if capture == nil { + return + } + + records := capture.Snapshot() + if len(records) == 0 { + return + } + + config := invocation.GetConfiguration() + scopeID := strings.TrimSpace(config.GetString(configuration.ORGANIZATION)) + if scopeID == "" { + return + } + + repoPath := defaultRepoPath(workingDirectory) + logger := invocation.GetEnhancedLogger() + httpClient := invocation.GetNetworkAccess().GetHttpClient() + ingestURL := config.GetString(configuration.API_URL) + authHeader := contributorBillingAuthHeader(config) + + type emitKey struct { + capability string + projectID string + } + seen := make(map[emitKey]struct{}, len(records)) + + for _, record := range records { + capability := billingCapability(record.Capability) + projectID := strings.TrimSpace(record.ProjectID) + if capability == "" || projectID == "" { + continue + } + + key := emitKey{capability: capability, projectID: projectID} + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + + contributorbilling.EmitContributorBilling(ctx, contributorbilling.EmitOptions{ + HTTPClient: httpClient, + IngestURL: ingestURL, + AuthHeader: authHeader, + Capability: capability, + ScopeID: scopeID, + RepoPath: repoPath, + CollectContributors: true, + Timeout: contributorbilling.DefaultTimeout, + Logger: logger, + Items: []contributorbilling.BillingItem{ + {EntityID: projectID}, + }, + }) + } +} diff --git a/cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go b/cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go new file mode 100644 index 0000000000..61020c8499 --- /dev/null +++ b/cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go @@ -0,0 +1,112 @@ +package basic_workflows + +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/snyk/go-application-framework/pkg/networking/contributorcapture" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_emitLegacyContributorBilling_EmitsPerCapturedProject(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() + + capture := contributorcapture.NewCapture() + capture.Add(contributorcapture.Record{ + Capability: contributorcapture.CapabilityOSS, + ProjectID: "22222222-2222-2222-2222-222222222222", + }) + capture.Add(contributorcapture.Record{ + Capability: contributorcapture.CapabilityIaC, + ProjectID: "33333333-3333-3333-3333-333333333333", + }) + capture.Add(contributorcapture.Record{ + Capability: contributorcapture.CapabilityOSS, + ProjectID: "22222222-2222-2222-2222-222222222222", + }) + + emitLegacyContributorBilling(context.Background(), invocation, capture, "/tmp/repo") + + require.True(t, contributorbilling.WaitWithTimeout(2*time.Second)) + + mu.Lock() + defer mu.Unlock() + require.Len(t, requests, 2) +} + +func Test_emitLegacyContributorBilling_SkipsWhenCaptureEmpty(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + invocation := mocks.NewMockInvocationContext(ctrl) + + emitLegacyContributorBilling(context.Background(), invocation, contributorcapture.NewCapture(), ".") + + assert.True(t, contributorbilling.WaitWithTimeout(time.Millisecond)) +} + +func Test_defaultRepoPath(t *testing.T) { + t.Parallel() + + assert.Equal(t, ".", defaultRepoPath("")) + assert.Equal(t, "/tmp/repo", defaultRepoPath("/tmp/repo")) +} + +func Test_contributorBillingAuthHeader(t *testing.T) { + t.Parallel() + + config := configuration.NewWithOpts() + config.Set(configuration.AUTHENTICATION_TOKEN, "abc") + assert.Equal(t, "token abc", contributorBillingAuthHeader(config)) + + config = configuration.NewWithOpts() + config.Set(configuration.AUTHENTICATION_BEARER_TOKEN, "oauth-token") + assert.Equal(t, "Bearer oauth-token", contributorBillingAuthHeader(config)) +} diff --git a/cliv2/pkg/basic_workflows/legacycli.go b/cliv2/pkg/basic_workflows/legacycli.go index 24e4a7ffac..c0546184fa 100644 --- a/cliv2/pkg/basic_workflows/legacycli.go +++ b/cliv2/pkg/basic_workflows/legacycli.go @@ -3,6 +3,7 @@ package basic_workflows import ( "bufio" "bytes" + "context" "fmt" "io" "os" @@ -18,6 +19,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/networking/contributorcapture" pkg_utils "github.com/snyk/go-application-framework/pkg/utils" "github.com/snyk/go-application-framework/pkg/workflow" "github.com/spf13/pflag" @@ -142,18 +144,23 @@ func legacycliWorkflow( cli.SetIoStreams(os.Stdin, os.Stdout, stderr) } + capture := contributorcapture.NewCapture() + invocationCtx := contributorcapture.WithCapture(invocation.Context(), capture) + wrapperProxy, err := createInternalProxy( config, debugLogger, invocation, + invocationCtx, ) if err != nil { return output, err } + defer wrapperProxy.Close() // run the cli with context from invocation (allows cancellation on signal) proxyInfo := wrapperProxy.ProxyInfo() - err = cli.Execute(invocation.Context(), proxyInfo, finalizeArguments(args, config.GetStringSlice(configuration.UNKNOWN_ARGS))) + err = cli.Execute(invocationCtx, proxyInfo, finalizeArguments(args, config.GetStringSlice(configuration.UNKNOWN_ARGS))) if !useStdIo { _ = outWriter.Flush() @@ -174,10 +181,19 @@ func legacycliWorkflow( invocation.GetAnalytics().AddExtensionIntegerValue("exitcode", exitError.ExitCode()) } + if err == nil { + emitLegacyContributorBilling(invocationCtx, invocation, capture, workingDirectory) + } + return output, err } -func createInternalProxy(config configuration.Configuration, debugLogger *zerolog.Logger, invocation workflow.InvocationContext) (*proxy.WrapperProxy, error) { +func createInternalProxy( + config configuration.Configuration, + debugLogger *zerolog.Logger, + invocation workflow.InvocationContext, + requestContext context.Context, +) (*proxy.WrapperProxy, error) { caData, err := GetGlobalCertAuthority(config, debugLogger) if err != nil { return nil, err @@ -193,7 +209,7 @@ func createInternalProxy(config configuration.Configuration, debugLogger *zerolo // The networkinjector intercepts all requests from the legacy CLI and re-routes them to the existing networking // layer. It should therefore be kept as the last interceptor in the chain, as it circuit breaks goproxy's own // routing. Any interceptor added later will not be called. - wrapperProxy.RegisterInterceptor(interceptor.NewNetworkInjector(invocation)) + wrapperProxy.RegisterInterceptor(interceptor.NewNetworkInjector(invocation, requestContext)) err = wrapperProxy.Start() if err != nil { diff --git a/cliv2/pkg/basic_workflows/legacycli_test.go b/cliv2/pkg/basic_workflows/legacycli_test.go index 2cc1fe008a..fc9e4ef938 100644 --- a/cliv2/pkg/basic_workflows/legacycli_test.go +++ b/cliv2/pkg/basic_workflows/legacycli_test.go @@ -81,7 +81,7 @@ func Test_proxyWithErrorHandler(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { config.Set(configuration.API_URL, tc.configureApiUrl) - wp, err := createInternalProxy(config, &logger, invocationCtxMock) + wp, err := createInternalProxy(config, &logger, invocationCtxMock, context.Background()) assert.Nil(t, err) defer wp.Close() diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index bc832ba635..9cd5658595 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -30,6 +30,7 @@ import ( "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/contributorbilling" "github.com/snyk/go-application-framework/pkg/instrumentation" "github.com/snyk/go-application-framework/pkg/logging" @@ -542,6 +543,8 @@ func tearDown(err error, errorList []error, startTime time.Time, ua networking.U writeLogFooter(exitCode, allErrors, globalConfiguration, networkAccess) } + contributorbilling.WaitWithTimeout(contributorbilling.DefaultTimeout) + return exitCode } From e2e960204fc28fb04dfc66e85aa085f448192d17 Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Mon, 3 Aug 2026 00:27:57 -0400 Subject: [PATCH 2/7] feat(cliv2): attach capture bag and finish billing in teardown [IANDT-240] Create one command-scoped capture bag for workflow and legacy proxy traffic, call clibilling.Finish from tearDown on exit code 0, and remove the bespoke legacy_contributor_billing emit path. Requires GAF 237+238; use local replace for development until those land on main. Co-authored-by: Cursor --- .vscode/settings.json | 4 + cliv2/go.sum | 2 - .../proxy/interceptor/networkinjector_test.go | 8 +- .../legacy_contributor_billing.go | 109 ----------------- .../legacy_contributor_billing_test.go | 112 ------------------ cliv2/pkg/basic_workflows/legacycli.go | 12 +- cliv2/pkg/core/contributor_billing.go | 51 ++++++++ cliv2/pkg/core/contributor_billing_test.go | 43 +++++++ cliv2/pkg/core/main.go | 10 +- 9 files changed, 113 insertions(+), 238 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 cliv2/pkg/basic_workflows/legacy_contributor_billing.go delete mode 100644 cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go create mode 100644 cliv2/pkg/core/contributor_billing.go create mode 100644 cliv2/pkg/core/contributor_billing_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..170cfc667a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "snyk.advanced.organization": "01d959ff-2459-428a-aadb-381574aaa317", + "snyk.advanced.autoSelectOrganization": true +} \ No newline at end of file diff --git a/cliv2/go.sum b/cliv2/go.sum index def86324c1..157cd7ceb4 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -547,8 +547,6 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3 github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 h1:j2ZPhi78wKIHTiL9EFTNVXMIbsk56FVF2d5Sy1ZwSYk= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= -github.com/snyk/go-application-framework v0.7.2 h1:WzZ7BeFL0pKoB2YdheHtEgEdeB+9nunmwQP8XI+rKcc= -github.com/snyk/go-application-framework v0.7.2/go.mod h1:0YC7xCETnFTdz6rq8OQPL0aWekMLOkBQu1FE4/cReMA= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg= github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao= diff --git a/cliv2/internal/proxy/interceptor/networkinjector_test.go b/cliv2/internal/proxy/interceptor/networkinjector_test.go index 27cc4b4178..69b086d91b 100644 --- a/cliv2/internal/proxy/interceptor/networkinjector_test.go +++ b/cliv2/internal/proxy/interceptor/networkinjector_test.go @@ -9,7 +9,7 @@ import ( "github.com/golang/mock/gomock" "github.com/rs/zerolog" "github.com/snyk/go-application-framework/pkg/mocks" - "github.com/snyk/go-application-framework/pkg/networking/contributorcapture" + "github.com/snyk/go-application-framework/pkg/clibilling" "github.com/elazarl/goproxy" "github.com/stretchr/testify/assert" @@ -64,8 +64,8 @@ func TestNetworkInjector_AttachesRequestContext(t *testing.T) { defer ctrl.Finish() logger := zerolog.Nop() - capture := contributorcapture.NewCapture() - requestContext := contributorcapture.WithCapture(context.Background(), capture) + capture := clibilling.NewCapture() + requestContext := clibilling.WithCapture(context.Background(), capture) var capturedContext context.Context roundTripperMock := mockRoundTripperWithContext{onRoundTrip: func(req *http.Request) { @@ -87,7 +87,7 @@ func TestNetworkInjector_AttachesRequestContext(t *testing.T) { _, _ = handler(req, proxyCtx) assert.Equal(t, requestContext, capturedContext) - assert.Equal(t, capture, contributorcapture.FromContext(capturedContext)) + assert.Equal(t, capture, clibilling.FromContext(capturedContext)) } type mockRoundTripperWithContext struct { diff --git a/cliv2/pkg/basic_workflows/legacy_contributor_billing.go b/cliv2/pkg/basic_workflows/legacy_contributor_billing.go deleted file mode 100644 index 19379639fa..0000000000 --- a/cliv2/pkg/basic_workflows/legacy_contributor_billing.go +++ /dev/null @@ -1,109 +0,0 @@ -package basic_workflows - -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/networking/contributorcapture" - "github.com/snyk/go-application-framework/pkg/workflow" -) - -func defaultRepoPath(workingDirectory string) string { - if strings.TrimSpace(workingDirectory) == "" { - return "." - } - return workingDirectory -} - -func contributorBillingAuthHeader(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 "" -} - -func billingCapability(capability contributorcapture.Capability) string { - switch capability { - case contributorcapture.CapabilityOSS: - return contributorbilling.CapabilityOSS - case contributorcapture.CapabilityIaC: - return contributorbilling.CapabilityIaC - case contributorcapture.CapabilityCode: - return contributorbilling.CapabilityCode - default: - return "" - } -} - -// emitLegacyContributorBilling posts contributor billing for project IDs captured during a -// legacy CLI invocation. It is fire-and-forget and must not affect command exit codes. -// -// TODO(IANDT-238): delegate to contributorcapture.EmitCapturedRecords once GAF wires HTTP -// client fields on EmitOptions (depends on IANDT-237 + IANDT-238 landing). -func emitLegacyContributorBilling( - ctx context.Context, - invocation workflow.InvocationContext, - capture *contributorcapture.Capture, - workingDirectory string, -) { - if capture == nil { - return - } - - records := capture.Snapshot() - if len(records) == 0 { - return - } - - config := invocation.GetConfiguration() - scopeID := strings.TrimSpace(config.GetString(configuration.ORGANIZATION)) - if scopeID == "" { - return - } - - repoPath := defaultRepoPath(workingDirectory) - logger := invocation.GetEnhancedLogger() - httpClient := invocation.GetNetworkAccess().GetHttpClient() - ingestURL := config.GetString(configuration.API_URL) - authHeader := contributorBillingAuthHeader(config) - - type emitKey struct { - capability string - projectID string - } - seen := make(map[emitKey]struct{}, len(records)) - - for _, record := range records { - capability := billingCapability(record.Capability) - projectID := strings.TrimSpace(record.ProjectID) - if capability == "" || projectID == "" { - continue - } - - key := emitKey{capability: capability, projectID: projectID} - if _, exists := seen[key]; exists { - continue - } - seen[key] = struct{}{} - - contributorbilling.EmitContributorBilling(ctx, contributorbilling.EmitOptions{ - HTTPClient: httpClient, - IngestURL: ingestURL, - AuthHeader: authHeader, - Capability: capability, - ScopeID: scopeID, - RepoPath: repoPath, - CollectContributors: true, - Timeout: contributorbilling.DefaultTimeout, - Logger: logger, - Items: []contributorbilling.BillingItem{ - {EntityID: projectID}, - }, - }) - } -} diff --git a/cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go b/cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go deleted file mode 100644 index 61020c8499..0000000000 --- a/cliv2/pkg/basic_workflows/legacy_contributor_billing_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package basic_workflows - -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/snyk/go-application-framework/pkg/networking/contributorcapture" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_emitLegacyContributorBilling_EmitsPerCapturedProject(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() - - capture := contributorcapture.NewCapture() - capture.Add(contributorcapture.Record{ - Capability: contributorcapture.CapabilityOSS, - ProjectID: "22222222-2222-2222-2222-222222222222", - }) - capture.Add(contributorcapture.Record{ - Capability: contributorcapture.CapabilityIaC, - ProjectID: "33333333-3333-3333-3333-333333333333", - }) - capture.Add(contributorcapture.Record{ - Capability: contributorcapture.CapabilityOSS, - ProjectID: "22222222-2222-2222-2222-222222222222", - }) - - emitLegacyContributorBilling(context.Background(), invocation, capture, "/tmp/repo") - - require.True(t, contributorbilling.WaitWithTimeout(2*time.Second)) - - mu.Lock() - defer mu.Unlock() - require.Len(t, requests, 2) -} - -func Test_emitLegacyContributorBilling_SkipsWhenCaptureEmpty(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - invocation := mocks.NewMockInvocationContext(ctrl) - - emitLegacyContributorBilling(context.Background(), invocation, contributorcapture.NewCapture(), ".") - - assert.True(t, contributorbilling.WaitWithTimeout(time.Millisecond)) -} - -func Test_defaultRepoPath(t *testing.T) { - t.Parallel() - - assert.Equal(t, ".", defaultRepoPath("")) - assert.Equal(t, "/tmp/repo", defaultRepoPath("/tmp/repo")) -} - -func Test_contributorBillingAuthHeader(t *testing.T) { - t.Parallel() - - config := configuration.NewWithOpts() - config.Set(configuration.AUTHENTICATION_TOKEN, "abc") - assert.Equal(t, "token abc", contributorBillingAuthHeader(config)) - - config = configuration.NewWithOpts() - config.Set(configuration.AUTHENTICATION_BEARER_TOKEN, "oauth-token") - assert.Equal(t, "Bearer oauth-token", contributorBillingAuthHeader(config)) -} diff --git a/cliv2/pkg/basic_workflows/legacycli.go b/cliv2/pkg/basic_workflows/legacycli.go index c0546184fa..d739a64327 100644 --- a/cliv2/pkg/basic_workflows/legacycli.go +++ b/cliv2/pkg/basic_workflows/legacycli.go @@ -18,8 +18,8 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" + "github.com/snyk/go-application-framework/pkg/clibilling" "github.com/snyk/go-application-framework/pkg/configuration" - "github.com/snyk/go-application-framework/pkg/networking/contributorcapture" pkg_utils "github.com/snyk/go-application-framework/pkg/utils" "github.com/snyk/go-application-framework/pkg/workflow" "github.com/spf13/pflag" @@ -144,8 +144,10 @@ func legacycliWorkflow( cli.SetIoStreams(os.Stdin, os.Stdout, stderr) } - capture := contributorcapture.NewCapture() - invocationCtx := contributorcapture.WithCapture(invocation.Context(), capture) + invocationCtx := invocation.Context() + if clibilling.FromContext(invocationCtx) == nil { + invocationCtx = clibilling.WithCapture(invocationCtx, clibilling.NewCapture()) + } wrapperProxy, err := createInternalProxy( config, @@ -181,10 +183,6 @@ func legacycliWorkflow( invocation.GetAnalytics().AddExtensionIntegerValue("exitcode", exitError.ExitCode()) } - if err == nil { - emitLegacyContributorBilling(invocationCtx, invocation, capture, workingDirectory) - } - return output, err } diff --git a/cliv2/pkg/core/contributor_billing.go b/cliv2/pkg/core/contributor_billing.go new file mode 100644 index 0000000000..6b7c0aca13 --- /dev/null +++ b/cliv2/pkg/core/contributor_billing.go @@ -0,0 +1,51 @@ +package core + +import ( + "context" + "strings" + "sync" + + "github.com/snyk/go-application-framework/pkg/clibilling" + "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/workflow" +) + +var ( + commandBillingMu sync.Mutex + commandBillingBag *clibilling.Capture + commandBillingRepoPath string +) + +func beginContributorBilling(ctx context.Context, config configuration.Configuration) context.Context { + commandBillingMu.Lock() + commandBillingBag = clibilling.NewCapture() + commandBillingRepoPath = billingRepoPath(config) + commandBillingMu.Unlock() + return clibilling.WithCapture(ctx, commandBillingBag) +} + +func finishContributorBilling( + ctx context.Context, + engine workflow.Engine, + config configuration.Configuration, + success bool, +) { + commandBillingMu.Lock() + bag := commandBillingBag + repoPath := commandBillingRepoPath + commandBillingBag = nil + commandBillingRepoPath = "" + commandBillingMu.Unlock() + + opts := clibilling.FinishOptionsFromConfig(config, engine) + opts.RepoPath = repoPath + clibilling.Finish(ctx, bag, opts, success) +} + +func billingRepoPath(config configuration.Configuration) string { + dirs := config.GetStringSlice(configuration.INPUT_DIRECTORY) + if len(dirs) > 0 && strings.TrimSpace(dirs[0]) != "" { + return dirs[0] + } + return "." +} diff --git a/cliv2/pkg/core/contributor_billing_test.go b/cliv2/pkg/core/contributor_billing_test.go new file mode 100644 index 0000000000..89f12cc269 --- /dev/null +++ b/cliv2/pkg/core/contributor_billing_test.go @@ -0,0 +1,43 @@ +package core + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/snyk/go-application-framework/pkg/clibilling" + "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/workflow" +) + +func Test_beginContributorBilling_attachesBagToContext(t *testing.T) { + t.Parallel() + + config := configuration.NewWithOpts() + config.Set(configuration.INPUT_DIRECTORY, []string{"/tmp/repo"}) + + ctx := beginContributorBilling(context.Background(), config) + assert.NotNil(t, clibilling.FromContext(ctx)) +} + +func Test_finishContributorBilling_clearsActiveBag(t *testing.T) { + t.Parallel() + + config := configuration.NewWithOpts() + engine := workflow.NewDefaultWorkFlowEngine() + engine.SetConfiguration(config) + + ctx := beginContributorBilling(context.Background(), config) + finishContributorBilling(ctx, engine, config, false) +} + +func Test_billingRepoPath(t *testing.T) { + t.Parallel() + + config := configuration.NewWithOpts() + assert.Equal(t, ".", billingRepoPath(config)) + + config.Set(configuration.INPUT_DIRECTORY, []string{"/tmp/repo"}) + assert.Equal(t, "/tmp/repo", billingRepoPath(config)) +} diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index 9cd5658595..3dffed86dc 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -30,7 +30,6 @@ import ( "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" - "github.com/snyk/go-application-framework/pkg/contributorbilling" "github.com/snyk/go-application-framework/pkg/instrumentation" "github.com/snyk/go-application-framework/pkg/logging" @@ -213,7 +212,7 @@ func runMainWorkflow(config configuration.Configuration, cmd *cobra.Command, arg globalLogger.Print("Running ", name) globalEngine.GetAnalytics().SetCommand(name) - err = runWorkflowAndProcessData(globalContext, globalEngine, globalLogger, name) + err = runWorkflowAndProcessData(beginContributorBilling(globalContext, config), globalEngine, globalLogger, name) return err } @@ -251,7 +250,10 @@ func defaultCmd(args []string) error { // * by specifying the raw cmd args for it globalConfiguration.Set(configuration.WORKFLOW_USE_STDIO, true) globalConfiguration.Set(configuration.RAW_CMD_ARGS, args) - _, err := globalEngine.Invoke(basic_workflows.WORKFLOWID_LEGACY_CLI) + _, err := globalEngine.Invoke( + basic_workflows.WORKFLOWID_LEGACY_CLI, + workflow.WithContext(beginContributorBilling(globalContext, globalConfiguration)), + ) return err } @@ -543,7 +545,7 @@ func tearDown(err error, errorList []error, startTime time.Time, ua networking.U writeLogFooter(exitCode, allErrors, globalConfiguration, networkAccess) } - contributorbilling.WaitWithTimeout(contributorbilling.DefaultTimeout) + finishContributorBilling(teardownCtx, globalEngine, globalConfiguration, exitCode == 0) return exitCode } From 3cd170939bd7967000f10d8b1f798ee54ac5db69 Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Mon, 3 Aug 2026 00:28:20 -0400 Subject: [PATCH 3/7] chore: drop accidental vscode settings from branch Co-authored-by: Cursor --- .vscode/settings.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 170cfc667a..0000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "snyk.advanced.organization": "01d959ff-2459-428a-aadb-381574aaa317", - "snyk.advanced.autoSelectOrganization": true -} \ No newline at end of file From 5e90c7b18dd38fdb391191cecb19c779a07a0c44 Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Mon, 3 Aug 2026 01:08:40 -0400 Subject: [PATCH 4/7] refactor(cliv2): thin CaptureEngine host wiring for billing [IANDT-240] Delegate bag lifecycle to GAF CaptureEngine (begin/finish at command boundaries). Remove cliv2 networkinjector ctx and legacycli capture duplication; GAF HTTP transport injection handles legacy proxy traffic. Co-authored-by: Cursor --- cliv2/go.mod | 4 +- cliv2/go.sum | 2 + .../proxy/interceptor/networkinjector.go | 13 ++--- .../proxy/interceptor/networkinjector_test.go | 46 +---------------- cliv2/pkg/basic_workflows/legacycli.go | 20 ++------ cliv2/pkg/basic_workflows/legacycli_test.go | 2 +- cliv2/pkg/core/contributor_billing.go | 49 +++++++------------ cliv2/pkg/core/contributor_billing_test.go | 38 +++++++------- cliv2/pkg/core/main.go | 12 +++-- 9 files changed, 58 insertions(+), 128 deletions(-) diff --git a/cliv2/go.mod b/cliv2/go.mod index e7d0c9ebf6..c6ad206b94 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -270,8 +270,8 @@ require ( // version 2491eb6c1c75 contains a valid license replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 -// Requires go-application-framework release with IANDT-237 (contributorbilling) and -// IANDT-238 (contributorcapture middleware). For local dev against ../../go-application-framework: +// Requires go-application-framework release with IANDT-237, IANDT-238, and IANDT-240 +// (contributorbilling, capture middleware, CaptureEngine). For local dev: // replace github.com/snyk/go-application-framework => ../../go-application-framework // replace github.com/snyk/snyk-ls => ../../snyk-ls diff --git a/cliv2/go.sum b/cliv2/go.sum index 157cd7ceb4..def86324c1 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -547,6 +547,8 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3 github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 h1:j2ZPhi78wKIHTiL9EFTNVXMIbsk56FVF2d5Sy1ZwSYk= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= +github.com/snyk/go-application-framework v0.7.2 h1:WzZ7BeFL0pKoB2YdheHtEgEdeB+9nunmwQP8XI+rKcc= +github.com/snyk/go-application-framework v0.7.2/go.mod h1:0YC7xCETnFTdz6rq8OQPL0aWekMLOkBQu1FE4/cReMA= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg= github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao= diff --git a/cliv2/internal/proxy/interceptor/networkinjector.go b/cliv2/internal/proxy/interceptor/networkinjector.go index eef41c797d..a1151ad375 100644 --- a/cliv2/internal/proxy/interceptor/networkinjector.go +++ b/cliv2/internal/proxy/interceptor/networkinjector.go @@ -1,18 +1,15 @@ package interceptor import ( - "context" - "net/http" - "regexp" - "github.com/elazarl/goproxy" "github.com/snyk/go-application-framework/pkg/workflow" + "net/http" + "regexp" ) type networkInjector struct { requestCondition goproxy.ReqCondition invocationCtx workflow.InvocationContext - requestContext context.Context } func (ni networkInjector) GetCondition() goproxy.ReqCondition { @@ -24,9 +21,6 @@ func (ni networkInjector) GetCondition() goproxy.ReqCondition { // and the gocli in two different places. func (ni networkInjector) GetHandler() goproxy.FuncReqHandler { return func(req *http.Request, proxyCtx *goproxy.ProxyCtx) (*http.Request, *http.Response) { - if ni.requestContext != nil { - req = req.WithContext(ni.requestContext) - } resp, err := ni.invocationCtx.GetNetworkAccess().GetRoundTripper().RoundTrip(req) if err != nil { ni.invocationCtx.GetEnhancedLogger().Trace().Msgf("intercepting call failed with error: %v", err) @@ -41,11 +35,10 @@ func (ni networkInjector) GetHandler() goproxy.FuncReqHandler { } } -func NewNetworkInjector(invocationCtx workflow.InvocationContext, requestContext context.Context) Interceptor { +func NewNetworkInjector(invocationCtx workflow.InvocationContext) Interceptor { i := networkInjector{ requestCondition: goproxy.UrlMatches(regexp.MustCompile(".*")), invocationCtx: invocationCtx, - requestContext: requestContext, } return i } diff --git a/cliv2/internal/proxy/interceptor/networkinjector_test.go b/cliv2/internal/proxy/interceptor/networkinjector_test.go index 69b086d91b..9a9633c0ed 100644 --- a/cliv2/internal/proxy/interceptor/networkinjector_test.go +++ b/cliv2/internal/proxy/interceptor/networkinjector_test.go @@ -1,7 +1,6 @@ package interceptor import ( - "context" "errors" "net/http" "testing" @@ -9,7 +8,6 @@ import ( "github.com/golang/mock/gomock" "github.com/rs/zerolog" "github.com/snyk/go-application-framework/pkg/mocks" - "github.com/snyk/go-application-framework/pkg/clibilling" "github.com/elazarl/goproxy" "github.com/stretchr/testify/assert" @@ -39,7 +37,7 @@ func TestNetworkInjector_ErrorHandling(t *testing.T) { invocationCtxMock.EXPECT().GetNetworkAccess().Return(networkAccessMock).AnyTimes() invocationCtxMock.EXPECT().GetEnhancedLogger().Return(&logger).AnyTimes() - ni := NewNetworkInjector(invocationCtxMock, context.Background()) + ni := NewNetworkInjector(invocationCtxMock) handler := ni.GetHandler() req := &http.Request{} @@ -58,45 +56,3 @@ func TestNetworkInjector_ErrorHandling(t *testing.T) { // Goproxy will send the request again if the response is nil, why it's imperative this does not happen. assert.Nil(t, resp, "response should not be nil when RoundTrip returns an error") } - -func TestNetworkInjector_AttachesRequestContext(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - logger := zerolog.Nop() - capture := clibilling.NewCapture() - requestContext := clibilling.WithCapture(context.Background(), capture) - - var capturedContext context.Context - roundTripperMock := mockRoundTripperWithContext{onRoundTrip: func(req *http.Request) { - capturedContext = req.Context() - }} - - networkAccessMock := mocks.NewMockNetworkAccess(ctrl) - networkAccessMock.EXPECT().GetRoundTripper().Return(roundTripperMock) - - invocationCtxMock := mocks.NewMockInvocationContext(ctrl) - invocationCtxMock.EXPECT().GetNetworkAccess().Return(networkAccessMock).AnyTimes() - invocationCtxMock.EXPECT().GetEnhancedLogger().Return(&logger).AnyTimes() - - ni := NewNetworkInjector(invocationCtxMock, requestContext) - handler := ni.GetHandler() - - req := &http.Request{} - proxyCtx := &goproxy.ProxyCtx{} - _, _ = handler(req, proxyCtx) - - assert.Equal(t, requestContext, capturedContext) - assert.Equal(t, capture, clibilling.FromContext(capturedContext)) -} - -type mockRoundTripperWithContext struct { - onRoundTrip func(*http.Request) -} - -func (m mockRoundTripperWithContext) RoundTrip(req *http.Request) (*http.Response, error) { - if m.onRoundTrip != nil { - m.onRoundTrip(req) - } - return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil -} diff --git a/cliv2/pkg/basic_workflows/legacycli.go b/cliv2/pkg/basic_workflows/legacycli.go index d739a64327..24e4a7ffac 100644 --- a/cliv2/pkg/basic_workflows/legacycli.go +++ b/cliv2/pkg/basic_workflows/legacycli.go @@ -3,7 +3,6 @@ package basic_workflows import ( "bufio" "bytes" - "context" "fmt" "io" "os" @@ -18,7 +17,6 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/snyk/go-application-framework/pkg/clibilling" "github.com/snyk/go-application-framework/pkg/configuration" pkg_utils "github.com/snyk/go-application-framework/pkg/utils" "github.com/snyk/go-application-framework/pkg/workflow" @@ -144,25 +142,18 @@ func legacycliWorkflow( cli.SetIoStreams(os.Stdin, os.Stdout, stderr) } - invocationCtx := invocation.Context() - if clibilling.FromContext(invocationCtx) == nil { - invocationCtx = clibilling.WithCapture(invocationCtx, clibilling.NewCapture()) - } - wrapperProxy, err := createInternalProxy( config, debugLogger, invocation, - invocationCtx, ) if err != nil { return output, err } - defer wrapperProxy.Close() // run the cli with context from invocation (allows cancellation on signal) proxyInfo := wrapperProxy.ProxyInfo() - err = cli.Execute(invocationCtx, proxyInfo, finalizeArguments(args, config.GetStringSlice(configuration.UNKNOWN_ARGS))) + err = cli.Execute(invocation.Context(), proxyInfo, finalizeArguments(args, config.GetStringSlice(configuration.UNKNOWN_ARGS))) if !useStdIo { _ = outWriter.Flush() @@ -186,12 +177,7 @@ func legacycliWorkflow( return output, err } -func createInternalProxy( - config configuration.Configuration, - debugLogger *zerolog.Logger, - invocation workflow.InvocationContext, - requestContext context.Context, -) (*proxy.WrapperProxy, error) { +func createInternalProxy(config configuration.Configuration, debugLogger *zerolog.Logger, invocation workflow.InvocationContext) (*proxy.WrapperProxy, error) { caData, err := GetGlobalCertAuthority(config, debugLogger) if err != nil { return nil, err @@ -207,7 +193,7 @@ func createInternalProxy( // The networkinjector intercepts all requests from the legacy CLI and re-routes them to the existing networking // layer. It should therefore be kept as the last interceptor in the chain, as it circuit breaks goproxy's own // routing. Any interceptor added later will not be called. - wrapperProxy.RegisterInterceptor(interceptor.NewNetworkInjector(invocation, requestContext)) + wrapperProxy.RegisterInterceptor(interceptor.NewNetworkInjector(invocation)) err = wrapperProxy.Start() if err != nil { diff --git a/cliv2/pkg/basic_workflows/legacycli_test.go b/cliv2/pkg/basic_workflows/legacycli_test.go index fc9e4ef938..2cc1fe008a 100644 --- a/cliv2/pkg/basic_workflows/legacycli_test.go +++ b/cliv2/pkg/basic_workflows/legacycli_test.go @@ -81,7 +81,7 @@ func Test_proxyWithErrorHandler(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { config.Set(configuration.API_URL, tc.configureApiUrl) - wp, err := createInternalProxy(config, &logger, invocationCtxMock, context.Background()) + wp, err := createInternalProxy(config, &logger, invocationCtxMock) assert.Nil(t, err) defer wp.Close() diff --git a/cliv2/pkg/core/contributor_billing.go b/cliv2/pkg/core/contributor_billing.go index 6b7c0aca13..a5634995cb 100644 --- a/cliv2/pkg/core/contributor_billing.go +++ b/cliv2/pkg/core/contributor_billing.go @@ -2,26 +2,22 @@ package core import ( "context" - "strings" - "sync" "github.com/snyk/go-application-framework/pkg/clibilling" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/workflow" ) -var ( - commandBillingMu sync.Mutex - commandBillingBag *clibilling.Capture - commandBillingRepoPath string -) - -func beginContributorBilling(ctx context.Context, config configuration.Configuration) context.Context { - commandBillingMu.Lock() - commandBillingBag = clibilling.NewCapture() - commandBillingRepoPath = billingRepoPath(config) - commandBillingMu.Unlock() - return clibilling.WithCapture(ctx, commandBillingBag) +func beginContributorBilling( + ctx context.Context, + engine workflow.Engine, + config configuration.Configuration, +) context.Context { + captureEngine, ok := clibilling.AsCaptureEngine(engine) + if !ok { + return ctx + } + return captureEngine.BeginContributorCommandFromConfig(ctx, config) } func finishContributorBilling( @@ -30,22 +26,13 @@ func finishContributorBilling( config configuration.Configuration, success bool, ) { - commandBillingMu.Lock() - bag := commandBillingBag - repoPath := commandBillingRepoPath - commandBillingBag = nil - commandBillingRepoPath = "" - commandBillingMu.Unlock() - - opts := clibilling.FinishOptionsFromConfig(config, engine) - opts.RepoPath = repoPath - clibilling.Finish(ctx, bag, opts, success) -} - -func billingRepoPath(config configuration.Configuration) string { - dirs := config.GetStringSlice(configuration.INPUT_DIRECTORY) - if len(dirs) > 0 && strings.TrimSpace(dirs[0]) != "" { - return dirs[0] + captureEngine, ok := clibilling.AsCaptureEngine(engine) + if !ok { + return } - return "." + captureEngine.FinishContributorCommand( + ctx, + clibilling.FinishOptionsFromConfig(config, engine), + success, + ) } diff --git a/cliv2/pkg/core/contributor_billing_test.go b/cliv2/pkg/core/contributor_billing_test.go index 89f12cc269..e72a807ea4 100644 --- a/cliv2/pkg/core/contributor_billing_test.go +++ b/cliv2/pkg/core/contributor_billing_test.go @@ -6,38 +6,38 @@ import ( "github.com/stretchr/testify/assert" + "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/clibilling" "github.com/snyk/go-application-framework/pkg/configuration" - "github.com/snyk/go-application-framework/pkg/workflow" ) -func Test_beginContributorBilling_attachesBagToContext(t *testing.T) { +func Test_beginContributorBilling_noOpWhenCaptureDisabled(t *testing.T) { t.Parallel() - config := configuration.NewWithOpts() - config.Set(configuration.INPUT_DIRECTORY, []string{"/tmp/repo"}) - - ctx := beginContributorBilling(context.Background(), config) - assert.NotNil(t, clibilling.FromContext(ctx)) + engine := app.CreateAppEngineWithOptions(app.WithConfiguration(configuration.NewWithOpts())) + ctx := beginContributorBilling(context.Background(), engine, configuration.NewWithOpts()) + assert.Nil(t, clibilling.FromContext(ctx)) } -func Test_finishContributorBilling_clearsActiveBag(t *testing.T) { +func Test_beginContributorBilling_attachesBagWhenCaptureEnabled(t *testing.T) { t.Parallel() config := configuration.NewWithOpts() - engine := workflow.NewDefaultWorkFlowEngine() - engine.SetConfiguration(config) - - ctx := beginContributorBilling(context.Background(), config) - finishContributorBilling(ctx, engine, config, false) + config.Set(configuration.INPUT_DIRECTORY, []string{"/tmp/repo"}) + engine := clibilling.EnableIfConfigured( + app.CreateAppEngineWithOptions( + app.WithConfiguration(config), + app.WithContributorBillingCapture(), + ), + ) + + ctx := beginContributorBilling(context.Background(), engine, config) + assert.NotNil(t, clibilling.FromContext(ctx)) } -func Test_billingRepoPath(t *testing.T) { +func Test_finishContributorBilling_noOpWhenCaptureDisabled(t *testing.T) { t.Parallel() - config := configuration.NewWithOpts() - assert.Equal(t, ".", billingRepoPath(config)) - - config.Set(configuration.INPUT_DIRECTORY, []string{"/tmp/repo"}) - assert.Equal(t, "/tmp/repo", billingRepoPath(config)) + engine := app.CreateAppEngineWithOptions(app.WithConfiguration(configuration.NewWithOpts())) + finishContributorBilling(context.Background(), engine, configuration.NewWithOpts(), true) } diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index 3dffed86dc..2228979c87 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -29,6 +29,7 @@ import ( "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/app" + "github.com/snyk/go-application-framework/pkg/clibilling" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/instrumentation" "github.com/snyk/go-application-framework/pkg/logging" @@ -212,7 +213,7 @@ func runMainWorkflow(config configuration.Configuration, cmd *cobra.Command, arg globalLogger.Print("Running ", name) globalEngine.GetAnalytics().SetCommand(name) - err = runWorkflowAndProcessData(beginContributorBilling(globalContext, config), globalEngine, globalLogger, name) + err = runWorkflowAndProcessData(beginContributorBilling(globalContext, globalEngine, config), globalEngine, globalLogger, name) return err } @@ -252,7 +253,7 @@ func defaultCmd(args []string) error { globalConfiguration.Set(configuration.RAW_CMD_ARGS, args) _, err := globalEngine.Invoke( basic_workflows.WORKFLOWID_LEGACY_CLI, - workflow.WithContext(beginContributorBilling(globalContext, globalConfiguration)), + workflow.WithContext(beginContributorBilling(globalContext, globalEngine, globalConfiguration)), ) return err } @@ -597,7 +598,12 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { debugEnabled := globalConfiguration.GetBool(configuration.DEBUG) globalLogger, scrubbedLogger = initDebugLogger(globalConfiguration) - globalEngine = app.CreateAppEngineWithOptions(app.WithZeroLogger(globalLogger), app.WithConfiguration(globalConfiguration), app.WithRuntimeInfo(rInfo)) + globalEngine = clibilling.EnableIfConfigured(app.CreateAppEngineWithOptions( + app.WithZeroLogger(globalLogger), + app.WithConfiguration(globalConfiguration), + app.WithRuntimeInfo(rInfo), + app.WithContributorBillingCapture(), + )) globalConfiguration.AddDefaultValue(configuration.FF_OAUTH_AUTH_FLOW_ENABLED, defaultOAuthFF(globalConfiguration)) globalConfiguration.AddDefaultValue(configuration.FF_TRANSFORMATION_WORKFLOW, configuration.StandardDefaultValueFunction(true)) From e0fbc5f8e328410326ee627306fd6ce96965be4f Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Mon, 3 Aug 2026 01:14:38 -0400 Subject: [PATCH 5/7] refactor(cliv2): call clibilling BeginCommand/FinishCommand directly [IANDT-240] Remove contributor_billing.go wrapper; host lifecycle helpers now live in GAF. Co-authored-by: Cursor --- cliv2/pkg/core/contributor_billing.go | 38 ------------------- cliv2/pkg/core/contributor_billing_test.go | 43 ---------------------- cliv2/pkg/core/main.go | 6 +-- 3 files changed, 3 insertions(+), 84 deletions(-) delete mode 100644 cliv2/pkg/core/contributor_billing.go delete mode 100644 cliv2/pkg/core/contributor_billing_test.go diff --git a/cliv2/pkg/core/contributor_billing.go b/cliv2/pkg/core/contributor_billing.go deleted file mode 100644 index a5634995cb..0000000000 --- a/cliv2/pkg/core/contributor_billing.go +++ /dev/null @@ -1,38 +0,0 @@ -package core - -import ( - "context" - - "github.com/snyk/go-application-framework/pkg/clibilling" - "github.com/snyk/go-application-framework/pkg/configuration" - "github.com/snyk/go-application-framework/pkg/workflow" -) - -func beginContributorBilling( - ctx context.Context, - engine workflow.Engine, - config configuration.Configuration, -) context.Context { - captureEngine, ok := clibilling.AsCaptureEngine(engine) - if !ok { - return ctx - } - return captureEngine.BeginContributorCommandFromConfig(ctx, config) -} - -func finishContributorBilling( - ctx context.Context, - engine workflow.Engine, - config configuration.Configuration, - success bool, -) { - captureEngine, ok := clibilling.AsCaptureEngine(engine) - if !ok { - return - } - captureEngine.FinishContributorCommand( - ctx, - clibilling.FinishOptionsFromConfig(config, engine), - success, - ) -} diff --git a/cliv2/pkg/core/contributor_billing_test.go b/cliv2/pkg/core/contributor_billing_test.go deleted file mode 100644 index e72a807ea4..0000000000 --- a/cliv2/pkg/core/contributor_billing_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package core - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/snyk/go-application-framework/pkg/app" - "github.com/snyk/go-application-framework/pkg/clibilling" - "github.com/snyk/go-application-framework/pkg/configuration" -) - -func Test_beginContributorBilling_noOpWhenCaptureDisabled(t *testing.T) { - t.Parallel() - - engine := app.CreateAppEngineWithOptions(app.WithConfiguration(configuration.NewWithOpts())) - ctx := beginContributorBilling(context.Background(), engine, configuration.NewWithOpts()) - assert.Nil(t, clibilling.FromContext(ctx)) -} - -func Test_beginContributorBilling_attachesBagWhenCaptureEnabled(t *testing.T) { - t.Parallel() - - config := configuration.NewWithOpts() - config.Set(configuration.INPUT_DIRECTORY, []string{"/tmp/repo"}) - engine := clibilling.EnableIfConfigured( - app.CreateAppEngineWithOptions( - app.WithConfiguration(config), - app.WithContributorBillingCapture(), - ), - ) - - ctx := beginContributorBilling(context.Background(), engine, config) - assert.NotNil(t, clibilling.FromContext(ctx)) -} - -func Test_finishContributorBilling_noOpWhenCaptureDisabled(t *testing.T) { - t.Parallel() - - engine := app.CreateAppEngineWithOptions(app.WithConfiguration(configuration.NewWithOpts())) - finishContributorBilling(context.Background(), engine, configuration.NewWithOpts(), true) -} diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index 2228979c87..af2d73ff35 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -213,7 +213,7 @@ func runMainWorkflow(config configuration.Configuration, cmd *cobra.Command, arg globalLogger.Print("Running ", name) globalEngine.GetAnalytics().SetCommand(name) - err = runWorkflowAndProcessData(beginContributorBilling(globalContext, globalEngine, config), globalEngine, globalLogger, name) + err = runWorkflowAndProcessData(clibilling.BeginCommand(globalContext, globalEngine, config), globalEngine, globalLogger, name) return err } @@ -253,7 +253,7 @@ func defaultCmd(args []string) error { globalConfiguration.Set(configuration.RAW_CMD_ARGS, args) _, err := globalEngine.Invoke( basic_workflows.WORKFLOWID_LEGACY_CLI, - workflow.WithContext(beginContributorBilling(globalContext, globalEngine, globalConfiguration)), + workflow.WithContext(clibilling.BeginCommand(globalContext, globalEngine, globalConfiguration)), ) return err } @@ -546,7 +546,7 @@ func tearDown(err error, errorList []error, startTime time.Time, ua networking.U writeLogFooter(exitCode, allErrors, globalConfiguration, networkAccess) } - finishContributorBilling(teardownCtx, globalEngine, globalConfiguration, exitCode == 0) + clibilling.FinishCommand(teardownCtx, globalEngine, globalConfiguration, exitCode == 0) return exitCode } From ea446e1a38280856529143bbb40b5b6c19048114 Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Mon, 3 Aug 2026 01:44:51 -0400 Subject: [PATCH 6/7] chore(cliv2): pin GAF pre-release for contributor billing [IANDT-240] Use pseudo-version from go-application-framework #681 so CI can compile pkg/clibilling before the tagged release. Co-authored-by: Cursor --- cliv2/go.mod | 7 +++---- cliv2/go.sum | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cliv2/go.mod b/cliv2/go.mod index c6ad206b94..71056f5bd3 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -22,7 +22,7 @@ require ( github.com/snyk/code-client-go v1.27.0 github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 - github.com/snyk/go-application-framework v0.7.2 + github.com/snyk/go-application-framework v0.10.1-0.20260803054435-e1e028c3dd98 github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 github.com/snyk/snyk-iac-capture v0.6.5 github.com/snyk/snyk-ls v0.0.0-20260626083941-77c2abaeaaaa @@ -270,9 +270,8 @@ require ( // version 2491eb6c1c75 contains a valid license replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 -// Requires go-application-framework release with IANDT-237, IANDT-238, and IANDT-240 -// (contributorbilling, capture middleware, CaptureEngine). For local dev: -// replace github.com/snyk/go-application-framework => ../../go-application-framework +// Pinned to GAF IANDT-237+238+240 pre-release; bump to tagged release after #681 merges. +// For local dev: replace github.com/snyk/go-application-framework => ../../go-application-framework // replace github.com/snyk/snyk-ls => ../../snyk-ls diff --git a/cliv2/go.sum b/cliv2/go.sum index def86324c1..5551ef07ae 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -547,8 +547,8 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3 github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 h1:j2ZPhi78wKIHTiL9EFTNVXMIbsk56FVF2d5Sy1ZwSYk= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= -github.com/snyk/go-application-framework v0.7.2 h1:WzZ7BeFL0pKoB2YdheHtEgEdeB+9nunmwQP8XI+rKcc= -github.com/snyk/go-application-framework v0.7.2/go.mod h1:0YC7xCETnFTdz6rq8OQPL0aWekMLOkBQu1FE4/cReMA= +github.com/snyk/go-application-framework v0.10.1-0.20260803054435-e1e028c3dd98 h1:ygqC0yh7WJyYYzEOiLAw4p28dEVg/Y2/vQct4X+7a/U= +github.com/snyk/go-application-framework v0.10.1-0.20260803054435-e1e028c3dd98/go.mod h1:9GV/CTAhM8PT9MbxwYt/Za7tKDtw/Wuq6SyCu1XFzvk= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg= github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao= From eba85b5578df1f1b3f6bb2a3e47faf7433891e58 Mon Sep 17 00:00:00 2001 From: Brett Gurman Date: Thu, 6 Aug 2026 04:01:55 -0400 Subject: [PATCH 7/7] feat(cliv2): lazy-open contributor billing via middleware [IANDT-240] Drop BeginCommand and WithContributorBillingCapture; capture opens on first billable HTTP and FinishCommand at teardown emits. Pin GAF to lazy-open stack. Co-authored-by: Cursor --- cliv2/go.mod | 2 +- cliv2/go.sum | 4 ++-- cliv2/pkg/core/main.go | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cliv2/go.mod b/cliv2/go.mod index 71056f5bd3..e32f161fa2 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -22,7 +22,7 @@ require ( github.com/snyk/code-client-go v1.27.0 github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 - github.com/snyk/go-application-framework v0.10.1-0.20260803054435-e1e028c3dd98 + github.com/snyk/go-application-framework v0.10.1-0.20260806080023-f868e847d10f github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 github.com/snyk/snyk-iac-capture v0.6.5 github.com/snyk/snyk-ls v0.0.0-20260626083941-77c2abaeaaaa diff --git a/cliv2/go.sum b/cliv2/go.sum index 5551ef07ae..21a893ff4e 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -547,8 +547,8 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3 github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 h1:j2ZPhi78wKIHTiL9EFTNVXMIbsk56FVF2d5Sy1ZwSYk= github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= -github.com/snyk/go-application-framework v0.10.1-0.20260803054435-e1e028c3dd98 h1:ygqC0yh7WJyYYzEOiLAw4p28dEVg/Y2/vQct4X+7a/U= -github.com/snyk/go-application-framework v0.10.1-0.20260803054435-e1e028c3dd98/go.mod h1:9GV/CTAhM8PT9MbxwYt/Za7tKDtw/Wuq6SyCu1XFzvk= +github.com/snyk/go-application-framework v0.10.1-0.20260806080023-f868e847d10f h1:amQet+3+p2q9+CVscPmne/X8FCzLDf72RQnyA4gdnzU= +github.com/snyk/go-application-framework v0.10.1-0.20260806080023-f868e847d10f/go.mod h1:9GV/CTAhM8PT9MbxwYt/Za7tKDtw/Wuq6SyCu1XFzvk= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk= github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg= github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao= diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index af2d73ff35..06f874a54d 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -213,7 +213,7 @@ func runMainWorkflow(config configuration.Configuration, cmd *cobra.Command, arg globalLogger.Print("Running ", name) globalEngine.GetAnalytics().SetCommand(name) - err = runWorkflowAndProcessData(clibilling.BeginCommand(globalContext, globalEngine, config), globalEngine, globalLogger, name) + err = runWorkflowAndProcessData(globalContext, globalEngine, globalLogger, name) return err } @@ -253,7 +253,7 @@ func defaultCmd(args []string) error { globalConfiguration.Set(configuration.RAW_CMD_ARGS, args) _, err := globalEngine.Invoke( basic_workflows.WORKFLOWID_LEGACY_CLI, - workflow.WithContext(clibilling.BeginCommand(globalContext, globalEngine, globalConfiguration)), + workflow.WithContext(globalContext), ) return err } @@ -602,7 +602,6 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { app.WithZeroLogger(globalLogger), app.WithConfiguration(globalConfiguration), app.WithRuntimeInfo(rInfo), - app.WithContributorBillingCapture(), )) globalConfiguration.AddDefaultValue(configuration.FF_OAUTH_AUTH_FLOW_ENABLED, defaultOAuthFF(globalConfiguration))