Integration with OpenText Enterprise Performance EngineeringThis extension enables you to include OpenText Enterprise Performance Engineering operations as tasks in a Microsoft Azure DevOps Server CI/CD pipeline. Configure your performance tests and script repositories once and automate them on every build — no manual intervention required. The extension ships two tasks:
Key FeaturesEnterprise Performance Engineering Test task
Enterprise Performance Engineering Workspace Sync task
Supported Product VersionsThis extension supports the 3 latest versions of OpenText Enterprise Performance Engineering. What's New in Version 3.6.0
🆕
|
| Scenario | Recommended setting |
|---|---|
| Production server with a valid TLS 1.2+ certificate from a trusted CA | Leave unchecked (default) |
| Development / lab server with a self-signed certificate | Check to skip certificate validation |
| Server still running on TLS 1.0 or TLS 1.1 | Check to lower the minimum accepted TLS version |
When enabled, the task:
- Accepts TLS 1.0 and TLS 1.1 connections (in addition to the default TLS 1.2/1.3)
- Skips certificate chain validation — self-signed certificates, expired certificates, and certificates from an untrusted internal CA are all accepted
⚠️ Security notice: Enabling this option reduces transport security. Man-in-the-middle attacks on the connection between the Azure DevOps agent and the Enterprise Performance Engineering server become possible. Do not enable this on production pipelines unless you fully control the network path between the agent and the server. This option exists to unblock teams whose server infrastructure cannot be immediately updated — it should be treated as a temporary workaround, not a permanent setting.
A warning line is emitted in the build log whenever the option is active:
Allow insecure TLS: enabled — TLS 1.0/1.1 and untrusted certificates are accepted.
🔧 Improved error reporting for connection failures
Authentication and HTTP errors that result in no server response (network unreachable, DNS failure, TLS handshake rejected, connection refused) now include the underlying system error code in the log message, making it easier to diagnose the root cause without requiring a support engagement:
Authentication failed (exception): No response from server [CERT_HAS_EXPIRED: certificate has expired]. Check network/proxy settings.
Authentication failed (exception): No response from server [ERR_TLS_PROTOCOL_VERSION]. Check network/proxy settings.
Authentication failed (exception): No response from server [ECONNREFUSED]. Check network/proxy settings.
Authentication failed (exception): No response from server [ENOTFOUND]. Check network/proxy settings.
What's New in Version 3.3.0
July 2026
🆕 YAML-based test creation (varTestID accepts .yaml / .yml)
The varTestID input of the Enterprise Performance Engineering Test task now accepts either a numeric test ID (existing behaviour) or a path to a YAML file that describes the test topology.
# perf-tests/api-load.yaml
test_name: "API Load Test"
test_folder_path: "ci-tests/api"
test_content:
lg_amount: 1
group:
- group_name: "API Group"
vusers: 50
script_path: "scripts\\api\\my_script"
scheduler:
rampup: 120
duration: 600
When a YAML path is provided the task:
- Parses the file (full-test or content-only format)
- Resolves
script_pathentries to numeric script IDs viaGET /Scripts - Creates any missing test-plan folders via
POST /testplan POST /teststo create the test, orPUT /tests/{id}if a test with that name already exists (idempotent)- Runs the test as usual and downloads result artifacts
See YAML-TEST-CREATION-FEATURE.md for the complete schema reference and implementation notes.
🔧 Bug fixes in 3.3.0
| Fix |
|---|
getScripts() always returned empty list — LRE returns XML not JSON; no <ScriptList> wrapper in real responses |
ensureTestPlanFolderExists() threw on HTTP 400 "already exists" — now treated as a no-op (same as 409) |
New varWorkspaceDir task input — allows PluginUI to pass an absolute workspace root while using a relative YAML path |
GlobalCommandLine XML used wrong field names (<GroupName>/<CommandLine>) — corrected to <Name>/<Value> per the LRE API spec |
🔧 PluginUI — "Browse…" button for YAML files
The Test ID field in PluginsUI has been renamed to "Test ID or YAML" and now includes a Browse… button to navigate to a .yaml / .yml file. The validation accepts either a positive integer or a file path ending in .yaml/.yml.
YAML Test Definition Reference
When varTestID is a path to a .yaml or .yml file, the task parses the file and creates or updates an LRE test before running it.
Two shapes are supported:
Shape 1 — Full-test (name + folder + content)
##################################################
test_name: "My Performance Test"
test_folder_path: "ci-tests/api" # relative to Subject\ in LRE test plan
test_content:
controller: "Controller01" # optional — pin to a specific controller host
lg_amount: 2 # number of LGs; ignored when every group lists lg_name[]
group:
- group_name: "API Load"
vusers: 50
script_path: "scripts\\api\\my_script" # resolved to script ID automatically
lg_name: # when ALL groups have lg_name → manual LG distribution
- LG1
- LG2
command_line: "-param value" # optional — runtime CLI args passed to the script
rts:
pacing:
number_of_iterations: 0 # 0 = infinite
type: "random interval" # see pacing types below
delay: 60 # seconds
delay_random_range: 10 # upper-bound offset for "random" types
thinktime:
type: "random" # ignore | replay | modify | random
min_percentage: 50
max_percentage: 150
limit_seconds: 20
java_vm: # Java protocol scripts only
jdk_home: "C:\\Java\\jdk-17"
java_vm_parameters: "-Xms64m -Xmx512m"
java_env_class_paths:
- "C:\\mylib\\mylib.jar"
jmeter: # JMeter scripts only
jmeter_home_path: "C:\\jmeter"
start_measurements: true
jmeter_min_port: 10000 # optional custom port range
jmeter_max_port: 10099
jmeter_additional_properties: "prop=val"
selenium: # Selenium scripts only
jre_path: "C:\\Java\\jre"
class_path: "C:\\selenium\\selenium.jar"
test_ng_files: "testng.xml"
scheduler:
rampup: 120 # seconds; 0 = start all simultaneously
duration: 600 # seconds; 0 = run until completion
automatic_trending: # optional — attach results to a trend report
report_id: 5
max_runs_in_report: 10
lg_elastic_configuration: # optional — elastic load generators
image_id: 1
memory_limit: 2048
cpu_limit: 2
controller_elastic_configuration: # optional — elastic controller
image_id: 2
memory_limit: 4096
cpu_limit: 4
##################################################
Shape 2 — Content-only (name from filename, folder from path)
##################################################
group:
- group_name: "Smoke"
vusers: 10
script_path: "scripts\\smoke\\login_flow"
lg_name:
- LG1
scheduler:
rampup: 0
duration: 300
##################################################
Lines wrapped in
##...##(sentinel markers used in the GitHub Action format) are stripped automatically.
The test name is derived from the YAML filename and the test-plan folder from the file's directory path relative to the workspace root.
Field reference
group[]
| Field | Type | Default | Notes |
|---|---|---|---|
group_name |
string | Group_N |
|
vusers |
number or string | 1 |
|
script_id |
number | — | Provide script_id or script_path |
script_path |
string | — | folder\\scriptName as seen in LRE Scripts; matched case-insensitively |
lg_name |
string[] | — | When all groups have lg_name → manual LG distribution. LG\d+ → automatch · DOCKER\d+ → dynamic · anything else → specific |
command_line |
string | — | Global command-line override passed to the LRE controller for this group |
rts.pacing
| Field | Type | Default | Notes |
|---|---|---|---|
number_of_iterations |
number | 1 |
0 = infinite |
type |
string | immediately |
immediately · fixed interval · fixed delay · random interval · random delay |
delay |
number (sec) | 0 |
Delay for fixed/random types |
delay_random_range |
number (sec) | 0 |
For random types: range = [delay, delay + delay_random_range] |
rts.thinktime
| Field | Type | Notes |
|---|---|---|
type |
string | ignore · replay · modify · random |
limit_seconds |
number | Max think time cap (replay / random) |
min_percentage |
number | For random type |
max_percentage |
number | For random type |
multiply_factor |
number | For modify type |
rts.java_vm
| Field | Type | Notes |
|---|---|---|
jdk_home |
string | Path to JDK root; when set, UserSpecifiedJdk = true |
java_vm_parameters |
string | JVM args, e.g. -Xms64m -Xmx512m |
java_env_class_paths |
string[] | Additional classpath entries |
use_xboot |
boolean | Default false |
enable_classloader_per_vuser |
boolean | Default false |
rts.jmeter
| Field | Type | Notes |
|---|---|---|
jmeter_home_path |
string | JMeter installation directory |
start_measurements |
boolean | Default false |
jmeter_min_port |
number | Custom port range start |
jmeter_max_port |
number | Custom port range end |
jmeter_additional_properties |
string | Extra JMeter properties string |
rts.selenium
| Field | Type |
|---|---|
jre_path |
string |
class_path |
string |
test_ng_files |
string |
scheduler
| Field | Type | Default | Ramp-up behaviour |
|---|---|---|---|
rampup |
number (sec) | 0 |
0–1 → all simultaneously · 2–30 → two batches · >30 → one-vuser-at-a-time (min interval 15 s) |
duration |
number (sec) | 0 |
0 = run until completion |
automatic_trending
| Field | Type | Notes |
|---|---|---|
report_id |
number | Required |
max_runs_in_report |
number | Default 10 |
lg_elastic_configuration / controller_elastic_configuration
| Field | Type |
|---|---|
image_id |
number |
memory_limit |
number (MB) |
cpu_limit |
number |
What's New in Version 3.2.0
July 2026
🆕 Differential sync (varBaseCommitSha)
A new optional input varBaseCommitSha enables differential sync: the task runs git diff --name-only <sha> HEAD inside the workspace directory and uploads only the script folders containing changed files since that commit. All unchanged scripts are skipped.
| Scenario | Behaviour |
|---|---|
varBaseCommitSha is empty |
Full sync — every detected script folder is uploaded (default) |
varBaseCommitSha contains a valid SHA |
Differential sync — only folders with changed files are uploaded |
git diff fails (shallow clone, bad SHA) |
Warning logged; falls back to full sync automatically |
Pipeline example — automatic differential sync (Azure DevOps Server on-premises)
The snippet below stores the GitLab HEAD SHA as a build artifact and retrieves it at the start of each build. The first run performs a full sync; subsequent runs upload only changed scripts.
Requirement: enable "Allow scripts to access the OAuth token" on the agent job (pipeline Settings → Agent job → Additional options).
steps:
- script: |
git clone https://$(GitLabUser):$(GitLabToken)@<your-gitlab-host>/<repo>.git gitlab-src
displayName: 'Checkout GitLab repo'
# ── Find the last successful build and download its stored SHA ────────────────
- powershell: |
$orgUri = $env:SYSTEM_TEAMFOUNDATIONSERVERURI
$project = $env:SYSTEM_TEAMPROJECTID
$defId = $env:SYSTEM_DEFINITIONID
$curBuild = [int]$env:BUILD_BUILDID
$token = $env:SYSTEM_ACCESSTOKEN
$headers = @{ Authorization = "Bearer $token" }
$buildsUrl = "${orgUri}${project}/_apis/build/builds?definitions=${defId}&resultFilter=succeeded&statusFilter=completed&`$top=10&api-version=6.0"
$builds = Invoke-RestMethod -Uri $buildsUrl -Headers $headers -ErrorAction SilentlyContinue
$prev = $builds.value | Where-Object { $_.id -ne $curBuild } | Select-Object -First 1
if (-not $prev) {
Write-Host "No previous successful build — full sync will run."
Write-Host "##vso[task.setvariable variable=lastSyncSha]"
exit 0
}
Write-Host "Previous successful build: $($prev.id)"
$artUrl = "${orgUri}${project}/_apis/build/builds/$($prev.id)/artifacts?artifactName=last-sync-sha&api-version=6.0"
try {
$art = Invoke-RestMethod -Uri $artUrl -Headers $headers -ErrorAction Stop
$zipPath = "$(Agent.TempDirectory)\last-sync-sha-dl.zip"
Invoke-WebRequest -Uri $art.resource.downloadUrl -Headers $headers -OutFile $zipPath
Expand-Archive -Path $zipPath -DestinationPath "$(System.ArtifactsDirectory)\last-sync-sha" -Force
$shaFile = Get-ChildItem -Path "$(System.ArtifactsDirectory)\last-sync-sha" -Filter sha.txt -Recurse | Select-Object -First 1
if ($shaFile) {
$sha = (Get-Content $shaFile.FullName).Trim()
Write-Host "Last sync SHA: $sha"
Write-Host "##vso[task.setvariable variable=lastSyncSha]$sha"
} else {
Write-Host "sha.txt not found — full sync will run."
Write-Host "##vso[task.setvariable variable=lastSyncSha]"
}
} catch {
Write-Host "Artifact not found in build $($prev.id) — full sync will run."
Write-Host "##vso[task.setvariable variable=lastSyncSha]"
}
displayName: 'Download last sync SHA'
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
# ── Sync (differential when SHA available, full on first run) ─────────────────
- task: LoadRunnerEnterpriseSync@3
inputs:
varPCServer: 'https://<lre-server>:<port>/?tenant=<guid>'
varUserName: '<username>'
varPassWord: '$(PCPassword)'
varDomain: '<domain>'
varProject: '<project>'
varWorkspaceDir: '$(Build.SourcesDirectory)/gitlab-src'
varBaseCommitSha: '$(lastSyncSha)' # empty on first run → full sync
varParallelUploads: '5'
varSuccessThreshold: '80'
# ── Save HEAD SHA for the next build ─────────────────────────────────────────
- powershell: |
$sha = (& git -C "$(Build.SourcesDirectory)\gitlab-src" rev-parse HEAD).Trim()
$dir = "$(Agent.TempDirectory)\last-sync-sha"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Set-Content -Path "$dir\sha.txt" -Value $sha
Write-Host "Saved SHA: $sha"
displayName: 'Save current sync SHA'
condition: succeeded()
- task: PublishBuildArtifacts@1
displayName: 'Publish last sync SHA'
condition: succeeded()
inputs:
PathtoPublish: '$(Agent.TempDirectory)\last-sync-sha'
ArtifactName: 'last-sync-sha'
publishLocation: 'Container'
What's New in Version 3.1.0
July 2026
🆕 Enterprise Performance Engineering Workspace Sync — varSuccessThreshold parameter
A new optional parameter controls how many upload failures the pipeline tolerates before failing the task.
varSuccessThreshold value |
Task result |
|---|---|
| (empty / not set) | Default: passes when ≥ 50% of scripts upload successfully |
0 |
Passes even if no scripts were uploaded (authentication failure still fails the task) |
100 |
Fails if even one script fails to upload |
| Outside 0–100 | Falls back to the default (50%) |
Always fails on 5 consecutive upload failures — this abort rule applies regardless of the threshold setting.
⚠️ Enterprise Performance Engineering Workspace Sync — varParallelUploads default changed to 1
What's New in Version 3.0.0
July 2026
Version 3.0.0 is a complete rewrite of the extension in TypeScript / Node.js, replacing the legacy C# implementation. All existing task inputs and behaviours are preserved.
Highlights
🔧 Node.js Runtime Compatibility
- Requires Node.js 16 or later; Node 20 is recommended
- Execution handlers declared for Node 20.1, Node 20, and Node 16 in priority order — Azure DevOps agents automatically select the highest supported version
- Polyfills injected at bootstrap cover built-ins introduced between Node 16 and Node 20 (
crypto.randomUUID,Object.fromEntries,Array.flat/flatMap,Promise.allSettled,String.trimStart/trimEnd,queueMicrotask,globalThis)
🛠️ Auto-Provisioning of Test Infrastructure
- When a project has no test sets or instances for the selected test, the task now automatically:
- Retrieves existing test set folders and locates (or creates) a "CI Test Sets" folder under the project Root
- Creates a new test set inside that folder
- Creates a test instance for the configured test
- Eliminates the most common first-run failure mode — no manual Enterprise Performance Engineering UI setup required
⚙️ Azure DevOps Compliance
- Minimum agent version requirement updated to
2.144.0 - Agent proxy auto-detected from Azure DevOps agent configuration when no explicit proxy URL is provided in the task inputs
- Every log line is simultaneously written to a log file in the artifacts directory
Installation Instructions
For full installation instructions, see the Installation and environment set up.
Note: You must have Administrator privileges to install the extension on your Azure DevOps server.
Documentation
Full usage documentation: Continuous integration with Azure DevOps.