Skip to content

Persist evals to evals/* branches and make audit/logs branch-aware#45492

Open
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/add-storage-of-evals-results
Open

Persist evals to evals/* branches and make audit/logs branch-aware#45492
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/add-storage-of-evals-results

Conversation

Copilot AI commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

This change adds durable git-branch storage for eval results and reuses the existing experiments-state push path instead of introducing a separate persistence mechanism. It also updates audit/logs eval filtering to fall back to branch-backed evals.jsonl when artifacts are unavailable.

  • Compiler/job graph updates

    • Adds push_evals_state as a built-in post-evals job.
    • Wires it after evals (and activation context), with contents: write.
    • Ensures conclusion depends on push_evals_state so branch persistence completes before finalization.
    • Uses evals/<sanitized-workflow-id> naming, aligned with existing workflow ID sanitization semantics.
  • Refactor + reuse of state push logic

    • Generalizes push_experiment_state.cjs via GH_AW_STATE_* envs (DIR, BRANCH, FILES, LABEL) while preserving experiment aliases for backward compatibility.
    • Reuses the same signed-commit push path for both experiments and evals state persistence.
  • Branch-backed eval lookup in CLI

    • Adds shared branch-file retrieval/decoding helpers for gh api repos/.../contents/...@ref.
    • logs and audit eval filtering now hydrate local evals.jsonl from evals/* branch when local artifact-derived eval files are missing.
    • Reuses shared workflow-state branch naming to avoid divergent branch derivation logic.
  • Targeted compatibility and wiring coverage

    • Extends workflow compiler tests for push_evals_state creation and conclusion dependency wiring.
    • Keeps existing experiment flows functional while enabling eval branch storage with minimal new surface area.
# generated job env (excerpt)
env:
  GH_AW_STATE_DIR: /tmp/gh-aw/evals-state
  GH_AW_STATE_BRANCH: evals/myworkflow
  GH_AW_STATE_FILES: evals.jsonl
  GH_AW_STATE_LABEL: evals results

Copilot AI and others added 2 commits July 14, 2026 13:19
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 Add evals branch persistence and branch-aware eval lookup in audit/logs Persist evals to evals/* branches and make audit/logs branch-aware Jul 14, 2026
Copilot AI requested a review from pelikhan July 14, 2026 13:31
@pelikhan pelikhan marked this pull request as ready for review July 14, 2026 13:36
Copilot AI review requested due to automatic review settings July 14, 2026 13:36

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

Adds branch-backed eval persistence and fallback lookup for logs and audit.

Changes:

  • Adds and wires the push_evals_state workflow job.
  • Generalizes experiment-state pushing for eval files.
  • Adds shared branch-file retrieval and eval hydration.
Show a summary per file
File Description
pkg/workflow/jobs.go Registers the eval persistence job.
pkg/workflow/evals_job.go Builds the eval branch push job.
pkg/workflow/compiler_jobs.go Wires persistence into compilation and conclusion.
pkg/workflow/compiler_jobs_test.go Tests job generation and dependencies.
pkg/workflow/compiler_experiments.go Shares state-branch naming logic.
pkg/cli/logs_run_processor.go Attempts branch hydration during log processing.
pkg/cli/logs_orchestrator_filters.go Adds branch fallback to eval filtering.
pkg/cli/experiments_command.go Reuses the branch-file reader.
pkg/cli/evals_branch.go Implements eval branch lookup and hydration.
pkg/cli/branch_file_reader.go Adds shared remote branch-file retrieval.
pkg/cli/audit.go Adds branch fallback to audit filtering.
actions/setup/js/push_experiment_state.cjs Generalizes signed state persistence.

Review details

Tip

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

  • Files reviewed: 12/12 changed files
  • Comments generated: 5
  • Review effort level: Medium

Comment on lines +19 to +24
args := []string{"api",
"repos/{owner}/{repo}/contents/" + filePath,
"--field", "ref=" + branchName,
"--jq", ".content",
"--repo", repoOverride,
}
Comment thread pkg/cli/evals_branch.go Outdated
return false
}

decoded, err := readRemoteRepoBranchFileContext(ctx, repoOverride, branchName, constants.EvalsResultFilename, host)
Comment thread pkg/cli/evals_branch.go
Comment on lines +23 to +25
workflowID := workflowIDFromRunPath(run.WorkflowPath)
if workflowID == "" {
return false
Comment thread pkg/cli/logs_run_processor.go Outdated
Comment thread pkg/cli/logs_orchestrator_filters.go Outdated
// Apply evals filtering if --evals flag is specified.
if opts.evalsOnly {
if !runHasEvals(result.LogsPath, verbose) {
if !runHasEvals(result.LogsPath, verbose) && !ensureEvalsResultsFromBranch(context.Background(), result.Run, result.LogsPath, "", "", "", verbose) {
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, update this branch as needed, address any remaining feedback, and rerun checks before requesting maintainer attention.

Generated by 👨‍🍳 PR Sous Chef · 8.23 AIC · ⌖ 7.97 AIC · ⊞ 7.5K ·
Comment /souschef to run again

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

@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: Persist evals to evals/* branches

The PR is well-structured and the generalization of push_experiment_state.cjs is clean. However, there are blocking correctness and reliability issues already flagged in the inline comments that need to be addressed before merging.

Blocking issues:

  1. branch_file_reader.go line 24--field ref=<branch> causes gh api to send a POST instead of GET to the contents endpoint, breaking all branch reads. Use --raw-field or append ?ref=<branch> as a query parameter.\n\n2. evals_branch.go line 33 — The branch stores only the latest evals run; fetching it for an older run silently returns wrong data. The branch-backed path needs to be run-scoped or limited to the current run.\n\n3. evals_branch.go line 25WorkflowPath is empty for runs discovered via gh run list without --json workflowPath, so workflowIDFromRunPath returns "" and the branch lookup is silently skipped — defeating the feature for those runs.\n\n4. logs_orchestrator_filters.go line 138context.Background() discards the caller context; slow API calls won't respect cancellation or timeouts.\n\n5. logs_run_processor.go line 131 — Cache-hit path uses the fresh run.WorkflowPath (empty from gh run list) instead of summary.Run.WorkflowPath for the branch lookup.

Please address the blocking items — especially (1) which makes the entire feature non-functional — before merging.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 33.2 AIC · ⌖ 5.03 AIC · ⊞ 5K

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

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/45492-persist-evals-results-to-git-branches.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-45492: Persist Eval Results to Durable Git Branches

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., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 69 AIC · ⌖ 13.2 AIC · ⊞ 8.5K ·
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.

Skills-Based Review 🧠

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

📋 Key Themes & Highlights

Key Themes

  • Job condition drops failed evals (line 185, evals_job.go): push_evals_state only fires when evals succeed, so runs with partial failures never have their results persisted to the branch.
  • Duplicated constant (evalsBranchPrefix): defined independently in pkg/workflow and pkg/cli; these can drift silently. Move to pkg/constants.
  • Sanitization logic in workflowIDFromRunPath: manually strips .yml/.yaml after NormalizeWorkflowName, duplicating work done by SanitizeWorkflowIDForCacheKey. No unit tests cover this path.

Positive Highlights

  • ✅ Clean reuse of push_experiment_state.cjs via generic GH_AW_STATE_* env vars — minimal new surface area.
  • ✅ Correct fallback chain (artifact → branch) wired consistently in both audit and logs paths.
  • WorkflowStateBranchName correctly extracted as a shared utility.
  • ✅ New compiler tests cover both job creation and conclusion dependency wiring.

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

Comment thread pkg/workflow/evals_job.go
BuildStringLiteral("success"),
)
notCancelled := &NotNode{Child: BuildFunctionCall("cancelled")}
jobCondition := RenderCondition(BuildAnd(BuildAnd(BuildFunctionCall("always"), notCancelled), evalsSucceeded))

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] The push_evals_state job condition requires evals == success, so results from failed evals runs are silently dropped and never persisted to the branch.

💡 Suggested fix

The push job should run whenever the evals job ran to completion (success or failure), not only on success. Consider:

evalsFinished := BuildNotEquals(
    BuildPropertyAccess(fmt.Sprintf("needs.%s.result", constants.EvalsJobName)),
    BuildStringLiteral("skipped"),
)
jobCondition := RenderCondition(BuildAnd(BuildAnd(BuildFunctionCall("always"), notCancelled), evalsFinished))

This mirrors how push_experiments_state is conditioned in the experiments job — check that pattern for the canonical approach.

@copilot please address this.

Comment thread pkg/cli/evals_branch.go
if workflowPath == "" {
return ""
}
base := filepath.Base(workflowPath)

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] workflowIDFromRunPath manually strips .yml/.yaml extensions after calling NormalizeWorkflowName, but NormalizeWorkflowName already removes .md and .lock.yml. The double-strip suggests this helper duplicates sanitization that is already done elsewhere (e.g. SanitizeWorkflowIDForCacheKey).

💡 Suggested fix

Check whether SanitizeWorkflowIDForCacheKey(filepath.Base(workflowPath)) already gives the correct result. If so, collapse this function:

func workflowIDFromRunPath(workflowPath string) string {
    if workflowPath == "" {
        return ""
    }
    return workflow.SanitizeWorkflowIDForCacheKey(filepath.Base(workflowPath))
}

At minimum, add a test for .yaml and .yml inputs to document expected behaviour.

@copilot please address this.

Comment thread pkg/workflow/evals_job.go

var evalsJobLog = logger.New("workflow:evals_job")

const evalsBranchPrefix = "evals"

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] evalsBranchPrefix = "evals" is defined in both pkg/workflow/evals_job.go and pkg/cli/evals_branch.go. This duplication means the two packages can drift independently.

💡 Suggested fix

Move the canonical constant to pkg/constants (where EvalsArtifactName, EvalsResultFilename, etc. already live) and import it from both packages:

// pkg/constants/constants.go
const EvalsBranchPrefix = "evals"

Then in both files:

branchName := workflow.WorkflowStateBranchName(constants.EvalsBranchPrefix, workflowID)

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 70/100 — Acceptable

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

📊 Metrics (2 tests)
Metric Value
Analyzed 2 (Go: 2, JS: 0)
✅ Design 2 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 0 (0%)
Duplicate clusters 0
Inflation No (1.91:1)
🚨 Violations 0
Test File Classification Issues
TestBuildPushEvalsStateJob_WithEvals pkg/workflow/compiler_jobs_test.go Design (unit) No edge cases tested
TestBuildMemoryManagementJobs_PushEvalsIncludedInConclusion pkg/workflow/compiler_jobs_test.go Design (integration) No error/edge cases tested
i️ Test Details

TestBuildPushEvalsStateJob_WithEvals (unit test, 32 LOC)

  • Tests direct call to buildPushEvalsStateJob() with valid evals config
  • 8 assertions: verifies job name, condition, permissions, needs, and step content
  • All assertions have descriptive messages ✅
  • Value: Catches regressions in job structure (permissions, dependencies, branch naming)
  • Gap: No error paths tested (e.g., what if evals config is invalid?)

TestBuildMemoryManagementJobs_PushEvalsIncludedInConclusion (integration test, 33 LOC)

  • End-to-end workflow compilation: writes markdown, compiles, inspects lock file
  • 6 assertions: verifies push_evals_state job exists and is included in conclusion dependencies
  • All assertions have descriptive messages ✅
  • Value: Validates the complete pipeline (workflow → lock file → job dependencies)
  • Gap: No error cases (malformed evals, compilation failures); no edge cases (empty evals, nil config)

Verdict

Passed. 0% implementation tests (threshold: 30%). No violations detected.

Summary: Both tests verify important design invariants—that push_evals_state jobs are created with the correct configuration and integrated into the workflow compilation pipeline. However, neither test covers error paths or edge cases, which limits their ability to catch unexpected failures. Test inflation is healthy (1.91:1, under 2:1 threshold).

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 · 25.6 AIC · ⌖ 10.4 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: 70/100. 0% implementation tests (threshold: 30%). No violations detected.

Both new tests verify important design invariants for the evals persistence feature: the push_evals_state job is created with correct configuration and integrated into the workflow compilation pipeline. While edge cases and error paths could be more thoroughly tested, the tests provide solid coverage of the behavioral contracts.

@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 — several correctness and durability bugs must be fixed

Five existing comments (from a prior round) plus three new ones below identify blocking issues. The most critical:

### Blocking issues summary

1. triggers POST, not GET () — the entire branch-backed read path is broken out of the box. encodes parameters as a POST body; the Contents endpoint only accepts GET. Use with or embed the in the URL as a query param.

2. Failed evals are never persisted () — the job is gated on , so failed/partial eval runs produce no branch entry. This defeats the durability purpose of the feature. Loosen to .

3. Branch file overwrites per-run evals with all-runs aggregate () — on the branch accumulates results for every run of the workflow. Writing it verbatim into a per-run corrupts per-run metrics and audit reports. Results must be filtered to the current run's ID before writing.

4. leaks in filter path () — passes an unscoped context for network I/O, breaking Ctrl-C/timeout cancellation.

**5. Cache-hit path uses wrong ** () — is empty on cache hits; should be used instead, or branch lookup silently fails and cache is unnecessarily bypassed.

6. Silent error regression () — refactoring removed the decode-error log; non-404 failures are now invisible.

7. poisons base64 decode () — stderr bytes mixed into decoded content cause confusing errors; use .

🔎 Code quality review by PR Code Quality Reviewer · 72.5 AIC · ⌖ 4.73 AIC · ⊞ 5.6K
Comment /review to run again

Comments that could not be inline-anchored

pkg/workflow/evals_job.go:195

Failed evals are never persisted to the branch: the job condition requires the evals job to succeed, so any run where evals fail or partially fail is silently skipped and no results land on the branch.

<details>
<summary>💡 Suggested fix</summary>

Loosen the condition to only exclude cancelled runs:

notCancelled := &amp;NotNode{Child: BuildFunctionCall(&quot;cancelled&quot;)}
jobCondition := RenderCondition(BuildAnd(BuildFunctionCall(&quot;always&quot;), notCancelled))

The push script already handles…

pkg/cli/experiments_command.go:320

Silent error regression: the refactored readRemoteExperimentState silently discards all errors from readRemoteRepoBranchFile, including non-404 failures like network errors or malformed responses. The previous implementation logged decode errors explicitly.

<details>
<summary>💡 Suggested fix</summary>

Distinguish not-found (expected, silent) from unexpected errors:

decoded, err := readRemoteRepoBranchFile(repoOverride, branchName, &quot;state.json&quot;, &quot;&quot;)
if err != nil {
    if !isRe</details>

<details><summary>pkg/cli/branch_file_reader.go:29</summary>

**`CombinedOutput()` merges stderr into the content bytes that are then base64-decoded**: if `gh api` writes any warning or diagnostic text to stderr before exiting, those bytes will be prepended to the file content and cause a spurious decode error that hides the real issue.

&lt;details&gt;
&lt;summary&gt;💡 Suggested fix&lt;/summary&gt;

Use `cmd.Output()` (stdout only) and capture stderr separately for error reporting:

```go
var stderr bytes.Buffer
cmd.Stderr = &amp;stderr
out, err := cmd.Output()
if err != nil…

</details>

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
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.

4 participants