From 4b8809a2f88432a5958d0fcb487faa55b8027f21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 03:55:29 +0000 Subject: [PATCH 1/2] Initial plan From 61754e15ba511656e359ed074574077ba1a4b043 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 04:09:37 +0000 Subject: [PATCH 2/2] fix: resource lifecycle and context propagation fixes (lint-monster) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/token_usage.go | 107 +++++++++++++++------------ pkg/parser/remote_fetch.go | 4 +- pkg/workflow/action_resolver.go | 39 +++++++--- pkg/workflow/safe_outputs_actions.go | 14 ++-- 4 files changed, 99 insertions(+), 65 deletions(-) diff --git a/pkg/cli/token_usage.go b/pkg/cli/token_usage.go index cca3620de64..a8cbb315a4b 100644 --- a/pkg/cli/token_usage.go +++ b/pkg/cli/token_usage.go @@ -613,62 +613,77 @@ func sumAICFromUsageJSONLFiles(filePaths []string) (float64, bool, error) { found := false for _, filePath := range filePaths { - file, err := os.Open(filepath.Clean(filePath)) + fileAIC, fileFound, err := processOneUsageJSONLFile(filePath) if err != nil { - return 0, false, fmt.Errorf("failed to open usage JSONL file %s: %w", filePath, err) + return 0, false, err } + totalAIC += fileAIC + if fileFound { + found = true + } + } - scanner := bufio.NewScanner(file) - scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || !strings.HasPrefix(line, "{") { - continue - } + return totalAIC, found, nil +} - var parsed map[string]any - if err := json.Unmarshal([]byte(line), &parsed); err != nil { - continue - } +// processOneUsageJSONLFile reads a single usage JSONL file and returns the total AIC +// accumulated from its records. The file is deferred-closed immediately after open. +func processOneUsageJSONLFile(filePath string) (total float64, found bool, err error) { + file, err := os.Open(filepath.Clean(filePath)) + if err != nil { + return 0, false, fmt.Errorf("failed to open usage JSONL file %s: %w", filePath, err) + } + defer func() { + if closeErr := file.Close(); closeErr != nil && err == nil { + err = fmt.Errorf("failed to close usage JSONL file %s: %w", filePath, closeErr) + } + }() - usage := extractUsageRecord(parsed["usage"]) - explicitAICredits := usageNumericValue(parsed, usage, "ai_credits", "aiCredits") - if explicitAICredits > 0 { - totalAIC += explicitAICredits - found = true - continue - } - explicitAIC := usageNumericValue(parsed, usage, "aic") - if explicitAIC > 0 { - totalAIC += explicitAIC - found = true - continue - } + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || !strings.HasPrefix(line, "{") { + continue + } - computedAIC := computeModelInferenceAIC( - usageStringValue(parsed, usage, "provider"), - usageStringValue(parsed, usage, "model"), - int(usageNumericValue(parsed, usage, "input_tokens", "inputTokens")), - int(usageNumericValue(parsed, usage, "output_tokens", "outputTokens")), - int(usageNumericValue(parsed, usage, "cache_read_tokens", "cacheReadTokens")), - int(usageNumericValue(parsed, usage, "cache_write_tokens", "cacheWriteTokens")), - int(usageNumericValue(parsed, usage, "reasoning_tokens", "reasoningTokens")), - ) - if computedAIC > 0 { - totalAIC += computedAIC - found = true - } + var parsed map[string]any + if jsonErr := json.Unmarshal([]byte(line), &parsed); jsonErr != nil { + continue } - closeErr := file.Close() - if err := scanner.Err(); err != nil { - return 0, false, fmt.Errorf("error reading usage JSONL file %s: %w", filePath, err) + + usage := extractUsageRecord(parsed["usage"]) + explicitAICredits := usageNumericValue(parsed, usage, "ai_credits", "aiCredits") + if explicitAICredits > 0 { + total += explicitAICredits + found = true + continue } - if closeErr != nil { - return 0, false, fmt.Errorf("failed to close usage JSONL file %s: %w", filePath, closeErr) + explicitAIC := usageNumericValue(parsed, usage, "aic") + if explicitAIC > 0 { + total += explicitAIC + found = true + continue } - } - return totalAIC, found, nil + computedAIC := computeModelInferenceAIC( + usageStringValue(parsed, usage, "provider"), + usageStringValue(parsed, usage, "model"), + int(usageNumericValue(parsed, usage, "input_tokens", "inputTokens")), + int(usageNumericValue(parsed, usage, "output_tokens", "outputTokens")), + int(usageNumericValue(parsed, usage, "cache_read_tokens", "cacheReadTokens")), + int(usageNumericValue(parsed, usage, "cache_write_tokens", "cacheWriteTokens")), + int(usageNumericValue(parsed, usage, "reasoning_tokens", "reasoningTokens")), + ) + if computedAIC > 0 { + total += computedAIC + found = true + } + } + if scanErr := scanner.Err(); scanErr != nil { + return 0, false, fmt.Errorf("error reading usage JSONL file %s: %w", filePath, scanErr) + } + return total, found, nil } // analyzeTokenUsageAICOnly parses token usage inputs and computes only TotalAIC. diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index d317a4fb979..613e08edd53 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -624,8 +624,8 @@ func downloadFileViaGit(ctx context.Context, owner, repo, path, ref, host string // git archive command: git archive --remote= // #nosec G204 -- repoURL, ref, and path are from workflow import configuration authored by the - // developer; exec.Command with separate args (not shell execution) prevents shell injection. - cmd := exec.Command("git", "archive", "--remote="+repoURL, ref, path) + // developer; exec.CommandContext with separate args (not shell execution) prevents shell injection. + cmd := exec.CommandContext(ctx, "git", "archive", "--remote="+repoURL, ref, path) archiveOutput, err := cmd.Output() if err != nil { // If git archive fails, try with git clone + git show as a fallback diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index 9c6a187e2eb..424b072eebc 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -222,21 +222,13 @@ func (r *ActionResolver) resolveFromGitHub(ctx context.Context, repo, version st return "", fmt.Errorf("failed to resolve %s@%s: exceeded max tag peel depth %d", repo, version, maxTagPeelDepth) } resolverLog.Printf("Detected annotated tag for %s@%s (depth %d, tag object SHA: %s), peeling to underlying object", repo, version, depth, sha) - tagPath := fmt.Sprintf("/repos/%s/git/tags/%s", baseRepo, sha) // Each peel gets its own fresh 30-second timeout derived from the original // caller context (ctx), not from callCtx, so we don't accidentally shrink - // the budget for subsequent peels. - peelCtx, peelCancel := context.WithTimeout(ctx, 30*time.Second) - cmd2 := ExecGHContext(peelCtx, "api", tagPath, "--jq", "[.object.sha, .object.type] | @tsv") - ForceGHHostEnv(cmd2, "github.com") - output2, peelErr := cmd2.Output() - peelCancel() - if peelErr != nil { - return "", fmt.Errorf("failed to peel annotated tag %s@%s: %w", repo, version, peelErr) - } - sha, objType, err = ParseTagRefTSV(string(output2)) + // the budget for subsequent peels. peelTagObject defers cancel within its + // own function scope to satisfy resource-lifecycle lint rules. + sha, objType, err = peelTagObject(ctx, baseRepo, repo, version, sha) if err != nil { - return "", fmt.Errorf("failed to parse peeled tag API response for %s@%s: %w", repo, version, err) + return "", err } } resolverLog.Printf("Resolved %s@%s to %s SHA: %s", repo, version, objType, sha) @@ -244,6 +236,29 @@ func (r *ActionResolver) resolveFromGitHub(ctx context.Context, repo, version st return sha, nil } +// peelTagObject resolves a single annotated-tag object to its underlying object by +// querying the GitHub API. It is called iteratively for chained tag objects. +// The timeout context is created and immediately deferred within this function so +// that cancel is always called when the function returns (not deferred inside a loop). +func peelTagObject(ctx context.Context, baseRepo, repo, version, sha string) (newSHA, objType string, err error) { + tagPath := fmt.Sprintf("/repos/%s/git/tags/%s", baseRepo, sha) + // Each peel gets a fresh 30-second timeout derived from the original caller + // context so we don't accidentally shrink the budget across iterations. + peelCtx, peelCancel := context.WithTimeout(ctx, 30*time.Second) + defer peelCancel() + cmd := ExecGHContext(peelCtx, "api", tagPath, "--jq", "[.object.sha, .object.type] | @tsv") + ForceGHHostEnv(cmd, "github.com") + output, peelErr := cmd.Output() + if peelErr != nil { + return "", "", fmt.Errorf("failed to peel annotated tag %s@%s: %w", repo, version, peelErr) + } + newSHA, objType, err = ParseTagRefTSV(string(output)) + if err != nil { + return "", "", fmt.Errorf("failed to parse peeled tag API response for %s@%s: %w", repo, version, err) + } + return newSHA, objType, nil +} + // ResolveGhAwRef resolves a branch, tag, or SHA ref in the github/gh-aw // repository to its full 40-character commit SHA. // If ref is already a valid full SHA it is returned unchanged. diff --git a/pkg/workflow/safe_outputs_actions.go b/pkg/workflow/safe_outputs_actions.go index 896d706c30a..a513f557208 100644 --- a/pkg/workflow/safe_outputs_actions.go +++ b/pkg/workflow/safe_outputs_actions.go @@ -287,11 +287,15 @@ func fetchRemoteActionYAML(repo, subdir, ref string) (*actionYAMLFile, error) { apiPath := fmt.Sprintf("/repos/%s/contents/%s?ref=%s", repo, contentPath, ref) safeOutputActionsLog.Printf("Fetching action YAML from: %s", apiPath) - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - cmd := ExecGHContext(ctx, "api", apiPath, "--jq", ".content") - output, err := cmd.Output() - cancel() + // Wrap the context creation and command execution in a function literal so that + // defer cancel() runs when the literal returns (not when the outer function returns), + // avoiding a defer-inside-loop resource-lifecycle issue. + output, err := func() ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + cmd := ExecGHContext(ctx, "api", apiPath, "--jq", ".content") + return cmd.Output() + }() if err != nil { safeOutputActionsLog.Printf("Failed to fetch %s from %s@%s: %v", filename, repo, ref, err) continue