diff --git a/.github/workflows/examples-fresh-clone.yml b/.github/workflows/examples-fresh-clone.yml
new file mode 100644
index 000000000..e4eb3185b
--- /dev/null
+++ b/.github/workflows/examples-fresh-clone.yml
@@ -0,0 +1,124 @@
+name: Examples from a fresh clone
+
+# Builds each product's console examples as AGENTS.md says, from a clone
+# with its submodules, against the packages published on Maven Central, with
+# the JDK and Maven the runner has. Then runs the getting started example
+# from the repository root with no settings. A fresh clone holds only a link
+# for each free data file. On Ubuntu the example downloads its file, which
+# tests the download, and the job keeps the file in the cache. The other
+# systems take it from there, so each file is downloaded once a run.
+
+on:
+ pull_request:
+ paths:
+ - 'examples/**'
+ - 'cxx'
+ - '.github/workflows/examples-fresh-clone.yml'
+ - 'ci/run-fresh-clone-example.ps1'
+ push:
+ branches:
+ - main
+ - rc
+ schedule:
+ - cron: '30 4 * * *'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ download:
+ name: ${{ matrix.product }} on ubuntu-latest, downloading its data file
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - product: device-detection
+ example: fiftyone.devicedetection.examples.console.GettingStartedOnPrem
+ file: cxx/data/device-detection/51Degrees-LiteV4.1.hash
+ - product: ip-intelligence
+ example: fiftyone.ipintelligence.examples.console.GettingStartedOnPrem
+ file: cxx/data/ip-intelligence/51Degrees-LiteV41.ipi
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the repository with its submodules
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ persist-credentials: false
+
+ - name: The runner's Java and Maven
+ shell: pwsh
+ run: |
+ java -version
+ mvn -v
+
+ - name: Build the console examples against the published packages
+ working-directory: examples/${{ matrix.product }}
+ shell: pwsh
+ run: mvn -B --no-transfer-progress -pl console -am package -DskipTests
+
+ - name: Run the getting started example from the repository root
+ shell: pwsh
+ run: >-
+ ./ci/run-fresh-clone-example.ps1 -Product '${{ matrix.product }}'
+ -Example '${{ matrix.example }}' -ExpectDownload $true
+
+ # The key follows the link, so a new free file gets a new entry, and
+ # the same file is kept once rather than once a run.
+ - name: Keep the data file for the other systems
+ uses: actions/cache/save@v4
+ with:
+ path: ${{ matrix.file }}
+ key: fresh-clone-${{ matrix.product }}-${{ hashFiles(format('{0}.link', matrix.file)) }}
+ enableCrossOsArchive: true
+
+ other-systems:
+ name: ${{ matrix.product }} on ${{ matrix.os }}
+ needs: download
+ strategy:
+ fail-fast: false
+ matrix:
+ # macOS runners are shared by the whole organisation, so pull
+ # requests leave them out and the pushes and the daily run use them.
+ os: ${{ fromJSON(github.event_name == 'pull_request' && '["windows-latest"]' || '["windows-latest", "macos-latest"]') }}
+ product: [device-detection, ip-intelligence]
+ include:
+ - product: device-detection
+ example: fiftyone.devicedetection.examples.console.GettingStartedOnPrem
+ file: cxx/data/device-detection/51Degrees-LiteV4.1.hash
+ - product: ip-intelligence
+ example: fiftyone.ipintelligence.examples.console.GettingStartedOnPrem
+ file: cxx/data/ip-intelligence/51Degrees-LiteV41.ipi
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Clone the repository with its submodules
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ persist-credentials: false
+
+ - name: Take the data file the Ubuntu job downloaded
+ uses: actions/cache/restore@v4
+ with:
+ path: ${{ matrix.file }}
+ key: fresh-clone-${{ matrix.product }}-${{ hashFiles(format('{0}.link', matrix.file)) }}
+ enableCrossOsArchive: true
+ fail-on-cache-miss: true
+
+ - name: The runner's Java and Maven
+ shell: pwsh
+ run: |
+ java -version
+ mvn -v
+
+ - name: Build the console examples against the published packages
+ working-directory: examples/${{ matrix.product }}
+ shell: pwsh
+ run: mvn -B --no-transfer-progress -pl console -am package -DskipTests
+
+ - name: Run the getting started example from the repository root
+ shell: pwsh
+ run: >-
+ ./ci/run-fresh-clone-example.ps1 -Product '${{ matrix.product }}'
+ -Example '${{ matrix.example }}' -ExpectDownload $false
diff --git a/AGENTS.md b/AGENTS.md
index e27e80b3a..6998a5278 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -24,7 +24,9 @@ the native engines from `cxx` and does need them, as the README says.
### Prerequisites
- Git.
-- A JDK, 8 or later. CI tests 8, 11, 17 and 21 (`ci/options.json`).
+- A JDK, 8 or later. CI tests 8, 11, 17 and 21 (`ci/options.json`), and
+ builds and runs the examples on the JDK each runner has (see
+ [What CI tests](#pitfalls)).
- Maven 3.8 or later, or the `mvnw` wrapper at the root.
- Network access to GitHub, to Maven Central for the packages the examples
use, and to `distributor.51degrees.com` for the data files.
@@ -215,12 +217,20 @@ example. The IP Intelligence cloud examples read their key from
for Windows x64 and x86, Linux x64 and aarch64, and macOS x64 and aarch64.
The `ip-intelligence.engine.on-premise` 4.5.128 jar has the same, without
Windows x86. On a platform a jar lacks, the example cannot load the engine.
-- **What CI does not test.** On a pull request CI builds and tests the
- examples with the `dev` profile (`ci/build-project.ps1` and
- `ci/run-unit-tests.ps1`), and against the published packages only when it
- publishes (`ci/run-integration-tests.ps1`). It never runs the console
- example jars as above, and the example tests use the paid data files when
- CI has them.
+- **What CI tests.** On a pull request CI builds and tests the examples
+ with the `dev` profile (`ci/build-project.ps1` and
+ `ci/run-unit-tests.ps1`), and the example tests use the paid data files
+ when CI has them. Publish tests them against the packages it has just
+ built (`ci/run-integration-tests.ps1`).
+ [examples-fresh-clone.yml](.github/workflows/examples-fresh-clone.yml)
+ does what [Run the examples](#run-the-examples) says, with the JDK and
+ Maven each runner has. It
+ builds the console examples against the published packages and runs each
+ product's `GettingStartedOnPrem` jar from the repository root with no
+ settings. On Ubuntu the free data file is downloaded, as from a fresh
+ clone, and the Windows and macOS jobs take that file from the run's cache,
+ so each file is downloaded once a run. It runs on each push to `main` and
+ `rc`, daily, and on a pull request that changes the examples.
## Environment variable names
diff --git a/ci/README.md b/ci/README.md
index 9b205e57f..e14c6793b 100644
--- a/ci/README.md
+++ b/ci/README.md
@@ -40,6 +40,14 @@ repositories used to do for one package tree each.
of cxx. No test job downloads a data file, so a file added to the data
repository needs an entry in common-ci's `steps/fetch-assets.ps1` before CI
can use it.
+- [run-fresh-clone-example.ps1](run-fresh-clone-example.ps1) runs a
+ product's console example jar from the repository root for
+ [examples-fresh-clone.yml](../.github/workflows/examples-fresh-clone.yml).
+ It fails unless the example exits with 0 and prints results, and unless it
+ downloads its free data file exactly when the job expects it to. That
+ workflow's Ubuntu job downloads each product's free data file once a run,
+ as a test of the download, and its other jobs take the file from the cache
+ the Ubuntu job saves.
- [run-performance-tests.ps1](run-performance-tests.ps1) measures the Pipeline
API, Device Detection and IP Intelligence and writes one results file per
configuration. The Device Detection and IP Intelligence metrics are prefixed
diff --git a/ci/run-fresh-clone-example.ps1 b/ci/run-fresh-clone-example.ps1
new file mode 100644
index 000000000..5c1e2102b
--- /dev/null
+++ b/ci/run-fresh-clone-example.ps1
@@ -0,0 +1,44 @@
+param (
+ [Parameter(Mandatory)][string]$Product,
+ [Parameter(Mandatory)][string]$Example,
+ # True where the clone holds only the data file's link, so the example
+ # has to download the file. False where the file is already in place.
+ [Parameter(Mandatory)][bool]$ExpectDownload
+)
+
+# Runs a product's console example jar from the repository root with no
+# settings, as AGENTS.md's "Run the examples" does, and fails unless it ends
+# with exit code 0, prints results, and downloads its data file exactly when
+# expected.
+
+$jar = Get-ChildItem "examples/$Product/console/target" `
+ -Filter '*-jar-with-dependencies.jar' | Select-Object -First 1
+if ($null -eq $jar) {
+ Write-Output '::error::The build made no jar with dependencies.'
+ exit 1
+}
+$output = & java -cp $jar.FullName $Example 2>&1 | ForEach-Object { "$_" }
+$code = $LASTEXITCODE
+$output
+$text = $output -join "`n"
+$failed = $false
+if ($code -ne 0) {
+ Write-Output "::error::The example exited with $code."
+ $failed = $true
+}
+$downloaded = $text -match 'is missing, so it is being downloaded'
+if ($ExpectDownload -and -not $downloaded) {
+ Write-Output '::error::The example did not download its free data file.'
+ $failed = $true
+}
+if (-not $ExpectDownload -and $downloaded) {
+ Write-Output '::error::The example downloaded its data file, which should have come from the cache.'
+ $failed = $true
+}
+if ($text -notmatch 'Results:') {
+ Write-Output '::error::The example printed no results.'
+ $failed = $true
+}
+if ($failed) {
+ exit 1
+}
diff --git a/examples/device-detection/pom.xml b/examples/device-detection/pom.xml
index 8e9dd78bc..05c6fe34a 100644
--- a/examples/device-detection/pom.xml
+++ b/examples/device-detection/pom.xml
@@ -62,7 +62,7 @@
2.0.20
1.3.16
- 3.26.0
+ 3.28.0
diff --git a/examples/ip-intelligence/pom.xml b/examples/ip-intelligence/pom.xml
index 8d7ff14cf..585aaa426 100644
--- a/examples/ip-intelligence/pom.xml
+++ b/examples/ip-intelligence/pom.xml
@@ -63,7 +63,7 @@
2.0.20
1.3.16
- 3.26.0
+ 3.28.0