Skip to content

Always write result.json, even on job failure#66

Open
Tim-Pohlmann wants to merge 11 commits into
feat/capture-agent-stderrfrom
feat/always-write-result-json
Open

Always write result.json, even on job failure#66
Tim-Pohlmann wants to merge 11 commits into
feat/capture-agent-stderrfrom
feat/always-write-result-json

Conversation

@Tim-Pohlmann

Copy link
Copy Markdown
Owner

Summary

  • rix job now writes result.json regardless of outcome, not just on success.
  • rix submit already rejected a non-success result.json (SubmitRunner.cs), so this is safe with no changes needed there.
  • This gives any caller (CI included) one reliable file to read the outcome from, instead of scraping stdout.

Test plan

  • dotnet build Rix.slnx -c Release
  • dotnet test Rix.slnx -c Release (173 passed)

Tim-Pohlmann added a commit that referenced this pull request Jul 8, 2026
The matrix's failure-mode legs asserted on the agent CLI's own prose
error text (e.g. "Not logged in", "Incorrect API key"), which doesn't
scale - every new failure mode meant hardcoding more vendor wording into
workflow YAML, and the jq/case-based structural checks needed to make
that robust don't belong embedded in a workflow script.

agent-plumbing now only proves the plumbing itself works end-to-end
(checkout, build, composite actions, output capture) via the one
config that needs no credentials: opencode's free default model. Real
per-failure-mode assertions move to a dedicated rix-job-e2e test next,
where they can do proper JSON parsing instead of workflow-embedded jq.

This also lets run-rix-job's "Run rix job" step drop back to a plain
invocation - the stdout-capture/jq-parsed status+error outputs it grew
existed only to feed the assertions removed here, and are unused now
that rix always writes result.json (see #66) rather than only on
success.
Callers previously had no reliable place to read a failed job's outcome
from disk - result.json was only persisted on success, forcing consumers
(e.g. CI) to scrape stdout instead. rix submit already rejects a
non-success result.json, so writing it unconditionally is safe.
@Tim-Pohlmann
Tim-Pohlmann force-pushed the feat/always-write-result-json branch from 547c6f5 to 55344e4 Compare July 9, 2026 17:44
@Tim-Pohlmann
Tim-Pohlmann changed the base branch from feat/multi-provider-model-passthrough to feat/capture-agent-stderr July 9, 2026 17:44
Tim-Pohlmann added a commit that referenced this pull request Jul 9, 2026
The matrix's failure-mode legs asserted on the agent CLI's own prose
error text (e.g. "Not logged in", "Incorrect API key"), which doesn't
scale - every new failure mode meant hardcoding more vendor wording into
workflow YAML, and the jq/case-based structural checks needed to make
that robust don't belong embedded in a workflow script.

agent-plumbing now only proves the plumbing itself works end-to-end
(checkout, build, composite actions, output capture) via the one
config that needs no credentials: opencode's free default model. Real
per-failure-mode assertions move to a dedicated rix-job-e2e test next,
where they can do proper JSON parsing instead of workflow-embedded jq.

This also lets run-rix-job's "Run rix job" step drop back to a plain
invocation - the stdout-capture/jq-parsed status+error outputs it grew
existed only to feed the assertions removed here, and are unused now
that rix always writes result.json (see #66) rather than only on
success.
@Tim-Pohlmann
Tim-Pohlmann requested a review from Copilot July 9, 2026 20:52

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 makes rix job persist result.json for both success and failure outcomes so callers (including CI) can read a stable machine-readable result from disk instead of parsing stdout.

Changes:

  • Write result.json unconditionally in Startup.ExecuteJobAsync (previously only on JobSuccess).
  • Update the job runner tests to expect result.json to be written on failure.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
tests/Rix.Tests/JobRunnerTests.cs Updates the failure-path test to expect result.json to be written.
src/Rix/Startup.cs Writes result.json for all IJobResult outcomes instead of only on success.

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

Comment thread src/Rix/Startup.cs Outdated
Comment thread tests/Rix.Tests/JobRunnerTests.cs
…just existence

The PR's contract change is that callers can reliably read the job
outcome from result.json, so the failure-path test should confirm its
content actually says "failure", not just that the file exists.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread tests/Rix.Tests/JobRunnerTests.cs
Comment thread src/Rix/Startup.cs Outdated
Comment thread src/Rix/Startup.cs Outdated
- Wrap the failure-path JsonDocument.Parse in using var (it's disposable
  and holding pooled buffers longer than needed is unnecessary).
- Use consistent <c> tags instead of markdown backticks, and an em-dash
  instead of a hyphen, in ExecuteJobAsync's doc comment.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/Rix/Startup.cs Outdated
Comment thread src/Rix/Startup.cs Outdated
CloneAsync threw InvalidOperationException uncaught, so a clone failure
(bad repo, auth, network) skipped ExecuteJobAsync's result.json write
entirely - contradicting the "regardless of outcome" guarantee this PR
adds. Convert it to a SetupFailure instead, matching how agent-install
failures and CreateBundleAsync failures are already handled.

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread src/Rix/Startup.cs Outdated
Comment thread tests/Rix.Tests/JobRunnerTests.cs
Comment thread src/Rix/Job/JobRunner.cs Outdated
…ror, test SetupFailure

- Make the result.json write uncancellable and best-effort (catch IOException),
  so a cancellation racing right after the job finishes - or a transient disk
  error - can't stop the correct exit code from being returned.
- Drop the redundant "clone failed: " prefix - RunGitAsync's own exception
  message already says "git clone failed: ...".
- Add RunAsync_WritesResultJson_OnSetupFailure: the existing coverage only
  exercised the JobFailure path, leaving the other non-success outcome
  (installer failure) unverified.
Startup.ExecuteJobAsync is async; the new best-effort result.json warning
used the blocking Console.Error.WriteLine instead of awaiting the async form.
…ailure path

File.WriteAllTextAsync throws UnauthorizedAccessException (not IOException)
when the target path exists as a directory or lacks write permission - the
exact "permission issues" scenario the best-effort write was meant to guard
against, so the catch clause needed to include it too. Added a test that
forces the write to fail this way and asserts the exit code is still correct,
closing the new-code coverage gap SonarCloud's quality gate flagged.

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread tests/Rix.Tests/TestStubs.cs Outdated
Comment thread src/Rix/Startup.cs
- TestStubs.cs: drop the redundant/incorrect Repository. qualifier from
  the <see cref> so it resolves (GitHubReadHost is already in scope via
  the existing using directive).
- Startup.cs: wrap the Console.Error warning write in its own try/catch
  so a closed/broken stderr can't defeat the "must not stop the correct
  exit code from being returned" guarantee around the result.json write.

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread tests/Rix.Tests/TestStubs.cs Outdated
<paramref> must name a parameter of the member it's attached to, but
these doc comments are on CloneAsync/CreateBundleAsync while naming the
class's primary-constructor parameters (clone, createBundle) - a
mismatch that's misleading even where no compiler warning fires.
Reworded both as plain <c> references to the constructor parameter
instead. Fixed createBundle's copy too since it's the same mistake,
even though only the clone one was in this PR's diff.

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread src/Rix/Startup.cs Outdated
Console.WriteLine(json) could throw on a broken/closed stdout pipe
(e.g. the reader end goes away) and prevent ExecuteJobAsync from ever
reaching the exit-code switch or the result.json write below it -
undermining the same "must not stop the correct exit code from being
returned" guarantee already established for the result.json write and
its own warning write. Wrapped it in a matching best-effort try/catch.

Added a regression test with a TextWriter that fails every write, swapped
in via Console.SetOut. Note: Console.SetOut wraps the writer in a
synchronized TextWriter whose async methods delegate to the synchronous
ones under a lock, so the fake has to override WriteLine, not
WriteLineAsync, or the override is silently never reached (verified by
temporarily reverting the fix and confirming the test fails only once
the writer overrides the right method).

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread src/Rix/Startup.cs Outdated
Comment on lines +105 to +106
try { await Console.Out.WriteLineAsync(json); }
catch (IOException) { /* nothing left to report to */ }
Comment thread src/Rix/Startup.cs Outdated
Comment on lines +117 to +118
try { await Console.Error.WriteLineAsync($"warning: failed to write result.json: {ex.Message}"); }
catch (IOException) { /* nothing left to report to */ }
An already-disposed stdout/stderr fails the same way a broken pipe does
(IOException), but only the pipe case was caught. Extracted the two
identical try/catch blocks into a single WriteBestEffortAsync helper
so both call sites pick up the wider catch instead of drifting apart
again.
@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants