Skip to content

Add nodeId scoping to getBuildLog/searchBuildLog plus getFlowNodes for discovery - #221

Open
neeldugar wants to merge 2 commits into
jenkinsci:mainfrom
neeldugar:feature/node-scoped-build-logs
Open

Add nodeId scoping to getBuildLog/searchBuildLog plus getFlowNodes for discovery#221
neeldugar wants to merge 2 commits into
jenkinsci:mainfrom
neeldugar:feature/node-scoped-build-logs

Conversation

@neeldugar

@neeldugar neeldugar commented Aug 10, 2026

Copy link
Copy Markdown

A multi-stage Pipeline's console log interleaves every stage's output, so answering "what failed in the Test stage?" means reading past everything else. getBuildLog and searchBuildLog now take an optional nodeId that scopes the read to a single node of the Pipeline flow graph. getFlowNodes lists the graph so callers can discover valid IDs.

{ "jobFullName": "my-pipeline", "stageName": "Test", "onlyWithLogs": true }
{ "nodes": [{ "id": "10", "displayName": "Print Message", "hasLog": true, ... }],
  "skip": 0, "matched": 2, "totalInGraph": 13, "hasMore": false }
{ "jobFullName": "my-pipeline", "nodeId": "10", "limit": 100 }

Omitting nodeId is byte-for-byte the previous behaviour.

Notes

Two commits. The first fixes two pre-existing getBuildLog window-bounds defects
and stands alone — happy to split it into a separate PR if preferred.

  • A negative limit.max system property inverted forward reads. SystemProperties.getInteger
    uses Integer.decode, which accepts "-5" rather than falling back to the default.
  • Clamped tail windows became unrecoverable. readTail's ring buffer retains only the
    trailing capacity lines, but the window and line numbers came from the full total, so a
    clamped window could fall entirely inside the evicted region: zero lines with
    hasMoreContent=true and nextCursor=null.

Refactor. The paginated read helpers now take a LogSource (log bytes, liveness
predicate, cursor identity) instead of a Run, so one set of helpers serves both whole-build
and per-node reads. An earlier draft forked them and the copies immediately drifted.

Cursor scope. Cursors now carry job, build and node. The job was previously absent, so a
cursor issued for one job was honoured against another. Cursors from earlier versions are
rejected with Invalid cursor.

Optional-dependency safety. workflow-api types are confined to PipelineLogUtil and
PipelineGraphExtension. Lazy class resolution means that is not sufficient alone:
PipelineLogUtil loads fine without workflow-api and fails only on the instanceof
opening resolveNodeLogSource, raising NoClassDefFoundError — an Error that
McpToolWrapper's catch (Exception) misses. A by-name Class.forName probe converts
that into a tool error naming the missing plugins. workflow-api is also now declared
explicitly rather than relied on transitively.

Conflict note. This touches the exhaustive tool-name whitelist in
EndPointTest.testListTools, as does #215. Whichever merges second needs a one-line fix
there.

Testing done

mvn verify green — 273 tests, 0 failures. New coverage:

  • PipelineNodeLogTest (13 × 2 transports) — basic read, cursor pagination, tail read,
    node-not-found, non-Pipeline build, block-boundary empty result, in-progress non-blocking,
    cursor rejected across nodes and across jobs, malformed nodeId rejected, omitting nodeId
    unchanged, searchBuildLog scoping, searchBuildLog rejects unknown node.
  • PipelineGraphExtensionTest (9 × 2) — listing, execution order, round-trip (IDs from
    getFlowNodes accepted by getBuildLog), stage attribution, filters, pagination, scale
    bounds.
  • BuildLogWindowBoundsTest (3 × 2) — uses @SetSystemProperty to lower limit.max to
    a value reachable with a small fixture.
  • BuildLogsWithoutPipelineTest (8, no JenkinsRule, ~0.07s) — hides
    org.jenkinsci.plugins.workflow.** behind a classloader; proves BuildLogsExtension
    loads and instantiates, tool signatures resolve, and the by-name Pipeline guard fires before
    any Error can escape.

Verified by mutation: reverting isActive()isRunning(), dropping the cursor scope-key
check, removing the pagination caps, or deleting the Pipeline guard call each fails the suite.

Submitter checklist

  • Make sure you are opening from a topic/feature/bugfix branch (right side) and not your main branch!
  • Ensure that the pull request title represents the desired changelog entry
  • Please describe what you did
  • Link to relevant issues in GitHub or Jira
  • Link to relevant pull requests, esp. upstream and downstream changes
  • Ensure you have provided tests that demonstrate the feature works or the issue is fixed

A negative limit.max inverted forward reads. SystemProperties.getInteger uses
Integer.decode, which accepts "-5" rather than falling back to the default; the
negative ceiling flipped limit's sign, so a request for the first 100 lines
returned the single last line with no cursor to recover the rest.

Clamped tail windows became unrecoverable. readTail's ring buffer keeps only the
trailing `capacity` lines, but the window and line numbers came from the full
total, so a window clamped by limit.max could fall entirely inside the evicted
region: zero lines with hasMoreContent=true and nextCursor=null.

Co-Authored-By: Claude <noreply@anthropic.com>
@neeldugar
neeldugar requested a review from a team as a code owner August 10, 2026 02:51
A multi-stage Pipeline's console log interleaves every stage's output, so
getBuildLog and searchBuildLog now take an optional nodeId that scopes the read
to one flow-graph node. Omitting it is unchanged behaviour.

The read helpers take a LogSource (log bytes, whether more may arrive, cursor
identity) instead of a Run, so one set of window-arithmetic helpers serves both
whole-build and per-node reads rather than two forked copies.

Cursors now carry job, build and node. The job was previously absent, so a
cursor issued for one job was honoured against another and returned an offset
into an unrelated log. Cursors from earlier versions are rejected.

getFlowNodes lists the graph so callers can obtain node IDs, which no @exported
property on WorkflowRun exposes. Paginated, since graphs reach thousands of
nodes.

workflow-api types stay in PipelineLogUtil and PipelineGraphExtension so
BuildLogsExtension still loads without the Pipeline plugins. Lazy class
resolution makes that insufficient alone, so a by-name probe converts the
resulting NoClassDefFoundError into a tool error.

Co-Authored-By: Claude <noreply@anthropic.com>
@neeldugar
neeldugar force-pushed the feature/node-scoped-build-logs branch from d9f64b1 to 09b146f Compare August 10, 2026 02:55
* @param live whether more output may still be appended
* @param scopeKey identity a cursor is bound to; must distinguish job, build, and node
*/
public record LogSource(AnnotatedLargeText<?> text, boolean live, String scopeKey) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

not a password, just unique identifier of scope

@timja
timja requested a balanced review from Copilot August 11, 2026 06:34

Copilot AI 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.

Pull request overview

Adds Pipeline-node discovery and node-scoped build-log reading/searching, while improving pagination bounds and cursor isolation.

Changes:

  • Adds getFlowNodes with filtering and pagination.
  • Adds optional nodeId scoping and stronger cursor identity.
  • Adds extensive Pipeline, bounds, and optional-dependency tests.

Reviewed changes

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

Show a summary per file
File Description
BuildLogsExtension.java Implements scoped log access and pagination fixes.
PipelineGraphExtension.java Adds flow-node discovery.
PipelineLogUtil.java Resolves per-node Pipeline logs.
LogSource.java Abstracts log source and cursor scope.
README.md Documents the new tools and workflow.
pom.xml Declares workflow-api.
EndPointTest.java Registers expected tool metadata.
PipelineNodeLogTest.java Tests node-scoped log behavior.
PipelineGraphExtensionTest.java Tests graph discovery and filtering.
BuildLogWindowBoundsTest.java Tests pagination bounds.
BuildLogsWithoutPipelineTest.java Tests optional Pipeline dependencies.
Suppressed comments (1)

src/main/java/io/jenkins/plugins/mcp/server/extensions/BuildLogsExtension.java:475

  • Clamping resolvedSkip to the retained tail silently returns a different window. For example, with 30 lines, maxLimit=10, skip=-24, and limit=1, the requested seventh line is replaced by line 21. The implementation should preserve the requested offset (for example, count then read the bounded result in a second pass) or reject an unsupported lookback rather than return incorrect log content.
        long earliestRetained = Math.max(0, total - capacity);
        if (resolvedSkip < earliestRetained) {
            log.warn(
                    "End-relative window started at line {} but only the last {} of {} lines were retained;"
                            + " returning from line {} instead",
                    resolvedSkip + 1,
                    capacity,
                    total,
                    earliestRetained + 1);
            resolvedSkip = earliestRetained;
        }

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +118 to +121
if (src == null) {
// Block boundary: an empty window, not an error.
return new BuildLogResponse(List.of(), false, -1, -1, 0, null);
}
Comment on lines +255 to +258
for (BlockStartNode enclosing : node.getEnclosingBlocks()) {
LabelAction label = enclosing.getPersistentAction(LabelAction.class);
if (label != null) {
return label.getDisplayName();
Comment on lines +40 to +43
public record LogSource(AnnotatedLargeText<?> text, boolean live, String scopeKey) {

public static LogSource ofRun(Run<?, ?> run) {
return new LogSource(run.getLogText(), run.isLogUpdated(), scopeKey(run, null));
Comment thread README.md
Comment on lines +453 to +455
- **Only nodes with `hasLog: true` return output.** Block boundary nodes (a `stage`'s start/end, `node`
wrappers) delegate their output to the leaf steps nested inside them, so they return an empty result —
not an error. Use `onlyWithLogs: true` to list just the useful ones.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants