Skip to content

Append persisted eval results across workflow runs - #50766

Merged
pelikhan merged 6 commits into
mainfrom
copilot/evals-append-entries
Aug 6, 2026
Merged

Append persisted eval results across workflow runs#50766
pelikhan merged 6 commits into
mainfrom
copilot/evals-append-entries

Conversation

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Persisting evals.jsonl replaced the existing eval branch history on every run. New results should append without losing prior or concurrent entries.

  • Persistence

    • Mark evals.jsonl as append-only in the generated eval-state job.
    • Merge existing branch records with the current run before committing.
  • Concurrency

    • Merge append-only JSONL conflicts during push retries.
    • Deduplicate shared records without applying experiment-ledger pruning.
  • Coverage

    • Cover history preservation beyond 512 records and concurrent merges.
    • Verify generated jobs configure append behavior.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Append persisted eval history across workflow runs Append persisted eval results across workflow runs Aug 6, 2026
Copilot AI requested a review from pelikhan August 6, 2026 05:20
@pelikhan

pelikhan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot the "state file" and the "append state file" are the same file. It's a JSONL where one simply appends new entries.

Copilot AI and others added 2 commits August 6, 2026 05:31
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot the "state file" and the "append state file" are the same file. It's a JSONL where one simply appends new entries.

Fixed in db4b512: GH_AW_STATE_FILES now directly identifies the append-only JSONL, with no separate append-state file configuration.

@pelikhan
pelikhan marked this pull request as ready for review August 6, 2026 06:02
Copilot AI balanced review requested due to automatic review settings August 6, 2026 06:02
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (only 1 addition in business logic paths).

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

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.

🟡 Changes recommended

The implementation breaks existing file-list semantics and can discard malformed historical records.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Updates eval persistence to preserve history and merge concurrent results.

Changes:

  • Adds append-only JSONL merging and deduplication.
  • Integrates merging into copy and rebase-conflict paths.
  • Adds history and concurrency tests.
File summaries
File Description
pkg/workflow/compiler_jobs_test.go Verifies generated eval-state configuration.
actions/setup/js/push_experiment_state.test.cjs Tests append and deduplication behavior.
actions/setup/js/push_experiment_state.cjs Implements append-only persistence and conflict merging.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment on lines +387 to +391
const appendFiles = new Set(
(process.env.GH_AW_STATE_FILES || "")
.split(",")
.map(name => name.trim())
.filter(Boolean)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 51472ea. GH_AW_STATE_FILES remains the candidate file list, and append-only merge now applies only to additional *.jsonl files (excluding state.jsonl), so state.json replacement semantics are preserved.

Comment on lines +205 to +229
function mergeAppendOnlyJSONL(remoteContent, localContent) {
const merged = [];
const seen = new Set();
for (const content of [remoteContent, localContent]) {
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
let entry;
try {
entry = JSON.parse(trimmed);
} catch {
core.warning(`mergeAppendOnlyJSONL: skipping unparseable line during merge`);
continue;
}
const key = stableJSONStringify(entry);
if (!seen.has(key)) {
seen.add(key);
merged.push(entry);
}
}
}
return merged.length > 0 ? `${merged.map(entry => JSON.stringify(entry)).join("\n")}\n` : "";
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 51472ea. mergeAppendOnlyJSONL now preserves malformed nonblank lines as opaque entries (with a warning) instead of dropping them, while still deduplicating valid JSON records.

Comment on lines +264 to +269
const result = mergeAppendOnlyJSONL(remote, local)
.trim()
.split("\n")
.map(line => JSON.parse(line));

expect(result.map(entry => entry.id)).toEqual(["shared", "remote", "local"]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 51472ea. Added concurrent-push integration coverage for evals.jsonl in actions/setup/js/push_signed_commits.test.cjs, validating resolver stage reads, append-file env handling, dedupe, and staged output in the signed-commit payload.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Test Quality Sentinel 🧪

Summary

This PR contains no new or modified behavioral tests — only test cleanup and assertion adjustments to reflect production code refactoring.

Files Analyzed

  • JS Test: actions/setup/js/push_experiment_state.test.cjs — 1 line deleted (env var cleanup)
  • Go Test: pkg/workflow/compiler_jobs_test.go — 1 assertion modified (now validates absence of env var)
  • No new test functions added or modified

Test Coverage Status

Existing test suite (18 JS test cases):

  • ✅ Merging concurrent state updates
  • ✅ Entry deduplication during concurrent merges
  • ✅ JSONL appending without pruning history
  • ✅ Timestamp ordering
  • ✅ Git retry logic (transient vs. deterministic failures)
  • ✅ Orphan branch creation
  • ✅ Repository allowlist validation
  • ✅ File I/O error reporting

Go tests:

  • ✅ State job generation validates env var usage

Assessment

Score: N/A — Infrastructure Only

No new behavioral test functions were added or modified. The PR refactors the test environment to match production code changes (consolidating GH_AW_STATE_APPEND_FILES into GH_AW_STATE_FILES).

The existing test suite provides comprehensive coverage of:

  • Append-only merge semantics ✅
  • Deduplication guarantees ✅
  • Git operations with retry logic ✅
  • Configuration validation ✅

No violations — assertions include descriptive failure messages; test infrastructure is sound.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 13.1 AIC · ⊞ 7.7K ·
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: Infrastructure only. No new behavioral tests. Existing 18 JS test cases provide comprehensive coverage of append-only merge semantics, deduplication, git retry logic, and configuration validation. No violations detected.

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

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 29.4 AIC · ⊞ 5.3K

}
}

for (const file of conflictedFiles.filter(name => appendFiles.has(name))) {

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.

Potential double-processing of state.jsonl/state.json: If a user passes GH_AW_STATE_FILES=state.jsonl (or state.json), the file is handled first by the hardcoded block above (lines 280–289 or 268–278) using mergeExperimentStateJSONL/mergeExperimentStateJSON, and then immediately overwritten by this loop using mergeAppendOnlyJSONL. The two strategies behave differently (JSONL prunes stale runs; append-only does not), so the winner is whichever runs last.

Consider guarding the loop to skip files already handled by the hardcoded blocks:

for (const file of conflictedFiles.filter(name => appendFiles.has(name) && name !== "state.json" && name !== "state.jsonl")) {

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 51472ea. state.jsonl is no longer in the append-only conflict set, so the generic append loop does not overwrite the dedicated state.jsonl merge result.

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

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes on two correctness issues.

📋 Key Themes & Highlights

Key Themes

  • Double-merge bug: when state.jsonl is in GH_AW_STATE_FILES (the default), the rebase conflict resolver writes it twice — first with mergeExperimentStateJSONL (pruning-aware), then overwritten by mergeAppendOnlyJSONL (append-only). One of the two merge strategies is silently lost.
  • Overly broad append scope: appendFiles in main() is identical to candidateFiles, so every file in GH_AW_STATE_FILES — including state.json and assignments.json — is treated as append-only JSONL on copy. This is a semantic change to the env var that may break existing callers.
  • Missing integration-level test: the main() merge-on-copy branch (line 476) has no test; only mergeAppendOnlyJSONL itself is covered.

Positive Highlights

  • mergeAppendOnlyJSONL is cleanly factored and handles unparseable lines gracefully
  • ✅ New tests cover both the >512-entry history preservation and concurrent-update deduplication paths
  • ✅ Go compiler test correctly asserts the absence of GH_AW_STATE_APPEND_FILES in generated YAML
> 🧠 *Reviewed using Matt Pocock's skills by [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/runs/31075980354)* · sonnet46 · 39.4 AIC · ⊞ 7.1K > Comment /matt to run again

}
}

for (const file of conflictedFiles.filter(name => appendFiles.has(name))) {

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] state.jsonl may be merged twice if it appears in GH_AW_STATE_FILES. The existing block at line 280 already resolves state.jsonl conflicts with mergeExperimentStateJSONL; this new loop then overwrites that result with mergeAppendOnlyJSONL when state.jsonl is in appendFiles.

💡 Suggested fix

Filter appendFiles to exclude files handled by the specific-file blocks:

for (const file of conflictedFiles.filter(
  name => appendFiles.has(name) && name !== "state.json" && name !== "state.jsonl" && name !== "assignments.json"
)) {

Or remove the hard-coded state.jsonl block and let appendFiles drive it uniformly.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 51472ea. state.jsonl is excluded from append-only conflict handling, so it is merged only by the dedicated mergeExperimentStateJSONL path and not processed twice.

.split(",")
.map(name => name.trim())
.filter(Boolean)
);

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] appendFiles in main() equals candidateFiles — every file in GH_AW_STATE_FILES is treated as append-only. The doc comment change on line 15 says all files are now JSONL to append, but the default value of GH_AW_STATE_FILES is state.jsonl,state.json,assignments.json. Non-JSONL files (state.json, assignments.json) would be passed to mergeAppendOnlyJSONL, which would parse them fine but silently ignore non-top-level-object keys via JSON.parse, producing unexpected behaviour.

💡 Suggested fix

Introduce a separate GH_AW_STATE_APPEND_FILES env var (or filter to .jsonl suffix by convention) so the append semantic is explicit and callers using the old default value are not broken. The PR description itself already references evals.jsonl as a single append file.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 51472ea. GH_AW_STATE_FILES is treated as the copy candidate list again, and append behavior is limited to non-state.jsonl *.jsonl files (for evals.jsonl), avoiding append-JSONL handling for state.json and assignments.json.

const existingContent = fs.readFileSync(dest, "utf8");
const newContent = fs.readFileSync(src, "utf8");
fs.writeFileSync(dest, mergeAppendOnlyJSONL(existingContent, newContent), "utf8");
} else {

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] The append-merge path in main() (merge-on-copy when the destination already exists) has no direct test. The new unit tests cover mergeAppendOnlyJSONL in isolation, but no test exercises the branch at line 476 where appendFiles.has(name) && fs.existsSync(dest) triggers. A failure here (e.g. permission error on readFileSync) silently falls through to core.setFailed rather than being caught at the unit level.

💡 Suggested test sketch
it("main: merges append-only files when destination exists", async () => {
  // write existing content to workspaceDir/evals.jsonl
  // write new content to stateDir/evals.jsonl
  // run main() with GH_AW_STATE_FILES=evals.jsonl
  // assert merged result in workspaceDir/evals.jsonl
});

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by adding integration coverage in 51472ea for the newly wired retry resolver path (evals.jsonl concurrent-push case in push_signed_commits.test.cjs). This verifies append-file env wiring and that the resolved file is staged into the commit payload.

@pelikhan

pelikhan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Completed in 51472ea. I ran the PR finisher pass, applied the actionable review fixes, added integration coverage for evals.jsonl concurrent merges, and pushed the updated branch.

@pelikhan
pelikhan merged commit e77a2d7 into main Aug 6, 2026
38 checks passed
@pelikhan
pelikhan deleted the copilot/evals-append-entries branch August 6, 2026 06:47
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.0

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.

3 participants