Repository: safedep/vet (go)

Issue:
Add Sync Reporter Environment Resolver for GitHub
The sync reporter syncs `vet` data with SafeDep Cloud. This reporter need _optional_ input from the environment such as source (GitHub, Gitlab, Git etc), repository URL, trigger etc. The contract is defined as:

https://github.com/safedep/vet/blob/main/pkg/reporter/sync.go#L33

Currently we are using a default resolver which just returns empty values.

We need to add support for different CI resolvers such as GitHub, Gitlab etc. To start with, let us support only GitHub because thats where our primary user base exist.

Add support for GitHub Action environment resolver based on GitHub Actions default environment variables.

We also need to add a factory function such as `NewSyncReporterEnvironmentResolver` that leverages some heuristic to check if we are running in GitHub Actions, if so, return the GHA environment resolver.

We should fallback to the existing default resolver (No Op) if GHA environment is not detected.

Pull request description:
Add github actions sync resolver
Fixes #530


Patch:
diff --git a/pkg/reporter/github_sync_resolver.go b/pkg/reporter/github_sync_resolver.go
new file mode 100644
index 0000000..1c27b94
--- /dev/null
+++ b/pkg/reporter/github_sync_resolver.go
@@ -0,0 +1,53 @@
+package reporter
+
+import (
+	"os"
+
+	controltowerv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
+	controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/controltower/v1"
+)
+
+type githubActionResolver struct{}
+
+func GithubActionsSyncReporterResolver() SyncReporterEnvResolver {
+	return &githubActionResolver{}
+}
+
+var _ SyncReporterEnvResolver = &githubActionResolver{}
+
+func (g *githubActionResolver) GetProjectSource() controltowerv1pb.Project_Source {
+	return controltowerv1pb.Project_SOURCE_GITHUB
+}
+
+func (g *githubActionResolver) GetProjectURL() string {
+	return os.Getenv("GITHUB_SERVER_URL") + "/" + os.Getenv("GITHUB_REPOSITORY")
+}
+
+func (g *githubActionResolver) GitRef() string {
+	return os.Getenv("GITHUB_REF")
+}
+
+func (g *githubActionResolver) GitSha() string {
+	return os.Getenv("GITHUB_SHA")
+}
+
+func (g *githubActionResolver) Trigger() controltowerv1.ToolTrigger {
+	switch eventName := os.Getenv("GITHUB_EVENT_NAME"); eventName {
+	case "push":
+		return controltowerv1.ToolTrigger_TOOL_TRIGGER_PUSH
+	case "pull_request", "pull_request_target":
+		return controltowerv1.ToolTrigger_TOOL_TRIGGER_PULL_REQUEST
+	case "create":
+		// In GitHub Actions, 'create' event with ref_type=tag indicates a tag was created
+		if os.Getenv("GITHUB_REF_TYPE") == "tag" {
+			return controltowerv1.ToolTrigger_TOOL_TRIGGER_TAG
+		}
+		return controltowerv1.ToolTrigger_TOOL_TRIGGER_UNSPECIFIED
+	case "schedule":
+		return controltowerv1.ToolTrigger_TOOL_TRIGGER_SCHEDULED
+	case "workflow_dispatch", "repository_dispatch":
+		return controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL
+	default:
+		return controltowerv1.ToolTrigger_TOOL_TRIGGER_UNSPECIFIED
+	}
+}
diff --git a/pkg/reporter/sync.go b/pkg/reporter/sync.go
index ce1e435..1f7ea2b 100644
--- a/pkg/reporter/sync.go
+++ b/pkg/reporter/sync.go
@@ -3,6 +3,7 @@ package reporter
 import (
 	"context"
 	"fmt"
+	"os"
 	"strings"
 	"sync"
 
@@ -187,6 +188,15 @@ type syncReporter struct {
 // Verify syncReporter implements the Reporter interface
 var _ Reporter = (*syncReporter)(nil)
 
+func NewSyncReporterEnvironmentResolver() SyncReporterEnvResolver {
+	// The `GITHUB_ACTIONS` environment variable is always set to true when GitHub Actions is running the workflow
+	if _, exists := os.LookupEnv("GITHUB_ACTIONS"); exists {
+		return GithubActionsSyncReporterResolver()
+	}
+
+	return DefaultSyncReporterEnvResolver()
+}
+
 // NewSyncReporter creates a new sync reporter.
 func NewSyncReporter(config SyncReporterConfig, envResolver SyncReporterEnvResolver, callbacks SyncReporterCallbacks) (*syncReporter, error) {
 	if config.ClientConnection == nil {
diff --git a/scan.go b/scan.go
index 1f4e805..cde0375 100644
--- a/scan.go
+++ b/scan.go
@@ -679,7 +679,7 @@ func internalStartScan() error {
 			ProjectVersion:         syncReportStream,
 			EnableMultiProjectSync: syncEnableMultiProject,
 			ClientConnection:       clientConn,
-		}, reporter.DefaultSyncReporterEnvResolver(), reporter.SyncReporterCallbacks{
+		}, reporter.NewSyncReporterEnvironmentResolver(), reporter.SyncReporterCallbacks{
 			OnSyncStart: func() {
 				ui.PrintMsg("🌐 Syncing data to SafeDep Cloud...")
 			},


Test patch:
diff --git a/pkg/reporter/sync_test.go b/pkg/reporter/sync_test.go
index 70b9b13..d650328 100644
--- a/pkg/reporter/sync_test.go
+++ b/pkg/reporter/sync_test.go
@@ -3,6 +3,7 @@ package reporter
 import (
 	"context"
 	"errors"
+	"os"
 	"testing"
 
 	controltowerv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
@@ -111,6 +112,77 @@ func mockDependencyGraph(pkg *models.Package) {
 	pkg.Manifest.DependencyGraph = dg
 }
 
+func TestNewSyncReporterEnvironmentResolver(t *testing.T) {
+	tests := []struct {
+		name                  string
+		setupEnv              func(t *testing.T)
+		expectedProjectSource controltowerv1pb.Project_Source
+		expectedProjectUrl    string
+		expectedTrigger       controltowerv1.ToolTrigger
+		expectedGitRef        string
+		expectedGitSha        string
+	}{
+		{
+			name: "should return GitHub Actions values when GITHUB_ACTIONS is set",
+			setupEnv: func(t *testing.T) {
+				t.Setenv("GITHUB_ACTIONS", "true")
+				t.Setenv("GITHUB_SERVER_URL", "https://github.com")
+				t.Setenv("GITHUB_REPOSITORY", "safedep/vet")
+				t.Setenv("GITHUB_EVENT_NAME", "push")
+				t.Setenv("GITHUB_REF", "refs/heads/main")
+				t.Setenv("GITHUB_SHA", "abc123")
+			},
+			expectedProjectSource: controltowerv1pb.Project_SOURCE_GITHUB,
+			expectedTrigger:       controltowerv1.ToolTrigger_TOOL_TRIGGER_PUSH,
+			expectedProjectUrl:    "https://github.com/safedep/vet",
+			expectedGitRef:        "refs/heads/main",
+			expectedGitSha:        "abc123",
+		},
+		{
+			name: "should return default values when GITHUB_ACTIONS is not set",
+			setupEnv: func(t *testing.T) {
+				// No environment setup needed
+				os.Unsetenv("GITHUB_ACTIONS")
+				os.Unsetenv("GITHUB_REPOSITORY")
+				os.Unsetenv("GITHUB_SERVER_URL")
+				os.Unsetenv("GITHUB_REF")
+				os.Unsetenv("GITHUB_SHA")
+				os.Unsetenv("GITHUB_EVENT_NAME")
+			},
+			expectedProjectSource: controltowerv1pb.Project_SOURCE_UNSPECIFIED,
+			expectedTrigger:       controltowerv1.ToolTrigger_TOOL_TRIGGER_MANUAL,
+			expectedProjectUrl:    "",
+			expectedGitRef:        "",
+			expectedGitSha:        "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Setup test environment
+			tt.setupEnv(t)
+
+			// Get the resolver
+			resolver := NewSyncReporterEnvironmentResolver()
+
+			source := resolver.GetProjectSource()
+			assert.Equal(t, tt.expectedProjectSource, source, "unexpected project source")
+
+			url := resolver.GetProjectURL()
+			assert.Equal(t, tt.expectedProjectUrl, url, "unexpected project URL")
+
+			trigger := resolver.Trigger()
+			assert.Equal(t, tt.expectedTrigger, trigger, "unexpected trigger")
+
+			gitRef := resolver.GitRef()
+			assert.Equal(t, tt.expectedGitRef, gitRef, "unexpected git ref")
+
+			gitSha := resolver.GitSha()
+			assert.Equal(t, tt.expectedGitSha, gitSha, "unexpected git sha")
+		})
+	}
+}
+
 func TestSyncPackage(t *testing.T) {
 	tests := []struct {
 		name          string