From d4ade77eb54350584313596dc211e3e3603dbfe8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:03:35 +0000 Subject: [PATCH 1/5] Initial plan From 3ba4f9cd4c8e9eecadbe4b43c22aab43c2c2de60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:08:01 +0000 Subject: [PATCH 2/5] chore: outline plan for cancellable job details fetch Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../workflows/smoke-call-workflow.lock.yml | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke-call-workflow.lock.yml b/.github/workflows/smoke-call-workflow.lock.yml index 15a3ed21b8c..eacad788ec7 100644 --- a/.github/workflows/smoke-call-workflow.lock.yml +++ b/.github/workflows/smoke-call-workflow.lock.yml @@ -619,6 +619,11 @@ jobs: "inputSchema": { "additionalProperties": false, "properties": { + "aw_context": { + "default": "", + "description": "Agent caller context (used internally by Agentic Workflows).", + "type": "string" + }, "payload": { "description": "Input parameter 'payload' for workflow smoke-workflow-call", "type": "string" @@ -1104,15 +1109,25 @@ jobs: needs: safe_outputs if: needs.safe_outputs.outputs.call_workflow_name == 'smoke-workflow-call' # Imported from called workflow "smoke-workflow-call" because GitHub requires the caller job to grant permissions requested by reusable workflow jobs. - # Review the called workflow's frontmatter permissions in ./.github/workflows/smoke-workflow-call.md. + # Review the called workflow's job-level permissions in ./.github/workflows/smoke-workflow-call.lock.yml. permissions: + actions: read contents: read - pull-requests: read + issues: write + pull-requests: write uses: ./.github/workflows/smoke-workflow-call.lock.yml with: + aw_context: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).aw_context }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} task-description: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload)['task-description'] }} - secrets: inherit + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_OTEL_GRAFANA_AUTHORIZATION: ${{ secrets.GH_AW_OTEL_GRAFANA_AUTHORIZATION }} + GH_AW_OTEL_GRAFANA_ENDPOINT: ${{ secrets.GH_AW_OTEL_GRAFANA_ENDPOINT }} + GH_AW_OTEL_SENTRY_AUTHORIZATION: ${{ secrets.GH_AW_OTEL_SENTRY_AUTHORIZATION }} + GH_AW_OTEL_SENTRY_ENDPOINT: ${{ secrets.GH_AW_OTEL_SENTRY_ENDPOINT }} conclusion: needs: From 82708a54424d07eba0e1a9693f0c3353f0cfec56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:19:08 +0000 Subject: [PATCH 3/5] fix: pass context through fetchJobDetailsWithCounts Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/audit.go | 14 +++++++------- pkg/cli/logs_github_api.go | 8 ++++---- pkg/cli/logs_github_api_test.go | 5 +++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/cli/audit.go b/pkg/cli/audit.go index 965a70e38fc..e10a6b3bb5a 100644 --- a/pkg/cli/audit.go +++ b/pkg/cli/audit.go @@ -370,7 +370,7 @@ func AuditWorkflowRun(ctx context.Context, runID int64, opts AuditOptions) error if err != nil { return err } - results := collectAuditAnalysisResults(run, cfg.outputDir, cfg.verbose, artifactMatchesFilter(constants.AgentArtifactName, cfg.artifactFilter)) + results := collectAuditAnalysisResults(ctx, run, cfg.outputDir, cfg.verbose, artifactMatchesFilter(constants.AgentArtifactName, cfg.artifactFilter)) run = applyAuditMetrics(run, results) processedRun := buildProcessedAuditRun(run, results) saveAuditRunSummary(cfg.outputDir, run, processedRun, results, cfg.verbose) @@ -653,10 +653,10 @@ func prepareRunForAnalysis(run WorkflowRun, cfg auditRunConfig, useLocalCache bo return run } -func collectAuditAnalysisResults(run WorkflowRun, runOutputDir string, verbose bool, hasFirewallArtifact bool) auditAnalysisResults { +func collectAuditAnalysisResults(ctx context.Context, run WorkflowRun, runOutputDir string, verbose bool, hasFirewallArtifact bool) auditAnalysisResults { results := auditAnalysisResults{} var wg sync.WaitGroup - launchCoreAuditAnalyses(&wg, &results, run, runOutputDir, verbose) + launchCoreAuditAnalyses(ctx, &wg, &results, run, runOutputDir, verbose) if hasFirewallArtifact { launchFirewallAuditAnalyses(&wg, &results, runOutputDir, verbose) } @@ -665,13 +665,13 @@ func collectAuditAnalysisResults(run WorkflowRun, runOutputDir string, verbose b return results } -func launchCoreAuditAnalyses(wg *sync.WaitGroup, results *auditAnalysisResults, run WorkflowRun, runOutputDir string, verbose bool) { +func launchCoreAuditAnalyses(ctx context.Context, wg *sync.WaitGroup, results *auditAnalysisResults, run WorkflowRun, runOutputDir string, verbose bool) { // Resolve experiment assignment once so all goroutines reuse the same values // rather than each reading state.json independently. expName, expVariant, _ := firstExperimentAssignment(extractExperimentData(runOutputDir)) launchMetricsAnalysis(wg, results, runOutputDir, verbose, run.WorkflowPath) - launchJobDetailsAnalysis(wg, results, run.DatabaseID, verbose) + launchJobDetailsAnalysis(ctx, wg, results, run.DatabaseID, verbose) runAuditAnalysis(wg, verbose, "extractMissingToolsFromRun", "Failed to extract missing tools", func(v []MissingToolReport) { results.missingTools = v }, func() ([]MissingToolReport, error) { @@ -713,9 +713,9 @@ func launchMetricsAnalysis(wg *sync.WaitGroup, results *auditAnalysisResults, ru }) } -func launchJobDetailsAnalysis(wg *sync.WaitGroup, results *auditAnalysisResults, runID int64, verbose bool) { +func launchJobDetailsAnalysis(ctx context.Context, wg *sync.WaitGroup, results *auditAnalysisResults, runID int64, verbose bool) { wg.Go(func() { - jobDetails, failedJobCount, err := fetchJobDetailsWithCounts(runID, verbose) + jobDetails, failedJobCount, err := fetchJobDetailsWithCounts(ctx, runID, verbose) if err != nil { auditLog.Printf("fetchJobDetailsWithCounts failed: %v", err) if verbose { diff --git a/pkg/cli/logs_github_api.go b/pkg/cli/logs_github_api.go index 582d2be9576..73cb9d01dc6 100644 --- a/pkg/cli/logs_github_api.go +++ b/pkg/cli/logs_github_api.go @@ -76,13 +76,13 @@ func buildCreatedFilter(startDate, endDate, beforeDate string) string { // call and returns the full detail slice together with the count of failed jobs. // It is the single source of truth for the jobs endpoint; fetchJobDetails and // fetchJobStatuses are thin wrappers that each return only the value they need. -func fetchJobDetailsWithCounts(runID int64, verbose bool) ([]JobInfoWithDuration, int, error) { +func fetchJobDetailsWithCounts(ctx context.Context, runID int64, verbose bool) ([]JobInfoWithDuration, int, error) { logsGitHubAPILog.Printf("Fetching job details: runID=%d", runID) if verbose { fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching job details for run %d", runID))) } - output, err := workflow.RunGHCombined("Fetching job details...", "api", + output, err := workflow.RunGHCombinedContext(ctx, "Fetching job details...", "api", fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), "--jq", ".jobs[] | {name: .name, status: .status, conclusion: (.conclusion // \"\"), started_at: .started_at, completed_at: .completed_at, steps: ((.steps // []) | map({name: .name, status: .status, conclusion: (.conclusion // \"\")}))}") if err != nil { @@ -131,7 +131,7 @@ func fetchJobDetailsWithCounts(runID int64, verbose bool) ([]JobInfoWithDuration // Errors from the underlying API call are suppressed so that callers can continue // processing even when job data is unavailable (e.g. missing permissions). func fetchJobDetails(runID int64, verbose bool) ([]JobInfoWithDuration, error) { - jobs, _, err := fetchJobDetailsWithCounts(runID, verbose) + jobs, _, err := fetchJobDetailsWithCounts(context.Background(), runID, verbose) if err != nil { // Don't fail the entire operation if we can't get job info return nil, nil @@ -143,7 +143,7 @@ func fetchJobDetails(runID int64, verbose bool) ([]JobInfoWithDuration, error) { // Errors from the underlying API call are suppressed so that callers can continue // processing even when job data is unavailable (e.g. missing permissions). func fetchJobStatuses(runID int64, verbose bool) (int, error) { - _, failedJobs, err := fetchJobDetailsWithCounts(runID, verbose) + _, failedJobs, err := fetchJobDetailsWithCounts(context.Background(), runID, verbose) if err != nil { // Don't fail the entire operation if we can't get job info return 0, nil diff --git a/pkg/cli/logs_github_api_test.go b/pkg/cli/logs_github_api_test.go index 8fc68bed793..8dd40b00767 100644 --- a/pkg/cli/logs_github_api_test.go +++ b/pkg/cli/logs_github_api_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "encoding/json" "os" "path/filepath" @@ -239,7 +240,7 @@ func TestFetchJobDetailsWithCountsIncludesSteps(t *testing.T) { t.Setenv("PATH", fakeBinDir+string(os.PathListSeparator)+os.Getenv("PATH")) - jobs, failedJobs, err := fetchJobDetailsWithCounts(28307653871, false) + jobs, failedJobs, err := fetchJobDetailsWithCounts(context.Background(), 28307653871, false) require.NoError(t, err) require.Len(t, jobs, 1) assert.Equal(t, 1, failedJobs, "failed job count should include failed jobs") @@ -270,7 +271,7 @@ func TestFetchJobDetailsWithCountsNullConclusion(t *testing.T) { t.Setenv("PATH", fakeBinDir+string(os.PathListSeparator)+os.Getenv("PATH")) - jobs, failedJobs, err := fetchJobDetailsWithCounts(28307653871, false) + jobs, failedJobs, err := fetchJobDetailsWithCounts(context.Background(), 28307653871, false) require.NoError(t, err) require.Len(t, jobs, 1, "in-progress jobs with null conclusion should not be dropped") assert.Equal(t, 0, failedJobs, "in-progress job should not count as failed") From c0d95443642ca92cacbfa71b2f548a46ed7903b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:37:05 +0000 Subject: [PATCH 4/5] fix: propagate context through job detail/status fetch wrappers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs_github_api.go | 8 ++++---- pkg/cli/logs_orchestrator_filters.go | 2 +- pkg/cli/logs_orchestrator_filters_test.go | 14 +++++++------- pkg/cli/logs_run_processor.go | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/cli/logs_github_api.go b/pkg/cli/logs_github_api.go index 73cb9d01dc6..d9aa161f01a 100644 --- a/pkg/cli/logs_github_api.go +++ b/pkg/cli/logs_github_api.go @@ -130,8 +130,8 @@ func fetchJobDetailsWithCounts(ctx context.Context, runID int64, verbose bool) ( // fetchJobDetails gets detailed job information including durations for a workflow run. // Errors from the underlying API call are suppressed so that callers can continue // processing even when job data is unavailable (e.g. missing permissions). -func fetchJobDetails(runID int64, verbose bool) ([]JobInfoWithDuration, error) { - jobs, _, err := fetchJobDetailsWithCounts(context.Background(), runID, verbose) +func fetchJobDetails(ctx context.Context, runID int64, verbose bool) ([]JobInfoWithDuration, error) { + jobs, _, err := fetchJobDetailsWithCounts(ctx, runID, verbose) if err != nil { // Don't fail the entire operation if we can't get job info return nil, nil @@ -142,8 +142,8 @@ func fetchJobDetails(runID int64, verbose bool) ([]JobInfoWithDuration, error) { // fetchJobStatuses gets the count of failed jobs for a workflow run. // Errors from the underlying API call are suppressed so that callers can continue // processing even when job data is unavailable (e.g. missing permissions). -func fetchJobStatuses(runID int64, verbose bool) (int, error) { - _, failedJobs, err := fetchJobDetailsWithCounts(context.Background(), runID, verbose) +func fetchJobStatuses(ctx context.Context, runID int64, verbose bool) (int, error) { + _, failedJobs, err := fetchJobDetailsWithCounts(ctx, runID, verbose) if err != nil { // Don't fail the entire operation if we can't get job info return 0, nil diff --git a/pkg/cli/logs_orchestrator_filters.go b/pkg/cli/logs_orchestrator_filters.go index 195d3dac165..62c285cf030 100644 --- a/pkg/cli/logs_orchestrator_filters.go +++ b/pkg/cli/logs_orchestrator_filters.go @@ -164,7 +164,7 @@ func buildProcessedRun(result DownloadResult, verbose, logFailedJobs bool) Proce } // Add failed jobs to error count. - if failedJobCount, err := fetchJobStatusesForProcessedRun(run.DatabaseID, verbose); err == nil { + if failedJobCount, err := fetchJobStatusesForProcessedRun(context.Background(), run.DatabaseID, verbose); err == nil { run.ErrorCount += failedJobCount if verbose && logFailedJobs && failedJobCount > 0 { fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Added %d failed jobs to error count for run %d", failedJobCount, run.DatabaseID))) diff --git a/pkg/cli/logs_orchestrator_filters_test.go b/pkg/cli/logs_orchestrator_filters_test.go index 11ea4dba877..285eee2e652 100644 --- a/pkg/cli/logs_orchestrator_filters_test.go +++ b/pkg/cli/logs_orchestrator_filters_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func stubFetchJobStatusesForProcessedRun(t *testing.T, fn func(int64, bool) (int, error)) { +func stubFetchJobStatusesForProcessedRun(t *testing.T, fn func(context.Context, int64, bool) (int, error)) { t.Helper() previous := fetchJobStatusesForProcessedRun fetchJobStatusesForProcessedRun = fn @@ -211,7 +211,7 @@ func TestApplyRunFilters_FilteredIntegrity(t *testing.T) { // the ProcessedRun fields from a DownloadResult. func TestBuildProcessedRun(t *testing.T) { t.Run("basic fields are propagated", func(t *testing.T) { - stubFetchJobStatusesForProcessedRun(t, func(int64, bool) (int, error) { return 0, nil }) + stubFetchJobStatusesForProcessedRun(t, func(context.Context, int64, bool) (int, error) { return 0, nil }) now := time.Now() tmpDir := t.TempDir() awCtx := &AwContext{Repo: "owner/repo"} @@ -235,7 +235,7 @@ func TestBuildProcessedRun(t *testing.T) { }) t.Run("duration and action minutes are computed", func(t *testing.T) { - stubFetchJobStatusesForProcessedRun(t, func(int64, bool) (int, error) { return 0, nil }) + stubFetchJobStatusesForProcessedRun(t, func(context.Context, int64, bool) (int, error) { return 0, nil }) base := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) result := DownloadResult{ Run: WorkflowRun{ @@ -253,7 +253,7 @@ func TestBuildProcessedRun(t *testing.T) { }) t.Run("zero timestamps leave duration unset", func(t *testing.T) { - stubFetchJobStatusesForProcessedRun(t, func(int64, bool) (int, error) { return 0, nil }) + stubFetchJobStatusesForProcessedRun(t, func(context.Context, int64, bool) (int, error) { return 0, nil }) result := DownloadResult{ Run: WorkflowRun{DatabaseID: 7}, LogsPath: t.TempDir(), @@ -264,7 +264,7 @@ func TestBuildProcessedRun(t *testing.T) { }) t.Run("effective tokens are propagated", func(t *testing.T) { - stubFetchJobStatusesForProcessedRun(t, func(int64, bool) (int, error) { return 0, nil }) + stubFetchJobStatusesForProcessedRun(t, func(context.Context, int64, bool) (int, error) { return 0, nil }) usage := &TokenUsageSummary{TotalEffectiveTokens: 5000} result := DownloadResult{ Run: WorkflowRun{DatabaseID: 3}, @@ -276,7 +276,7 @@ func TestBuildProcessedRun(t *testing.T) { }) t.Run("zero effective tokens not propagated", func(t *testing.T) { - stubFetchJobStatusesForProcessedRun(t, func(int64, bool) (int, error) { return 0, nil }) + stubFetchJobStatusesForProcessedRun(t, func(context.Context, int64, bool) (int, error) { return 0, nil }) usage := &TokenUsageSummary{TotalEffectiveTokens: 0} result := DownloadResult{ Run: WorkflowRun{DatabaseID: 4}, @@ -288,7 +288,7 @@ func TestBuildProcessedRun(t *testing.T) { }) t.Run("failed job count is added via test seam", func(t *testing.T) { - stubFetchJobStatusesForProcessedRun(t, func(runID int64, verbose bool) (int, error) { + stubFetchJobStatusesForProcessedRun(t, func(_ context.Context, runID int64, verbose bool) (int, error) { assert.Equal(t, int64(88), runID) assert.False(t, verbose) return 2, nil diff --git a/pkg/cli/logs_run_processor.go b/pkg/cli/logs_run_processor.go index ac3a18b1c2a..960e3e9e088 100644 --- a/pkg/cli/logs_run_processor.go +++ b/pkg/cli/logs_run_processor.go @@ -231,7 +231,7 @@ func processSingleRunDownload( if params.evalsArtifactRequested && !runHasEvals(runOutputDir, params.verbose) { tryDownloadEvalsArtifactFallback(ctx, run.DatabaseID, runOutputDir, perRunParams) } - analyzeRunArtifacts(result, runOutputDir, params.verbose, params.artifactFilter) + analyzeRunArtifacts(ctx, result, runOutputDir, params.verbose, params.artifactFilter) } } else { logsOrchestratorLog.Printf("Cache hit for run %d, using cached summary", run.DatabaseID) @@ -329,7 +329,7 @@ func tryLoadCachedRunResult( // analyzeRunArtifacts populates a DownloadResult with all analysis data derived from // freshly-downloaded artifacts in runOutputDir. Called only when the download succeeded // and no valid cached summary was found. -func analyzeRunArtifacts(result *DownloadResult, runOutputDir string, verbose bool, artifactFilter []string) { +func analyzeRunArtifacts(ctx context.Context, result *DownloadResult, runOutputDir string, verbose bool, artifactFilter []string) { metrics := extractRunMetricsAndMetadata(result, runOutputDir, verbose) usageActivitySummary, usageActivityErr := loadUsageActivitySummary(runOutputDir) @@ -350,7 +350,7 @@ func analyzeRunArtifacts(result *DownloadResult, runOutputDir string, verbose bo applyRunUsageMetrics(result, &metrics, runOutputDir, verbose, usageActivitySummary, hasFirewallArtifact) - finalizeAndSaveRunSummary(result, runOutputDir, metrics, verbose) + finalizeAndSaveRunSummary(ctx, result, runOutputDir, metrics, verbose) } // extractRunMetricsAndMetadata extracts log metrics, infers missing workflow path, and @@ -488,8 +488,8 @@ func applyRunUsageMetrics(result *DownloadResult, metrics *LogMetrics, runOutput // finalizeAndSaveRunSummary fetches job details, derives the agentic analysis, builds the // RunSummary struct, and writes it to disk. It also sets the agentic-analysis fields on // result directly so they are available to the caller. -func finalizeAndSaveRunSummary(result *DownloadResult, runOutputDir string, metrics LogMetrics, verbose bool) { - jobDetails, jobErr := fetchJobDetails(result.Run.DatabaseID, verbose) +func finalizeAndSaveRunSummary(ctx context.Context, result *DownloadResult, runOutputDir string, metrics LogMetrics, verbose bool) { + jobDetails, jobErr := fetchJobDetails(ctx, result.Run.DatabaseID, verbose) if jobErr != nil && verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch job details for run %d: %v", result.Run.DatabaseID, jobErr))) } else { From 12247f5841fe06ea9d354680fa805bbaee15658b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:02:22 +0000 Subject: [PATCH 5/5] fix: resolve merge conflict in audit.go - preserve doc comments and ctx parameter Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/audit.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pkg/cli/audit.go b/pkg/cli/audit.go index 48d2bfdbd44..0fd05c29e32 100644 --- a/pkg/cli/audit.go +++ b/pkg/cli/audit.go @@ -672,12 +672,8 @@ func collectAuditAnalysisResults(ctx context.Context, run WorkflowRun, runOutput return results } -<<<<<<< HEAD -func launchCoreAuditAnalyses(ctx context.Context, wg *sync.WaitGroup, results *auditAnalysisResults, run WorkflowRun, runOutputDir string, verbose bool) { -======= // launchCoreAuditAnalyses exclusively writes missingTools, missingData, noops, mcpFailures, and accessAnalysis. -func launchCoreAuditAnalyses(wg *sync.WaitGroup, results *auditAnalysisResults, run WorkflowRun, runOutputDir string, verbose bool) { ->>>>>>> origin/main +func launchCoreAuditAnalyses(ctx context.Context, wg *sync.WaitGroup, results *auditAnalysisResults, run WorkflowRun, runOutputDir string, verbose bool) { // Resolve experiment assignment once so all goroutines reuse the same values // rather than each reading state.json independently. expName, expVariant, _ := firstExperimentAssignment(extractExperimentData(runOutputDir)) @@ -726,12 +722,8 @@ func launchMetricsAnalysis(wg *sync.WaitGroup, results *auditAnalysisResults, ru }) } -<<<<<<< HEAD -func launchJobDetailsAnalysis(ctx context.Context, wg *sync.WaitGroup, results *auditAnalysisResults, runID int64, verbose bool) { -======= // launchJobDetailsAnalysis exclusively writes results.jobDetails and results.failedJobCount. -func launchJobDetailsAnalysis(wg *sync.WaitGroup, results *auditAnalysisResults, runID int64, verbose bool) { ->>>>>>> origin/main +func launchJobDetailsAnalysis(ctx context.Context, wg *sync.WaitGroup, results *auditAnalysisResults, runID int64, verbose bool) { wg.Go(func() { jobDetails, failedJobCount, err := fetchJobDetailsWithCounts(ctx, runID, verbose) if err != nil {