Skip to content

fix: populate engine_id from lock file metadata when aw_info.json is absent - #43732

Closed
pelikhan with Copilot wants to merge 6 commits into
mainfrom
copilot/cli-tools-test-fix-engine-id-null
Closed

fix: populate engine_id from lock file metadata when aw_info.json is absent#43732
pelikhan with Copilot wants to merge 6 commits into
mainfrom
copilot/cli-tools-test-fix-engine-id-null

Conversation

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Runs that pre-date the aw_info.json artifact (e.g. daily-cli-tools-tester) return engine_id: null, making the --engine filter silently exclude them. The fix adds a fallback that reads agent_id from the # gh-aw-metadata: comment compiled into the lock file.

Changes

  • logs_parsing_core.go — adds extractEngineIDFromLockFile(workflowPath, verbose) which reads the local .lock.yml, parses its gh-aw-metadata JSON comment, and returns agent_id. Also adds resolveToLockFilePath to normalise .yml / .md / .lock.yml inputs.

  • logs_orchestrator.gomatchEngineFilter gains a lockFileEngineID string parameter used as fallback when aw_info.json is absent or carries no engine_id. Both call sites compute it via extractEngineIDFromLockFile(result.Run.WorkflowPath, verbose).

  • logs_report.gobuildLogsData falls back to extractEngineIDFromLockFile when aw_info.json yields no engine_id, so engine_id, engine, and engine_counts in the JSON output reflect the lock file metadata for older runs.

Precedence

aw_info.json remains authoritative; the lock file is only consulted when aw_info.json is missing or empty:

aw_info.json engine_id   →  authoritative
lock file agent_id        →  fallback (no aw_info.json or engine_id is "")
(none)                    →  engine unknown, run excluded from --engine filter

pr-sous-chef run: https://github.com/github/gh-aw/actions/runs/28776499241

Generated by 👨‍🍳 PR Sous Chef · 7.38 AIC · ⌖ 10.2 AIC · ⊞ 5.6K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 6, 2026 07:31
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…on is absent

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix logs tool to correctly return engine_id for older runs fix: populate engine_id from lock file metadata when aw_info.json is absent Jul 6, 2026
Copilot AI requested a review from pelikhan July 6, 2026 07:45
@pelikhan
pelikhan marked this pull request as ready for review July 6, 2026 07:46
Copilot AI review requested due to automatic review settings July 6, 2026 07:46
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 6, 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

This PR fixes gh aw logs --engine <id> filtering and JSON reporting for historical workflow runs that pre-date the aw_info.json artifact by falling back to the engine identifier stored in the workflow lock file’s # gh-aw-metadata: comment.

Changes:

  • Add lock-file metadata parsing (agent_id → engine id) as a fallback when aw_info.json is missing or has an empty engine_id.
  • Update engine filtering to accept a lock-file-derived engine id fallback.
  • Add/extend tests covering lock-file fallback behavior for both filtering and report aggregation.
Show a summary per file
File Description
pkg/cli/logs_parsing_core.go Adds extractEngineIDFromLockFile and lock-file path normalization helper.
pkg/cli/logs_orchestrator.go Extends engine filtering logic to support lock-file fallback.
pkg/cli/logs_report.go Falls back to lock-file metadata when building JSON report engine fields/counts.
pkg/cli/logs_report_test.go Adds report test coverage for lock-file fallback when aw_info.json is absent.
pkg/cli/logs_engine_filter_test.go Adds tests for lock-file engine-id extraction and filter precedence.
.github/workflows/workflow-skill-extractor.lock.yml Generated lockfile formatting update.
.github/workflows/workflow-health-manager.lock.yml Generated lockfile formatting update.
.github/workflows/weekly-blog-post-writer.lock.yml Generated lockfile formatting update.
.github/workflows/unbloat-docs.lock.yml Generated lockfile formatting update.
.github/workflows/uk-ai-operational-resilience.lock.yml Generated lockfile formatting update.
.github/workflows/test-quality-sentinel.lock.yml Generated lockfile formatting update.
.github/workflows/step-name-alignment.lock.yml Generated lockfile formatting update.
.github/workflows/static-analysis-report.lock.yml Generated lockfile formatting update.
.github/workflows/stale-repo-identifier.lock.yml Generated lockfile formatting update.
.github/workflows/spec-extractor.lock.yml Generated lockfile formatting update.
.github/workflows/schema-consistency-checker.lock.yml Generated lockfile formatting update.
.github/workflows/repository-quality-improver.lock.yml Generated lockfile formatting update.
.github/workflows/release.lock.yml Generated lockfile formatting update.
.github/workflows/prompt-clustering-analysis.lock.yml Generated lockfile formatting update.
.github/workflows/pr-description-caveman.lock.yml Generated lockfile formatting update.
.github/workflows/objective-impact-report.lock.yml Generated lockfile formatting update.
.github/workflows/mergefest.lock.yml Generated lockfile formatting update.
.github/workflows/mattpocock-skills-reviewer.lock.yml Generated lockfile formatting update.
.github/workflows/linter-miner.lock.yml Generated lockfile formatting update.
.github/workflows/lint-monster.lock.yml Generated lockfile formatting update.
.github/workflows/issue-arborist.lock.yml Generated lockfile formatting update.
.github/workflows/impeccable-skills-reviewer.lock.yml Generated lockfile formatting update.
.github/workflows/gpclean.lock.yml Generated lockfile formatting update.
.github/workflows/glossary-maintainer.lock.yml Generated lockfile formatting update.
.github/workflows/eslint-monster.lock.yml Generated lockfile formatting update.
.github/workflows/docs-noob-tester.lock.yml Generated lockfile formatting update.
.github/workflows/designer-drift-audit.lock.yml Generated lockfile formatting update.
.github/workflows/design-decision-gate.lock.yml Generated lockfile formatting update.
.github/workflows/dependabot-burner.lock.yml Generated lockfile formatting update.
.github/workflows/delight.lock.yml Generated lockfile formatting update.
.github/workflows/dataflow-pr-discussion-dataset.lock.yml Generated lockfile formatting update.
.github/workflows/daily-spdd-spec-planner.lock.yml Generated lockfile formatting update.
.github/workflows/daily-security-observability.lock.yml Generated lockfile formatting update.
.github/workflows/daily-multi-device-docs-tester.lock.yml Generated lockfile formatting update.
.github/workflows/daily-community-attribution.lock.yml Generated lockfile formatting update.
.github/workflows/daily-ambient-context-optimizer.lock.yml Generated lockfile formatting update.
.github/workflows/copilot-pr-nlp-analysis.lock.yml Generated lockfile formatting update.
.github/workflows/copilot-centralization-drilldown.lock.yml Generated lockfile formatting update.
.github/workflows/contribution-check.lock.yml Generated lockfile formatting update.
.github/workflows/code-simplifier.lock.yml Generated lockfile formatting update.
.github/workflows/cli-consistency-checker.lock.yml Generated lockfile formatting update.
.github/workflows/ci-doctor.lock.yml Generated lockfile formatting update.
.github/workflows/aw-failure-investigator.lock.yml Generated lockfile formatting update.
.github/workflows/auto-triage-issues.lock.yml Generated lockfile formatting update.
.github/workflows/architecture-guardian.lock.yml Generated lockfile formatting update.
.github/workflows/agentic-token-trend-audit.lock.yml Generated lockfile formatting update.
.github/workflows/agentic-token-optimizer.lock.yml Generated lockfile formatting update.
.github/workflows/agentic-token-audit.lock.yml Generated lockfile formatting update.

Review details

Tip

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

  • Files reviewed: 53/53 changed files
  • Comments generated: 3
  • Review effort level: Low

Comment thread pkg/cli/logs_orchestrator.go Outdated
Comment on lines +520 to +521
lockFileEngineID := extractEngineIDFromLockFile(result.Run.WorkflowPath, verbose)
engineMatches, detectedEngineID := matchEngineFilter(awInfo, awInfoErr, engine, lockFileEngineID)
Comment thread pkg/cli/logs_orchestrator.go Outdated
Comment on lines +1128 to +1129
lockFileEngineID := extractEngineIDFromLockFile(result.Run.WorkflowPath, opts.Verbose)
engineMatches, detectedEngineID := matchEngineFilter(awInfo, awInfoErr, opts.Engine, lockFileEngineID)
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Build Inventory
run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n# Run compilation validation and capture output\ngh aw compile --validate > /tmp/gh-aw/agent/compile-validate.txt 2>&1 || true\n# List executable workflow files (exclude shared/ subdirectory)\nls .github/workflows/*.md 2>/dev/null > /tmp/gh-aw/agent/workflow-list.txt || true\necho \"Inventory complete: $(wc -l < /tmp/gh-aw/agent/workflow-list.txt | tr -d ' ') workflows found\"\n"
run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n# Run compilation validation and capture output\ngh aw compile --validate > /tmp/gh-aw/agent/compile-validate.txt 2>&1 || true\n# List executable workflow files (exclude shared/ subdirectory)\nls .github/workflows/*.md 2>/dev/null > /tmp/gh-aw/agent/workflow-list.txt || true\necho \"Inventory complete: $(wc -l < /tmp/gh-aw/agent/workflow-list.txt | tr -d ' ') workflows found\""
ADR documents the two-tier precedence decision: aw_info.json is
authoritative for engine_id resolution; the lock file gh-aw-metadata
comment serves as fallback for pre-aw_info.json runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/43732-engine-id-lock-file-fallback.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 and description
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and verify the listed alternatives match what was actually 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-43732: Two-Tier Fallback for Resolving engine_id in Workflow Run Logs

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

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

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., 43732-engine-id-lock-file-fallback.md for PR #43732).

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

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

🔶 Test Quality Score: 68/100 — Acceptable (60-79 range)

Analyzed 18 test(s): 11 design, 7 implementation, 1 guideline violation.

📊 Metrics (18 tests)
Metric Value
Analyzed 18 (Go: 18, JS: 0)
✅ Design 11 (61%)
⚠️ Implementation 7 (39%)
Edge/error coverage 14 (78%)
Duplicate clusters 0
Inflation YES (2.42:1, threshold: 2.0:1)
🚨 Violations 1
⚠️ Flagged Tests (1 violation)

Implementation Ratio Violation: 7/18 = 38.9% (threshold: 30%)

The test suite includes TestResolveToLockFilePath with 7 cases testing path normalization logic. While these tests provide good regression protection, they focus on implementation details (string manipulation) rather than behavioral contracts.

TestResolveToLockFilePath (logs_engine_filter_test.go:167-182)

  • Classification: Implementation-focused utility test
  • Issue: Tests internal path transformation (.md.lock.yml, .yml.lock.yml, etc.)
  • Impact: 7 test cases counting toward implementation ratio
  • Recommendation: Consider consolidating path resolution testing into TestExtractEngineIDFromLockFilePlainYml (already tests .yml.lock.yml resolution end-to-end), which would reduce implementation ratio to 0/11 = 0%.

Test Inflation Flag: 201 test lines / 83 prod lines = 2.42:1 (threshold: 2.0:1)

  • Test file additions: 144 + 57 = 201 lines
  • Production additions: 18 + 59 + 6 = 83 lines
  • Status: Marginally elevated but defensible for comprehensive fallback testing

Verdict

REQUEST_CHANGES. Implementation test ratio (38.9%) exceeds the 30% threshold. The core design tests are strong (11/18 = 61% with comprehensive edge case coverage), but TestResolveToLockFilePath is a pure utility test counting against the threshold.

Core design coverage is solid:

  • ✅ Lock file fallback behavior: 4 new test cases in TestMatchEngineFilter
  • ✅ Engine ID extraction from metadata: 5 test cases in TestExtractEngineIDFromLockFile
  • ✅ Backward compatibility (no aw_info.json): end-to-end test in TestBuildLogsDataEngineCountsFromLockFileFallback
  • ✅ Precedence rules: covered in TestMatchEngineFilter
  • ✅ No forbidden mocks (gomock, testify/mock) detected
  • ✅ All test files have required //go:build !integration tag

Recommendation to pass sentinel: Remove or consolidate TestResolveToLockFilePath. The path resolution logic (.yml.lock.yml) is already tested end-to-end in TestExtractEngineIDFromLockFilePlainYml, which creates actual files and verifies the resolution works in practice.

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 · 16.3 AIC · ⌖ 13.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: 68/100. Implementation tests comprise 38.9% of new tests (threshold: 30%). Core design coverage is strong (11/18 = 61%), but TestResolveToLockFilePath (7 utility-focused path normalization cases) counts against threshold. This test can be consolidated into TestExtractEngineIDFromLockFilePlainYml which already verifies path resolution end-to-end. See detailed analysis in the comment above.

@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 /diagnosing-bugs and /tdd — requesting changes on 5 issues: two correctness/observability gaps and three test quality issues.

📋 Key Themes & Highlights

Key Issues

  1. CWD assumption (logs_parsing_core.go:134) — os.ReadFile is given a relative path straight from the GitHub API. The fallback silently returns "" when the process isn't run from the repo root, which means the original bug reappears in any non-standard invocation. No guard or diagnostic surfaced.

  2. Double lock-file I/O (logs_report.go:249) — When --engine is active, each run reads its lock file twice: once in the filter loop and again in buildLogsData. Propagating the already-computed value via ProcessedRun would eliminate the second read entirely.

  3. verbose hardcoded to false (logs_report.go:249) — Inconsistent with both orchestrator call sites, which correctly pass the caller's verbose flag. The diagnostic "Detected engine from lock file metadata: ..." is silenced for this path.

  4. Missing precedence regression test (logs_report_test.go:991) — TestBuildLogsDataEngineCountsFromLockFileFallback has no case that verifies aw_info.json beats the lock file when both are present with differing agent_id. The precedence rule could break silently.

  5. Fragile name-string sentinel in test (logs_engine_filter_test.go:170) — tc.name != "empty workflow path returns empty" is a brittle guard; a struct flag like skipFileCreate bool is refactor-safe.

Positive Highlights

  • ✅ Precedence logic in matchEngineFilter is clean and correct: aw_info.json wins when present, lock file is a true fallback.
  • resolveToLockFilePath switch ordering correctly handles .lock.yml before the plain .yml case.
  • ✅ Good coverage added for matchEngineFilter fallback scenarios and resolveToLockFilePath path-normalisation.
  • ✅ PR description is clear about the precedence hierarchy and the affected workflow (daily-cli-tools-tester).

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 89.1 AIC · ⌖ 5.84 AIC · ⊞ 6.7K
Comment /matt to run again

Comment thread pkg/cli/logs_engine_filter_test.go Outdated

// Use .lock.yml extension by default
workflowPath := tc.workflowPath
if workflowPath == "" && tc.name != "empty workflow path returns empty" {

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] Test control flow uses tc.name != "empty workflow path returns empty" as a string-match sentinel — this is fragile: a typo or rename silently breaks the guard without a compile error.

💡 Suggested fix

Add a skipFileCreate bool field to the test case struct:

cases := []struct {
    name            string
    lockFileContent string
    workflowPath    string
    skipFileCreate  bool  // pass workflowPath directly without creating a file
    expectEngineID  string
}{
    ...
    {
        name:           "empty workflow path returns empty",
        workflowPath:   "",
        skipFileCreate: true,
        expectEngineID: "",
    },
}

Then the guard becomes:

if !tc.skipFileCreate {
    // create lock file and set workflowPath
}

This makes intent explicit and is refactor-safe.

@copilot please address this.

Comment thread pkg/cli/logs_parsing_core.go Outdated
return ""
}

content, err := os.ReadFile(filepath.Clean(lockPath))

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.

[/diagnosing-bugs] os.ReadFile(filepath.Clean(lockPath)) is called with a path that comes directly from the GitHub API — a relative path like .github/workflows/foo.lock.yml. This works only if the process CWD is the repository root, but there is no guard or assertion to enforce that assumption. If the tool is run from a different directory, the fallback silently returns "" (the lock file is unreadable), and the bug reappears without any error surfaced to the user.

💡 Suggested fix

Consider anchoring the path explicitly. If a repo-root concept exists in context, prefix lockPath with it:

if !filepath.IsAbs(lockPath) {
    lockPath = filepath.Join(repoRoot, lockPath)
}

Or at minimum, add a verbose log that reveals the resolved path when the file is missing, so operators can diagnose the mismatch:

if err != nil {
    logsParsingCoreLog.Printf("Cannot read lock file %s (cwd: %s): %v", lockPath, cwdForLog(), err)
    return ""
}

This makes silent CWD-mismatch failures diagnosable.

@copilot please address this.

Comment thread pkg/cli/logs_report.go Outdated
// Fall back to the lock file's gh-aw-metadata when aw_info.json is absent
// or does not carry an engine_id (e.g. older runs that pre-date the artifact).
if engineID == "" {
engineID = extractEngineIDFromLockFile(run.WorkflowPath, false)

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.

[/diagnosing-bugs] When --engine is active, extractEngineIDFromLockFile is called twice per run: once in the filter loop (logs_orchestrator.go:520) and again here for report population. On a 200-run download this doubles the lock-file I/O, and the second call is entirely avoidable since the value was already computed (and the run already passed the filter).

💡 Suggested fix

Propagate the already-computed lockFileEngineID from the orchestrator into ProcessedRun (or pass it through buildLogsData), then skip the second read:

// In ProcessedRun or a new field:
LockFileEngineID string

// In orchestrator, before appending:
result.LockFileEngineID = lockFileEngineID

// In buildLogsData:
if engineID == "" {
    engineID = pr.LockFileEngineID  // already computed, no I/O
}

Alternatively, a simple sync.Map-based per-path cache in extractEngineIDFromLockFile would also avoid repeated disk reads.

@copilot please address this.

@@ -991,6 +991,63 @@ func TestBuildLogsDataEngineCountsFromAwInfo(t *testing.T) {
}

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] TestBuildLogsDataEngineCountsFromLockFileFallback covers the happy-path lock-file read but is missing a case that verifies aw_info.json takes precedence: when both a valid aw_info.json and a lock file with a different agent_id are present, the report's engine_id should reflect aw_info.json, not the lock file. Without this regression test, the precedence rule could silently break.

💡 Suggested test case
func TestBuildLogsDataAwInfoTakesPrecedenceOverLockFile(t *testing.T) {
    dir := t.TempDir()
    // aw_info.json says "claude"
    awInfoContent := `{"engine_id":"claude"}`
    require.NoError(t, os.WriteFile(filepath.Join(dir, "aw_info.json"), []byte(awInfoContent), 0600))

    // lock file says "copilot"
    lockContent := `# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"x","agent_id":"copilot"}\nname: wf\n`
    lockPath := filepath.Join(dir, "wf.lock.yml")
    require.NoError(t, os.WriteFile(lockPath, []byte(lockContent), 0600))

    processedRuns := []ProcessedRun{
        {Run: WorkflowRun{DatabaseID: 1, WorkflowName: "wf", LogsPath: dir, WorkflowPath: lockPath}},
    }
    data := buildLogsData(processedRuns, "/tmp/logs", nil)
    assert.Equal(t, 1, data.Summary.EngineCounts["claude"], "aw_info should win")
    assert.Equal(t, 0, data.Summary.EngineCounts["copilot"], "lock file should be ignored")
}

@copilot please address this.

Comment thread pkg/cli/logs_report.go Outdated
// Fall back to the lock file's gh-aw-metadata when aw_info.json is absent
// or does not carry an engine_id (e.g. older runs that pre-date the artifact).
if engineID == "" {
engineID = extractEngineIDFromLockFile(run.WorkflowPath, false)

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.

[/diagnosing-bugs] extractEngineIDFromLockFile is called with a hardcoded false for verbose, unlike the two call sites in logs_orchestrator.go which correctly pass the caller's verbose flag. This means the "Detected engine from lock file metadata: ..." diagnostic message is never shown in buildLogsData's path, even when the user passes --verbose.

💡 Suggested fix

buildLogsData doesn't currently accept a verbose parameter, so the simplest fix is to add one and thread it through:

func buildLogsData(processedRuns []ProcessedRun, outputDir string, continuation *ContinuationData, verbose bool) LogsData {
    ...
    engineID = extractEngineIDFromLockFile(run.WorkflowPath, verbose)
}

Update the three call sites accordingly.

@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.

Review: fix: populate engine_id from lock file metadata when aw_info.json is absent

Summary: The fix is correct and well-tested. The fallback precedence (aw_info.json authoritative, lock file as fallback) is sound, and the new helper functions (extractEngineIDFromLockFile, resolveToLockFilePath) are clean and well-documented. Test coverage for the new paths is thorough.

One non-blocking issue found: In logs_orchestrator.go, extractEngineIDFromLockFile is called unconditionally on every run even when awInfo.EngineID is already set — triggering an unnecessary disk read in the common case. logs_report.go already uses the correct lazy pattern (if engineID == "" { ... }). The same guard should be applied at both call sites in the orchestrator.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 81.9 AIC · ⌖ 5.92 AIC · ⊞ 4.9K

Comment thread pkg/cli/logs_orchestrator.go Outdated
// Apply engine filtering if specified
if engine != "" {
engineMatches, detectedEngineID := matchEngineFilter(awInfo, awInfoErr, engine)
lockFileEngineID := extractEngineIDFromLockFile(result.Run.WorkflowPath, verbose)

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.

The lock file is read unconditionally here — even when awInfo already carries a valid engine_id. This wastes a disk read per run in the common case. logs_report.go uses the correct lazy pattern:

if engineID == "" {
    engineID = extractEngineIDFromLockFile(run.WorkflowPath, false)
}

Same fix applies to line 1128. Consider only calling extractEngineIDFromLockFile when awInfo has no engine_id:

var lockFileEngineID string
if awInfoErr != nil || awInfo == nil || awInfo.EngineID == "" {
    lockFileEngineID = extractEngineIDFromLockFile(result.Run.WorkflowPath, verbose)
}

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.
Unresolved reviews:

  • @github-actions requested changes

Generated by 👨‍🍳 PR Sous Chef · 7.38 AIC · ⌖ 10.2 AIC · ⊞ 5.6K ·
Comment /souschef 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.

REQUEST_CHANGES — The fix is correct in intent but has three issues that need addressing before merge.

🔍 Blocking findings (3)

1. N+1 lock file reads across the pipeline

extractEngineIDFromLockFile is called independently at lines 520, 1128, and logs_report.go:249. For every run that passes the engine filter its lock file is read from disk at minimum three times. For a batch of N runs sharing the same workflow this is O(N) redundant disk reads. A map[string]string cache keyed on workflowPath would eliminate all redundant reads. See inline comment at line 520.

2. Filter and report can disagree on the engine ID

The orchestrator resolves and discards the lock-file engine ID after the filter decision. buildLogsData then independently re-reads the lock file. If the file is replaced between the two phases, --engine filtering and RunData.Agent/EngineCounts in the JSON report will silently reflect different values. The computed ID should be threaded through ProcessedRun rather than re-derived. See inline comment at logs_report.go:249.

3. .yaml extension silently returns empty — fallback never fires

resolveToLockFilePath has no case for .yaml. Any workflow file with that extension causes extractEngineIDFromLockFile to return "", silently defeating the entire fallback. See inline comment at logs_parsing_core.go:157.

⚠️ Non-blocking findings (2)

verbose=false hardcoded in report path

Orchestrator call sites forward the runtime verbose flag; the report call always passes false. Users running --verbose get incomplete engine-detection output.

Test sentinel using test name string

tc.name != "empty workflow path returns empty" is a magic string gate that silently breaks if the test case is renamed. Replace with an explicit skipFileCreate bool field.

Warning

Firewall blocked 1 domain

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

  • patchdiff.githubusercontent.com

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

network:
  allowed:
    - defaults
    - "patchdiff.githubusercontent.com"

See Network Configuration for more information.

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

Comment thread pkg/cli/logs_orchestrator.go Outdated
// Apply engine filtering if specified
if engine != "" {
engineMatches, detectedEngineID := matchEngineFilter(awInfo, awInfoErr, engine)
lockFileEngineID := extractEngineIDFromLockFile(result.Run.WorkflowPath, verbose)

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.

N+1 lock file reads in hot loop: the same lock file is read from disk on every run iteration.

💡 Details and suggested fix

extractEngineIDFromLockFile performs os.ReadFile + metadata parse on every loop iteration. For N runs sharing the same workflow path this is N redundant disk reads with no memoization. The same problem exists at the second call site (line 1128) and in buildLogsData (logs_report.go:249), meaning a run that passes the engine filter has its lock file read at minimum three times across the processing pipeline.

Suggested fix: introduce a map[string]string cache keyed on workflowPath and pass it through both call sites (or hoist it into a dedicated helper struct):

lockFileIDCache := make(map[string]string)

// inside loop:
if _, seen := lockFileIDCache[result.Run.WorkflowPath]; !seen {
    lockFileIDCache[result.Run.WorkflowPath] = extractEngineIDFromLockFile(result.Run.WorkflowPath, verbose)
}
lockFileEngineID := lockFileIDCache[result.Run.WorkflowPath]

This also eliminates the consistency hazard where filter and report phases could disagree if the lock file is modified between reads.

Comment thread pkg/cli/logs_report.go Outdated
// Fall back to the lock file's gh-aw-metadata when aw_info.json is absent
// or does not carry an engine_id (e.g. older runs that pre-date the artifact).
if engineID == "" {
engineID = extractEngineIDFromLockFile(run.WorkflowPath, false)

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.

Engine ID recomputed from disk in report phase instead of reusing value already resolved during filtering — filter and report can disagree.

💡 Details

The filtering code in logs_orchestrator.go (lines 520, 1128) already computes the lock-file engine ID and makes a pass/fail decision. That resolved value is then thrown away. buildLogsData independently re-reads the same lock file a third time. Two consequences:

  1. I/O waste — redundant disk read per run (see the N+1 comment at the filter call site).
  2. Consistency gap — if the lock file on disk is replaced between the filter phase and the report phase, the engine ID used to decide which runs pass the filter will differ from the engine ID recorded in RunData.Agent / EngineCounts. This is unlikely in practice but makes the code brittle and harder to reason about.

The correct fix is to thread the already-computed engine ID through ProcessedRun (or pass it as a field on WorkflowRun) so buildLogsData consumes the pre-resolved value rather than re-reading the file.

// Handles .lock.yml paths (returned as-is), plain .yml paths (converted by
// appending ".lock" before the ".yml" suffix), and .md source paths.
// Returns "" for unrecognised extensions.
func resolveToLockFilePath(workflowPath string) string {

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.

.yaml extension silently returns "", making the fallback silently fail for any GitHub Actions workflow named with .yaml.

💡 Details and fix

GitHub Actions accepts both .yml and .yaml as valid workflow file extensions. resolveToLockFilePath handles .yml but has no case for .yaml, so any WorkflowPath ending in .yaml falls through to default: return "". extractEngineIDFromLockFile then silently returns "" — the engine ID is never populated for those workflows, and --engine filter silently excludes all their runs.

Fix:

case strings.HasSuffix(workflowPath, ".yaml"):
    return strings.TrimSuffix(workflowPath, ".yaml") + ".lock.yml"

A corresponding test case in TestResolveToLockFilePath should be added.

Comment thread pkg/cli/logs_report.go Outdated
// Fall back to the lock file's gh-aw-metadata when aw_info.json is absent
// or does not carry an engine_id (e.g. older runs that pre-date the artifact).
if engineID == "" {
engineID = extractEngineIDFromLockFile(run.WorkflowPath, false)

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.

verbose is hardcoded to false here, making lock-file engine detection messages invisible when --verbose is set.

💡 Details

Both orchestrator call sites (lines 520, 1128) correctly forward the runtime verbose flag, so users running with --verbose see "Detected engine from lock file metadata: copilot" messages. This call site always passes false, silently suppressing those messages for engine IDs resolved during report building. The result is incomplete and inconsistent verbose output.

buildLogsData currently has no verbose parameter — that parameter should be threaded in from the caller, or false should be replaced with the appropriate flag value.

Comment thread pkg/cli/logs_engine_filter_test.go Outdated

// Use .lock.yml extension by default
workflowPath := tc.workflowPath
if workflowPath == "" && tc.name != "empty workflow path returns empty" {

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 setup is gated on a magic test-name string sentinel — renaming the case silently breaks its setup logic.

💡 Details and fix

The condition tc.name != "empty workflow path returns empty" uses the human-readable description as a run-time flag to suppress file creation. If anyone renames that test case the condition becomes vacuously false and the workflowPath == "" case will now try to create a lock file for an empty path, silently changing test semantics or panicking.

Replace with an explicit boolean field:

struct {
    name            string
    lockFileContent string
    workflowPath    string
    skipFileCreate  bool   // true when workflowPath is intentionally empty
    expectEngineID  string
}
// ...
{
    name:           "empty workflow path returns empty",
    workflowPath:   "",
    skipFileCreate: true,
    expectEngineID: "",
},
// in setup:
if !tc.skipFileCreate {
    // create lock file ...
}

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

PR Triage

Category bug
Risk Medium
Score 60/100 (impact 28 + urgency 18 + quality 14)
Action fast_track
Batch pr-batch:go-runtime-fixes

Fallback reads agent_id from lock file gh-aw-metadata when aw_info.json is absent. Fixes --engine filter silently excluding older runs. Explicit precedence rules, well-described.

Run §28776188550

Generated by 🔧 PR Triage Agent · 98.5 AIC · ⌖ 11.4 AIC · ⊞ 5.5K ·

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
@pelikhan pelikhan closed this Jul 6, 2026
Copilot AI requested a review from gh-aw-bot July 6, 2026 08:25
Copilot stopped work on behalf of gh-aw-bot due to an error July 6, 2026 08:25
@github-actions
github-actions Bot deleted the copilot/cli-tools-test-fix-engine-id-null branch July 14, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cli-tools-test] logs tool returns engine_id: null for runs without aw_info.json artifact (engine filter ineffective for older runs)

4 participants