From 6c36acd965cd5f25f5b74e494200faaebcf36fa7 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 10:14:19 +0000 Subject: [PATCH 1/5] Build and run the examples from a fresh clone on the runner's own JDK A workflow clones the repository with its submodules, builds each product's console examples against the packages on Maven Central as AGENTS.md says, with the JDK and Maven the runner has, and runs the getting started example from the repository root with no settings. It checks the example exits 0, downloads its free data file, and prints results. --- .github/workflows/examples-fresh-clone.yml | 89 ++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/examples-fresh-clone.yml diff --git a/.github/workflows/examples-fresh-clone.yml b/.github/workflows/examples-fresh-clone.yml new file mode 100644 index 00000000..fed21efb --- /dev/null +++ b/.github/workflows/examples-fresh-clone.yml @@ -0,0 +1,89 @@ +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, so the example downloads its file first. + +on: + pull_request: + paths: + - 'examples/**' + - 'cxx' + - '.github/workflows/examples-fresh-clone.yml' + push: + branches: + - main + - rc + schedule: + - cron: '30 4 * * *' + workflow_dispatch: + +permissions: + contents: read + +jobs: + fresh-clone: + name: ${{ matrix.product }} on ${{ matrix.os }} + 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' && '["ubuntu-latest", "windows-latest"]' || '["ubuntu-latest", "windows-latest", "macos-latest"]') }} + product: [device-detection, ip-intelligence] + include: + - product: device-detection + example: fiftyone.devicedetection.examples.console.GettingStartedOnPrem + - product: ip-intelligence + example: fiftyone.ipintelligence.examples.console.GettingStartedOnPrem + runs-on: ${{ matrix.os }} + 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: | + $jar = Get-ChildItem 'examples/${{ matrix.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 '${{ matrix.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 + } + if ($text -notmatch 'is missing, so it is being downloaded') { + Write-Output '::error::The example did not download its free data file.' + $failed = $true + } + if ($text -notmatch 'Results:') { + Write-Output '::error::The example printed no results.' + $failed = $true + } + if ($failed) { + exit 1 + } From 7964dcdadb176f42f90f59f728e482a04d6a763a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 10:14:21 +0000 Subject: [PATCH 2/5] Build the examples with maven-pmd-plugin 3.28.0, which reads Java 25 classes PMD 7.7.0, which maven-pmd-plugin 3.26.0 runs, cannot read the class files of Java 25, so building the examples on a JDK 25 runner failed in the resource check. maven-pmd-plugin 3.28.0 runs PMD 7.17.0. --- examples/device-detection/pom.xml | 2 +- examples/ip-intelligence/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/device-detection/pom.xml b/examples/device-detection/pom.xml index 8e9dd78b..05c6fe34 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 8d7ff14c..585aaa42 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 From 118b2d39556ab4e5124d53bcd2b8e9a82dec9319 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 10:43:10 +0000 Subject: [PATCH 3/5] Say in AGENTS.md what the fresh clone workflow tests The pitfall said CI never runs the console example jars, which the new workflow now does on the runner's own JDK against the published packages. --- AGENTS.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e27e80b3..208b7984 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,17 @@ 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 this section 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, so the free data file is downloaded. It runs on each push to + `main` and `rc`, daily, and on a pull request that changes the examples. ## Environment variable names From c2de14e666137202781716aa499fdae434d482bd Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 17:24:53 +0000 Subject: [PATCH 4/5] Name the section the fresh clone workflow follows in AGENTS.md The pitfall sits under Pitfalls, so 'this section' read as the pitfalls. It now links Run the examples. --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 208b7984..ee50cecf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -223,7 +223,8 @@ example. The IP Intelligence cloud examples read their key from 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 this section says, with the JDK and Maven each runner has. It + 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, so the free data file is downloaded. It runs on each push to From d8ffbf4043dfed230cfcffb38369b153a2d512e3 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sun, 15 Nov 2026 02:13:01 +0000 Subject: [PATCH 5/5] Download each free data file once a run in the fresh clone workflow Every job of the workflow let its example download the free data file, so a run fetched the 455 MB IP Intelligence Lite file two or three times. The Ubuntu job now tests the download and saves the file in the cache, keyed by the file's link, and the Windows and macOS jobs restore it and fail if their example downloads it again. The run step moves to ci/run-fresh-clone-example.ps1 so both jobs share it. --- .github/workflows/examples-fresh-clone.yml | 97 +++++++++++++++------- AGENTS.md | 6 +- ci/README.md | 8 ++ ci/run-fresh-clone-example.ps1 | 44 ++++++++++ 4 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 ci/run-fresh-clone-example.ps1 diff --git a/.github/workflows/examples-fresh-clone.yml b/.github/workflows/examples-fresh-clone.yml index fed21efb..e4eb3185 100644 --- a/.github/workflows/examples-fresh-clone.yml +++ b/.github/workflows/examples-fresh-clone.yml @@ -4,7 +4,9 @@ name: Examples from a fresh 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, so the example downloads its file first. +# 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: @@ -12,6 +14,7 @@ on: - 'examples/**' - 'cxx' - '.github/workflows/examples-fresh-clone.yml' + - 'ci/run-fresh-clone-example.ps1' push: branches: - main @@ -24,20 +27,69 @@ permissions: contents: read jobs: - fresh-clone: + 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' && '["ubuntu-latest", "windows-latest"]' || '["ubuntu-latest", "windows-latest", "macos-latest"]') }} + 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 @@ -46,6 +98,14 @@ jobs: 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: | @@ -59,31 +119,6 @@ jobs: - name: Run the getting started example from the repository root shell: pwsh - run: | - $jar = Get-ChildItem 'examples/${{ matrix.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 '${{ matrix.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 - } - if ($text -notmatch 'is missing, so it is being downloaded') { - Write-Output '::error::The example did not download its free data file.' - $failed = $true - } - if ($text -notmatch 'Results:') { - Write-Output '::error::The example printed no results.' - $failed = $true - } - if ($failed) { - exit 1 - } + run: >- + ./ci/run-fresh-clone-example.ps1 -Product '${{ matrix.product }}' + -Example '${{ matrix.example }}' -ExpectDownload $false diff --git a/AGENTS.md b/AGENTS.md index ee50cecf..6998a527 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -227,8 +227,10 @@ example. The IP Intelligence cloud examples read their key from 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, so the free data file is downloaded. It runs on each push to - `main` and `rc`, daily, and on a pull request that changes the examples. + 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 9b205e57..e14c6793 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 00000000..5c1e2102 --- /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 +}