Run pb-diff in CI
A CI job uses pb-diff-ci.exe, the pb-diff tool for build
machines. It is one file, installed beside pb-diff.exe and
pb-diff-cli.exe and shipped in the ZIP, and it runs copied
on its own to any Windows machine, a hosted runner included, with
nothing else installed.
- It needs no licence. Nothing to activate, no licence file to keep as a secret: it never checks a licence and never exits with the code that means one is missing.
- It makes no network call of any kind: no licence check, no update check, no telemetry. A runner with no Internet access runs it the same way.
- It answers at the level of the object. It says which objects are new, modified, deleted or renamed, with their types, sizes and dates, and which sources are damaged, but never what changed inside an object: that is what the desktop application is for, and each report says so in its last line.
Its commands are the commands of pb-diff-cli that stay
at that level - compare-lib, the summary and
summary-changes reports, check,
info, list, validate,
schema and help - with the same names,
options, JSON and exit codes, so the Command-Line
Tool Reference describes both.
Put it on the build machine
Copy pb-diff-ci.exe from a pb-diff installation or from
the ZIP to wherever the job can reach it. Two arrangements work
well:
- In the repository, under a folder such as
tools\. Every job then runs the version the repository was tested with, and a hosted runner needs nothing installed. The examples below assume this. - On the build server, in a folder on its
PATH, for a self-hosted agent that builds several repositories.
It may be used on any machine, hosted runners included, for the work of an organisation with a pb-diff subscription; the usage terms say so in full.
Gate on what changed
compare-lib compares two libraries, two folders or a
library with a folder, object by object, as the desktop application's
Compare does. A folder can be a ws_objects folder, a
PowerBuilder 2025 Solution or one of its PBL folders, or a folder of
exported objects. Read the exit code before the output:
| Code | Meaning |
|---|---|
| 0 | Success; for compares, no differences; for check, nothing noticed |
| 1 | Compares: differences found. Validate: warnings only. Check: findings |
| 2 | Invalid input, missing file or entry, or bad arguments |
| 4 | Internal error |
Exit code 1 is an answer and not a failure, so a gate that treats every non-zero code as broken turns "this pull request changes three objects" into a red build. In PowerShell, which every Windows runner has:
$raw = .\tools\pb-diff-ci.exe compare-lib release\orders.pbl src\ws_objects --changed-only --format json | Out-String
switch ($LASTEXITCODE) {
0 { 'No object changed.' }
1 { ($raw | ConvertFrom-Json).entries | ForEach-Object { '{0,-9} {1}' -f $_.status, $_.name } }
default { throw "Comparing failed with exit code $LASTEXITCODE." }
}
A name two inputs share - the same object in two subfolders - is
never paired by guessing: both are listed, and notes in the
JSON says why. Renames are paired with --pair old=new.
Keep the report, and summarise it in the job
report summary-changes turns the same comparison into a
report: HTML, PDF, CSV or RTF to keep as a build artefact, and Markdown
for the page a CI system shows beside the job.
.\tools\pb-diff-ci.exe report summary release\orders.pbl src\ws_objects --output pb-diff-summary.html
.\tools\pb-diff-ci.exe report summary-changes release\orders.pbl src\ws_objects --report-format markdown >> $env:GITHUB_STEP_SUMMARY
The Markdown is one table row per object, with its status, type and
sizes, and the notes, and it fits a GitHub Actions job summary, an Azure
DevOps build summary or a pull-request comment. A pull-request comment
holds 65,536 characters, so a large application is better summarised
with summary-changes than with summary. Every
report pb-diff-ci writes, but the JSON, ends with the line
Generated by pb-diff-ci and its version, and a link to where
the detail of each change is shown.
Check sources after a merge
A merge left half-done does not fail until PowerBuilder imports the
file. check finds the damage first: merge conflict markers,
a block or a DataWindow statement that never closes, a file that is not
the format its extension names.
.\tools\pb-diff-ci.exe check src\ws_objects --format json --out check.json
switch ($LASTEXITCODE) {
0 { 'Nothing noticed.' }
1 { (Get-Content -Raw check.json | ConvertFrom-Json).findings | ForEach-Object { "$($_.path)($($_.line)): $($_.code)" }; throw 'Sources left broken.' }
default { throw "check could not run (exit code $LASTEXITCODE)." }
}
Each finding names the file, the code and the first line concerned,
and never quotes the file. A folder that holds no source at all is
refused rather than passed, so a mistyped path cannot turn the check
green. check works in one direction: a finding means a file
is damaged, and no finding means nothing was noticed, not that the code
compiles - the build itself is what says that.
Check the libraries for damage
validate checks libraries the way check
checks sources: give it the workspace, a target or the folder of
libraries, and it reports each library's state, with exit code 2 when
any is damaged, unreadable or missing.
.\tools\pb-diff-ci.exe validate src\app.pbw --output library-check.md
$code = $LASTEXITCODE
Get-Content library-check.md >> $env:GITHUB_STEP_SUMMARY
if ($code -ge 2) { throw 'A library is damaged, unreadable or missing.' }
Check libraries for damage says what each finding means and what to do.
GitHub Actions
A pull request is checked, compared with the branch it targets, summarised on the job's page and kept as an artefact:
name: PowerBuilder changes
on: pull_request
jobs:
pb-diff:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Check the sources
shell: pwsh
run: |
.\tools\pb-diff-ci.exe check src\ws_objects --format json --out check.json
if ($LASTEXITCODE -ne 0) { Get-Content check.json; exit 1 }
- name: Compare with the target branch
shell: pwsh
run: |
git worktree add ..\target "origin/${{ github.base_ref }}"
.\tools\pb-diff-ci.exe report summary-changes ..\target\src\ws_objects src\ws_objects --report-format markdown >> $env:GITHUB_STEP_SUMMARY
.\tools\pb-diff-ci.exe report summary ..\target\src\ws_objects src\ws_objects --output pb-diff-summary.html
if ($LASTEXITCODE -ne 0) { exit 1 }
- uses: actions/upload-artifact@v6
with:
name: pb-diff-summary
path: pb-diff-summary.html
pb-diff runs this pipeline itself, on a GitHub-hosted Windows runner,
over the Order Desk examples, with pb-diff-ci.exe copied
there alone and its network blocked: that is how the claim that it runs
on a hosted runner is checked before each release.
Azure DevOps
##vso[task.uploadsummary] attaches the Markdown to the
build's summary page:
trigger:
- main
pool:
vmImage: windows-latest
steps:
- checkout: self
fetchDepth: 0
- pwsh: |
.\tools\pb-diff-ci.exe check src\ws_objects --format json --out check.json
if ($LASTEXITCODE -ne 0) { Get-Content check.json; exit 1 }
displayName: Check the sources
- pwsh: |
$summary = "$(Build.ArtifactStagingDirectory)\pb-diff-summary.md"
.\tools\pb-diff-ci.exe report summary-changes release\orders.pbl src\ws_objects --output $summary
if ($LASTEXITCODE -ne 0) { exit 1 }
Write-Host "##vso[task.uploadsummary]$summary"
.\tools\pb-diff-ci.exe report summary release\orders.pbl src\ws_objects --output "$(Build.ArtifactStagingDirectory)\pb-diff-summary.html"
displayName: Summarise what changed
- publish: $(Build.ArtifactStagingDirectory)
artifact: pb-diff
Jenkins
On a Windows agent, in a declarative Jenkinsfile:
pipeline {
agent { label 'windows' }
stages {
stage('Check the sources') {
steps {
powershell '''
& .\\tools\\pb-diff-ci.exe check src\\ws_objects --format json --out check.json
if ($LASTEXITCODE -ne 0) { Get-Content check.json; exit 1 }
'''
}
}
stage('Summarise what changed') {
steps {
powershell '''
& .\\tools\\pb-diff-ci.exe report summary release\\orders.pbl src\\ws_objects --output pb-diff-summary.html
if ($LASTEXITCODE -ne 0) { exit 1 }
'''
archiveArtifacts artifacts: 'pb-diff-summary.html'
}
}
}
}
The Azure DevOps and Jenkins pipelines run the same commands as the GitHub Actions one, which is the one pb-diff runs itself; adjust the paths to where your libraries and sources are.
When a job needs more than the object
pb-diff-ci stops at the object by design. A job that has
to show what changed inside an object, export objects or merge them runs
pb-diff-cli, which needs a licence: activate the build
server's service account once, as for a person, on a named subscription
seat. See Automate with pb-diff-cli and Licence.