Skip to content

[release] comment the observability agent analysis on the test's github issue - #66477

Merged
elliot-barn merged 20 commits into
masterfrom
sai-miduthuri/obs-agent-github-issue-comment
Oct 2, 2026
Merged

elliot-barn merged 20 commits into
masterfrom
sai-miduthuri/obs-agent-github-issue-comment

Conversation

@sai-miduthuri

@sai-miduthuri sai-miduthuri commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

When a release test fails on master, #65906 asks the Anyscale observability
agent why, #65910 prints the answer in its own buildkite group, and #66006
attaches it to the job as an annotation. All three reach whoever opens the
build. None of them reaches the person who owns the failing test, who is
watching its GitHub issue.

This posts the analysis there. When the state machine is already tracking an
open issue for the test, the reporter comments the agent's summary on it, with
the failing build in the opening line and a link to the slack thread holding the
full report.

Nothing new is created: the issue is the one ReleaseTestStateMachine opened
when the test started failing, and the comment only appears while that issue is
open. A test with no tracked issue is skipped before the repo handle is built,
so the common case does not pay for the AWS Secrets Manager fetch that building
it costs.

Test.get_open_github_issue is extracted from is_jailed_with_open_issue,
which now delegates to it via has_open_github_issue. Checking that the issue is
open is required rather than defensive: _close_github_issue closes the issue
but leaves KEY_GITHUB_ISSUE_NUMBER on the test, so a recovered test points at a
closed issue indefinitely.

Related issues

None.

Additional information

What the comment looks like

Latest run: https://buildkite.com/ray-project/release/builds/108659#01a0e9e5-…

Observability Agent RCA:

<the agent's summary>

[Full report and feedback](<slack thread>) — rate it with the 'All good' or 'Needs correction' buttons in the thread.

Laid out like the buildkite annotation, and the test is not named because the
comment is on that test's own issue. Each part is dropped rather than left empty
when the agent did not supply it; a response with neither a summary nor a thread
is not posted at all.

One comment per build, not one per job

A test with repeated_run gets one job per repeat — microbenchmark.aws has
five — and a manual retry adds another. All of them are separate processes in
one build, so the only state they share is build-scoped agent meta-data. Each
job claims obs-agent-commented-<test> before posting; a job that finds it
claimed skips.

The claim is written and then read back, because buildkite-agent meta-data
has set/get/exists/keys and no compare-and-set. set is
last-writer-wins, which is what makes the read-back work: when two jobs both
find the key unset and both write, the one whose write landed last reads its own
id back and owns the claim, and the other reads a stranger's and stands down.
Two details make that safe:

  • The claimant is BUILDKITE_JOB_ID, and the protocol is skipped entirely when
    there is none — two jobs writing the same constant would each read it back and
    both proceed.
  • A read-back returning nothing means the agent call failed, not that somebody
    else won: the key was just written. Standing down there would let a broken
    agent silently drop the analysis, so it stands down only on positively reading
    another job's id.

It narrows the race rather than closing it: both jobs still post if one reads
back before the other writes at all, which needs the second write to land in the
gap between one job's two consecutive local calls.

A failed post hands the claim back, by writing an empty value since the agent
cannot delete a key — but only when retrying could succeed (>= 500, 429,
timeouts). Releasing on a permanent failure would make every remaining job fail
the same way.

The buildkite annotation is deliberately not deduped this way. One agent
report per test job is the point of it, and test_the_annotation_is_not_deduped_by_build
pins that.

Everything interpolated into the comment is defused first

The body is markdown and the summary is free-form text from an LLM, so three
things in it would be side effects rather than intentions:

  • @name notifies a real person and #123 posts a backlink on that issue. Both
    autolinks are broken with an empty html comment, which renders as nothing.
  • Anything github reads as an html tag is dropped by its sanitizer — <lambda>
    and <module> are routine in a traceback — so the markup is escaped.
  • Code spans and fenced blocks are left alone entirely: github autolinks neither
    and renders neither as html there, and rewriting inside one would show the
    escapes and the comment marker literally in a quoted log line.

The slack thread url is another value the agent's response decides, so only a
plain http url becomes a link, in the <...> destination form; anything else is
shown as code rather than given the chance to close the link early.

Oversized summaries are trimmed, not dropped

Github rejects a body over 65536 characters. The rest of the body is measured
first and the summary given what is left, so the failing build and the slack link
always survive. The trim cuts the raw summary and re-sanitizes, because
sanitizing expands — < becomes four characters and @ eight — so a budget
spent on raw characters would still overflow, and cutting sanitized text would
cut through a half-written entity. Each pass shortens in proportion to the
overshoot rather than by it; subtracting it outright discarded the whole analysis
for a summary full of @ or <.

This matters beyond tidiness: the 422 an over-long body produces is exactly the
permanent failure the claim above does not release on, so without the trim one
oversized analysis meant no comment from any job in the build.

Nothing here can change a test result

glue.py runs the reporting loop without guarding reporter.report_result(...),
so every failure in this path is caught and logged. GitHubClient also gained a
30s timeout on every request: it had none, and requests waits forever, so a
github that accepted the connection and never answered would have held the step
open until buildkite killed it.

Not duplicating existing work

gh pr list --repo ray-project/ray --state open --search "observability agent github issue comment"
gh pr list --repo ray-project/ray --state open --search "ray_release reporter github issue"

Neither returns anything in this area. The three PRs this builds on (#65906,
#65910, #66006) are all merged.

Tests

//release/... cannot be analysed on current master — //release:requirements_py310_test
references @rules_python//python/pip_install:pip_compile.py, which no longer
exists in the pinned rules_python. That reproduces on a clean master checkout
with no changes, so the release_unit targets were run explicitly instead:

$ bazel test $(bazel query 'attr(tags, release_unit, tests(//release:all))') --test_output=errors
Executed 29 out of 29 tests: 29 tests pass.

$ pre-commit run
# all hooks passed

New coverage in test_observability_agent_reporter.py and test_test.py:

Behavior Test
Comments on an open issue, fetching it exactly once test_comments_on_an_open_issue
Silent on a closed issue, and without a tracked issue test_does_not_comment_on_a_closed_issue, test_does_not_reach_github_without_a_tracked_issue
A github failure never propagates test_a_github_failure_does_not_propagate
The agent's prose cannot mention people or link issues test_the_comment_cannot_mention_people_or_link_issues
Markup is escaped, but not inside code test_the_comment_escapes_what_the_agent_sent, test_the_comment_leaves_the_agents_code_spans_alone
The slack url cannot escape its link test_the_slack_link_target_cannot_escape_the_link, test_the_slack_link_uses_a_bounded_destination
One comment per build across five jobs test_only_one_job_per_build_comments
A simultaneous claim is settled by the read-back test_the_loser_of_a_simultaneous_claim_does_not_comment
An unverifiable claim still comments; no job id means no claim test_an_unverifiable_claim_still_comments, test_no_claim_without_a_job_id_to_claim_with
Transient failures release the claim, permanent ones keep it test_a_transient_failure_hands_the_claim_back, test_a_permanent_failure_keeps_the_claim
The annotation is not deduped test_the_annotation_is_not_deduped_by_build
Oversized summaries are trimmed and still fit test_an_oversized_summary_is_trimmed_to_fit
With no summary the thread is the body; with neither, no comment test_the_comment_points_at_the_thread_when_there_is_no_summary, test_no_comment_when_the_agent_returned_nothing
get_open_github_issue over open / closed / unreachable test_get_open_github_issue

The dedupe and response-shape tests were checked against the bug rather than
only the fix: with them in place, removing the claim fails five of them, and
removing just the read-back fails the simultaneous-claim one.

Note that bazel test //release/... cannot be analysed on current master —
//release:requirements_py310_test references
@rules_python//python/pip_install:pip_compile.py, which no longer exists in the
pinned rules_python. That reproduces on a clean master checkout, hence the
explicit target list above.

Exercised end to end on a real build

The reporter is gated to AUTOMATIC=1 runs on master, so a PR pipeline would
normally run none of this. Four temporary commits opened that path — a canned
agent response, a stand-in anyscale job id and issue number, a forced failure of
hello_world.aws before anything reached anyscale, and an unconditional export
of the flag — and all four are reverted in this PR (73cc06406a,
07d9850788, 34c63a2014, 3eb60acc13). The canned summary was written to
carry exactly what the comment path defuses.

Verified on builds 108426 and 108659, commenting on a scratch issue:

Behaviour
Comment posted on the tracked issue verified
Latest run: names the failing job verified
<class 'ValueError'> and <lambda> render as text, not eaten by the sanitizer verified
@someone and #1234 render plainly — no notification, no issue backlink verified
A fenced code block passes through verbatim verified
A url #fragment in the prose survives verified
Slack link in the bounded <...> destination form verified
One comment per build, across the final automatic retry and each manual one verified
The annotation is not deduped — one per job, per attempt verified

One defect surfaced and was fixed: the comment came out with no Latest run:
line, because result.buildkite_url is filled in by _load_test_configuration
and the forced failure raised above the call to it. That was the scaffolding
rather than the feature — a real failure cannot reach the reporters that way,
since that call is the first thing in the try.

AI assistance

AI assistance (Claude Code) was used for this change.

🤖 Generated with Claude Code

sai-miduthuri and others added 8 commits September 24, 2026 14:45
…ub issue

Parts 2 and 3 of the plan; part 1, the TRIGGER_OBSERVABILITY_AGENT gate,
is in the first PR of this stack.

The analysis reaches whoever opens the buildkite step, or the build page
via the annotation. Neither reaches the person who owns the failing test,
who is watching its github issue. When the state machine is tracking an
open issue for the test, comment the summary there, with the failing build
url and a link to the slack thread that holds the full report.

Test.has_open_github_issue is extracted from is_jailed_with_open_issue,
which now delegates to it. Checking that the issue is *open* is required
rather than defensive: _close_github_issue leaves the issue number on the
test, so a recovered test points at a closed issue indefinitely.

The issue number is checked before the repo handle is built, because
building it fetches the github bot token from AWS Secrets Manager. Most
failing tests track no issue and should not pay for that call, nor carry
its failure modes on the critical path of a failed test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
A github comment renders markdown, and the summary is free-form text from
the agent interpolated into it. An `@name` in that text notifies a real
person, and a `#123` posts a cross-reference backlink onto that issue --
both side effects on people and issues that have nothing to do with the
failure.

Break the autolinks with an empty html comment, which renders as nothing,
so the text reads exactly as written. `#` only before a digit, so url
fragments in the prose survive. Applied to the summary alone: our own
lines, including the buildkite url and its fragment, are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Five findings from the local review of the commits above, in descending
order of what they cost:

- The summary went into the comment unescaped, so github's sanitizer ate
  anything it read as a tag. `<lambda>` and `<module>` are routine in a
  traceback, and the reader would have been handed prose with holes in
  it. It is escaped now, as the buildkite annotation already did.
- Escaping and the mention rewrite are both wrong inside a code span or
  fence: github autolinks neither and renders neither as html there, so
  the markers would have shown up literally in a quoted log line. Both
  now skip those regions.
- GitHubClient made every request with no timeout, and requests waits
  forever. A github that accepts the connection and never answers would
  have held the step open until buildkite killed it -- losing the test
  result, which is the one thing this reporter must never touch.
- The analysis is now written before the annotation and the comment:
  it is already computed, and writing it is local and free, so a slow
  github cannot cost the build the analysis it just paid for.
- The slack thread url is another value the agent's response decides,
  and it went into a markdown link target raw. Only a plain http url is
  linked now; anything else is shown as code.

Also: guard the issue number with `not` rather than `is None`, as
state_machine.py does, and stop the skip log claiming an issue is closed
when github may simply have been unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Review item O3. has_open_github_issue fetched the issue and threw it
away, and then the reporter fetched the same issue again to comment on
it: two GET /repos/anyscale/ray/issues/N for one comment, on the critical
path of a failed test, with a window in between where the issue could
change.

Test.get_open_github_issue returns the issue rather than a yes/no, so the
caller comments on the object the open-check already read.
has_open_github_issue delegates to it and keeps its signature, so
is_jailed_with_open_issue and filter.py are untouched.

Measured with the FakeRepo hook the tests already had and never asserted:
['123', '123'] before, ['123'] after, for both an open and a closed
issue. Now asserted, in both cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Two review items, both in the comment path only. The buildkite annotation
is untouched: one agent report per test job is the point of it.

O1 -- a test with `repeated_run` gets one job per repeat (five for
microbenchmark.aws) and a manual retry adds another, all in one build and
all posting their own comment. They are separate processes, so the only
state they share is build-scoped agent meta-data. Each job claims
`obs-agent-commented-<test>` before posting; a job that finds it claimed
skips.

The claim is taken immediately before the post, after the issue lookup, so
that an unreachable github or a closed issue cannot consume the build's
one comment. A failed post hands the claim back -- by writing an empty
value, since the agent CLI has set/get/exists/keys and no delete -- but
only when retrying could succeed. Releasing on a permanent failure would
make every remaining job fail the same way: a body over github's size cap
422s every time, and so does a missing permission.

`get`-then-`set` is not atomic, so two jobs can still both claim. The
window is two fast local calls against repeats that arrive here after
their own minute-long agent queries, so the worst case is a second
comment, not a fifth. Off buildkite there is no build to dedupe within and
no meta-data call is made.

O2 -- with no summary the comment used to say only that there was no
summary, which is an email to every watcher of the issue carrying nothing.
The slack thread is then the whole of what the agent has to say, so paste
it as the body instead. With neither a summary nor a thread there is
nothing to post at all, and that is checked before the claim so a job with
nothing to say cannot take the claim from one that has an analysis.

Verified by neutering the claim with the tests in place: the five
dedupe tests fail, three of them on the permanent-failure release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The claim was get-then-set with no compare-and-set behind it, so two jobs
that both found the key unset both wrote and both posted.

Write, then read back. `set` is last-writer-wins, and that is what makes
this work: the job whose write landed last reads its own id back and owns
the claim; the other reads a stranger's and stands down.

Two details that decide whether it is safe:

- The claim is identified by BUILDKITE_JOB_ID, so a job without one cannot
  run the protocol at all -- two jobs writing the same constant would each
  read it back and both proceed. Without a job id it falls open instead.
- A read-back that fails is not evidence somebody else won. The key was
  just written, so it is set; None means the agent call failed. Stand down
  only on positively reading another job's id, or a broken agent silently
  drops the analysis.

Still narrowed rather than closed: both jobs post if one reads back before
the other writes at all, which needs the second write to land in the gap
between the first job's two consecutive local calls.

Verified by removing the read-back with the tests in place:
test_the_loser_of_a_simultaneous_claim_does_not_comment fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
It sat below the analysis, which put it behind whatever the agent wrote --
and with no summary it trailed the slack link, where the build url is the
one thing a reader of the issue needs in order to go and look.

The opening line now reads "The observability agent looked at the latest
failure of `<test>` at <buildkite url>.", and the separate "Failing build"
line is gone. A result without a buildkite url keeps the sentence without
the "at" clause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Review items O4 and O6.

O4 -- github rejects a comment body over 65536 characters, and the 422 it
answers with was caught and logged, so an over-long analysis produced no
comment at all. It was also the one failure the build claim deliberately
does not release on, since every job would fail the same way.

The summary is the only part of the body whose length this reporter does
not control, so it is what gets trimmed. The rest is measured first and
the summary is given what is left, which keeps the parts worth having: the
failing build in the opening line, and the slack thread holding the
analysis that was cut.

The trim works on the raw summary and re-sanitizes, because sanitizing
expands -- `<` becomes four characters and `@` eight -- so a budget spent
on raw characters would still overflow, and cutting sanitized text would
cut through a half-written entity. Each pass shortens in proportion to the
overshoot rather than by it: subtracting it outright threw away the whole
analysis for a summary full of `@` or `<`, where proportion keeps ~65k
characters of it. Checked against five shapes, including one that is all
mentions and one exactly at the limit.

O6 -- the open/closed/unreachable matrix was written out three times, once
per method, against the real http layer. It is now parametrized over
(issue, expected) on get_open_github_issue, where the state logic actually
lives, plus a delegation test for each of the two one-line methods above
it and one for the jailed short-circuit that asserts github is never
asked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
sai-miduthuri and others added 6 commits September 24, 2026 16:24
Three things this reporter needs that a PR build cannot give it: a real
anyscale job to analyse, a real debug session, and a test whose github
issue the state machine is tracking. All three are stood in for here, so
that everything downstream -- the sanitizing, the trimming, the build
claim, the annotation and the comment itself -- runs its real path.

The canned summary is written to exercise what the comment path defuses:
an `@name`, a `#1234`, `<class 'ValueError'>` and `<lambda>` outside a
code fence and again inside one, and a url with a `#fragment`. Rendered on
a real github issue it shows whether the escaping and the autolink
breaking actually hold.

The fake issue lives on the state machine's repo and receives only canned
data; it tracks nothing and is closed once this is reverted.

Revert this commit, and delete release/obs_agent_fake_response.json,
before merging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
RELEASE_TEST_FORCE_FAILURE names one test to fail at the very top of
run_release_test_anyscale, before the cluster or the job exist. Scoped to
a single test rather than all of them, so the rest of the release pipeline
on this PR behaves normally.

TestCommandError carries COMMAND_ERROR, which update_result_from_exception
maps to the ERROR status -- one the agent triggers on. runtime is set high
first: _is_transient_error compares the runtime captured before the
rewrite against BUILDKITE_TIME_LIMIT_FOR_RETRY, so a zero-runtime failure
becomes TRANSIENT_INFRA_ERROR on every attempt but the last, and the agent
would sit out the first three.

Revert before merging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The real gate only exports TRIGGER_OBSERVABILITY_AGENT for AUTOMATIC runs
on master, so a PR build gets no reporter at all -- not the analysis, the
log group, the annotation or the comment. Exported unconditionally here.

The four fake values go in run_release_test.sh, which already runs inside
the release test container, so nothing has to be threaded through the step
env: the canned response file, the stand-in anyscale job id, the fake
github issue to comment on (anyscale#2040) and the name of the test to
fail. Each is `${VAR:-default}`, so a build can override any of them from
the buildkite env without another commit.

Revert all three DO NOT MERGE commits before merging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The comment opened with a sentence naming the test, which the issue it is
posted on already identifies, and buried the feedback ask in a paragraph
of its own wording. The annotation had already settled a better shape, so
use it: the run first, because it is what a reader of the issue needs in
order to go and look, then the analysis under a heading, then the same
"Full report and feedback -- rate it with the 'All good' or 'Needs
correction' buttons in the thread." line the annotation ends with.

    Latest run: https://buildkite.com/ray-project/release/builds/1#job

    Observability Agent RCA:

    <summary>

    [Full report and feedback](<slack thread>) -- rate it with ...

Each part is dropped rather than left empty when the agent did not supply
it, so a response with no summary leaves no heading standing over an
analysis that never arrived. The case where dropping them all would leave
nothing worth posting -- neither a summary nor a thread -- is already
refused before the body is built.

Truncation is unaffected: the summary is still the only part measured
against what the rest of the body leaves, so an oversized analysis still
fits with the run and the thread intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The comment on anyscale#2040 came out with no "Latest run:" line.
That is this scaffolding, not the feature: result.buildkite_url is filled
in by _load_test_configuration, and the forced failure raises above the
call to it, so the reporters saw an empty url. A real failure cannot reach
them that way, because that call is the first thing in the try.

Mirror the four lines from _load_test_configuration into the forced
failure rather than calling it, so that nothing else it does -- chdir,
constructing the cluster manager and command runner -- runs on a path
whose whole point is never reaching anyscale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Most of them restated the line below. Keep only what cannot be read off
the code: why a guard is `not` rather than `is None`, why the claim is
read back, why the summary is trimmed raw rather than sanitized.

Scoped to comments this PR introduces. The ones already on master are
equally overgrown, but trimming them here would bury the actual change in
this diff; that is a separate branch.

Comments and docstrings only, no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
@sai-miduthuri

Copy link
Copy Markdown
Contributor Author

GitHub Issue comment example created at anyscale#2040 through release pipeline build buildkite/release for commit 224a50a.

This reverts commit 224a50a.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
This reverts commit a3c97d6.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
This reverts commit bc33527.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
… target"

This reverts commit dac8282.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
@sai-miduthuri
sai-miduthuri marked this pull request as ready for review September 28, 2026 22:29
@sai-miduthuri
sai-miduthuri requested a review from a team as a code owner September 28, 2026 22:29

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request enables the Observability Agent to post its Root Cause Analysis (RCA) directly as a comment on a test's open GitHub issue. It includes logic to prevent duplicate comments from multiple jobs in the same Buildkite build, sanitizes agent output to avoid unwanted mentions or autolinks, and handles truncation to respect GitHub's comment size limits. Additionally, a default timeout is introduced for GitHub API requests. The feedback suggests adding a timeout to the buildkite-agent subprocess call to prevent potential hangs during metadata operations.

Comment thread release/ray_release/reporter/observability_agent.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3eb60ac. Configure here.

Comment thread release/ray_release/test.py
Comment thread release/ray_release/reporter/observability_agent.py
sai-miduthuri and others added 2 commits September 28, 2026 16:03
`buildkite-agent meta-data` talks to buildkite's api, so it can hang like
any other request. 30s, matching GitHubClient.TIMEOUT. TimeoutExpired is
an Exception, so the existing guard already turns it into "no claim
recorded" and falls open to commenting.

get_open_github_issue caught only GitHubException, but the timeout added
earlier in this PR raises requests.Timeout, which is not one. That breaks
the documented "unreachable github answers None" contract, and
is_jailed_with_open_issue is called from filter.py with nothing around it,
so a slow github would have failed the whole init job. It catches
RequestException too now.

CODE_REGION treated ```x``` on one line as an unclosed fence and skipped
everything after it, so an `@name` later in the summary still autolinked.
A fence opener's info string may not contain a backtick, so that is an
inline span; the regex now says so. Checked against a one-line span, a
plain fence, a fence with an info string, a tilde fence and an unclosed
fence.

Each of the three fails its test when reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
`buildkite-agent annotate` reaches buildkite's api exactly as `meta-data`
does, and was the last unbounded call in the file. Same 30s; the existing
`except Exception` already turns a TimeoutExpired into a warning, since an
annotation must not change the outcome of a test run.

It came in with #66006 rather than this PR, so it is strictly speaking
out of scope, but it is one argument and leaving the two inconsistent
invites the question.

The test now asserts that *every* buildkite-agent invocation is bounded,
rather than the meta-data ones only, so a future unbounded call fails it.
Audited the rest of the file: the only other external call is
requests.post in _post_json, which already carries its own timeouts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
@sai-miduthuri sai-miduthuri added the go add ONLY when ready to merge, run all tests label Sep 28, 2026
@ray-gardener ray-gardener Bot added the core Issues that should be addressed in Ray Core label Sep 29, 2026

# Github rejects a longer comment body. The summary is the only part this
# reporter does not control the length of, so it is what gets trimmed.
GITHUB_COMMENT_LIMIT = 65536

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

theres an argument to be made where we should point users to the agent's findings in slack if we want users to comment their responses on the threads. I'm open to approving this for now but lets assess if this increases engagement with the observability agent findings

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

*Point users to the slack message instead of outputting the content as a github comment

@sai-miduthuri sai-miduthuri Oct 2, 2026 •

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.

This is for a hypothetical, unlikely case where the "summary" (which isn't the full root cause analysis by the agent, and should never actually be 65536 chars long) created by the agent is just so large that it reaches the GITHUB_COMMENT_LIMIT. When this happens, the summary is truncated with enough room to accommodate the Slack message link at the end.

The Slack message link is guaranteed to always be printed.

@elliot-barn
elliot-barn merged commit f733abf into master Oct 2, 2026
7 checks passed
@elliot-barn
elliot-barn deleted the sai-miduthuri/obs-agent-github-issue-comment branch October 2, 2026 20:40
nadongjun pushed a commit to nadongjun/ray that referenced this pull request Oct 6, 2026
…ub issue (ray-project#66477)

## Description

When a release test fails on master, ray-project#65906 asks the Anyscale
observability
agent why, ray-project#65910 prints the answer in its own buildkite group, and
ray-project#66006
attaches it to the job as an annotation. All three reach whoever opens
the
build. None of them reaches the person who owns the failing test, who is
watching its **GitHub issue**.

This posts the analysis there. When the state machine is already
tracking an
open issue for the test, the reporter comments the agent's summary on
it, with
the failing build in the opening line and a link to the slack thread
holding the
full report.

Nothing new is created: the issue is the one `ReleaseTestStateMachine`
opened
when the test started failing, and the comment only appears while that
issue is
open. A test with no tracked issue is skipped before the repo handle is
built,
so the common case does not pay for the AWS Secrets Manager fetch that
building
it costs.

`Test.get_open_github_issue` is extracted from
`is_jailed_with_open_issue`,
which now delegates to it via `has_open_github_issue`. Checking that the
issue is
*open* is required rather than defensive: `_close_github_issue` closes
the issue
but leaves `KEY_GITHUB_ISSUE_NUMBER` on the test, so a recovered test
points at a
closed issue indefinitely.

## Related issues

None.

## Additional information

### What the comment looks like

```
Latest run: https://buildkite.com/ray-project/release/builds/108659#01a0e9e5-…

Observability Agent RCA:

<the agent's summary>

[Full report and feedback](<slack thread>) — rate it with the 'All good' or 'Needs correction' buttons in the thread.
```

Laid out like the buildkite annotation, and the test is not named
because the
comment is on that test's own issue. Each part is dropped rather than
left empty
when the agent did not supply it; a response with neither a summary nor
a thread
is not posted at all.

### One comment per build, not one per job

A test with `repeated_run` gets one job per repeat —
`microbenchmark.aws` has
five — and a manual retry adds another. All of them are separate
processes in
one build, so the only state they share is build-scoped agent meta-data.
Each
job claims `obs-agent-commented-<test>` before posting; a job that finds
it
claimed skips.

The claim is written and then **read back**, because `buildkite-agent
meta-data`
has `set`/`get`/`exists`/`keys` and no compare-and-set. `set` is
last-writer-wins, which is what makes the read-back work: when two jobs
both
find the key unset and both write, the one whose write landed last reads
its own
id back and owns the claim, and the other reads a stranger's and stands
down.
Two details make that safe:

- The claimant is `BUILDKITE_JOB_ID`, and the protocol is skipped
entirely when
there is none — two jobs writing the same constant would each read it
back and
  both proceed.
- A read-back returning nothing means the *agent call* failed, not that
somebody
else won: the key was just written. Standing down there would let a
broken
agent silently drop the analysis, so it stands down only on positively
reading
  another job's id.

It narrows the race rather than closing it: both jobs still post if one
reads
back before the other writes at all, which needs the second write to
land in the
gap between one job's two consecutive local calls.

A failed post hands the claim back, by writing an empty value since the
agent
cannot delete a key — but **only when retrying could succeed** (`>=
500`, `429`,
timeouts). Releasing on a permanent failure would make every remaining
job fail
the same way.

**The buildkite annotation is deliberately not deduped this way.** One
agent
report per test *job* is the point of it, and
`test_the_annotation_is_not_deduped_by_build`
pins that.

### Everything interpolated into the comment is defused first

The body is markdown and the summary is free-form text from an LLM, so
three
things in it would be side effects rather than intentions:

- `@name` notifies a real person and `ray-project#123` posts a backlink on that
issue. Both
autolinks are broken with an empty html comment, which renders as
nothing.
- Anything github reads as an html tag is dropped by its sanitizer —
`<lambda>`
  and `<module>` are routine in a traceback — so the markup is escaped.
- Code spans and fenced blocks are left alone entirely: github autolinks
neither
and renders neither as html there, and rewriting inside one would show
the
  escapes and the comment marker literally in a quoted log line.

The slack thread url is another value the agent's response decides, so
only a
plain http url becomes a link, in the `<...>` destination form; anything
else is
shown as code rather than given the chance to close the link early.

### Oversized summaries are trimmed, not dropped

Github rejects a body over 65536 characters. The rest of the body is
measured
first and the summary given what is left, so the failing build and the
slack link
always survive. The trim cuts the *raw* summary and re-sanitizes,
because
sanitizing expands — `<` becomes four characters and `@` eight — so a
budget
spent on raw characters would still overflow, and cutting sanitized text
would
cut through a half-written entity. Each pass shortens in proportion to
the
overshoot rather than by it; subtracting it outright discarded the whole
analysis
for a summary full of `@` or `<`.

This matters beyond tidiness: the 422 an over-long body produces is
exactly the
permanent failure the claim above does not release on, so without the
trim one
oversized analysis meant no comment from any job in the build.

### Nothing here can change a test result

`glue.py` runs the reporting loop without guarding
`reporter.report_result(...)`,
so every failure in this path is caught and logged. `GitHubClient` also
gained a
30s timeout on every request: it had none, and `requests` waits forever,
so a
github that accepted the connection and never answered would have held
the step
open until buildkite killed it.

### Not duplicating existing work

```bash
gh pr list --repo ray-project/ray --state open --search "observability agent github issue comment"
gh pr list --repo ray-project/ray --state open --search "ray_release reporter github issue"
```

Neither returns anything in this area. The three PRs this builds on
(ray-project#65906,
ray-project#65910, ray-project#66006) are all merged.

### Tests

`//release/...` cannot be analysed on current master —
`//release:requirements_py310_test`
references `@rules_python//python/pip_install:pip_compile.py`, which no
longer
exists in the pinned rules_python. That reproduces on a clean master
checkout
with no changes, so the release_unit targets were run explicitly
instead:

```bash
$ bazel test $(bazel query 'attr(tags, release_unit, tests(//release:all))') --test_output=errors
Executed 29 out of 29 tests: 29 tests pass.

$ pre-commit run
# all hooks passed
```

New coverage in `test_observability_agent_reporter.py` and
`test_test.py`:

| Behavior | Test |
| --- | --- |
| Comments on an open issue, fetching it exactly once |
`test_comments_on_an_open_issue` |
| Silent on a closed issue, and without a tracked issue |
`test_does_not_comment_on_a_closed_issue`,
`test_does_not_reach_github_without_a_tracked_issue` |
| A github failure never propagates |
`test_a_github_failure_does_not_propagate` |
| The agent's prose cannot mention people or link issues |
`test_the_comment_cannot_mention_people_or_link_issues` |
| Markup is escaped, but not inside code |
`test_the_comment_escapes_what_the_agent_sent`,
`test_the_comment_leaves_the_agents_code_spans_alone` |
| The slack url cannot escape its link |
`test_the_slack_link_target_cannot_escape_the_link`,
`test_the_slack_link_uses_a_bounded_destination` |
| One comment per build across five jobs |
`test_only_one_job_per_build_comments` |
| A simultaneous claim is settled by the read-back |
`test_the_loser_of_a_simultaneous_claim_does_not_comment` |
| An unverifiable claim still comments; no job id means no claim |
`test_an_unverifiable_claim_still_comments`,
`test_no_claim_without_a_job_id_to_claim_with` |
| Transient failures release the claim, permanent ones keep it |
`test_a_transient_failure_hands_the_claim_back`,
`test_a_permanent_failure_keeps_the_claim` |
| The annotation is *not* deduped |
`test_the_annotation_is_not_deduped_by_build` |
| Oversized summaries are trimmed and still fit |
`test_an_oversized_summary_is_trimmed_to_fit` |
| With no summary the thread is the body; with neither, no comment |
`test_the_comment_points_at_the_thread_when_there_is_no_summary`,
`test_no_comment_when_the_agent_returned_nothing` |
| `get_open_github_issue` over open / closed / unreachable |
`test_get_open_github_issue` |

The dedupe and response-shape tests were checked against the bug rather
than
only the fix: with them in place, removing the claim fails five of them,
and
removing just the read-back fails the simultaneous-claim one.

Note that `bazel test //release/...` cannot be analysed on current
master —
`//release:requirements_py310_test` references
`@rules_python//python/pip_install:pip_compile.py`, which no longer
exists in the
pinned rules_python. That reproduces on a clean master checkout, hence
the
explicit target list above.

### Exercised end to end on a real build

The reporter is gated to `AUTOMATIC=1` runs on master, so a PR pipeline
would
normally run none of this. Four temporary commits opened that path — a
canned
agent response, a stand-in anyscale job id and issue number, a forced
failure of
`hello_world.aws` before anything reached anyscale, and an unconditional
export
of the flag — and **all four are reverted in this PR** (`73cc06406a`,
`07d9850788`, `34c63a2014`, `3eb60acc13`). The canned summary was
written to
carry exactly what the comment path defuses.

Verified on builds 108426 and 108659, commenting on a scratch issue:

| Behaviour | |
| --- | --- |
| Comment posted on the tracked issue | verified |
| `Latest run:` names the failing job | verified |
| `<class 'ValueError'>` and `<lambda>` render as text, not eaten by the
sanitizer | verified |
| `@someone` and `ray-project#1234` render plainly — no notification, no issue
backlink | verified |
| A fenced code block passes through verbatim | verified |
| A url `#fragment` in the prose survives | verified |
| Slack link in the bounded `<...>` destination form | verified |
| **One comment per build**, across the final automatic retry and each
manual one | verified |
| **The annotation is not deduped** — one per job, per attempt |
verified |

One defect surfaced and was fixed: the comment came out with no `Latest
run:`
line, because `result.buildkite_url` is filled in by
`_load_test_configuration`
and the forced failure raised above the call to it. That was the
scaffolding
rather than the feature — a real failure cannot reach the reporters that
way,
since that call is the first thing in the `try`.

### AI assistance

AI assistance (Claude Code) was used for this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
elliot-barn pushed a commit that referenced this pull request Oct 6, 2026
## Description

Stacked on #66477. **Review that one first; this PR's diff is only the
change on
top of it.**

A release test that fails on a `releases/*` branch gets triaged by a
human the
same way a master failure does, but gets none of the buildkite-side
reporting
the agent provides: no analysis in its own log group, no annotation on
the job.
The agent only reads — it asks the observability agent why a job failed
and
prints the answer — so there is no reason it cannot run there.

The single condition that exported both flags is split in two:

- `REPORT_TO_RAY_TEST_DB` keeps its master-only gate. Recording a result
moves
the test's state and feeds the state machine that opens, closes and
jails
  github issues.
- `TRIGGER_OBSERVABILITY_AGENT` widens to master **plus** `releases/*`,
for
  `AUTOMATIC=1` builds only. Release-branch tests are triggered through
automation, which is what keeps the builds trackable; a hand-kicked
build is
  the exception and is left alone.

| branch | `AUTOMATIC` | db | agent | comment |
| --- | --- | --- | --- | --- |
| `master` | 1 | yes | yes | yes |
| `releases/*` | 1 | no | **yes** | no |
| anything else | 1 | no | no | no |
| any | 0 | no | no | no |

## Related issues

None.

## Additional information

### This is not retroactive

The `release` pipeline is triggered with `branch:
"${BUILDKITE_BRANCH}"`, so a
release-branch build checks out that branch and runs *its* copy of the
init
script and the reporter. Merging here therefore covers release branches
cut
after this lands, not ones that already exist.

That is deliberate rather than overlooked: branches cut behind this
commit get
it cherry-picked, and the first backport target is `releases/2.60.0`
when that
branch is created. `releases/2.58.0` and earlier are not backported.

### The github comment gets its own flag

`OBS_AGENT_COMMENT_ON_GITHUB_ISSUE`, set only on master today, forwarded
into
the step env beside the other two, and checked first in
`_comment_on_github_issue` — before the issue lookup, so a build that is
not
commenting does no github work at all and never reaches the AWS Secrets
Manager
fetch behind the repo handle.

A flag rather than a branch check, for two reasons:

1. A release-branch build would in fact skip commenting anyway, because
nothing
populates `KEY_GITHUB_ISSUE_NUMBER` when `RayTestDBReporter` does not
run.
That is an accident of ordering rather than a decision. The flag makes
it a
   decision.
2. Turning comments on for release branches later is then one line in
`custom-image-build-and-test-init.sh`, with no code change and no branch
name
   anywhere in the reporter.

### Why a release-branch build cannot move a test's state

This is the property the change rests on, and it already held in two
independent ways:

- `REPORT_TO_RAY_TEST_DB` stays master-only, so `RayTestDBReporter` —
the only
reporter that calls `persist_result_to_s3()`,
`ReleaseTestStateMachine.move()`
  and `persist_to_s3()` — is never appended on a release branch.
- Even if it were, it guards itself: `reporter/ray_test_db.py` returns
early
when `BUILDKITE_BRANCH != "master"`, and again when the pipeline is not
in
  `ci_pipeline_postmerge`. Neither check consults the flag.

That second guarantee had **no test at all**, so this PR adds one rather
than
leaving it to be assumed.

Two things deliberately unchanged, to be explicit: `DBReporter`
(`reporter/db.py`) writes performance rows to databricks on every branch
today
via `report=True` in `buildkite/step.py`, and records results rather
than test
state. And the agent still posts a slack thread per analysis, so
release-branch
failures will appear in that channel alongside master's — the agent's
metadata
carries nothing that distinguishes the branch, so they are only tellable
apart
by the anyscale job id. Accepted; the analysis itself is still
attributable from
the buildkite side, since the log group and annotation both sit on the
job.

### Not duplicating existing work

```bash
gh pr list --repo ray-project/ray --state open --search "observability agent release branch"
gh pr list --repo ray-project/ray --state open --search "TRIGGER_OBSERVABILITY_AGENT"
```

Neither returns anything in this area beyond #66477, which this is
stacked on.

### Tests

`//release/...` cannot be analysed on current master —
`//release:requirements_py310_test`
references `@rules_python//python/pip_install:pip_compile.py`, which no
longer
exists in the pinned rules_python, and that reproduces on a clean master
checkout. The release_unit targets are run explicitly instead:

```bash
$ bazel test $(bazel query 'attr(tags, release_unit, tests(//release:all))') --test_output=errors
Executed 30 out of 30 tests: 30 tests pass.

$ pre-commit run
# all hooks passed, shellcheck included
```

New coverage:

| Behavior | Test |
| --- | --- |
| Each flag reaches the steps only when set to `1`, **and only itself**
— setting one must not export the other |
`test_custom_image_build_and_test_init_threads_the_obs_agent_flags`,
over `trigger_only` / `comment_only` / `both_falsy` |
| Comments off: no comment, and the github repo handle is never built |
`test_no_comment_when_github_comments_are_not_enabled` |
| Comments off: the log and the annotation still carry the analysis, and
the build's comment claim is left untouched |
`test_the_analysis_is_still_reported_when_comments_are_disabled` |
| Nothing is recorded off master |
`test_nothing_is_recorded_off_master`, over release branches, a feature
branch and an unset branch |
| Master alone is not enough; the pipeline is checked too |
`test_nothing_is_recorded_outside_the_postmerge_pipeline` |
| A master postmerge result **is** recorded — so the two guards above
can be told apart from a reporter that does nothing |
`test_a_master_postmerge_result_is_recorded` |

Each of these was checked by reproducing the defect it guards, not by
rerunning
green tests:

- Removing the comment-flag check fails the two comments-disabled tests.
Adding
  the check in the first place broke the 20 existing tests that exercise
commenting until the helpers set it, which is what confirmed the gate is
  load-bearing.
- Moving the flag check below `TestStateMachine.get_ray_repo()` fails
`test_no_comment_when_github_comments_are_not_enabled` — the assertion
is that
the handle is never built, not merely that the issue is never fetched,
because
  building it costs an AWS Secrets Manager fetch.
- `return` as the first statement of `RayTestDBReporter.report_result`
fails
`test_a_master_postmerge_result_is_recorded`. Without that case the file
asserted only absences and passed against a reporter that did nothing at
all.
- Breaking either db-reporter guard now fails on an assertion with no
`botocore`
in the log. Previously the unpatched `update_from_s3` /
`get_test_results`
  turned a regression into a real boto3 call and an opaque timeout.
- Collapsing the two forwarding blocks so the comment flag also exports
the
trigger fails `comment_only`. The earlier one-variable-at-a-time
parametrize
passed against that same mutation, which is why the cases are
asymmetric.

The shell conditions have no test target, so they were exercised
directly across
the six branch/`AUTOMATIC` combinations in the table above.

**Not exercised on a real build.** #66477 carried temporary commits for
that and
they have since been reverted; this change is gated the same way, so a
PR
pipeline runs none of it.

### AI assistance

AI assistance (Claude Code) was used for this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
sai-miduthuri added a commit that referenced this pull request Oct 8, 2026
## Description

Stacked on #66477. **Review that one first; this PR's diff is only the
change on
top of it.**

A release test that fails on a `releases/*` branch gets triaged by a
human the
same way a master failure does, but gets none of the buildkite-side
reporting
the agent provides: no analysis in its own log group, no annotation on
the job.
The agent only reads — it asks the observability agent why a job failed
and
prints the answer — so there is no reason it cannot run there.

The single condition that exported both flags is split in two:

- `REPORT_TO_RAY_TEST_DB` keeps its master-only gate. Recording a result
moves
the test's state and feeds the state machine that opens, closes and
jails
  github issues.
- `TRIGGER_OBSERVABILITY_AGENT` widens to master **plus** `releases/*`,
for
  `AUTOMATIC=1` builds only. Release-branch tests are triggered through
automation, which is what keeps the builds trackable; a hand-kicked
build is
  the exception and is left alone.

| branch | `AUTOMATIC` | db | agent | comment |
| --- | --- | --- | --- | --- |
| `master` | 1 | yes | yes | yes |
| `releases/*` | 1 | no | **yes** | no |
| anything else | 1 | no | no | no |
| any | 0 | no | no | no |

## Related issues

None.

## Additional information

### This is not retroactive

The `release` pipeline is triggered with `branch:
"${BUILDKITE_BRANCH}"`, so a
release-branch build checks out that branch and runs *its* copy of the
init
script and the reporter. Merging here therefore covers release branches
cut
after this lands, not ones that already exist.

That is deliberate rather than overlooked: branches cut behind this
commit get
it cherry-picked, and the first backport target is `releases/2.60.0`
when that
branch is created. `releases/2.58.0` and earlier are not backported.

### The github comment gets its own flag

`OBS_AGENT_COMMENT_ON_GITHUB_ISSUE`, set only on master today, forwarded
into
the step env beside the other two, and checked first in
`_comment_on_github_issue` — before the issue lookup, so a build that is
not
commenting does no github work at all and never reaches the AWS Secrets
Manager
fetch behind the repo handle.

A flag rather than a branch check, for two reasons:

1. A release-branch build would in fact skip commenting anyway, because
nothing
populates `KEY_GITHUB_ISSUE_NUMBER` when `RayTestDBReporter` does not
run.
That is an accident of ordering rather than a decision. The flag makes
it a
   decision.
2. Turning comments on for release branches later is then one line in
`custom-image-build-and-test-init.sh`, with no code change and no branch
name
   anywhere in the reporter.

### Why a release-branch build cannot move a test's state

This is the property the change rests on, and it already held in two
independent ways:

- `REPORT_TO_RAY_TEST_DB` stays master-only, so `RayTestDBReporter` —
the only
reporter that calls `persist_result_to_s3()`,
`ReleaseTestStateMachine.move()`
  and `persist_to_s3()` — is never appended on a release branch.
- Even if it were, it guards itself: `reporter/ray_test_db.py` returns
early
when `BUILDKITE_BRANCH != "master"`, and again when the pipeline is not
in
  `ci_pipeline_postmerge`. Neither check consults the flag.

That second guarantee had **no test at all**, so this PR adds one rather
than
leaving it to be assumed.

Two things deliberately unchanged, to be explicit: `DBReporter`
(`reporter/db.py`) writes performance rows to databricks on every branch
today
via `report=True` in `buildkite/step.py`, and records results rather
than test
state. And the agent still posts a slack thread per analysis, so
release-branch
failures will appear in that channel alongside master's — the agent's
metadata
carries nothing that distinguishes the branch, so they are only tellable
apart
by the anyscale job id. Accepted; the analysis itself is still
attributable from
the buildkite side, since the log group and annotation both sit on the
job.

### Not duplicating existing work

```bash
gh pr list --repo ray-project/ray --state open --search "observability agent release branch"
gh pr list --repo ray-project/ray --state open --search "TRIGGER_OBSERVABILITY_AGENT"
```

Neither returns anything in this area beyond #66477, which this is
stacked on.

### Tests

`//release/...` cannot be analysed on current master —
`//release:requirements_py310_test`
references `@rules_python//python/pip_install:pip_compile.py`, which no
longer
exists in the pinned rules_python, and that reproduces on a clean master
checkout. The release_unit targets are run explicitly instead:

```bash
$ bazel test $(bazel query 'attr(tags, release_unit, tests(//release:all))') --test_output=errors
Executed 30 out of 30 tests: 30 tests pass.

$ pre-commit run
# all hooks passed, shellcheck included
```

New coverage:

| Behavior | Test |
| --- | --- |
| Each flag reaches the steps only when set to `1`, **and only itself**
— setting one must not export the other |
`test_custom_image_build_and_test_init_threads_the_obs_agent_flags`,
over `trigger_only` / `comment_only` / `both_falsy` |
| Comments off: no comment, and the github repo handle is never built |
`test_no_comment_when_github_comments_are_not_enabled` |
| Comments off: the log and the annotation still carry the analysis, and
the build's comment claim is left untouched |
`test_the_analysis_is_still_reported_when_comments_are_disabled` |
| Nothing is recorded off master |
`test_nothing_is_recorded_off_master`, over release branches, a feature
branch and an unset branch |
| Master alone is not enough; the pipeline is checked too |
`test_nothing_is_recorded_outside_the_postmerge_pipeline` |
| A master postmerge result **is** recorded — so the two guards above
can be told apart from a reporter that does nothing |
`test_a_master_postmerge_result_is_recorded` |

Each of these was checked by reproducing the defect it guards, not by
rerunning
green tests:

- Removing the comment-flag check fails the two comments-disabled tests.
Adding
  the check in the first place broke the 20 existing tests that exercise
commenting until the helpers set it, which is what confirmed the gate is
  load-bearing.
- Moving the flag check below `TestStateMachine.get_ray_repo()` fails
`test_no_comment_when_github_comments_are_not_enabled` — the assertion
is that
the handle is never built, not merely that the issue is never fetched,
because
  building it costs an AWS Secrets Manager fetch.
- `return` as the first statement of `RayTestDBReporter.report_result`
fails
`test_a_master_postmerge_result_is_recorded`. Without that case the file
asserted only absences and passed against a reporter that did nothing at
all.
- Breaking either db-reporter guard now fails on an assertion with no
`botocore`
in the log. Previously the unpatched `update_from_s3` /
`get_test_results`
  turned a regression into a real boto3 call and an opaque timeout.
- Collapsing the two forwarding blocks so the comment flag also exports
the
trigger fails `comment_only`. The earlier one-variable-at-a-time
parametrize
passed against that same mutation, which is why the cases are
asymmetric.

The shell conditions have no test target, so they were exercised
directly across
the six branch/`AUTOMATIC` combinations in the table above.

**Not exercised on a real build.** #66477 carried temporary commits for
that and
they have since been reverted; this change is gated the same way, so a
PR
pipeline runs none of it.

### AI assistance

AI assistance (Claude Code) was used for this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0e0054d)
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
sai-miduthuri added a commit that referenced this pull request Oct 8, 2026
## Description

Stacked on #66477. **Review that one first; this PR's diff is only the
change on
top of it.**

A release test that fails on a `releases/*` branch gets triaged by a
human the
same way a master failure does, but gets none of the buildkite-side
reporting
the agent provides: no analysis in its own log group, no annotation on
the job.
The agent only reads — it asks the observability agent why a job failed
and
prints the answer — so there is no reason it cannot run there.

The single condition that exported both flags is split in two:

- `REPORT_TO_RAY_TEST_DB` keeps its master-only gate. Recording a result
moves
the test's state and feeds the state machine that opens, closes and
jails
  github issues.
- `TRIGGER_OBSERVABILITY_AGENT` widens to master **plus** `releases/*`,
for
  `AUTOMATIC=1` builds only. Release-branch tests are triggered through
automation, which is what keeps the builds trackable; a hand-kicked
build is
  the exception and is left alone.

| branch | `AUTOMATIC` | db | agent | comment |
| --- | --- | --- | --- | --- |
| `master` | 1 | yes | yes | yes |
| `releases/*` | 1 | no | **yes** | no |
| anything else | 1 | no | no | no |
| any | 0 | no | no | no |

## Related issues

None.

## Additional information

### This is not retroactive

The `release` pipeline is triggered with `branch:
"${BUILDKITE_BRANCH}"`, so a
release-branch build checks out that branch and runs *its* copy of the
init
script and the reporter. Merging here therefore covers release branches
cut
after this lands, not ones that already exist.

That is deliberate rather than overlooked: branches cut behind this
commit get
it cherry-picked, and the first backport target is `releases/2.60.0`
when that
branch is created. `releases/2.58.0` and earlier are not backported.

### The github comment gets its own flag

`OBS_AGENT_COMMENT_ON_GITHUB_ISSUE`, set only on master today, forwarded
into
the step env beside the other two, and checked first in
`_comment_on_github_issue` — before the issue lookup, so a build that is
not
commenting does no github work at all and never reaches the AWS Secrets
Manager
fetch behind the repo handle.

A flag rather than a branch check, for two reasons:

1. A release-branch build would in fact skip commenting anyway, because
nothing
populates `KEY_GITHUB_ISSUE_NUMBER` when `RayTestDBReporter` does not
run.
That is an accident of ordering rather than a decision. The flag makes
it a
   decision.
2. Turning comments on for release branches later is then one line in
`custom-image-build-and-test-init.sh`, with no code change and no branch
name
   anywhere in the reporter.

### Why a release-branch build cannot move a test's state

This is the property the change rests on, and it already held in two
independent ways:

- `REPORT_TO_RAY_TEST_DB` stays master-only, so `RayTestDBReporter` —
the only
reporter that calls `persist_result_to_s3()`,
`ReleaseTestStateMachine.move()`
  and `persist_to_s3()` — is never appended on a release branch.
- Even if it were, it guards itself: `reporter/ray_test_db.py` returns
early
when `BUILDKITE_BRANCH != "master"`, and again when the pipeline is not
in
  `ci_pipeline_postmerge`. Neither check consults the flag.

That second guarantee had **no test at all**, so this PR adds one rather
than
leaving it to be assumed.

Two things deliberately unchanged, to be explicit: `DBReporter`
(`reporter/db.py`) writes performance rows to databricks on every branch
today
via `report=True` in `buildkite/step.py`, and records results rather
than test
state. And the agent still posts a slack thread per analysis, so
release-branch
failures will appear in that channel alongside master's — the agent's
metadata
carries nothing that distinguishes the branch, so they are only tellable
apart
by the anyscale job id. Accepted; the analysis itself is still
attributable from
the buildkite side, since the log group and annotation both sit on the
job.

### Not duplicating existing work

```bash
gh pr list --repo ray-project/ray --state open --search "observability agent release branch"
gh pr list --repo ray-project/ray --state open --search "TRIGGER_OBSERVABILITY_AGENT"
```

Neither returns anything in this area beyond #66477, which this is
stacked on.

### Tests

`//release/...` cannot be analysed on current master —
`//release:requirements_py310_test`
references `@rules_python//python/pip_install:pip_compile.py`, which no
longer
exists in the pinned rules_python, and that reproduces on a clean master
checkout. The release_unit targets are run explicitly instead:

```bash
$ bazel test $(bazel query 'attr(tags, release_unit, tests(//release:all))') --test_output=errors
Executed 30 out of 30 tests: 30 tests pass.

$ pre-commit run
# all hooks passed, shellcheck included
```

New coverage:

| Behavior | Test |
| --- | --- |
| Each flag reaches the steps only when set to `1`, **and only itself**
— setting one must not export the other |
`test_custom_image_build_and_test_init_threads_the_obs_agent_flags`,
over `trigger_only` / `comment_only` / `both_falsy` |
| Comments off: no comment, and the github repo handle is never built |
`test_no_comment_when_github_comments_are_not_enabled` |
| Comments off: the log and the annotation still carry the analysis, and
the build's comment claim is left untouched |
`test_the_analysis_is_still_reported_when_comments_are_disabled` |
| Nothing is recorded off master |
`test_nothing_is_recorded_off_master`, over release branches, a feature
branch and an unset branch |
| Master alone is not enough; the pipeline is checked too |
`test_nothing_is_recorded_outside_the_postmerge_pipeline` |
| A master postmerge result **is** recorded — so the two guards above
can be told apart from a reporter that does nothing |
`test_a_master_postmerge_result_is_recorded` |

Each of these was checked by reproducing the defect it guards, not by
rerunning
green tests:

- Removing the comment-flag check fails the two comments-disabled tests.
Adding
  the check in the first place broke the 20 existing tests that exercise
commenting until the helpers set it, which is what confirmed the gate is
  load-bearing.
- Moving the flag check below `TestStateMachine.get_ray_repo()` fails
`test_no_comment_when_github_comments_are_not_enabled` — the assertion
is that
the handle is never built, not merely that the issue is never fetched,
because
  building it costs an AWS Secrets Manager fetch.
- `return` as the first statement of `RayTestDBReporter.report_result`
fails
`test_a_master_postmerge_result_is_recorded`. Without that case the file
asserted only absences and passed against a reporter that did nothing at
all.
- Breaking either db-reporter guard now fails on an assertion with no
`botocore`
in the log. Previously the unpatched `update_from_s3` /
`get_test_results`
  turned a regression into a real boto3 call and an opaque timeout.
- Collapsing the two forwarding blocks so the comment flag also exports
the
trigger fails `comment_only`. The earlier one-variable-at-a-time
parametrize
passed against that same mutation, which is why the cases are
asymmetric.

The shell conditions have no test target, so they were exercised
directly across
the six branch/`AUTOMATIC` combinations in the table above.

**Not exercised on a real build.** #66477 carried temporary commits for
that and
they have since been reverted; this change is gated the same way, so a
PR
pipeline runs none of it.

### AI assistance

AI assistance (Claude Code) was used for this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0e0054d)
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Signed-off-by: Sai Miduthuri <sai.miduthuri@anyscale.com>
elliot-barn pushed a commit that referenced this pull request Oct 9, 2026
…is on the test's github issue (#66477) (#66859)

Cherry-pick of #66477 (master commit
f733abf) onto `releases/2.60.0`.

Bottom of a two-PR stack: #66860 (the pick of #66557) sits on top of
this one and depends on it. #66557 isn't retroactive: a release-branch
build runs that branch's own copy of the init script and reporter, and
`releases/2.60.0` was cut before either PR merged. Both picks are needed
to run the observability agent on 2.60.0 release tests.

On its own this pick changes no behavior on the branch, since the agent
stays master-only until #66860. It adds the GitHub issue comment path
that #66557 builds on (and keeps master-only). Its prerequisites #65906,
#65910 and #66006 are already on the branch.

Applied cleanly with `git cherry-pick -x`; the diff is identical to the
master commit.

Not a duplicate: no existing PR against a release branch references
#66477.

Testing, from the repo root of this branch with the repo venv
(`responses` added to `PYTHONPATH` for `test_test.py`):

```
python -m pytest release/ray_release/tests/test_observability_agent_reporter.py release/ray_release/tests/test_test.py
107 passed
```

AI assistance was used to prepare this cherry-pick.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Signed-off-by: Sai Miduthuri <sai.miduthuri@anyscale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
elliot-barn pushed a commit that referenced this pull request Oct 9, 2026
… branches too (#66557) (#66860)

Cherry-pick of #66557 (master commit
0e0054d) onto `releases/2.60.0`.

Stacked on #66859 (the pick of #66477). **Review that one first; this
PR's diff is only the change on top of it.**

#66557 isn't retroactive: a release-branch build checks out that branch
and runs its own copy of the init script and reporter, so
`releases/2.60.0`, cut before it merged, never triggers the
observability agent. With this pick, automatic `releases/2.60.0` builds
run the agent on failed release tests (its own log group and the job
annotation). Reporting to the ray test DB and commenting on the test's
GitHub issue stay master-only.

Applied cleanly with `git cherry-pick -x`; the diff is identical to the
master commit. With both picks applied, all 11 files they touch match
master at 0e0054d. Elsewhere in `release/ray_release/`, the branch
differs from master only in BYOD lock and requirements files.

Not a duplicate: no existing PR against a release branch references
#66557.

Testing, from the repo root of this branch (both picks applied) with the
repo venv (`responses` added to `PYTHONPATH` for `test_test.py`):

```
python -m pytest release/ray_release/tests/test_observability_agent_reporter.py release/ray_release/tests/test_custom_image_build_and_test_init.py release/ray_release/tests/test_ray_test_db_reporter.py release/ray_release/tests/test_test.py
130 passed
```

AI assistance was used to prepare this cherry-pick.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Signed-off-by: Sai Miduthuri <sai.miduthuri@anyscale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Issues that should be addressed in Ray Core devprod go add ONLY when ready to merge, run all tests release-test release test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants