From 024a11ceeb096ab52e55474ee82d437b3d61ba0d Mon Sep 17 00:00:00 2001 From: ben51degrees Date: Tue, 3 Nov 2026 18:43:22 +0000 Subject: [PATCH 1/8] Find the Java example in 51degrees-java (#16) --- Examples/ExampleApps.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ExampleApps.cs b/Examples/ExampleApps.cs index fe7b8df..6974eee 100644 --- a/Examples/ExampleApps.cs +++ b/Examples/ExampleApps.cs @@ -230,7 +230,7 @@ public static ExampleAppOptions BuildOptions( Lang: "java", WorkingDir: Path.Combine( RepoPaths.SiblingsRoot, - "device-detection-java-examples"), + "51degrees-java", "examples", "device-detection"), Command: "java", Args: new[] { "-jar" }, ReadinessPath: "/", From e8fed5c62e67ca72eb1ad3ec6f66d59b949bd051 Mon Sep 17 00:00:00 2001 From: Jamesr51d <122470673+Jamesr51d@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:43:34 +0100 Subject: [PATCH 2/8] Find the dotnet example in 51degrees-dotnet (#17) device-detection-dotnet-examples has moved into 51degrees-dotnet, under examples/device-detection. Both dotnet demos, pmp-web and GettingStarted-Web, are found there as siblings, and the docs name the monorepo. --- Browser51Did/Demo.cs | 2 +- Browser51Did/README.md | 4 ++-- Examples/ExampleApps.cs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Browser51Did/Demo.cs b/Browser51Did/Demo.cs index 82e5bec..311bbe0 100644 --- a/Browser51Did/Demo.cs +++ b/Browser51Did/Demo.cs @@ -11,7 +11,7 @@ namespace FiftyOne.Pipeline.Cloud.SeleniumTests.Browser51Did; /// /// The routes every language's demo serves, as the dotnet demo in -/// device-detection-dotnet-examples defines them. A test names the page it +/// 51degrees-dotnet defines them. A test names the page it /// loads by one of these and nothing else, so the same test drives any /// demo that serves the same pages. /// diff --git a/Browser51Did/README.md b/Browser51Did/README.md index 7245f65..e432aca 100644 --- a/Browser51Did/README.md +++ b/Browser51Did/README.md @@ -12,8 +12,8 @@ resource key cannot reach a test report, and three, in is read from. They drive a **demo**, a small web app that serves every page the tests -load. The dotnet demo is `Examples/Cloud/pmp-web` in -[device-detection-dotnet-examples](https://github.com/51Degrees/device-detection-dotnet-examples). +load. The dotnet demo is `examples/device-detection/Cloud/pmp-web` in +[51degrees-dotnet](https://github.com/51Degrees/51degrees-dotnet). The pages, the recorder that watches them, the change watcher and the stub consent platform are plain static files in that demo, so a demo in another language copies them, fills the same placeholders, and these same tests then diff --git a/Examples/ExampleApps.cs b/Examples/ExampleApps.cs index 6974eee..a9e6a1d 100644 --- a/Examples/ExampleApps.cs +++ b/Examples/ExampleApps.cs @@ -145,8 +145,8 @@ public static bool TryCreateDemo( Lang: "dotnet", WorkingDir: Path.Combine( RepoPaths.SiblingsRoot, - "device-detection-dotnet-examples", - "Examples", "Cloud", "pmp-web"), + "51degrees-dotnet", "examples", "device-detection", + "Cloud", "pmp-web"), Command: "dotnet", Args: new[] { "run", "-c", "Release", "--no-launch-profile" }, ReadinessPath: "/cloud/common", @@ -211,8 +211,8 @@ public static ExampleAppOptions BuildOptions( Lang: "dotnet", WorkingDir: Path.Combine( RepoPaths.SiblingsRoot, - "device-detection-dotnet-examples", - "Examples", "Cloud", "GettingStarted-Web"), + "51degrees-dotnet", "examples", "device-detection", + "Cloud", "GettingStarted-Web"), Command: "dotnet", Args: new[] { "run", "-c", "Release", "--no-launch-profile" }, ReadinessPath: "/", From 31dda77e7e6b39781904be30c3b1569e5fa120e6 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 3 Nov 2026 19:43:45 +0100 Subject: [PATCH 3/8] Find the Node example in 51degrees-node (#18) * Find the Node example in 51degrees-node * Copy the monorepo packages in when installing the Node example --- Examples/ExampleApps.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Examples/ExampleApps.cs b/Examples/ExampleApps.cs index a9e6a1d..c086e39 100644 --- a/Examples/ExampleApps.cs +++ b/Examples/ExampleApps.cs @@ -253,7 +253,7 @@ public static ExampleAppOptions BuildOptions( Lang: "node", WorkingDir: Path.Combine( RepoPaths.SiblingsRoot, - "device-detection-node", + "51degrees-node", "device-detection", "fiftyone.devicedetection.cloud", "examples", "cloud", "gettingstarted-web"), Command: "node", @@ -269,8 +269,13 @@ public static ExampleAppOptions BuildOptions( ["RESOURCE_KEY"] = o.ResourceKey, }, BuildCommand: "npm", - // package.json lives one level up from examples/cloud/gettingstarted-web - BuildArgs: new[] { "install", "--prefix", "../../.." }, + // package.json lives one level up from examples/cloud/gettingstarted-web. + // The pipeline packages are taken from the monorepo, so its + // pipeline/fiftyone.pipeline.core/javascript-templates submodule + // must be checked out before this runs. --install-links copies + // them in with their own dependencies; a symlink would resolve + // those from the monorepo, where nothing is installed. + BuildArgs: new[] { "install", "--install-links", "--prefix", "../../.." }, BuildTimeoutSeconds: 300, JsonEndpointPath: "/json"), ["python"] = new ExampleDescriptor( From 2eaaedf101f2acf652e4b2864482f846331a8525 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 3 Nov 2026 18:43:58 +0000 Subject: [PATCH 4/8] Check that only rc merges into the default branch (#19) --- .github/workflows/only-rc-into-main.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/only-rc-into-main.yml diff --git a/.github/workflows/only-rc-into-main.yml b/.github/workflows/only-rc-into-main.yml new file mode 100644 index 0000000..69e19ca --- /dev/null +++ b/.github/workflows/only-rc-into-main.yml @@ -0,0 +1,11 @@ +name: Only rc into main + +# A pull request into the default branch must come from rc. See +# 51Degrees/common-ci .github/workflows/only-rc-into-main.yml. +on: + pull_request: + +jobs: + rc: + name: Only rc into main + uses: 51Degrees/common-ci/.github/workflows/only-rc-into-main.yml@main From a2dd8f338b9f52891de65cbb6e184c48c52b49a6 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 3 Nov 2026 22:31:14 +0100 Subject: [PATCH 5/8] Find the Python example in 51degrees-python (#20) --- Examples/ExampleApps.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ExampleApps.cs b/Examples/ExampleApps.cs index c086e39..cfbd07e 100644 --- a/Examples/ExampleApps.cs +++ b/Examples/ExampleApps.cs @@ -282,12 +282,12 @@ public static ExampleAppOptions BuildOptions( Lang: "python", WorkingDir: Path.Combine( RepoPaths.SiblingsRoot, - "device-detection-python", + "51degrees-python", "device-detection", "fiftyone_devicedetection_examples"), // the venv's python (absolute path) Command: VenvPython(Path.Combine( RepoPaths.SiblingsRoot, - "device-detection-python", + "51degrees-python", "device-detection", "fiftyone_devicedetection_examples", ".venv")), Args: new[] { "-m", "fiftyone_devicedetection_examples.cloud.gettingstarted_web" }, From 7ca1fe3aa36ddd52c2832896f09c762815053d3b Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 3 Nov 2026 22:32:18 +0100 Subject: [PATCH 6/8] Find the PHP example in 51degrees-php (#21) --- Examples/ExampleApps.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ExampleApps.cs b/Examples/ExampleApps.cs index cfbd07e..4f6414c 100644 --- a/Examples/ExampleApps.cs +++ b/Examples/ExampleApps.cs @@ -316,7 +316,7 @@ public static ExampleAppOptions BuildOptions( Lang: "php", WorkingDir: Path.Combine( RepoPaths.SiblingsRoot, - "device-detection-php", "examples"), + "51degrees-php", "device-detection", "cloud", "examples"), Command: "bash", // clear the cached pipeline so the endpoint override takes effect Args: new[] From 345a4a6409d19e2f037ace6cfe4c9b2c38ca9f4d Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 10 Nov 2026 08:44:44 +0000 Subject: [PATCH 7/8] Java uses the default JSON endpoint and still accepts the old one (#23) * Let Java use the default JSON endpoint and still accept its old one * Say once which JSON path each language uses --- BrowserCache/SessionStorageCacheTests.cs | 9 ++++++--- Examples/ExampleApps.cs | 21 ++++++++++++++++++++- Examples/ExampleDescriptor.cs | 10 ++++++++-- TestsCommon/Helpers/TestHelpers.cs | 22 +++++++++++++--------- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/BrowserCache/SessionStorageCacheTests.cs b/BrowserCache/SessionStorageCacheTests.cs index 0aebb0b..c0ee3d9 100644 --- a/BrowserCache/SessionStorageCacheTests.cs +++ b/BrowserCache/SessionStorageCacheTests.cs @@ -168,7 +168,7 @@ private void RunTest(WebDriver driver, bool enableCookies) ClientServerUrl, _app.BaseUrl.ToString(), BuildPage(enableCookies), - ExampleApps.JsonEndpointPath, + ExampleApps.JsonEndpointPaths, clientServerTokenSource.Token); clientServer = serverListener.Listener; clientHttpServer = serverListener.Server; @@ -265,9 +265,12 @@ private static void AssertSameResults( private int CountJsonPosts() { - var expected = $"POST {ExampleApps.JsonEndpointPath}"; + var expected = ExampleApps.JsonEndpointPaths + .Select(p => $"POST {p}") + .ToList(); return clientHttpServer.RequestLog - .Count(r => string.Equals(r, expected, StringComparison.OrdinalIgnoreCase)); + .Count(r => expected.Any(e => string.Equals( + r, e, StringComparison.OrdinalIgnoreCase))); } private static List GetSessionStorageKeys(IJavaScriptExecutor js) diff --git a/Examples/ExampleApps.cs b/Examples/ExampleApps.cs index 4f6414c..998fcb4 100644 --- a/Examples/ExampleApps.cs +++ b/Examples/ExampleApps.cs @@ -32,6 +32,25 @@ public static class ExampleApps ? descriptor.JsonEndpointPath : "/51dpipeline/json"; + /// + /// Every path the selected language's client-side script may post to, + /// being and any older path the + /// language still answers. + /// + public static IReadOnlyList JsonEndpointPaths + { + get + { + var paths = new List { JsonEndpointPath }; + if (Descriptors.TryGetValue(SelectedLang, out var descriptor) + && descriptor.AlternativeJsonEndpointPaths != null) + { + paths.AddRange(descriptor.AlternativeJsonEndpointPaths); + } + return paths; + } + } + /// /// Element the selected language's page renders its client-side results /// into. Read from the descriptor so it is available for the CI case @@ -245,7 +264,7 @@ public static ExampleAppOptions BuildOptions( BuildArgs: new[] { "-pl", "web/getting-started.cloud", "-am", "package", "-DskipTests" }, RunArtifactGlob: "web/getting-started.cloud/target/*-jar-with-dependencies.jar", BuildTimeoutSeconds: 600, - JsonEndpointPath: "/51Degrees.core.json", + AlternativeJsonEndpointPaths: new[] { "/51Degrees.core.json" }, // the java page renders the client-side results into its // Apple detection section rather than the shared container ClientResultsElementId: "apple-detection"), diff --git a/Examples/ExampleDescriptor.cs b/Examples/ExampleDescriptor.cs index f876a1d..f2909c2 100644 --- a/Examples/ExampleDescriptor.cs +++ b/Examples/ExampleDescriptor.cs @@ -23,7 +23,12 @@ namespace FiftyOne.Pipeline.Cloud.SeleniumTests.Examples /// integration picks its own, so a test that proxies or counts those /// requests has to ask the descriptor rather than assume one: dotnet and /// rust use the default, java uses /51Degrees.core.json, and node, python - /// and php use /json. + /// and php use /json, and java uses the default and still answers the + /// older /51Degrees.core.json. + /// + /// + /// Older paths the integration still answers, so a test accepts a build + /// whose script uses either. /// /// /// Id of the element the example's page asks the shared examples helper to @@ -44,5 +49,6 @@ public sealed record ExampleDescriptor( string RunArtifactGlob = null, int BuildTimeoutSeconds = 0, string JsonEndpointPath = "/51dpipeline/json", - string ClientResultsElementId = "content"); + string ClientResultsElementId = "content", + IReadOnlyList AlternativeJsonEndpointPaths = null); } diff --git a/TestsCommon/Helpers/TestHelpers.cs b/TestsCommon/Helpers/TestHelpers.cs index a4ab1b7..70ee654 100644 --- a/TestsCommon/Helpers/TestHelpers.cs +++ b/TestsCommon/Helpers/TestHelpers.cs @@ -103,28 +103,32 @@ public static ServerListener ReverseProxyListener( /// Creates an HttpListener that serves /// for page paths and proxies the 51Degrees script and pipeline /// endpoints to without caching. - /// is the path the client-side - /// script posts refreshed evidence to, which differs per web - /// integration, so the caller supplies the one its example uses. + /// are the paths the client-side + /// script may post refreshed evidence to, which differ per web + /// integration, so the caller supplies the ones its example uses. /// public static ServerListener ExampleProxyListener( string clientUrl, string exampleUrl, string pageData, - string jsonEndpointPath, + IEnumerable jsonEndpointPaths, CancellationToken token) { var listener = new HttpListener(); listener.Prefixes.Add(clientUrl); listener.Start(); + var routes = new Dictionary + { + ["/51Degrees.core.js"] = exampleUrl, + }; + foreach (var path in jsonEndpointPaths) + { + routes[path] = exampleUrl; + } var server = new HttpServer(listener, pageData) { - ProxyRoutes = new Dictionary - { - ["/51Degrees.core.js"] = exampleUrl, - [jsonEndpointPath] = exampleUrl, - }, + ProxyRoutes = routes, ProxiedHeaderOverrides = new Dictionary { ["Cache-Control"] = "no-store", From c7ca30f4b72379c35792265f122d9256f177c38b Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Wed, 11 Nov 2026 14:44:36 +0000 Subject: [PATCH 8/8] Give the .NET example the _51DEGREES_ variable names alongside the old ones (#25) * Test that the .NET example is given the underscore variable names The .NET cloud web example reads the resource key from _51DEGREES_RESOURCE_KEY and the cloud endpoint from _51DEGREES_CLOUD_ENDPOINT, with no other name, once its rename is released. * Give the .NET example the _51DEGREES_ variable names The .NET cloud web example now reads the resource key from _51DEGREES_RESOURCE_KEY and the endpoint from _51DEGREES_CLOUD_ENDPOINT, with no other name. Released versions of it read SUPER_RESOURCE_KEY and FIFTYONE_CLOUD_ENDPOINT, so the harness sets both pairs until every language has released with the new names. --- Examples/ExampleApps.cs | 5 +++++ Examples/ExampleAppsTests.cs | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/Examples/ExampleApps.cs b/Examples/ExampleApps.cs index 998fcb4..e234629 100644 --- a/Examples/ExampleApps.cs +++ b/Examples/ExampleApps.cs @@ -238,8 +238,13 @@ public static ExampleAppOptions BuildOptions( StartupTimeoutSeconds: 180, BuildEnv: o => new Dictionary { + // The example reads the _51DEGREES_ names. Released + // versions of it read the two older names, so both + // are set until every language has released. + ["_51DEGREES_RESOURCE_KEY"] = o.ResourceKey, ["SUPER_RESOURCE_KEY"] = o.ResourceKey, // the endpoint must include the api/v4 path + ["_51DEGREES_CLOUD_ENDPOINT"] = new Uri(o.CloudEndpoint, "api/v4/").ToString(), ["FIFTYONE_CLOUD_ENDPOINT"] = new Uri(o.CloudEndpoint, "api/v4/").ToString(), ["ASPNETCORE_URLS"] = $"http://localhost:{o.Port}", // run on a newer .NET runtime if the exact one isn't installed diff --git a/Examples/ExampleAppsTests.cs b/Examples/ExampleAppsTests.cs index 3f271e6..0e5ce16 100644 --- a/Examples/ExampleAppsTests.cs +++ b/Examples/ExampleAppsTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -74,5 +76,23 @@ public void Descriptors_Rust_BuildsAndRunsWithCargo() Assert.IsTrue(rust.Args.Contains("dd-web-getting-started-cloud"), "run args should target the cloud getting-started web binary"); } + + [TestMethod] + public void Descriptors_Dotnet_SetsTheUnderscoreNames() + { + var dotnet = ExampleApps.Descriptors["dotnet"]; + var env = dotnet.BuildEnv(new ExampleAppOptions( + Port: 5123, + CloudEndpoint: new Uri("https://cloud.example.test/"), + ResourceKey: "key", + ExtraEnv: new Dictionary())); + Assert.AreEqual("key", env["_51DEGREES_RESOURCE_KEY"], + "the example reads the resource key from this name"); + Assert.AreEqual( + "https://cloud.example.test/api/v4/", + env["_51DEGREES_CLOUD_ENDPOINT"], + "the example reads the endpoint, with its api/v4 path, " + + "from this name"); + } } }