Skip to content

[release] append the observability agent analysis to the buildkite job annotation - #66006

Merged
elliot-barn merged 13 commits into
masterfrom
sai-miduthuri/obs-agent-job-annotation
Sep 15, 2026
Merged

elliot-barn merged 13 commits into
masterfrom
sai-miduthuri/obs-agent-job-annotation

Conversation

@sai-miduthuri

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

Copy link
Copy Markdown
Contributor

Description

Stacked on #65910, which is stacked on #65906. Review those first; this PR's
diff is only the annotation on top of them.

#65910 puts the observability agent's analysis in its own buildkite group at the
end of the failed step — but that only reaches someone who opens the step. This
adds a buildkite annotation, so a red nightly shows its analyses without
digging through step output.

The annotation is job-scoped (--scope=job), so it renders on the job it
came from; buildkite also surfaces job-scoped annotations on the build page, so
one call reaches both places. Scope is what decides this: --scope defaults to
build, and --job alone only records which job an annotation came from — the
first run on this branch passed --job without --scope and the annotation
landed at the build level, which is how this was found.

The full command:

buildkite-agent annotate --style=info --scope=job --context=obs-agent-<test name> --append --job <job id>

Each attempt of a retried test gets its own annotation, because a buildkite
retry is a new job with a new id — the API shows retry_source.job_id differing
from id — and a job-scoped annotation is identified by its context within its
job
. So every attempt's report is kept, attributed to the attempt that produced
it, and buildkite shows them together on the build page.

The context is obs-agent-<test name> because an annotation needs one and
buildkite defaults it to "default"; the test name makes it identifiable in the
UI and keeps it clear of anything else annotating the same job. It is not what
separates the attempts — scope already does that. --append is passed for the
case of a job annotating twice under this context, which does not happen today
(one release test job runs one test once), but where replacing would be wrong.

Each block is headed with the test name, the attempt number, and a link to the
Anyscale job:

hello_world.aws — attempt 1 (BUILDKITE_RETRY_COUNT=0) — prodjob_w9q946e1l8xvk6p6bdywmzh8li

<the agent's summary>

Full report and feedback — rate it with the 'All good' or 'Needs correction' buttons in the thread.

Both numbers are rendered because they disagree by one: buildkite labels the
first try "Retry 1 of N", while BUILDKITE_RETRY_COUNT counts retries after
it and is 0 there. Rendering the raw value alone put "attempt 3" on the attempt
the UI calls "Retry 4 of 5"; rendering both reconciles the annotation with the
label it hangs off and with the job log, which prints the raw value.

Guarded on BUILDKITE, and wrapped so that neither a missing binary nor a
non-zero exit can change the outcome of the test run — the same rule the rest of
this reporter follows. Everything interpolated into the body is HTML-escaped:
buildkite renders the body as markup, and the agent's summary is free-form prose
where an unescaped < would be rendered rather than shown.

This is not new ground for the repo. buildkite-agent annotate already has
two call sites: release/ray_release/byod/build.py:71-81, from Python inside
ray_release, and .buildkite/hooks/post-command:19-20, from the host hook.

It also needs a change to the release test step config

The first CI run on this branch produced no annotation:

Could not annotate the buildkite job: [Errno 13] Permission denied: 'buildkite-agent'

Two separate causes, both in the step's docker plugin config, and both fixed in
f51462c6f4:

  • The step hand-mounted /usr/local/bin/buildkite-agent, but the agent on these
    hosts is /usr/bin/buildkite-agent-stable. The source path does not exist, so
    docker creates an empty directory at the destination, and executing a
    directory is EACCES — the error above, rather than the ENOENT a missing
    file would give. That volume had to be removed, not supplemented:
    /usr/local/bin precedes /usr/bin on PATH in the python image, so the
    stale directory shadows any working binary mounted elsewhere.
  • BUILDKITE_AGENT_ACCESS_TOKEN is not among the variables reaching the
    container — propagate-environment does not carry it — so even a working
    binary could not authenticate.

mount-buildkite-agent: true handles both. Note that the rayci keys
(allow_mount_buildkite_agent in .buildkite/release/config.yml,
mount_buildkite_agent in build.rayci.yml) do not apply here: release test
steps are written to release_tests.json and uploaded straight to buildkite by
custom_image_build_and_test_init, never generated by rayci, so the docker
plugin's own hyphenated option is the reachable lever.

Worth a reviewer's attention: this grants every release test container a
working buildkite-agent and the agent access token, so test code could
annotate the build or upload artifacts as the agent. It is the same trust level
the release build steps already run at, and release tests are internal code,
but it is a change in posture on a shared file rather than a change local to
this reporter.

Related issues

None.

Additional information

Verified on real builds

Three temporary commits carried this PR through CI and have all been reverted;
each pair is on the branch so the history shows what was run:

Temporary commit Revert What it was for
131706a3e3 serve a canned agent response 98c8f488b3 Iterating without creating a real debug session and slack thread on every run
e5f741e939 force hello_world to fail ab9f02ebf3 Getting a release test run to actually reach the agent
e456942239 export TRIGGER_OBSERVABILITY_AGENT unconditionally cb49f5edf4 #65906 gates the reporter to AUTOMATIC master runs, so a PR pipeline would otherwise run none of this

No temporary commit remains in effect. The final run was against a genuinely
failing test with the real agent, and produced the reports as intended.

What the builds settled

A job-scoped annotation does not accumulate across retries — each attempt
annotates its own job.
The docs were not explicit about whether --append
with a stable context across a new --job grows one annotation or opens
several. Build 106662 answers it: the two attempts of hello_world.aws that
reached the agent produced two annotations under the identical context
obs-agent-hello_world.aws, one block each — "attempt 3" on one job, "attempt 4"
on the next. Scope, not the context, is what separates them.

That is the behaviour this PR wants: every attempt's analysis is kept and
attributed to the attempt that produced it, rather than concatenated into one
block where it is unclear which run said what.

Two related notes for anyone reading these programmatically: Build.annotations
returns only build-scoped annotations over both the REST and GraphQL APIs — the
job-scoped ones are reachable under Job.annotations — and the build page
showing them is a UI behaviour, not API-level promotion.

Automatic retries produce exactly one trigger per build. Since #65907 landed, the step publishes
BUILDKITE_MAX_RETRIES=3 for hello_world (from its num_retries: 3) and the
script honours it, so _is_transient_error rewrites every attempt except the
last to TRANSIENT_INFRA_ERROR, which is not a trigger status:

attempt BUILDKITE_RETRY_COUNT retry_count >= max_retry status agent
0 0 false → transient TRANSIENT_INFRA_ERROR skipped
1, 2 1, 2 false → transient TRANSIENT_INFRA_ERROR skipped
3 3 true ERROR triggers

That is #65907 working as intended — one recorded result, and one analysis, per
build. No num_retries value changes it. The multiple-attempt case above was
therefore reached with a manual retry: once the automatic retries are exhausted at
retry_count=3, a manual retry gives retry_count=4, so 4 >= 3 holds and the
attempt reports a real error and triggers. That is also the realistic case, since
a manual retry is what a human does while debugging a failure.

Not duplicating existing work

gh pr list --repo ray-project/ray --state open --search "buildkite annotation observability agent"
gh pr list --repo ray-project/ray --state open --search "buildkite-agent annotate"

Neither returns anything in this area beyond the two PRs this one is stacked on.

Tests

$ bazel test --test_tag_filters=release_unit //release/... --test_output=errors
Executed 29 out of 29 tests: 29 tests pass.

$ bazel test //release:test_observability_agent_reporter
//release:test_observability_agent_reporter                              PASSED
# 31 tests, 6 new for the annotation

New coverage:

Behavior Test
argv: job scope, context keyed on the test name, style, append, --job for attribution test_the_annotation_is_job_scoped_and_keyed_on_the_test
Attempt number matches buildkite's own label test_the_attempt_number_matches_the_buildkite_label
The agent's prose cannot inject markup into the body test_the_annotation_escapes_what_the_agent_sent
No annotation off a buildkite agent test_no_annotation_outside_buildkite
A missing buildkite-agent binary is swallowed test_annotation_failures_never_propagate
A non-zero exit is logged, not raised test_a_non_zero_annotate_exit_is_logged_not_raised

Two bugs the tests caught while writing them: --job was appended after the
body, leaving the job id as the positional argument rather than the annotation
text; and the guard was "BUILDKITE" not in os.environ, which treats
BUILDKITE="" as being on an agent. The guard now matches byod/build.py.

AI assistance

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

@sai-miduthuri
sai-miduthuri added this pull request to stack #65911 September 10, 2026 22:07
@sai-miduthuri
sai-miduthuri marked this pull request as ready for review September 10, 2026 22:07
@sai-miduthuri
sai-miduthuri requested a review from a team as a code owner September 10, 2026 22:07

@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 integrates the observability agent with Buildkite annotations, allowing test failure summaries to be appended directly to Buildkite jobs. It configures the Docker plugin to mount the buildkite-agent binary and implements the annotation logic in the reporter, along with temporary mock responses and forced failures for testing. The review feedback suggests avoiding the use of !r (repr) inside HTML attribute values within the annotation formatting, as it can lead to fragile HTML rendering, and recommends using explicit double quotes instead.

Comment thread release/ray_release/reporter/observability_agent.py
@sai-miduthuri

Copy link
Copy Markdown
Contributor Author

The Observability Agent response can be viewed in the logs of the forced hello_world.aws test failure on buildkite/release on commit 3820a26, and buildkite/release on commit 79ff865.

The agent's summary and thread URL are printed in the buildkite job-level annotations and the build-level annotation, and also given their own Observability agent analysis group, in the Buildkite job logs on the 4th and 5th test attempts.

The 4th attempt is the final automated retry of the test, and the 5th attempt is a manual retry of the test. Attempts 1-3 do not query the Observability Agent because they are treated as having failed due to transient errors.

@sai-miduthuri
sai-miduthuri force-pushed the sai-miduthuri/obs-agent-job-annotation branch from 79ff865 to ec97b2c Compare September 11, 2026 00:59
@sai-miduthuri sai-miduthuri added the go add ONLY when ready to merge, run all tests label Sep 11, 2026
@ray-gardener ray-gardener Bot added core Issues that should be addressed in Ray Core observability Issues related to the Ray Dashboard, Logging, Metrics, Tracing, and/or Profiling devprod release-test release test labels Sep 11, 2026
@sai-miduthuri
sai-miduthuri force-pushed the sai-miduthuri/obs-agent-job-annotation branch from d7682a6 to e434abd Compare September 11, 2026 21:33
@sai-miduthuri

Copy link
Copy Markdown
Contributor Author

Actual Observability Agent reports are visible at buildkite/release for commit 4792b54.

@elliot-barn elliot-barn left a comment

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.

Approved! But can you update the pr description that mentions the annotation showing up on the build page

@sai-miduthuri
sai-miduthuri force-pushed the sai-miduthuri/obs-agent-job-annotation branch from 60ca086 to cb49f5e Compare September 14, 2026 18:39

@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 1 potential issue.

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 cb49f5e. Configure here.

Comment thread release/ray_release/reporter/observability_agent.py
@elliot-barn
elliot-barn force-pushed the sai-miduthuri/obs-agent-job-annotation branch from 2c92eed to 600a4c6 Compare September 15, 2026 18:40
Base automatically changed from sai-miduthuri/obs-agent-analysis-log-group to master September 15, 2026 18:40
sai-miduthuri and others added 8 commits September 15, 2026 11:44
Iterating on what happens after the agent responds -- the log group, and
the buildkite annotation this branch is about -- otherwise creates a real
debug session and posts a real slack thread on every run, which is noise
for the people watching that channel.

RELEASE_TEST_OBS_AGENT_FAKE_RESPONSE names a file holding a query
response; when it is set the reporter serves that and calls nothing. The
response shape is copied from a real one, em dash included, so the
encoding and rendering paths stay exercised.

run_release_test.sh points it at the checked-in file, so every release
test run on this branch uses it. Revert this commit, and delete
release/obs_agent_fake_response.json, before merging.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Option D of the prominence plan. The step log only reaches someone who
opens the step; an annotation is on the build page, above every step, so a
red nightly shows its analyses without a single click.

The annotation is keyed on the test name, not the job id. A buildkite
retry is a new job with a new id, so keying on the job would open a
separate annotation per attempt rather than appending to one, and
`step_key` is null for release test jobs. `--job` is still passed, for
attribution, but it is not the identity.

Each attempt appends a block headed with the test name, the attempt number
from BUILDKITE_RETRY_COUNT, and a link to the anyscale job, so an
accumulated annotation reads as a sequence of attempts rather than one
run.

Guarded on BUILDKITE and wrapped so that neither a missing binary nor a
non-zero exit can change the outcome of the test run, the same rule the
rest of this reporter follows.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Temporarily raise an exception inside the hello_world ray task so that a
release test run on this PR fails the way a real test does, and the
buildkite annotation this branch adds can be observed on a real build.

The analysis itself is canned on this branch, so running this repeatedly
creates no debug sessions and no slack threads.

Note that the agent fires on the retried attempt rather than the first:
_is_transient_error rewrites the first attempt's status to
TRANSIENT_INFRA_ERROR, which is not a trigger status. hello_world sets
num_retries: 3, so the annotation should accumulate a block per triggering
attempt -- which is the behaviour this PR exists to observe.

Revert this commit before taking the PR out of draft.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The annotation added in the previous commit never reached buildkite. The
job log shows why:

    Could not annotate the buildkite job:
    [Errno 13] Permission denied: 'buildkite-agent'

Two separate reasons, both from the docker plugin config:

- The step hand-mounted /usr/local/bin/buildkite-agent, but the agent on
  these hosts is /usr/bin/buildkite-agent-stable, so the source path does
  not exist and docker creates an empty *directory* at the destination.
  Executing a directory is EACCES, which is the error above -- not the
  ENOENT a missing file would give. /usr/local/bin also precedes /usr/bin
  on PATH in the python image, so that directory shadows any working
  binary mounted elsewhere; removing the volume is required, not tidying.

- BUILDKITE_AGENT_ACCESS_TOKEN is not among the variables reaching the
  container. `propagate-environment` does not carry it, so even a working
  binary could not authenticate.

`mount-buildkite-agent` handles both: the plugin mounts the binary the
agent is running and passes the token. This is the same capability the
release build steps already get, though they ask for it through rayci's
`mount_buildkite_agent` key, which release test steps cannot use because
they are uploaded straight to buildkite rather than generated by rayci.

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The annotation landed on the build page rather than the job. `--job` was
not the reason: per the agent docs it only records "which job should the
annotation come from", while `--scope` is what "control[s] where the
annotation is displayed in the Buildkite UI", and it defaults to "build".
So the first run was always going to be build-scoped -- nothing was
passing --scope at all.

Pass --scope=job and keep --job for attribution.

Also log the command before running it. The first run left it ambiguous
whether --job had even been included, because the argv is built
conditionally and nothing recorded it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Buildkite labels the first try "Retry 1 of N", counting every attempt as a
retry, while BUILDKITE_RETRY_COUNT counts retries *after* the first and is
0 there. Rendering the raw value put "attempt 3" on the annotation hanging
off the attempt the UI calls "Retry 4 of 5" -- two numbers for the same
attempt, on the same screen.

Render both: the +1 lines up with the UI label the annotation is attached
to, and the raw value stays visible because that is what the job log
prints, so log, annotation and UI can all be reconciled.

No "of N": the total is not derivable from the environment, because a
manual retry takes it past the configured limit.

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

This reverts commit 162f134.

The forced failure has served its purpose: builds 106453, 106459 and
106662 exercised the annotation path end to end, and the findings are
recorded in the PR description. Restore hello_world so the PR carries only
the change itself.

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

The canned response existed so that iterating on the annotation did not
create a debug session and post a slack thread on every run. That
iteration is done: builds 106453, 106459 and 106662 settled how scoping
and appending behave, and the findings are in the PR description.

The revert conflicted in observability_agent.py because _annotate was
added immediately above _fake_response by a later commit, so git could not
tell the two apart. Resolved by keeping _annotate and removing only
_fake_response; the constants, the call site and
release/obs_agent_fake_response.json go with it.

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 5 commits September 15, 2026 11:44
Buildkite renders the annotation as markup, and the body interpolated
values straight in. `!r` on the two urls, flagged in review, is the wrong
tool: repr quotes for Python, not for HTML, and a value containing both
quote characters comes out backslash-escaped, which HTML does not
understand.

Switching to plain double quotes, as suggested, would have been worse for
the case that matters. The slack thread url comes from the agent's
response, and a `"` in it closes the attribute and turns the rest into
markup; repr happened to survive that by switching quote style. Neither
escapes HTML.

Escape every interpolated value instead, and quote the attributes
explicitly. That includes the summary, which review did not mention and
which is the likeliest source of stray markup: it is free-form prose from
the agent, the same field that already had to be defended against opening
a buildkite log group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The gate added in the first PR of this stack exports the flag only for
AUTOMATIC runs on master, so a PR pipeline gets no reporter at all: no
analysis, no log group, no annotation. Export it unconditionally as well,
to exercise the agent end to end on this PR.

Revert this commit before merging.

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

This reverts commit 4792b54.

The unconditional export existed so that a PR pipeline, where AUTOMATIC is
0 and the branch is not master, would run the reporter at all. Build 106873
exercised it end to end against a real failing test and produced the
reports as intended, so the flag goes back to what it gates in production:
automatic runs on master, at any release frequency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
The comments claimed that a stable context plus --append make a retry add
to the previous attempt's annotation. Build 106662 says otherwise: the two
attempts of hello_world.aws that reached the agent produced two separate
job-scoped annotations under the identical context obs-agent-hello_world.aws,
one block each -- "attempt 3" on one job, "attempt 4" on the next.

Scope is what separates them. A job-scoped annotation is identified by its
context *within its job*, and a retry is a new job, so each attempt
annotates separately whatever the context is. Every attempt's report is
still kept and buildkite still shows them together on the build page --
the outcome the comments described -- but the context and --append are not
what produce it.

So the context is explained by what it is actually for: an annotation needs
a context, buildkite defaults it to "default", and the test name makes this
one identifiable in the UI and clear of anything else annotating the job.
--append stays, for the case of a job annotating twice under this context;
that does not happen today, but replacing would be wrong if it ever did.
The test keeps its assertions and loses the reason it had wrong, name
included.

Comments and one test name. No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Three comments explained a flag by what an earlier run on this branch did.
The branch does not survive the squash-merge, so a reader a year from now
has nothing to resolve "the first run on this branch" against -- and the
anecdote was never the reason anyway, only how the reason was found.

Each now states the fact that outlives the PR:

- ANNOTATION_SCOPE: scope decides placement and defaults to "build", so
  passing --job alone leaves the annotation on the build page rather than
  the job it is about.
- The annotate logging: a build should be self-describing about what was
  actually run, because the flags an annotation was created with are
  otherwise invisible from the job log and decide where it lands.
- The same note in the annotation test, which cited the same run.

Comments 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
sai-miduthuri force-pushed the sai-miduthuri/obs-agent-job-annotation branch from 600a4c6 to e4682f4 Compare September 15, 2026 18:46
@elliot-barn
elliot-barn merged commit bc3f272 into master Sep 15, 2026
6 checks passed
@elliot-barn
elliot-barn deleted the sai-miduthuri/obs-agent-job-annotation branch September 15, 2026 19:22
sai-miduthuri added a commit that referenced this pull request Sep 28, 2026
`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>
elliot-barn pushed a commit that referenced this pull request Oct 2, 2026
…ub issue (#66477)

## 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

```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
(#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:

```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 `#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>
nadongjun pushed a commit to nadongjun/ray that referenced this pull request Oct 6, 2026
…b annotation (ray-project#66006)

## Description

Stacked on ray-project#65910, which is stacked on ray-project#65906. **Review those first;
this PR's
diff is only the annotation on top of them.**

ray-project#65910 puts the observability agent's analysis in its own buildkite
group at the
end of the failed step — but that only reaches someone who opens the
step. This
adds a buildkite **annotation**, so a red nightly shows its analyses
without
digging through step output.

The annotation is **job-scoped** (`--scope=job`), so it renders on the
job it
came from; buildkite also surfaces job-scoped annotations on the build
page, so
one call reaches both places. Scope is what decides this: `--scope`
defaults to
`build`, and `--job` alone only records which job an annotation came
from — the
first run on this branch passed `--job` without `--scope` and the
annotation
landed at the build level, which is how this was found.

The full command:

```
buildkite-agent annotate --style=info --scope=job --context=obs-agent-<test name> --append --job <job id>
```

**Each attempt of a retried test gets its own annotation**, because a
buildkite
retry is a *new job with a new id* — the API shows `retry_source.job_id`
differing
from `id` — and a job-scoped annotation is identified by its context
*within its
job*. So every attempt's report is kept, attributed to the attempt that
produced
it, and buildkite shows them together on the build page.

The context is `obs-agent-<test name>` because an annotation needs one
and
buildkite defaults it to `"default"`; the test name makes it
identifiable in the
UI and keeps it clear of anything else annotating the same job. It is
not what
separates the attempts — scope already does that. `--append` is passed
for the
case of a job annotating twice under this context, which does not happen
today
(one release test job runs one test once), but where replacing would be
wrong.

Each block is headed with the test name, the attempt number, and a link
to the
Anyscale job:

```
hello_world.aws — attempt 1 (BUILDKITE_RETRY_COUNT=0) — prodjob_w9q946e1l8xvk6p6bdywmzh8li

<the agent's summary>

Full report and feedback — rate it with the 'All good' or 'Needs correction' buttons in the thread.
```

Both numbers are rendered because they disagree by one: buildkite labels
the
first try "Retry 1 of N", while `BUILDKITE_RETRY_COUNT` counts retries
*after*
it and is `0` there. Rendering the raw value alone put "attempt 3" on
the attempt
the UI calls "Retry 4 of 5"; rendering both reconciles the annotation
with the
label it hangs off and with the job log, which prints the raw value.

Guarded on `BUILDKITE`, and wrapped so that neither a missing binary nor
a
non-zero exit can change the outcome of the test run — the same rule the
rest of
this reporter follows. Everything interpolated into the body is
HTML-escaped:
buildkite renders the body as markup, and the agent's summary is
free-form prose
where an unescaped `<` would be rendered rather than shown.

**This is not new ground for the repo.** `buildkite-agent annotate`
already has
two call sites: `release/ray_release/byod/build.py:71-81`, from Python
inside
`ray_release`, and `.buildkite/hooks/post-command:19-20`, from the host
hook.

### It also needs a change to the release test step config

The first CI run on this branch produced no annotation:

```
Could not annotate the buildkite job: [Errno 13] Permission denied: 'buildkite-agent'
```

Two separate causes, both in the step's docker plugin config, and both
fixed in
`f51462c6f4`:

- The step hand-mounted `/usr/local/bin/buildkite-agent`, but the agent
on these
hosts is `/usr/bin/buildkite-agent-stable`. The source path does not
exist, so
docker creates an empty *directory* at the destination, and executing a
directory is `EACCES` — the error above, rather than the `ENOENT` a
missing
  file would give. That volume had to be **removed**, not supplemented:
`/usr/local/bin` precedes `/usr/bin` on `PATH` in the `python` image, so
the
  stale directory shadows any working binary mounted elsewhere.
- `BUILDKITE_AGENT_ACCESS_TOKEN` is not among the variables reaching the
container — `propagate-environment` does not carry it — so even a
working
  binary could not authenticate.

`mount-buildkite-agent: true` handles both. Note that the rayci keys
(`allow_mount_buildkite_agent` in `.buildkite/release/config.yml`,
`mount_buildkite_agent` in `build.rayci.yml`) do not apply here: release
test
steps are written to `release_tests.json` and uploaded straight to
buildkite by
`custom_image_build_and_test_init`, never generated by rayci, so the
docker
plugin's own hyphenated option is the reachable lever.

**Worth a reviewer's attention:** this grants *every* release test
container a
working `buildkite-agent` and the agent access token, so test code could
annotate the build or upload artifacts as the agent. It is the same
trust level
the release *build* steps already run at, and release tests are internal
code,
but it is a change in posture on a shared file rather than a change
local to
this reporter.

## Related issues

None.

## Additional information

### Verified on real builds

Three temporary commits carried this PR through CI and have all been
reverted;
each pair is on the branch so the history shows what was run:

| Temporary commit | Revert | What it was for |
| --- | --- | --- |
| `131706a3e3` serve a canned agent response | `98c8f488b3` | Iterating
without creating a real debug session and slack thread on every run |
| `e5f741e939` force `hello_world` to fail | `ab9f02ebf3` | Getting a
release test run to actually reach the agent |
| `e456942239` export `TRIGGER_OBSERVABILITY_AGENT` unconditionally |
`cb49f5edf4` | ray-project#65906 gates the reporter to `AUTOMATIC` master runs, so
a PR pipeline would otherwise run none of this |

**No temporary commit remains in effect.** The final run was against a
genuinely
failing test with the real agent, and produced the reports as intended.

### What the builds settled

**A job-scoped annotation does not accumulate across retries — each
attempt
annotates its own job.** The docs were not explicit about whether
`--append`
with a stable context across a new `--job` grows one annotation or opens
several. Build 106662 answers it: the two attempts of `hello_world.aws`
that
reached the agent produced **two** annotations under the identical
context
`obs-agent-hello_world.aws`, one block each — "attempt 3" on one job,
"attempt 4"
on the next. Scope, not the context, is what separates them.

That is the behaviour this PR wants: every attempt's analysis is kept
and
attributed to the attempt that produced it, rather than concatenated
into one
block where it is unclear which run said what.

Two related notes for anyone reading these programmatically:
`Build.annotations`
returns only *build*-scoped annotations over both the REST and GraphQL
APIs — the
job-scoped ones are reachable under `Job.annotations` — and the build
page
showing them is a UI behaviour, not API-level promotion.

**Automatic retries produce exactly one trigger per build.** Since
ray-project#65907 landed, the step publishes
`BUILDKITE_MAX_RETRIES=3` for `hello_world` (from its `num_retries: 3`)
and the
script honours it, so `_is_transient_error` rewrites every attempt
except the
last to `TRANSIENT_INFRA_ERROR`, which is not a trigger status:

| attempt | `BUILDKITE_RETRY_COUNT` | `retry_count >= max_retry` |
status | agent |
| --- | --- | --- | --- | --- |
| 0 | 0 | false → transient | `TRANSIENT_INFRA_ERROR` | skipped |
| 1, 2 | 1, 2 | false → transient | `TRANSIENT_INFRA_ERROR` | skipped |
| 3 | 3 | **true** | `ERROR` | **triggers** |

That is ray-project#65907 working as intended — one recorded result, and one
analysis, per
build. No `num_retries` value changes it. The multiple-attempt case
above was
therefore reached with a manual retry: once the automatic retries are
exhausted at
`retry_count=3`, a manual retry gives `retry_count=4`, so `4 >= 3` holds
and the
attempt reports a real error and triggers. That is also the realistic
case, since
a manual retry is what a human does while debugging a failure.

### Not duplicating existing work

```bash
gh pr list --repo ray-project/ray --state open --search "buildkite annotation observability agent"
gh pr list --repo ray-project/ray --state open --search "buildkite-agent annotate"
```

Neither returns anything in this area beyond the two PRs this one is
stacked on.

### Tests

```bash
$ bazel test --test_tag_filters=release_unit //release/... --test_output=errors
Executed 29 out of 29 tests: 29 tests pass.

$ bazel test //release:test_observability_agent_reporter
//release:test_observability_agent_reporter                              PASSED
# 31 tests, 6 new for the annotation
```

New coverage:

| Behavior | Test |
| --- | --- |
| argv: job scope, context keyed on the test name, style, append,
`--job` for attribution |
`test_the_annotation_is_job_scoped_and_keyed_on_the_test` |
| Attempt number matches buildkite's own label |
`test_the_attempt_number_matches_the_buildkite_label` |
| The agent's prose cannot inject markup into the body |
`test_the_annotation_escapes_what_the_agent_sent` |
| No annotation off a buildkite agent |
`test_no_annotation_outside_buildkite` |
| A missing `buildkite-agent` binary is swallowed |
`test_annotation_failures_never_propagate` |
| A non-zero exit is logged, not raised |
`test_a_non_zero_annotate_exit_is_logged_not_raised` |

Two bugs the tests caught while writing them: `--job` was appended
*after* the
body, leaving the job id as the positional argument rather than the
annotation
text; and the guard was `"BUILDKITE" not in os.environ`, which treats
`BUILDKITE=""` as being on an agent. The guard now matches
`byod/build.py`.

### AI assistance

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

---------

Signed-off-by: sai.miduthuri <sai.miduthuri@anyscale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 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>
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 observability Issues related to the Ray Dashboard, Logging, Metrics, Tracing, and/or Profiling release-test release test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants