Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/monthly-copyright-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Monthly Copyright Update

on:
workflow_dispatch:
schedule:
# * is a special character in YAML so quote this string
- cron: '0 0 1 * *'

jobs:
# Run the common workflow that rewrites copyright headers and opens a PR.
# The resulting PR is validated and merged by the Pull Requests pipeline.
Monthly_Copyright_Update:
uses: 51Degrees/common-ci/.github/workflows/monthly-copyright-update.yml@main
with:
repo-name: ${{ github.event.repository.name }}
org-name: ${{ github.event.repository.owner.login }}
secrets:
token: ${{ secrets.ACCESS_TOKEN }}
48 changes: 17 additions & 31 deletions .github/workflows/template-tests.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,23 @@
name: Template tests

# The template is not executable on its own, so these checks render it with a
# model matching the one the .NET builder passes and then drive the rendered
# script against a fake endpoint. They take seconds and they reach what the
# consumer run cannot, being the environment with no window that the cloud's
# NiL.JS builder evaluates the script in, and the failure paths a browser test
# cannot force.
name: Pull Requests

on:
pull_request:
workflow_dispatch:

permissions:
contents: read
inputs:
dryrun:
type: boolean
default: false

jobs:
template-tests:
name: Render and drive the template
runs-on: ubuntu-latest
steps:
- name: Check out the template under review
uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
cache-dependency-path: tests/package-lock.json

- name: Install the renderer
working-directory: tests
run: npm ci

- name: Render and drive the template
working-directory: tests
run: npm test
PullRequests:
name: Pull Requests
uses: 51Degrees/common-ci/.github/workflows/nightly-pull-requests.yml@main
with:
repo-name: ${{ github.event.repository.name }}
org-name: ${{ github.event.repository.owner.login }}
dryrun: ${{ inputs.dryrun || false }}
cache-assets: true
secrets:
token: ${{ secrets.ACCESS_TOKEN }}
DeviceDetection: ${{ secrets.DEVICE_DETECTION_KEY }}
DeviceDetectionUrl: ${{ secrets.DEVICE_DETECTION_URL }}
25 changes: 25 additions & 0 deletions JavaScriptResource.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,13 @@ fiftyoneDegreesManager = function() {
if (callbackCounter === 0) {
{{#_updateEnabled}}
processRequest(resolve, reject);
{{/_updateEnabled}}
{{^_updateEnabled}}
failed = false;
completed = true;
fireChangeFuncs(json);
resolve(json);
roundEnded();
{{/_updateEnabled}}
} else if (callbackCounter < 0){
reject('Too many callbacks.');
Expand Down Expand Up @@ -797,9 +804,11 @@ fiftyoneDegreesManager = function() {
if (jsProperties !== undefined && jsProperties.length > 0) {

let valueSetPrefix = new RegExp('document\\.cookie\\s*=\\s*(("([A-Za-z0-9_"\\s\\+]+)\\s*=\\s*"\\s*\\+\\s*([^\\s};]+))|(`([A-Za-z0-9_]+)\\s*=\\s*\\$\\{([^}]+)\\}`))', 'g');
let valueGetPrefix = new RegExp('=\\s*document\\.cookie', 'g');
let session51DataPrefix = sessionKey + "_data_";
{{^_enableCookies}}
let sessionSetPatch = 'window.sessionStorage["' + session51DataPrefix + '$3$6"]=$4$7';
let sessionGetPatch = '= (()=>Array.from({length:sessionStorage.length},(_,i)=>sessionStorage.key(i)).filter(k=>k&&k.startsWith("' + session51DataPrefix + '")).map(k=>encodeURIComponent(k)+"="+encodeURIComponent(sessionStorage.getItem(k))).join("; "))()';
{{/_enableCookies}}

// Store an empty result for each value a snippet sets that has
Expand Down Expand Up @@ -883,7 +892,23 @@ fiftyoneDegreesManager = function() {
storeEmptyValues(body);

{{^_enableCookies}}
{{#_diagnoseUnconvertedCookies}}
var rawBody = body
{{/_diagnoseUnconvertedCookies}}
body = body.replaceAll(valueSetPrefix, sessionSetPatch);
body = body.replaceAll(valueGetPrefix, sessionGetPatch);
{{#_diagnoseUnconvertedCookies}}
var cookiePos = body.indexOf('document.cookie');
if (cookiePos !== -1) {
console.log("'document.cookie' found at: " + cookiePos)
catchError(new Error('Snippet ' + name + ' contains unconverted document.cookie after session storage patch'));
} else {
console.log("No 'document.cookie' found.")
}
console.log(body)
console.log("--- RAW BODY ---")
console.log(rawBody)
{{/_diagnoseUnconvertedCookies}}
{{/_enableCookies}}

if (body.indexOf(searchString) !== -1){
Expand Down
30 changes: 30 additions & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# CI scripts

These scripts implement the hook contract that the shared
[`common-ci`](https://github.com/51Degrees/common-ci) reusable workflows expect.
`nightly-pull-request.build-and-test.ps1` in `common-ci` clones this repository
and invokes the hooks below, in order, with the matrix entry from
[`options.json`](options.json) splatted in as parameters:

| Hook | Purpose |
| --- | --- |
| `fetch-assets.ps1` | Downloads the Hash data file the snippet tool generates from. |
| `setup-environment.ps1` | Installs the native toolchain (CMake, g++) used to build the snippet tool. |
| `build-project.ps1` | Installs Node deps and builds the C# snippet test project. |
| `run-unit-tests.ps1` | Renders `JavaScriptResource.mustache` and drives it with `node template-tests.js`. |
| `run-integration-tests.ps1` | Clones `device-detection-cxx`, builds `js-snippet-export`, generates `snippets/`, and drives every snippet through the template in a real browser. |

## The snippet pipeline

The template itself is not executable, so coverage comes in two layers:

* **Unit** (`run-unit-tests.ps1`) - fast, window-less checks of the rendered
script against a fake endpoint. No data file or browser required.
* **Integration** (`run-integration-tests.ps1`) - the producer/consumer chain.
`device-detection-cxx`'s `js-snippet-export` tool is the *producer* (the
equivalent of the examples repo that `device-detection-dotnet` clones for its
integration coverage); the C# `FiftyOne.JavascriptTemplateTests` project is
the *consumer* that renders and browser-checks each generated snippet.

`snippets/` is a build artifact and is not committed - it is populated afresh by
the integration step on every run.
53 changes: 53 additions & 0 deletions ci/build-project.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# *********************************************************************
# This Original Work is copyright of 51 Degrees Mobile Experts Limited.
# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
#
# This Original Work is licensed under the European Union Public Licence
# (EUPL) v.1.2 and is subject to its terms as set out below.
#
# If a copy of the EUPL was not distributed with this file, You can obtain
# one at https://opensource.org/licenses/EUPL-1.2.
#
# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
# amended by the European Commission) shall be deemed incompatible for
# the purposes of the Work and the provisions of the compatibility
# clause in Article 5 of the EUPL shall not apply.
#
# If using the Work as, or as part of, a network application, by
# including the attribution notice(s) required under Article 5 of the EUPL
# in the end user terms of the application under an appropriate heading,
# such notice(s) shall fulfill the requirements of that article.
# *********************************************************************

param(
[Parameter(Mandatory)][string]$RepoName,
[string]$ProjectDir = ".",
[string]$Name = "Release_x64",
[string]$Configuration = "Release",
[string]$Arch = "x64",
[string]$BuildMethod = "dotnet"
)
$ErrorActionPreference = "Stop"

$RepoPath = [IO.Path]::Combine($pwd, $RepoName)

# The Node checks (unit tests) only need their dev dependency installed.
Write-Host "Installing Node dependencies for the template renderer"
Push-Location ([IO.Path]::Combine($RepoPath, "tests"))
try {
npm ci
} finally {
Pop-Location
}

# Build the C# snippet test project. The snippets it drives are generated later
# by the integration step, so only compilation is required here.
$TestProject = [IO.Path]::Combine(
$RepoPath, "tests", "FiftyOne.JavascriptTemplateTests",
"FiftyOne.JavascriptTemplateTests.csproj")

Write-Host "Building $TestProject"
dotnet build $TestProject -c $Configuration

exit $LASTEXITCODE
34 changes: 34 additions & 0 deletions ci/fetch-assets.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# *********************************************************************
# This Original Work is copyright of 51 Degrees Mobile Experts Limited.
# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
#
# This Original Work is licensed under the European Union Public Licence
# (EUPL) v.1.2 and is subject to its terms as set out below.
#
# If a copy of the EUPL was not distributed with this file, You can obtain
# one at https://opensource.org/licenses/EUPL-1.2.
#
# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
# amended by the European Commission) shall be deemed incompatible for
# the purposes of the Work and the provisions of the compatibility
# clause in Article 5 of the EUPL shall not apply.
#
# If using the Work as, or as part of, a network application, by
# including the attribution notice(s) required under Article 5 of the EUPL
# in the end user terms of the application under an appropriate heading,
# such notice(s) shall fulfill the requirements of that article.
# *********************************************************************

param (
[string]$DeviceDetection,
[string]$DeviceDetectionUrl
)
$ErrorActionPreference = "Stop"

# The snippet integration test drives every JavaScript property snippet through
# the template. The full set of snippets only exists in the enterprise TAC data
# file - the Lite file carries only a handful - so TAC is required for complete
# coverage.
./steps/fetch-assets.ps1 -DeviceDetection:$DeviceDetection -DeviceDetectionUrl:$DeviceDetectionUrl `
-Assets "TAC-HashV41.hash"
20 changes: 20 additions & 0 deletions ci/options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"Image": "ubuntu-latest",
"Name": "Ubuntu_x64_Release",
"Configuration": "Release",
"Arch": "x64",
"BuildMethod": "dotnet",
"Language": "dotnet",
"LanguageVersion": "8.0.x"
},
{
"Image": "windows-latest",
"Name": "Windows_x64_Release",
"Configuration": "Release",
"Arch": "x64",
"BuildMethod": "dotnet",
"Language": "dotnet",
"LanguageVersion": "8.0.x"
}
]
119 changes: 119 additions & 0 deletions ci/run-integration-tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# *********************************************************************
# This Original Work is copyright of 51 Degrees Mobile Experts Limited.
# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
#
# This Original Work is licensed under the European Union Public Licence
# (EUPL) v.1.2 and is subject to its terms as set out below.
#
# If a copy of the EUPL was not distributed with this file, You can obtain
# one at https://opensource.org/licenses/EUPL-1.2.
#
# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
# amended by the European Commission) shall be deemed incompatible for
# the purposes of the Work and the provisions of the compatibility
# clause in Article 5 of the EUPL shall not apply.
#
# If using the Work as, or as part of, a network application, by
# including the attribution notice(s) required under Article 5 of the EUPL
# in the end user terms of the application under an appropriate heading,
# such notice(s) shall fulfill the requirements of that article.
# *********************************************************************

param(
[Parameter(Mandatory)][string]$RepoName,
[Parameter(Mandatory)][string]$OrgName,
[string]$Name = "Release_x64",
[string]$Configuration = "Release",
[string]$Arch = "x64",
[string]$BuildMethod = "dotnet",
# The device-detection-cxx repo owns the js-snippet-export tool that
# produces the snippets under test - the equivalent of the examples repo
# that device-detection-dotnet clones for its integration coverage.
[string]$SnippetToolRepo = "device-detection-cxx",
[string]$SnippetToolBranch = "feature/js-snippet-export"
)
$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $true
Set-StrictMode -Version 1.0

$RepoPath = [IO.Path]::Combine($pwd, $RepoName)

# The snippet sweep needs the enterprise TAC data file (it carries the full set
# of JavaScript property snippets - Lite has only a handful) and Chrome to run
# the rendered scripts. On runners where the data file is unavailable (e.g. an
# automation PR without the data-file secret), skip rather than fail the merge
# gate - this mirrors the guarded pattern in device-detection-dotnet.
$dataFile = Resolve-Path -ErrorAction SilentlyContinue `
([IO.Path]::Combine($pwd, "assets", "TAC-HashV41.hash"))
if (-not $dataFile) {
Write-Host "::warning::No TAC Hash data file found under assets/ - skipping snippet integration tests."
exit 0
}

# ---------------------------------------------------------------------------
# 1. Obtain the js-snippet-export tool source (device-detection-cxx).
# common-ci provides steps/clone-repo.ps1 which honours the org and token.
# ---------------------------------------------------------------------------
if (-not (Test-Path $SnippetToolRepo)) {
./steps/clone-repo.ps1 -RepoName $SnippetToolRepo -OrgName $OrgName -Branch $SnippetToolBranch
}

# common-cxx and device-detection-data are submodules of device-detection-cxx;
# the CMake configure includes them, so they must be initialised.
git -C $SnippetToolRepo submodule update --init --recursive

# ---------------------------------------------------------------------------
# 2. Build ONLY the js-snippet-export target. It sits outside
# if(BUILD_TESTING) in the root CMakeLists, so tests are not needed, but it
# links fiftyone-hash-cxx so the engine is built transitively.
# ---------------------------------------------------------------------------
$buildDir = [IO.Path]::Combine($pwd, $SnippetToolRepo, "build")
cmake -S $SnippetToolRepo -B $buildDir -DCMAKE_BUILD_TYPE=$Configuration -DBUILD_TESTING=OFF
cmake --build $buildDir --config $Configuration --target js-snippet-export

# The tool pins RUNTIME_OUTPUT_DIRECTORY to <build>/bin. Multi-config
# generators (Visual Studio) nest it under the configuration.
$exeName = if ($IsWindows) { "js-snippet-export.exe" } else { "js-snippet-export" }
$cli = @(
[IO.Path]::Combine($buildDir, "bin", $exeName),
[IO.Path]::Combine($buildDir, "bin", $Configuration, $exeName)
) | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $cli) {
throw "js-snippet-export was not found under '$buildDir/bin'."
}
Write-Host "Using snippet export tool: $cli"

# ---------------------------------------------------------------------------
# 3. Generate the snippets into the repo's ignored snippets/ directory.
# ---------------------------------------------------------------------------
$snippetsDir = [IO.Path]::Combine($RepoPath, "snippets")
New-Item -ItemType Directory -Path $snippetsDir -Force | Out-Null
& $cli -d $dataFile.Path -o $snippetsDir

$generated = @(Get-ChildItem -Path $snippetsDir -Filter "*.js" -ErrorAction SilentlyContinue)
Write-Host "Generated $($generated.Count) snippet(s)."
if ($generated.Count -eq 0) {
throw "The snippet export tool produced no .js files."
}

# ---------------------------------------------------------------------------
# 4. Drive every generated snippet through the template in a real browser.
# ---------------------------------------------------------------------------
$TestProject = [IO.Path]::Combine(
$RepoPath, "tests", "FiftyOne.JavascriptTemplateTests",
"FiftyOne.JavascriptTemplateTests.csproj")

# The EnricoMi publish step in common-ci globs test-results/integration/**/*.trx
# under the repo root. dotnet test defaults the TRX to the project's own
# TestResults/ dir, which that glob never matches (results silently unreported),
# so pin --results-directory to the location the reporter searches.
$ResultsDir = [IO.Path]::Combine($RepoPath, "test-results", "integration")
New-Item -ItemType Directory -Path $ResultsDir -Force | Out-Null

dotnet test $TestProject -c $Configuration `
--results-directory $ResultsDir `
--logger "console;verbosity=normal" `
--logger "trx;LogFileName=snippet-integration.trx"

exit $LASTEXITCODE
Loading
Loading