Skip to content

Apply the Datadog Jenkins plugin setup to Jenkinsfiles - #271

Draft
juan-fernandez wants to merge 5 commits into
mainfrom
juan-fernandez/jenkins-apply
Draft

juan-fernandez wants to merge 5 commits into
mainfrom
juan-fernandez/jenkins-apply

Conversation

@juan-fernandez

Copy link
Copy Markdown
Contributor

What

ddtest onboard --apply now writes the Datadog Jenkins plugin setup into Jenkinsfiles.

  • Declarative pipelines: it adds datadog(testOptimization: [enabled: true, serviceName: '<project>', languages: ['<LANG>']]) to the top-level options. If the pipeline already has an options block or a datadog(…) call (including the legacy testVisibility:), it merges into it.
  • Scripted pipelines: it wraps each node block that runs tests in datadog(…) { … }.
  • Vitest and ESM Mocha: it also adds the DD_TRACE_PATH --import line to the test sh, when that script is single-quoted.
  • JavaScript gets the full automatic flow: a preview without --yes, the edit with --yes, then the plugin confirmations, then validation.
  • Ruby gets --apply on Jenkins only. ddtest writes the Jenkinsfile edit, and the datadog-ci pin is returned as agent instructions.
  • Shared libraries, any job in the option's scope that runs steps in a container (the plugin's tracer variables reach it too), computed arguments, node blocks inside def functions and conflicting datadog(…) calls are refused. ddtest gives the reason and the manual instructions instead.
  • Running --apply again changes nothing.
  • Discovery also records the steps a Jenkins environment {} value runs (X = sh(script: …, returnStdout: true), Jenkinsfile functions, library calls), where Jenkins runs them, instead of reading only the value. Non-test commands there only add review notes.

Why

Under #266, every Jenkins user or agent edits the Groovy by hand, which is the step agents get wrong most often. Examples are a stage-level option, a missing serviceName, or a re-indented string. The plugin setup needs only the language's plugin token, so ddtest can write it safely from the parser's byte spans. It re-reads the result with #266's checker before writing anything. See the plugin docs.

E2E testing

Prerequisites: a ddtest built from this branch (make build), git, Node 22+ with npm, and Ruby 3.2+ with Bundler. For each scenario, copy the fixture into a fresh temp directory, then run git init && git add -A. Don't commit: --allow-dirty extends the staged files. No Jenkins is needed.

  1. JavaScript, declarative (internal/onboard/testdata/jenkins-javascript, then npm install):
    • ddtest onboard --framework jest --agent --ci-runtime node=22: ready, and next_command is onboard … --apply --yes.
    • ddtest onboard --framework jest --apply --ci-runtime node=22: a diff that adds options { datadog(testOptimization: [enabled: true, serviceName: 'jenkins-javascript-fixture', languages: ['JAVASCRIPT']]) } after agent, and Preview only; …. Jenkinsfile is unchanged.
    • Rerun with --yes --allow-dirty --format agent: ci_setup_confirmation_required, with changes listing Jenkinsfile and .gitignore.
    • Run plain --apply again: Jest CI setup is already configured; no edits.
    • Run --apply --yes --allow-dirty --format agent --ci-runtime node=22 --ci-confirm jenkins-plugin --ci-confirm jenkins-plugin-mode=agentless:datadoghq.com: complete_locally. ddtest testdrive --report with the same flags shows COMPLETE LOCALLY.
  2. Ruby, scripted (internal/onboard/testdata/jenkins-ruby):
    • ddtest onboard --agent --ci-runtime ruby=3.2: ci_setup_required, with next_action: run_command and next_command set to onboard --framework rspec --apply --yes ….
    • Run it with --allow-dirty. node { … } is now inside datadog(testOptimization: […, languages: ['RUBY']]) { … }, indented one more step (4 spaces). agent_instruction includes Pin datadog-ci in the bundle, and resume_command is testdrive … --all.
    • Add gem "datadog-ci", "1.42.0", group: :test to Gemfile, run bundle lock, and add /.testoptimization/ to .gitignore.
    • Run testdrive --framework rspec --all --agent --yes --ci-runtime ruby=3.2 with the two --ci-confirm flags: complete_locally.
    • ddtest onboard --apply --ci-runtime ruby=3.2: RSpec CI setup is already configured; no edits.
  3. Merge (JavaScript fixture):
    • Put options { timeout(time: 1, unit: 'HOURS') \n datadog(collectLogs: true) } (one option per line) after agent. The preview adds testOptimization: [...] to that datadog(…) call and keeps timeout and collectLogs.
    • Replace it with datadog(testVisibility: [enabled: false, serviceName: 'scigateway', languages: ['RUBY']]). The preview edits it in place to enabled: true and languages: ['RUBY', 'JAVASCRIPT'].
  4. Vitest (JavaScript fixture, with "test": "vitest run", vitest@3.2.4 and an ESM test, then npm install): ddtest onboard --framework vitest --apply --ci-runtime node=22. The preview also changes sh 'yarn test' to sh 'if [ -n "${DD_TRACE_PATH:-}" ]; then export NODE_OPTIONS="${NODE_OPTIONS:-} --import $DD_TRACE_PATH/register.js"; fi; yarn test'.
  5. Refusals (JavaScript fixture). In each case, check that Jenkinsfile is unchanged.
    • Add @Library('pipeline-lib') _ after the first line. onboard --framework jest --agent --ci-runtime node=22 returns ci_setup_required with apply_source_edit, and reason includes onboard --apply cannot write this setup (… loads a shared library (@Library) …). Plain --apply prints that refusal.
    • Use agent { docker { image 'node:22' } }. --apply --yes --format agent returns ci_setup_unsupported.
    • Restore agent { label 'sl7'} and add script { docker.image('node:22').inside { sh 'npm pack' } } after sh 'yarn test'. onboard --framework jest --agent --ci-runtime node=22 returns ci_setup_required, and reason says the option "would also apply to Jenkinsfile / Unit Tests (docker.image('node:22').inside), which run steps in a container". --apply --yes --allow-dirty --format agent returns ci_edit_unsupported with the same reason.
    • Use datadog(testOptimization: [enabled: params.TRACE, …]). The result names the computed enabled.

🤖 Generated with Claude Code

juan-fernandez and others added 4 commits October 30, 2026 22:50
onboard --apply now enables the Datadog Jenkins plugin in the Jenkinsfiles
whose host-agent jobs run the tests. The only language input is the
plugin's language token (and the service name the language reads from its
project), so Java and Python later only add a token.

- Declarative: add datadog(testOptimization: …) to the top-level options,
  creating the directive after agent or environment, or merge into an
  existing options/datadog(…) call (enabled: true, the language added to
  languages, every other argument kept, testVisibility edited in place).
- Scripted: wrap each outermost node block that runs the tests in
  datadog(testOptimization: …) { … }, indented one step; multi-line
  strings keep their bytes.
- Vitest and ESM Mocha: add the DD_TRACE_PATH loader line to a
  single-quoted test sh, or ask for it and resume with onboard --apply.
- Refuse, with the instructions and the reason instead: shared libraries,
  container-only test jobs, computed arguments, node blocks in def
  functions, several datadog calls that differ, and any result PR-A's
  checker does not read as a complete setup.

Edits use the parser's byte spans, keep indentation and line endings, and
are re-read before anything is written. JavaScript gets the automatic
setup, asking for the plugin confirmations right after the edit. Ruby gets
--apply on Jenkins only: it writes the Jenkinsfile edit and returns the
datadog-ci pin as agent instructions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a project name, serviceName is the origin remote's repository name,
falling back to the checkout directory only without a remote: a CI
checkout's directory is often a workspace name, not the repository's.

The Ruby handoff to onboard --apply now forwards the same flags as the
automatic handoff, and a golden case covers a container node outside the wrapped test node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
onboard --apply already refused a Jenkinsfile whose test jobs all run in
containers. It now refuses any edit that would put a job running steps in
a container inside the option's scope (the whole declarative pipeline, or
a wrapped node), because the plugin's tracer variables reach those
container steps too. The refusal names the jobs and the user's options;
onboarding then gives the instructions with that reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value in a pipeline's or stage's environment directive, such as
X = sh(script: 'git show -s --pretty=%an', returnStdout: true).trim() or an
sh in a GString's ${...}, runs when Jenkins enters that scope. The reader
now evaluates it like any other expression (sh, load, build, a Jenkinsfile
function or an unknown step) and records what it runs where Jenkins runs
it: before a stage's steps, in that stage's job, and for the pipeline, a
stage of stages or a matrix cell, in a job of its own named environment or
<stage> (environment). The variable's value stays unknown, and a
credentials() binding is read as before.

The steps see the Datadog plugin's variables of the pipeline's top-level
option, as the plugin sets them for every step. A command that runs no
tests lands in the shared review of non-test jobs, and a test command is
validated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@datadog-official

datadog-official Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Pipelines  Tests  Code Coverage

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
• Patch Coverage: 80.82%
• Overall Coverage: 88.25% (-0.05%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 32df9cc | Docs | View more details | Give us feedback!

@juan-fernandez

Copy link
Copy Markdown
Contributor Author

/datadog autotest review
@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T23:00:32.997308Z 66d1d82 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66d1d8281f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +117 to +120
target := ciSetupTarget{Workflow: workflow.Path, Job: name, Step: first}
if framework == "mocha" && !esm {
target.After = []string{}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Instrument every ESM test shell

When a Jenkins job contains multiple matching sh steps, this creates a target only for the first one. The Jenkins ESM bootstrap in request.After exports NODE_OPTIONS only in that individual shell, so a later Vitest/Playwright test step—or the actual ESM Mocha step when an earlier CommonJS step matched first—runs without the required --import loader even though the resulting setup is accepted as complete. Create targets for every matching test step that needs the shell-local bootstrap.

Useful? React with 👍 / 👎.

}
esm[calls[0].call] = target.after
}
for _, current := range calls {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict edits to the resolved shell call

For a scripted Jenkinsfile with multiple sh calls on one physical line, such as separate node { sh 'npm test' }; node { sh 'deploy' } blocks, calls contains every call on that line and this loop wraps or merges all of their enclosing scopes. For setups without an after command, no ambiguity check runs, so onboard --apply can instrument unrelated deployment or build nodes instead of only the node containing the discovered test command.

Useful? React with 👍 / 👎.

Comment on lines +786 to +788
text, _ := script.Value.literal()
if !slices.ContainsFunc(commands, func(command string) bool { return !strings.Contains(text, command) }) {
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify bootstrap commands precede the tests

This treats a required bootstrap command as satisfied when its text occurs anywhere in the sh literal. If the command is after the test command or appears only in a comment, the editor makes no change and subsequently reports the Jenkins setup as complete, although the ESM test ran before NODE_OPTIONS was extended. Check executable ordering relative to each test command, or refuse the edit when that ordering cannot be established.

Useful? React with 👍 / 👎.

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

Bits Code Review: FAIL

Two of the most critical issues are: (1) merging named arguments into an existing positional-argument datadog(…) call produces a broken Jenkinsfile that Jenkins rejects but the post-edit checker accepts, and (2) ESM loader injection targets only the first sh call in a multi-shell stage, leaving all subsequent shells uninstrumented despite the checker reporting success. Additionally, overlapping edits between node-block wrapping and loader insertion block automatic setup for scripted Vitest/ESM Mocha pipelines, and --command validation fires only after source files have already been modified.

Open Bits AI session

🤖 Bits Code Review · Commit 66d1d82

return nil
}
e.merged[call] = true
key, value, ok := jenkinsTestOptimization(call)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Reject positional arguments before merging datadog calls

For an existing scripted wrapper such as datadog([collectLogs: true]) { … }, merging appends testOptimization as a named argument outside the positional map. The resulting call supplies two map arguments, which Jenkins rejects. ddtest's post-edit checker nevertheless accepts it and writes the broken pipeline. Refuse positional/computed arguments or merge inside the existing literal map.

Suggested change
key, value, ok := jenkinsTestOptimization(call)
if positional, _ := jenkinsArgs(call); len(positional) > 0 {
return e.gap(call, "The datadog(…) call has positional or computed arguments, so ddtest cannot safely merge the setup into it.")
}
key, value, ok := jenkinsTestOptimization(call)

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

}
}
for _, call := range slices.SortedFunc(maps.Keys(esm), func(a, b *groovyCall) int { return a.Start - b.Start }) {
if err := e.prepend(call, esm[call]); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Compose loader insertions with scripted node wrapping

For an unconfigured scripted pipeline running Vitest or ESM Mocha, wrap schedules replacement of the entire node block, then prepend schedules a loader insertion inside that replacement. apply consequently returns “The planned edits overlap,” preventing automatic setup even for a simple single-quoted test script. Compose the loader change into the wrapper replacement or generate nonoverlapping edits.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

return nil, &SetupGap{location, err.Error()}
}
}
target := ciSetupTarget{Workflow: workflow.Path, Job: name, Step: first}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Add the ESM loader to every test shell in a Jenkins job

When a Jenkins job runs Vitest or ESM Mocha in multiple sh calls on separate lines, only its first test shell becomes an edit target. Each shell has its own environment, so later shells miss the ESM loader and run without the required instrumentation. The new setup checker also returns after the first test, allowing this incomplete configuration to be written. Collect and validate every test shell requiring the loader.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

_, err = fmt.Fprintln(output, "Preview only; no source files changed and no tests executed. Review this diff, then "+next)
return err
}
changes, err := plan.Apply(apply.AllowDirty)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Reject --command before applying Ruby Jenkins edits

On a Ruby Jenkins project with its CI runtime supplied, onboard --apply --yes --command … writes the Jenkinsfile before StartWithOptions rejects the command override as invalid_options. An invalid request therefore changes setup for all discovered tests; previously it was rejected without editing source. Delegate the rejection before applying the plan.

Suggested change
changes, err := plan.Apply(apply.AllowDirty)
if options.Command != "" {
return start()
}
changes, err := plan.Apply(apply.AllowDirty)

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

onboard --agent decided that onboard --apply writes a JavaScript
framework's Jenkins plugin setup by planning the edit for any site, so a
plugin the user confirmed reports to another Agentless site than DD_SITE
(or --site) was handed to onboard --apply, which then refused the edit as
incomplete. Plan it for the selected site, DD_SITE by default, as
onboard --apply does: a mismatch leaves the setup to the instructions,
whose ci_setup_required result names it, as before onboard --apply
edited Jenkinsfiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant