diff --git a/.github/workflows/cgo.yml b/.github/workflows/cgo.yml index 15f618ef359..2680da87f2f 100644 --- a/.github/workflows/cgo.yml +++ b/.github/workflows/cgo.yml @@ -1119,7 +1119,7 @@ jobs: # Enforce selected production custom analyzers without blocking on unrelated # legacy custom analyzer findings in tests or other analyzer families. - name: Run custom linters - run: make golint-custom LINTER_FLAGS="-errstringmatch -panicinlibrarycode -manualmutexunlock -osexitinlibrary -rawloginlib -regexpcompileinfunction -fprintlnsprintf -strconvparseignorederror -jsonmarshalignoredeerror -uncheckedtypeassertion -fmterrorfnoverbs -tolowerequalfold -test=false" + run: make golint-custom LINTER_FLAGS="-errstringmatch -panicinlibrarycode -manualmutexunlock -osexitinlibrary -rawloginlib -regexpcompileinfunction -fprintlnsprintf -strconvparseignorederror -jsonmarshalignoredeerror -uncheckedtypeassertion -fmterrorfnoverbs -tolowerequalfold -httpnoctx -test=false" # Ensure no action shell scripts invoke python or python3 - name: Lint action shell scripts diff --git a/pkg/cli/deps_outdated.go b/pkg/cli/deps_outdated.go index b054a9dd8d6..667bcc5ecb7 100644 --- a/pkg/cli/deps_outdated.go +++ b/pkg/cli/deps_outdated.go @@ -1,6 +1,7 @@ package cli import ( + "context" "encoding/json" "fmt" "io" @@ -39,7 +40,7 @@ type ProxyInfo struct { } // CheckOutdatedDependencies analyzes go.mod for outdated dependencies -func CheckOutdatedDependencies(verbose bool) ([]OutdatedDependency, error) { +func CheckOutdatedDependencies(ctx context.Context, verbose bool) ([]OutdatedDependency, error) { depsOutdatedLog.Print("Starting outdated dependency check") // Find go.mod file @@ -63,7 +64,7 @@ func CheckOutdatedDependencies(verbose bool) ([]OutdatedDependency, error) { // Check each dependency for updates var outdated []OutdatedDependency for _, dep := range deps { - latest, age, err := getLatestVersion(dep.Path, dep.Version, verbose) + latest, age, err := getLatestVersion(ctx, dep.Path, dep.Version, verbose) if err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: could not check %s: %v", dep.Path, err))) @@ -159,14 +160,18 @@ func parseGoMod(path string) ([]DependencyInfo, error) { } // getLatestVersion queries the Go proxy for the latest version -func getLatestVersion(modulePath, currentVersion string, verbose bool) (string, time.Duration, error) { +func getLatestVersion(ctx context.Context, modulePath, currentVersion string, verbose bool) (string, time.Duration, error) { depsOutdatedLog.Printf("Checking latest version for %s (current: %s)", modulePath, currentVersion) // Query Go proxy API url := fmt.Sprintf("https://proxy.golang.org/%s/@latest", modulePath) client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Get(url) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", 0, err + } + resp, err := client.Do(req) if err != nil { return "", 0, err } diff --git a/pkg/cli/deps_report.go b/pkg/cli/deps_report.go index 0fbe7e02411..08696bd3344 100644 --- a/pkg/cli/deps_report.go +++ b/pkg/cli/deps_report.go @@ -68,7 +68,7 @@ func GenerateDependencyReport(ctx context.Context, verbose bool) (*DependencyRep } // Check for outdated dependencies (only direct) - outdated, err := CheckOutdatedDependencies(verbose) + outdated, err := CheckOutdatedDependencies(ctx, verbose) if err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: could not check outdated dependencies: %v", err))) diff --git a/pkg/cli/mcp_inspect.go b/pkg/cli/mcp_inspect.go index 680de93eba1..51de62ad520 100644 --- a/pkg/cli/mcp_inspect.go +++ b/pkg/cli/mcp_inspect.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "fmt" "maps" @@ -26,7 +27,7 @@ var mcpInspectLog = logger.New("cli:mcp_inspect") const mcpScriptsServerShutdownDelay = 500 * time.Millisecond // InspectWorkflowMCP inspects MCP servers used by a workflow and lists available tools, resources, and roots -func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter string, verbose bool, useActionsSecrets bool) error { +func InspectWorkflowMCP(ctx context.Context, workflowFile string, serverFilter string, toolFilter string, verbose bool, useActionsSecrets bool) error { mcpInspectLog.Printf("Inspecting workflow MCP: workflow=%s, serverFilter=%s, toolFilter=%s", workflowFile, serverFilter, toolFilter) @@ -110,7 +111,7 @@ func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter str var mcpScriptsTmpDir string if workflowData != nil && workflowData.MCPScripts != nil && len(workflowData.MCPScripts.Tools) > 0 { // Start mcp-scripts server and add it to the list of MCP configs - config, serverCmd, tmpDir, err := startMCPScriptsServer(workflowData.MCPScripts, verbose) + config, serverCmd, tmpDir, err := startMCPScriptsServer(ctx, workflowData.MCPScripts, verbose) if err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to start mcp-scripts server: %v", err))) @@ -296,7 +297,7 @@ The command will: return spawnMCPInspector(workflowFile, serverFilter, verbose) } - return InspectWorkflowMCP(workflowFile, serverFilter, toolFilter, verbose, checkSecrets) + return InspectWorkflowMCP(cmd.Context(), workflowFile, serverFilter, toolFilter, verbose, checkSecrets) }, } diff --git a/pkg/cli/mcp_inspect_mcp_scripts_server.go b/pkg/cli/mcp_inspect_mcp_scripts_server.go index 15a19368c57..1cc7cf0c8ff 100644 --- a/pkg/cli/mcp_inspect_mcp_scripts_server.go +++ b/pkg/cli/mcp_inspect_mcp_scripts_server.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "fmt" "net" @@ -43,7 +44,7 @@ func findAvailablePort(startPort int, verbose bool) int { } // waitForServerReady waits for the HTTP server to be ready by polling the endpoint -func waitForServerReady(port int, timeout time.Duration, verbose bool) bool { +func waitForServerReady(ctx context.Context, port int, timeout time.Duration, verbose bool) bool { deadline := time.Now().Add(timeout) client := &http.Client{ Timeout: 1 * time.Second, @@ -51,7 +52,17 @@ func waitForServerReady(port int, timeout time.Duration, verbose bool) bool { url := fmt.Sprintf("http://localhost:%d/", port) for time.Now().Before(deadline) { - resp, err := client.Get(url) + select { + case <-ctx.Done(): + return false + default: + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + mcpInspectLog.Printf("Failed to create request: %v", err) + return false + } + resp, err := client.Do(req) if err == nil { if closeErr := resp.Body.Close(); closeErr != nil { mcpInspectLog.Printf("Warning: failed to close response body: %v", closeErr) @@ -61,7 +72,11 @@ func waitForServerReady(port int, timeout time.Duration, verbose bool) bool { } return true } - time.Sleep(mcpScriptsServerStartupDelay) + select { + case <-ctx.Done(): + return false + case <-time.After(mcpScriptsServerStartupDelay): + } } mcpInspectLog.Printf("Server did not become ready within timeout") @@ -100,7 +115,7 @@ func startMCPScriptsHTTPServer(dir string, port int, verbose bool) (*exec.Cmd, e } // startMCPScriptsServer starts the mcp-scripts HTTP server and returns the MCP config -func startMCPScriptsServer(mcpScriptsConfig *workflow.MCPScriptsConfig, verbose bool) (*parser.RegistryMCPServerConfig, *exec.Cmd, string, error) { +func startMCPScriptsServer(ctx context.Context, mcpScriptsConfig *workflow.MCPScriptsConfig, verbose bool) (*parser.RegistryMCPServerConfig, *exec.Cmd, string, error) { mcpInspectLog.Printf("Starting mcp-scripts server with %d tools", len(mcpScriptsConfig.Tools)) // Check if node is available @@ -165,7 +180,7 @@ func startMCPScriptsServer(mcpScriptsConfig *workflow.MCPScriptsConfig, verbose } // Wait for the server to start up - if !waitForServerReady(port, 5*time.Second, verbose) { + if !waitForServerReady(ctx, port, 5*time.Second, verbose) { if serverCmd.Process != nil { // Kill the process and log warning if it fails if err := serverCmd.Process.Kill(); err != nil && verbose { diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index c1cff456d3e..351fe303a5a 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -3,6 +3,7 @@ package parser import ( + "context" "encoding/base64" "encoding/json" "errors" @@ -554,7 +555,7 @@ func buildCommitLookupAPIPath(owner, repo, ref string) string { // downloadFileViaGit downloads a file from a Git repository using git commands // This is a fallback for when GitHub API authentication fails -func downloadFileViaGit(owner, repo, path, ref, host string) ([]byte, error) { +func downloadFileViaGit(ctx context.Context, owner, repo, path, ref, host string) ([]byte, error) { remoteLog.Printf("Attempting git fallback for %s/%s/%s@%s", owner, repo, path, ref) // First, try via raw.githubusercontent.com — no auth required for public repos and @@ -562,7 +563,7 @@ func downloadFileViaGit(owner, repo, path, ref, host string) ([]byte, error) { // Only attempt raw URL for github.com repos (not GHE) since raw.githubusercontent.com // only serves public GitHub content. if host == "" || host == "github.com" { - content, rawErr := downloadFileViaRawURL(owner, repo, path, ref) + content, rawErr := downloadFileViaRawURL(ctx, owner, repo, path, ref) if rawErr == nil { return content, nil } @@ -601,7 +602,7 @@ func downloadFileViaGit(owner, repo, path, ref, host string) ([]byte, error) { // downloadFileViaRawURL fetches a file using the raw.githubusercontent.com URL. // This requires no authentication for public repositories and no git installation. -func downloadFileViaRawURL(owner, repo, filePath, ref string) ([]byte, error) { +func downloadFileViaRawURL(ctx context.Context, owner, repo, filePath, ref string) ([]byte, error) { rawURL := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", owner, repo, ref, filePath) remoteLog.Printf("Attempting raw URL download: %s", rawURL) @@ -610,7 +611,11 @@ func downloadFileViaRawURL(owner, repo, filePath, ref string) ([]byte, error) { // #nosec G107 -- rawURL is constructed from workflow import configuration authored by // the developer; the owner, repo, filePath, and ref are user-supplied workflow spec fields. - resp, err := rawClient.Get(rawURL) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) + if err != nil { + return nil, fmt.Errorf("raw URL request failed for %s: %w", rawURL, err) + } + resp, err := rawClient.Do(req) if err != nil { return nil, fmt.Errorf("raw URL request failed for %s: %w", rawURL, err) } @@ -851,7 +856,7 @@ func downloadFileFromGitHubWithDepth(owner, repo, path, ref string, symlinkDepth if err != nil { if gitutil.IsAuthError(err.Error()) { remoteLog.Printf("REST client creation failed due to auth error, attempting git fallback for %s/%s/%s@%s: %v", owner, repo, path, ref, err) - content, gitErr := downloadFileViaGit(owner, repo, path, ref, host) + content, gitErr := downloadFileViaGit(context.Background(), owner, repo, path, ref, host) if gitErr != nil { remoteLog.Printf("Git fallback also failed for %s/%s/%s@%s: %v", owner, repo, path, ref, gitErr) return nil, fmt.Errorf("failed to fetch file content: %w", err) @@ -871,7 +876,7 @@ func downloadFileFromGitHubWithDepth(owner, repo, path, ref string, symlinkDepth if err != nil { if gitutil.IsAuthError(err.Error()) { remoteLog.Printf("GitHub API authentication failed, attempting git fallback for %s/%s/%s@%s", owner, repo, path, ref) - content, gitErr := downloadFileViaGit(owner, repo, path, ref, host) + content, gitErr := downloadFileViaGit(context.Background(), owner, repo, path, ref, host) if gitErr != nil { return nil, fmt.Errorf("failed to fetch file content via GitHub API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) }