Skip to content

refactor: split logs_orchestrator.go (1284 lines) into 5 focused modules#44051

Open
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/file-diet-refactor-logs-orchestrator
Open

refactor: split logs_orchestrator.go (1284 lines) into 5 focused modules#44051
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/file-diet-refactor-logs-orchestrator

Conversation

Copilot AI commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

pkg/cli/logs_orchestrator.go had grown to 1284 lines mixing orchestration, filter logic, type definitions, output rendering, and stdin processing — with ~150 lines of run-filter and ProcessedRun construction duplicated between DownloadWorkflowLogs and DownloadWorkflowLogsFromStdin.

Changes

  • logs_orchestrator_types.go (87 lines) — pure type declarations: LogsDownloadOptions, StdinLogsOptions, continuationOptions, renderLogsOutputOptions
  • logs_orchestrator_filters.go (184 lines) — filter logic with two new shared helpers:
    • applyRunFilters(result, opts, verbose) bool — consolidates engine / staged / firewall / safe-output / DIFC filter blocks that were duplicated across both download paths
    • buildProcessedRun(result, verbose) ProcessedRun — consolidates ProcessedRun{...} construction (duration, action minutes, effective tokens, job counts)
  • logs_orchestrator_render.go (158 lines) — renderLogsOutput + renderLogsArtifactHint
  • logs_orchestrator_stdin.go (229 lines) — DownloadWorkflowLogsFromStdin
  • logs_orchestrator.go trimmed from 1284 → 603 lines; retains only the pagination loop, DownloadWorkflowLogs, and small utilities
  • logs_orchestrator_filters_test.go — unit tests for applyRunFilters and buildProcessedRun

Example: deduplicated filter call

Before (identical block existed in both DownloadWorkflowLogs and DownloadWorkflowLogsFromStdin, ~80 lines each):

if engine != "" {
    engineMatches, detectedEngineID := matchEngineFilter(awInfo, awInfoErr, engine)
    if !engineMatches { ... continue }
}
if noStaged { ... continue }
if firewallOnly || noFirewall { ... continue }
if safeOutputType != "" { ... continue }
if filteredIntegrity { ... continue }

After (one call, both paths):

if applyRunFilters(result, filters, verbose) {
    continue
}

Public API (DownloadWorkflowLogs, DownloadWorkflowLogsFromStdin, LogsDownloadOptions, StdinLogsOptions) is unchanged.

Copilot AI and others added 2 commits July 7, 2026 14:56
- Extract types into logs_orchestrator_types.go (87 lines)
- Extract filter logic and ProcessedRun builder into logs_orchestrator_filters.go (184 lines)
  including new applyRunFilters and buildProcessedRun helpers that eliminate ~250 lines of duplication
- Extract rendering into logs_orchestrator_render.go (158 lines)
- Extract stdin processing into logs_orchestrator_stdin.go (229 lines)
- Trim logs_orchestrator.go from 1284 to 603 lines
- Add logs_orchestrator_filters_test.go with tests for new helpers

Closes #44036

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor logs_orchestrator.go into focused modules refactor: split logs_orchestrator.go (1284 lines) into 5 focused modules Jul 7, 2026
Copilot AI requested a review from pelikhan July 7, 2026 14:58
@pelikhan pelikhan marked this pull request as ready for review July 7, 2026 16:53
Copilot AI review requested due to automatic review settings July 7, 2026 16:53
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the logs download orchestrator by splitting the previously monolithic pkg/cli/logs_orchestrator.go into focused modules, while deduplicating run-filtering and ProcessedRun construction logic shared between the normal and stdin-driven download paths.

Changes:

  • Split orchestrator responsibilities into separate modules (types, filters, rendering, stdin path) while keeping the public API intact.
  • Consolidated duplicated filtering + ProcessedRun construction into shared helpers (applyRunFilters, buildProcessedRun).
  • Added unit tests covering the new shared helpers.
Show a summary per file
File Description
pkg/cli/logs_orchestrator.go Removes duplicated filter/run-building/render/stdin logic; keeps main pagination loop and DownloadWorkflowLogs orchestration.
pkg/cli/logs_orchestrator_types.go Extracts public option structs and internal option/helper types into a dedicated types module.
pkg/cli/logs_orchestrator_filters.go Introduces shared run-filtering and ProcessedRun construction helpers to eliminate duplication.
pkg/cli/logs_orchestrator_render.go Moves output rendering (renderLogsOutput, renderLogsArtifactHint) into a focused rendering module.
pkg/cli/logs_orchestrator_stdin.go Moves stdin-driven download path (DownloadWorkflowLogsFromStdin) into its own module and uses shared helpers.
pkg/cli/logs_orchestrator_filters_test.go Adds unit tests for the newly centralized filtering and ProcessedRun construction helpers.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 4
  • Review effort level: Low

Comment on lines +17 to +26
// runFilterOpts bundles the filter flags passed to applyRunFilters.
type runFilterOpts struct {
engine string
noStaged bool
firewallOnly bool
noFirewall bool
safeOutputType string
filteredIntegrity bool
}

Comment on lines +151 to +157
// Add failed jobs to error count.
if failedJobCount, err := fetchJobStatuses(run.DatabaseID, verbose); err == nil {
run.ErrorCount += failedJobCount
if verbose && failedJobCount > 0 {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Added %d failed jobs to error count for run %d", failedJobCount, run.DatabaseID)))
}
}
Comment on lines +181 to +182
func TestBuildProcessedRun(t *testing.T) {
t.Run("basic fields are propagated", func(t *testing.T) {
skip := applyRunFilters(result, runFilterOpts{safeOutputType: "create-issue"}, false)
assert.True(t, skip)
})
}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (929 new lines in pkg/cli/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/44051-split-logs-orchestrator-focused-modules.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-44051: Split logs_orchestrator.go into Focused Modules

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 44051-split-logs-orchestrator-focused-modules.md).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 72.8 AIC · ⌖ 13.2 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 95/100 — Excellent

Analyzed 6 test(s): 6 design, 0 implementation, 0 violation(s).

📊 Metrics (6 tests)
Metric Value
Analyzed 6 (Go: 6, JS: 0)
✅ Design 6 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 5 (83%)
Duplicate clusters 0
Inflation No (test/prod ratio: 1.37:1)
🚨 Violations 0
Test File Classification Issues
TestApplyRunFilters_NoFilters logs_orchestrator_filters_test.go design_test Happy-path only
TestApplyRunFilters_Engine (3 subtests) logs_orchestrator_filters_test.go design_test None
TestApplyRunFilters_NoStaged (3 subtests) logs_orchestrator_filters_test.go design_test None
TestApplyRunFilters_Firewall (4 subtests) logs_orchestrator_filters_test.go design_test None
TestApplyRunFilters_SafeOutputType (3 subtests) logs_orchestrator_filters_test.go design_test None
TestBuildProcessedRun (5 subtests) logs_orchestrator_filters_test.go design_test None
⚠️ Flagged Tests (1 — minor)

TestApplyRunFilters_NoFilters (logs_orchestrator_filters_test.go:31) — design_test, happy-path only.
This test verifies only the no-op case (empty opts never skip). It is intentionally minimal as a baseline sanity check. Consider adding a case with DownloadResult{LogsPath: ""} to confirm nil-safety before filter parsing.

Minor style note: assert.Equal(t, tt.wantSkip, skip) in table-driven loops lacks a descriptive message argument. Prefer assert.Equal(t, tt.wantSkip, skip, tt.name) for clearer failure output (low severity — t.Run already names the subtest).

Verdict

Passed. 0% implementation tests (threshold: 30%). No guideline violations. The new test file provides solid behavioral coverage of applyRunFilters and buildProcessedRun with well-structured table-driven tests covering pass and skip paths for every filter type, plus edge cases (missing files, zero timestamps, zero effective tokens).

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 43.3 AIC · ⌖ 16.5 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 95/100. 0% implementation tests (threshold: 30%). No guideline violations. Well-structured behavioral tests with comprehensive edge case coverage.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Summary

Applied /tdd and /codebase-design. Requesting changes on 3 issues before merge:

🔴 Issues to address (3)
  1. Missing filteredIntegrity test (logs_orchestrator_filters_test.go:252) — every other filter flag has a dedicated test; filteredIntegrity does not. The check-error path returning true (skip) is a subtle policy worth pinning.

  2. Hardcoded /tmp/test-run path (logs_orchestrator_filters_test.go:191) — use t.TempDir() as done everywhere else in the same file.

  3. Silent behaviour change in stdin path (logs_orchestrator_filters.go:155) — buildProcessedRun now emits a verbose failed-job-count log that DownloadWorkflowLogsFromStdin previously did not. Intentional unification should be noted; otherwise the verbose log should stay on the non-stdin call site only.

✅ What's working well
  • Excellent deduplication — ~150-line filter blocks collapsed into one applyRunFilters call
  • Clear module split: each file owns exactly one concern
  • 252 lines of new tests covering the primary filter/builder surface
  • Public API is unchanged

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 135.6 AIC · ⌖ 8.27 AIC · ⊞ 6.6K ·
Comment /matt to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /codebase-design — requesting changes on three issues.

📋 Key Themes & Highlights

Issues Found

  • Missing test coverage: filteredIntegrity filter has no test — every other filter flag in runFilterOpts is exercised but this one is absent (see inline comment)
  • Hardcoded /tmp path: one buildProcessedRun test case uses /tmp/test-run instead of t.TempDir(), inconsistent with the rest of the file
  • Silent behaviour change: buildProcessedRun now emits a verbose log for failedJobCount that the old DownloadWorkflowLogsFromStdin did not — the stdin path observable output changes

Positive Highlights

  • ✅ Excellent deduplication — the ~150-line filter block and ProcessedRun{} construction that lived in both download paths are now a single shared helper
  • ✅ Well-scoped split: each new file has a clear, single responsibility (_types, _filters, _render, _stdin)
  • ✅ Solid test additions — 252 lines covering engine, staged, firewall, safe-output-type, and buildProcessedRun edge cases
  • applyRunFilters lazy-parses aw_info.json once and reuses it across filters — good performance hygiene preserved
  • ✅ Public API (DownloadWorkflowLogs, DownloadWorkflowLogsFromStdin, LogsDownloadOptions, StdinLogsOptions) unchanged

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 135.6 AIC · ⌖ 8.27 AIC · ⊞ 6.6K
Comment /matt to run again

pr := buildProcessedRun(result, false)
assert.Equal(t, 0, pr.Run.EffectiveTokens)
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] filteredIntegrity is the only runFilterOpts field with no test coverage — a regression here is invisible.

💡 Suggested test skeleton
func TestApplyRunFilters_FilteredIntegrity(t *testing.T) {
	t.Run("run without DIFC items is skipped", func(t *testing.T) {
		result := makeDownloadResult(t, "")
		skip := applyRunFilters(result, runFilterOpts{filteredIntegrity: true}, false)
		assert.True(t, skip)
	})
}

Every other filter flag has a dedicated test; filteredIntegrity does not. Given that the check-error path returns true (skip) — a subtle policy choice — it should be explicitly tested.

@copilot please address this.

StartedAt: now.Add(-5 * time.Minute),
UpdatedAt: now,
},
LogsPath: "/tmp/test-run",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Hardcoded /tmp/test-run path makes this test fragile on systems where /tmp is not writable or differs in parallel runs.

💡 Fix

Replace with t.TempDir(), consistent with every other test case in this file:

tmpDir := t.TempDir()
result := DownloadResult{
    Run: WorkflowRun{
        DatabaseID: 1234,
        StartedAt:  now.Add(-5 * time.Minute),
        UpdatedAt:  now,
    },
    LogsPath:  tmpDir,
    AwContext: awCtx,
}
// assert.Equal(t, tmpDir, pr.Run.LogsPath)

@copilot please address this.

if failedJobCount, err := fetchJobStatuses(run.DatabaseID, verbose); err == nil {
run.ErrorCount += failedJobCount
if verbose && failedJobCount > 0 {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Added %d failed jobs to error count for run %d", failedJobCount, run.DatabaseID)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] This verbose log for failedJobCount is new for the stdin path — the old DownloadWorkflowLogsFromStdin called fetchJobStatuses silently (no matching fmt.Fprintln). The deduplicated buildProcessedRun now emits this message for both paths, changing the observable output of --stdin --verbose.

💡 Options

If the verbose log is intentionally unified, document the behaviour change in the PR description or a comment.

If the stdin path should stay silent, extract the verbose log out of buildProcessedRun and keep it only in the DownloadWorkflowLogs call site:

processedRun := buildProcessedRun(result, verbose)
// Only log failed-job count in the non-stdin path:
if verbose && processedRun.Run.ErrorCount > 0 { ... }

@copilot please address this.

}()
oldStdout := os.Stdout
defer func() { os.Stdout = oldStdout }()
os.Stdout = f

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] os.Stdout is a global; redirecting it here is not safe if any goroutine writes to stdout concurrently (or if tests ever call t.Parallel()). This pattern was carried over from the monolith — moving it into its own focused module is a good moment to flag it.

💡 Safer approach

Pass an io.Writer parameter instead of mutating the global:

func renderCrossRunReportMarkdown(report crossRunAuditReport, w io.Writer) { ... }

// call site:
renderCrossRunReportMarkdown(report, f)

Or use fmt.Fprintf(f, ...) throughout renderCrossRunReportMarkdown and accept an io.Writer. This refactor isn't required now, but the new module boundary makes it an easy next step.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — clean refactor with two issues that should be addressed before merge.

### Findings summary

High — concurrency: os.Stdout = f in renderLogsOutput is a non-goroutine-safe global reassignment. This was pre-existing, but this PR funnels both DownloadWorkflowLogs and DownloadWorkflowLogsFromStdin through a single shared renderLogsOutput call site, doubling the blast radius. The right fix is to pass an io.Writer down to renderCrossRunReportMarkdown.

Medium — test correctness: One subtest in TestBuildProcessedRun hard-codes LogsPath: "/tmp/test-run" while all sibling subtests correctly use t.TempDir(). The ErrorCount == 0 assertion is only stable because fetchJobStatuses fails in CI; it would flap if run 1234 became reachable.

Overall: The deduplication is solid — applyRunFilters and buildProcessedRun are well-structured, the new tests cover the important filter combinations, and the split into focused files is a genuine improvement. Fix the two items above and this is good to go.

🔎 Code quality review by PR Code Quality Reviewer · 289.4 AIC · ⌖ 5.24 AIC · ⊞ 5.4K
Comment /review to run again

oldStdout := os.Stdout
defer func() { os.Stdout = oldStdout }()
os.Stdout = f
renderCrossRunReportMarkdown(report)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.Stdout global reassignment is not goroutine-safe — any concurrent goroutine writing to stdout during this window will have output misdirected to the report file or vice versa.

💡 Suggested fix

Pass an io.Writer to renderCrossRunReportMarkdown instead of swapping the global:

if opts.reportFile != "" {
    f, err := os.Create(opts.reportFile)
    if err != nil {
        return fmt.Errorf("failed to create report file: %w", err)
    }
    defer f.Close()
    renderCrossRunReportMarkdown(report, f)
} else {
    renderCrossRunReportMarkdown(report, os.Stdout)
}

This was pre-existing, but this refactor makes renderLogsOutput the single consolidated path for both DownloadWorkflowLogs and DownloadWorkflowLogsFromStdin. It's exactly the right moment to fix it — and the blast radius just doubled.

func TestBuildProcessedRun(t *testing.T) {
t.Run("basic fields are propagated", func(t *testing.T) {
now := time.Now()
awCtx := &AwContext{Repo: "owner/repo"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded /tmp/test-run path makes this test non-hermeticbuildProcessedRun calls fetchJobStatuses (a live gh api network call) with run ID 1234, and ErrorCount == 0 only holds because the call fails in CI. Other subtests all use t.TempDir().

💡 Suggested fix

Replace the hardcoded path with t.TempDir() like every other subtest here:

t.Run("basic fields are propagated", func(t *testing.T) {
    now := time.Now()
    awCtx := &AwContext{Repo: "owner/repo"}
    result := DownloadResult{
        Run: WorkflowRun{
            DatabaseID: 1234,
            StartedAt:  now.Add(-5 * time.Minute),
            UpdatedAt:  now,
        },
        LogsPath:  t.TempDir(), // not a shared /tmp path
        AwContext: awCtx,
    }
    ...
})

The ErrorCount == 0 assertion should also be annotated with a comment explaining it reflects only the initial reset (before fetchJobStatuses succeeds) or the test should mock the API call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[file-diet] Refactor pkg/cli/logs_orchestrator.go (1284 lines) into focused modules

3 participants