From 9ebd349f38ccec8954e5dab7de4f1f8a1bec6d99 Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 8 Apr 2026 12:13:38 -0700 Subject: [PATCH 1/6] Add dependency version validation pipeline Add a GitHub Actions workflow that validates dependency version bumps in Directory.Packages.props do not break the DTFx NuGet packages (DurableTask.Core and DurableTask.Emulator) at build or runtime. The pipeline: - Packs DurableTask.Core and DurableTask.Emulator from source - Builds a console app using those packages from a local NuGet feed - Runs a HelloCities orchestration using the in-memory Emulator backend - Validates orchestration output and loaded assembly versions DTFx Core is consumed downstream by azure-functions-durable-extension (the WebJobs extension for Durable Functions) and by AAPT-DTMB (DTS data plane). This pipeline catches dep-related regressions before they propagate to those consumers. Mirrors the SDK-PR-Validation pipeline pattern used in AAPT-DTMB. --- .github/workflows/dep-version-validation.yml | 168 ++++++++++++++++++ Test/SmokeTests/EmulatorSmokeTest/.gitignore | 1 + .../EmulatorSmokeTest.csproj | 23 +++ .../SmokeTests/EmulatorSmokeTest/NuGet.config | 22 +++ Test/SmokeTests/EmulatorSmokeTest/Program.cs | 110 ++++++++++++ 5 files changed, 324 insertions(+) create mode 100644 .github/workflows/dep-version-validation.yml create mode 100644 Test/SmokeTests/EmulatorSmokeTest/.gitignore create mode 100644 Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj create mode 100644 Test/SmokeTests/EmulatorSmokeTest/NuGet.config create mode 100644 Test/SmokeTests/EmulatorSmokeTest/Program.cs diff --git a/.github/workflows/dep-version-validation.yml b/.github/workflows/dep-version-validation.yml new file mode 100644 index 000000000..275ef7bcf --- /dev/null +++ b/.github/workflows/dep-version-validation.yml @@ -0,0 +1,168 @@ +# ================================================================================================ +# Dependency Version Validation Pipeline +# ================================================================================================ +# Validates that changes to dependency versions (Directory.Packages.props) do not break +# the DurableTask Framework (DTFx) NuGet packages at build or runtime. +# +# The DTFx Core and Emulator packages are consumed downstream by: +# - azure-functions-durable-extension (WebJobs extension) +# - AAPT-DTMB (DTS data plane) +# +# This pipeline: +# 1. Packs DurableTask.Core and DurableTask.Emulator from source into a local feed +# 2. Builds a standalone console app that runs a HelloCities orchestration +# using the Emulator backend +# 3. Validates orchestration output and loaded assembly versions +# ================================================================================================ + +name: Dependency Version Validation + +on: + push: + branches: + - main + paths: + - 'Directory.Packages.props' + - 'tools/DurableTask.props' + - 'Test/SmokeTests/**' + pull_request: + paths: + - 'Directory.Packages.props' + - 'tools/DurableTask.props' + - 'Test/SmokeTests/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + dep-validation-smoke-test: + name: 'Emulator Orchestration Smoke Test (NuGet Packages)' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + # ---- Checkout & SDK Setup ---- + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET 8.0 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + # ---- Parse DTFx Version ---- + - name: Parse DTFx Core version + id: version + run: | + set -e + CSPROJ_FILE="src/DurableTask.Core/DurableTask.Core.csproj" + MAJOR=$(grep -oP '\K[^<]+' "$CSPROJ_FILE") + MINOR=$(grep -oP '\K[^<]+' "$CSPROJ_FILE") + PATCH=$(grep -oP '\K[^<]+' "$CSPROJ_FILE") + + # Bump patch version to distinguish local build from published + LOCAL_PATCH=$((PATCH + 1)) + LOCAL_VERSION="${MAJOR}.${MINOR}.${LOCAL_PATCH}" + + # Update version in Core csproj + sed -i "s|${PATCH}|${LOCAL_PATCH}|" "$CSPROJ_FILE" + + echo "Published version: ${MAJOR}.${MINOR}.${PATCH}" + echo "Local version: ${LOCAL_VERSION}" + echo "dtfx_version=${LOCAL_VERSION}" >> "$GITHUB_OUTPUT" + + # ---- Pack DTFx Packages from Source ---- + - name: Pack DTFx packages from source + run: | + set -e + LOCAL_PACKAGES="Test/SmokeTests/EmulatorSmokeTest/local-packages" + mkdir -p "$LOCAL_PACKAGES" + + # Build Core first (Emulator depends on it) + echo "Building DurableTask.Core..." + dotnet build src/DurableTask.Core/DurableTask.Core.csproj -c Release + + echo "Building DurableTask.Emulator..." + dotnet build src/DurableTask.Emulator/DurableTask.Emulator.csproj -c Release + + # Pack from build output + echo "Packing DurableTask.Core..." + dotnet pack src/DurableTask.Core/DurableTask.Core.csproj -c Release --no-build --output "$LOCAL_PACKAGES" + + echo "Packing DurableTask.Emulator..." + dotnet pack src/DurableTask.Emulator/DurableTask.Emulator.csproj -c Release --no-build --output "$LOCAL_PACKAGES" + + echo "Local packages:" + ls -la "$LOCAL_PACKAGES" + + # ---- Verify Packed Packages ---- + - name: Verify packed packages + env: + DTFX_VERSION: ${{ steps.version.outputs.dtfx_version }} + run: | + set -e + LOCAL_PACKAGES="Test/SmokeTests/EmulatorSmokeTest/local-packages" + + CORE_PKG="Microsoft.Azure.DurableTask.Core.${DTFX_VERSION}.nupkg" + if [ ! -f "$LOCAL_PACKAGES/$CORE_PKG" ]; then + echo "FAIL: Missing Core package: $CORE_PKG" + echo "Available packages:" + ls -1 "$LOCAL_PACKAGES" + exit 1 + fi + echo " OK: $CORE_PKG" + + # Emulator may use a different version scheme - check that at least one Emulator package exists + EMULATOR_FOUND=0 + for f in "$LOCAL_PACKAGES"/Microsoft.Azure.DurableTask.Emulator.*.nupkg; do + if [ -f "$f" ]; then + echo " OK: $(basename $f)" + EMULATOR_FOUND=1 + fi + done + if [ $EMULATOR_FOUND -eq 0 ]; then + echo "FAIL: No Emulator package found" + ls -1 "$LOCAL_PACKAGES" + exit 1 + fi + echo "PASS: DTFx packages verified." + + # ---- Build Smoke Test App ---- + - name: Build smoke test app + env: + DTFX_VERSION: ${{ steps.version.outputs.dtfx_version }} + run: | + set -e + cd Test/SmokeTests/EmulatorSmokeTest + dotnet build EmulatorSmokeTest.csproj -c Release -p:SmokeTestDtfxVersion=$DTFX_VERSION -v normal + + # ---- Verify DTFx packages resolved from local-packages ---- + - name: Verify DTFx packages from local source + run: | + set -e + echo "Verifying DTFx packages were restored from local-packages..." + ASSETS_FILE="Test/SmokeTests/EmulatorSmokeTest/obj/project.assets.json" + for pkg in "Microsoft.Azure.DurableTask.Core" "Microsoft.Azure.DurableTask.Emulator"; do + if grep -qi "$pkg" "$ASSETS_FILE"; then + echo " OK: $pkg found in project.assets.json" + else + echo " FAIL: $pkg NOT found" + exit 1 + fi + done + echo "PASS: DTFx packages verified in build output." + + # ---- Run Smoke Test ---- + - name: Run smoke test + env: + DTFX_VERSION: ${{ steps.version.outputs.dtfx_version }} + run: | + set -e + echo "Running Emulator Smoke Test..." + dotnet run --project Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj \ + -c Release \ + --no-build \ + -p:SmokeTestDtfxVersion=$DTFX_VERSION + + echo "Smoke test completed successfully." diff --git a/Test/SmokeTests/EmulatorSmokeTest/.gitignore b/Test/SmokeTests/EmulatorSmokeTest/.gitignore new file mode 100644 index 000000000..67d91e9d9 --- /dev/null +++ b/Test/SmokeTests/EmulatorSmokeTest/.gitignore @@ -0,0 +1 @@ +local-packages/ diff --git a/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj new file mode 100644 index 000000000..e5b0ec93e --- /dev/null +++ b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + false + false + false + + + + + + + + + + diff --git a/Test/SmokeTests/EmulatorSmokeTest/NuGet.config b/Test/SmokeTests/EmulatorSmokeTest/NuGet.config new file mode 100644 index 000000000..7365229f2 --- /dev/null +++ b/Test/SmokeTests/EmulatorSmokeTest/NuGet.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Test/SmokeTests/EmulatorSmokeTest/Program.cs b/Test/SmokeTests/EmulatorSmokeTest/Program.cs new file mode 100644 index 000000000..38f2b15bb --- /dev/null +++ b/Test/SmokeTests/EmulatorSmokeTest/Program.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DurableTask.Core; +using DurableTask.Emulator; +using Microsoft.Extensions.Logging; + +Console.WriteLine("=== DurableTask Emulator Smoke Test ==="); +Console.WriteLine(); + +using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Warning); +}); + +// Print loaded DTFx assembly versions +Console.WriteLine("Loaded DTFx assembly versions:"); +foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) +{ + string? name = asm.GetName().Name; + if (name != null && name.StartsWith("DurableTask.", StringComparison.OrdinalIgnoreCase)) + { + string? infoVersion = asm + .GetCustomAttribute() + ?.InformationalVersion; + if (infoVersion != null && infoVersion.Contains('+')) + { + infoVersion = infoVersion[..infoVersion.IndexOf('+')]; + } + Console.WriteLine($" {name} = {infoVersion ?? asm.GetName().Version?.ToString() ?? "unknown"}"); + } +} +Console.WriteLine(); + +// Create the in-memory orchestration service +var orchestrationService = new LocalOrchestrationService(); + +// Start the worker with our orchestration and activities +var worker = new TaskHubWorker(orchestrationService, loggerFactory); +await worker + .AddTaskOrchestrations(typeof(HelloCitiesOrchestration)) + .AddTaskActivities(typeof(SayHelloActivity)) + .StartAsync(); + +// Create a client and start the orchestration +var client = new TaskHubClient(orchestrationService, loggerFactory: loggerFactory); +OrchestrationInstance instance = await client.CreateOrchestrationInstanceAsync( + typeof(HelloCitiesOrchestration), input: null); + +Console.WriteLine($"Started orchestration: {instance.InstanceId}"); + +// Wait for completion +OrchestrationState result = await client.WaitForOrchestrationAsync( + instance, TimeSpan.FromSeconds(30), CancellationToken.None); + +Console.WriteLine($"Orchestration status: {result.OrchestrationStatus}"); +Console.WriteLine($"Orchestration output: {result.Output}"); + +await worker.StopAsync(true); + +// Validate result +if (result.OrchestrationStatus != OrchestrationStatus.Completed) +{ + Console.WriteLine("FAIL: Orchestration did not complete successfully."); + Environment.Exit(1); +} + +if (result.Output == null || + !result.Output.Contains("Hello, Tokyo!") || + !result.Output.Contains("Hello, London!") || + !result.Output.Contains("Hello, Seattle!")) +{ + Console.WriteLine("FAIL: Orchestration output did not contain expected greetings."); + Environment.Exit(1); +} + +Console.WriteLine(); +Console.WriteLine("PASS: HelloCities orchestration completed with expected output."); +Environment.Exit(0); + +// ---- Orchestration ---- + +/// +/// A simple function chaining orchestration that calls SayHello for three cities. +/// +public class HelloCitiesOrchestration : TaskOrchestration +{ + public override async Task RunTask(OrchestrationContext context, object input) + { + string result = ""; + result += await context.ScheduleTask(typeof(SayHelloActivity), "Tokyo") + " "; + result += await context.ScheduleTask(typeof(SayHelloActivity), "London") + " "; + result += await context.ScheduleTask(typeof(SayHelloActivity), "Seattle"); + return result; + } +} + +// ---- Activity ---- + +/// +/// A simple activity that returns a greeting for the given city name. +/// +public class SayHelloActivity : TaskActivity +{ + protected override string Execute(TaskContext context, string cityName) + { + return $"Hello, {cityName}!"; + } +} From 5c066ba85c4c130733428476ba8093510900b98b Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 8 Apr 2026 15:07:29 -0700 Subject: [PATCH 2/6] Fix dep validation: handle separate Core/Emulator versions - Core uses version 3.7.x, Emulator inherits 2.6.0 from DurableTask.props - Use separate SmokeTestCoreVersion and SmokeTestEmulatorVersion properties - Add missing 'using System.Reflection;' import - Replace Environment.Exit() with return for proper cleanup - Verify exact Emulator package version instead of wildcard --- .github/workflows/dep-version-validation.yml | 66 ++++++++++--------- .../EmulatorSmokeTest.csproj | 7 +- Test/SmokeTests/EmulatorSmokeTest/Program.cs | 7 +- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/.github/workflows/dep-version-validation.yml b/.github/workflows/dep-version-validation.yml index 275ef7bcf..fef266183 100644 --- a/.github/workflows/dep-version-validation.yml +++ b/.github/workflows/dep-version-validation.yml @@ -51,26 +51,26 @@ jobs: with: dotnet-version: '8.0.x' - # ---- Parse DTFx Version ---- - - name: Parse DTFx Core version + # ---- Parse DTFx Versions ---- + - name: Parse DTFx package versions id: version run: | set -e - CSPROJ_FILE="src/DurableTask.Core/DurableTask.Core.csproj" - MAJOR=$(grep -oP '\K[^<]+' "$CSPROJ_FILE") - MINOR=$(grep -oP '\K[^<]+' "$CSPROJ_FILE") - PATCH=$(grep -oP '\K[^<]+' "$CSPROJ_FILE") - # Bump patch version to distinguish local build from published - LOCAL_PATCH=$((PATCH + 1)) - LOCAL_VERSION="${MAJOR}.${MINOR}.${LOCAL_PATCH}" - - # Update version in Core csproj - sed -i "s|${PATCH}|${LOCAL_PATCH}|" "$CSPROJ_FILE" - - echo "Published version: ${MAJOR}.${MINOR}.${PATCH}" - echo "Local version: ${LOCAL_VERSION}" - echo "dtfx_version=${LOCAL_VERSION}" >> "$GITHUB_OUTPUT" + # Parse Core version from its csproj + CORE_CSPROJ="src/DurableTask.Core/DurableTask.Core.csproj" + MAJOR=$(grep -oP '\K[^<]+' "$CORE_CSPROJ") + MINOR=$(grep -oP '\K[^<]+' "$CORE_CSPROJ") + PATCH=$(grep -oP '\K[^<]+' "$CORE_CSPROJ") + CORE_VERSION="${MAJOR}.${MINOR}.${PATCH}" + echo "Core version: ${CORE_VERSION}" + echo "core_version=${CORE_VERSION}" >> "$GITHUB_OUTPUT" + + # Emulator inherits its version from tools/DurableTask.props + PROPS_FILE="tools/DurableTask.props" + EMULATOR_VERSION=$(grep -oP '\K[^<]+' "$PROPS_FILE") + echo "Emulator version: ${EMULATOR_VERSION}" + echo "emulator_version=${EMULATOR_VERSION}" >> "$GITHUB_OUTPUT" # ---- Pack DTFx Packages from Source ---- - name: Pack DTFx packages from source @@ -99,12 +99,13 @@ jobs: # ---- Verify Packed Packages ---- - name: Verify packed packages env: - DTFX_VERSION: ${{ steps.version.outputs.dtfx_version }} + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} run: | set -e LOCAL_PACKAGES="Test/SmokeTests/EmulatorSmokeTest/local-packages" - CORE_PKG="Microsoft.Azure.DurableTask.Core.${DTFX_VERSION}.nupkg" + CORE_PKG="Microsoft.Azure.DurableTask.Core.${CORE_VERSION}.nupkg" if [ ! -f "$LOCAL_PACKAGES/$CORE_PKG" ]; then echo "FAIL: Missing Core package: $CORE_PKG" echo "Available packages:" @@ -113,29 +114,28 @@ jobs: fi echo " OK: $CORE_PKG" - # Emulator may use a different version scheme - check that at least one Emulator package exists - EMULATOR_FOUND=0 - for f in "$LOCAL_PACKAGES"/Microsoft.Azure.DurableTask.Emulator.*.nupkg; do - if [ -f "$f" ]; then - echo " OK: $(basename $f)" - EMULATOR_FOUND=1 - fi - done - if [ $EMULATOR_FOUND -eq 0 ]; then - echo "FAIL: No Emulator package found" + EMULATOR_PKG="Microsoft.Azure.DurableTask.Emulator.${EMULATOR_VERSION}.nupkg" + if [ ! -f "$LOCAL_PACKAGES/$EMULATOR_PKG" ]; then + echo "FAIL: Missing Emulator package: $EMULATOR_PKG" + echo "Available packages:" ls -1 "$LOCAL_PACKAGES" exit 1 fi + echo " OK: $EMULATOR_PKG" echo "PASS: DTFx packages verified." # ---- Build Smoke Test App ---- - name: Build smoke test app env: - DTFX_VERSION: ${{ steps.version.outputs.dtfx_version }} + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} run: | set -e cd Test/SmokeTests/EmulatorSmokeTest - dotnet build EmulatorSmokeTest.csproj -c Release -p:SmokeTestDtfxVersion=$DTFX_VERSION -v normal + dotnet build EmulatorSmokeTest.csproj -c Release \ + -p:SmokeTestCoreVersion=$CORE_VERSION \ + -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION \ + -v normal # ---- Verify DTFx packages resolved from local-packages ---- - name: Verify DTFx packages from local source @@ -156,13 +156,15 @@ jobs: # ---- Run Smoke Test ---- - name: Run smoke test env: - DTFX_VERSION: ${{ steps.version.outputs.dtfx_version }} + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} run: | set -e echo "Running Emulator Smoke Test..." dotnet run --project Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj \ -c Release \ --no-build \ - -p:SmokeTestDtfxVersion=$DTFX_VERSION + -p:SmokeTestCoreVersion=$CORE_VERSION \ + -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION echo "Smoke test completed successfully." diff --git a/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj index e5b0ec93e..69c8c8258 100644 --- a/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj +++ b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj @@ -13,10 +13,11 @@ - - + + diff --git a/Test/SmokeTests/EmulatorSmokeTest/Program.cs b/Test/SmokeTests/EmulatorSmokeTest/Program.cs index 38f2b15bb..039d606ee 100644 --- a/Test/SmokeTests/EmulatorSmokeTest/Program.cs +++ b/Test/SmokeTests/EmulatorSmokeTest/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Reflection; using DurableTask.Core; using DurableTask.Emulator; using Microsoft.Extensions.Logging; @@ -63,7 +64,7 @@ await worker if (result.OrchestrationStatus != OrchestrationStatus.Completed) { Console.WriteLine("FAIL: Orchestration did not complete successfully."); - Environment.Exit(1); + return 1; } if (result.Output == null || @@ -72,12 +73,12 @@ await worker !result.Output.Contains("Hello, Seattle!")) { Console.WriteLine("FAIL: Orchestration output did not contain expected greetings."); - Environment.Exit(1); + return 1; } Console.WriteLine(); Console.WriteLine("PASS: HelloCities orchestration completed with expected output."); -Environment.Exit(0); +return 0; // ---- Orchestration ---- From 4ac04b0a8df754744d654f1221ddaa22900a30e3 Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 8 Apr 2026 15:25:29 -0700 Subject: [PATCH 3/6] Add default version fallbacks for standalone restore The Dependency Submission (submit-nuget) workflow restores all csproj files without passing -p:SmokeTestCoreVersion/-p:SmokeTestEmulatorVersion. Add default fallback versions in the csproj so standalone restore works. Also add nuget.org mapping for DTFx packages so restore succeeds when local-packages folder is empty (outside CI pipeline). --- Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj | 4 ++++ Test/SmokeTests/EmulatorSmokeTest/NuGet.config | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj index 69c8c8258..15b18255e 100644 --- a/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj +++ b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj @@ -8,6 +8,10 @@ false false false + + 3.7.1 + 2.6.0 diff --git a/Test/SmokeTests/EmulatorSmokeTest/NuGet.config b/Test/SmokeTests/EmulatorSmokeTest/NuGet.config index 7365229f2..c85dc7082 100644 --- a/Test/SmokeTests/EmulatorSmokeTest/NuGet.config +++ b/Test/SmokeTests/EmulatorSmokeTest/NuGet.config @@ -8,7 +8,9 @@ @@ -17,6 +19,8 @@ + + From bf40ad8bc9ba23dec3a252ced21dd48691c5c67e Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 8 Apr 2026 15:38:58 -0700 Subject: [PATCH 4/6] Add .gitkeep to local-packages dir for Dependency Submission workflow Ensures the local-packages directory exists in the repo so NuGet restore doesn't fail with NU1301 when the Dependency Submission workflow scans all csproj files. --- Test/SmokeTests/EmulatorSmokeTest/.gitignore | 2 +- Test/SmokeTests/EmulatorSmokeTest/local-packages/.gitkeep | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 Test/SmokeTests/EmulatorSmokeTest/local-packages/.gitkeep diff --git a/Test/SmokeTests/EmulatorSmokeTest/.gitignore b/Test/SmokeTests/EmulatorSmokeTest/.gitignore index 67d91e9d9..dcb681c34 100644 --- a/Test/SmokeTests/EmulatorSmokeTest/.gitignore +++ b/Test/SmokeTests/EmulatorSmokeTest/.gitignore @@ -1 +1 @@ -local-packages/ +local-packages/*.nupkg diff --git a/Test/SmokeTests/EmulatorSmokeTest/local-packages/.gitkeep b/Test/SmokeTests/EmulatorSmokeTest/local-packages/.gitkeep new file mode 100644 index 000000000..e69de29bb From 6e3b924cbbbabd350b1e83fb18f5aaf2b549a458 Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 8 Apr 2026 15:40:19 -0700 Subject: [PATCH 5/6] Address Copilot review: MSBuild version parsing and assembly version printing - Use dotnet msbuild -getProperty:PackageVersion instead of grep to resolve versions, handling imports/conditions/prerelease correctly - Move assembly version printing to after orchestration run so all DTFx assemblies are loaded (lazy loading means they may not be present at startup) --- .github/workflows/dep-version-validation.yml | 30 +++++++++++----- Test/SmokeTests/EmulatorSmokeTest/Program.cs | 38 ++++++++++---------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/.github/workflows/dep-version-validation.yml b/.github/workflows/dep-version-validation.yml index fef266183..83c5e8b58 100644 --- a/.github/workflows/dep-version-validation.yml +++ b/.github/workflows/dep-version-validation.yml @@ -57,18 +57,30 @@ jobs: run: | set -e - # Parse Core version from its csproj - CORE_CSPROJ="src/DurableTask.Core/DurableTask.Core.csproj" - MAJOR=$(grep -oP '\K[^<]+' "$CORE_CSPROJ") - MINOR=$(grep -oP '\K[^<]+' "$CORE_CSPROJ") - PATCH=$(grep -oP '\K[^<]+' "$CORE_CSPROJ") - CORE_VERSION="${MAJOR}.${MINOR}.${PATCH}" + # Use MSBuild to resolve the evaluated PackageVersion/Version + # so imports, conditions, and prerelease labels are handled correctly. + get_msbuild_version() { + local project_file="$1" + local version + + version=$(dotnet msbuild "$project_file" -nologo -getProperty:PackageVersion 2>/dev/null | tail -n 1 | tr -d '\r') + if [ -z "$version" ]; then + version=$(dotnet msbuild "$project_file" -nologo -getProperty:Version 2>/dev/null | tail -n 1 | tr -d '\r') + fi + + if [ -z "$version" ]; then + echo "Failed to resolve version for $project_file" >&2 + exit 1 + fi + + echo "$version" + } + + CORE_VERSION=$(get_msbuild_version "src/DurableTask.Core/DurableTask.Core.csproj") echo "Core version: ${CORE_VERSION}" echo "core_version=${CORE_VERSION}" >> "$GITHUB_OUTPUT" - # Emulator inherits its version from tools/DurableTask.props - PROPS_FILE="tools/DurableTask.props" - EMULATOR_VERSION=$(grep -oP '\K[^<]+' "$PROPS_FILE") + EMULATOR_VERSION=$(get_msbuild_version "src/DurableTask.Emulator/DurableTask.Emulator.csproj") echo "Emulator version: ${EMULATOR_VERSION}" echo "emulator_version=${EMULATOR_VERSION}" >> "$GITHUB_OUTPUT" diff --git a/Test/SmokeTests/EmulatorSmokeTest/Program.cs b/Test/SmokeTests/EmulatorSmokeTest/Program.cs index 039d606ee..3e76ee27f 100644 --- a/Test/SmokeTests/EmulatorSmokeTest/Program.cs +++ b/Test/SmokeTests/EmulatorSmokeTest/Program.cs @@ -15,25 +15,6 @@ builder.SetMinimumLevel(LogLevel.Warning); }); -// Print loaded DTFx assembly versions -Console.WriteLine("Loaded DTFx assembly versions:"); -foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) -{ - string? name = asm.GetName().Name; - if (name != null && name.StartsWith("DurableTask.", StringComparison.OrdinalIgnoreCase)) - { - string? infoVersion = asm - .GetCustomAttribute() - ?.InformationalVersion; - if (infoVersion != null && infoVersion.Contains('+')) - { - infoVersion = infoVersion[..infoVersion.IndexOf('+')]; - } - Console.WriteLine($" {name} = {infoVersion ?? asm.GetName().Version?.ToString() ?? "unknown"}"); - } -} -Console.WriteLine(); - // Create the in-memory orchestration service var orchestrationService = new LocalOrchestrationService(); @@ -60,6 +41,25 @@ await worker await worker.StopAsync(true); +// Print loaded DTFx assembly versions (after usage to ensure all assemblies are loaded) +Console.WriteLine(); +Console.WriteLine("Loaded DTFx assembly versions:"); +foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) +{ + string? name = asm.GetName().Name; + if (name != null && name.StartsWith("DurableTask.", StringComparison.OrdinalIgnoreCase)) + { + string? infoVersion = asm + .GetCustomAttribute() + ?.InformationalVersion; + if (infoVersion != null && infoVersion.Contains('+')) + { + infoVersion = infoVersion[..infoVersion.IndexOf('+')]; + } + Console.WriteLine($" {name} = {infoVersion ?? asm.GetName().Version?.ToString() ?? "unknown"}"); + } +} + // Validate result if (result.OrchestrationStatus != OrchestrationStatus.Completed) { From 9a6250226e54ec7a6b32018bd126b42a43aca4cd Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 9 Apr 2026 17:47:54 -0700 Subject: [PATCH 6/6] Align with durable-extension PR lessons learned - Generate NuGet.config dynamically (not committed) - Remove .gitkeep and dual nuget.org mapping - Strict packageSourceMapping (local-packages only for DTFx) - Case-insensitive grep for assets.json verification - Add description comment at top of workflow - Simplify workflow structure --- .github/workflows/dep-version-validation.yml | 171 ++++++------------ Test/SmokeTests/EmulatorSmokeTest/.gitignore | 3 +- .../SmokeTests/EmulatorSmokeTest/NuGet.config | 26 --- .../EmulatorSmokeTest/local-packages/.gitkeep | 0 4 files changed, 55 insertions(+), 145 deletions(-) delete mode 100644 Test/SmokeTests/EmulatorSmokeTest/NuGet.config delete mode 100644 Test/SmokeTests/EmulatorSmokeTest/local-packages/.gitkeep diff --git a/.github/workflows/dep-version-validation.yml b/.github/workflows/dep-version-validation.yml index 83c5e8b58..e7623d0f6 100644 --- a/.github/workflows/dep-version-validation.yml +++ b/.github/workflows/dep-version-validation.yml @@ -1,26 +1,12 @@ -# ================================================================================================ -# Dependency Version Validation Pipeline -# ================================================================================================ -# Validates that changes to dependency versions (Directory.Packages.props) do not break -# the DurableTask Framework (DTFx) NuGet packages at build or runtime. -# -# The DTFx Core and Emulator packages are consumed downstream by: -# - azure-functions-durable-extension (WebJobs extension) -# - AAPT-DTMB (DTS data plane) -# -# This pipeline: -# 1. Packs DurableTask.Core and DurableTask.Emulator from source into a local feed -# 2. Builds a standalone console app that runs a HelloCities orchestration -# using the Emulator backend -# 3. Validates orchestration output and loaded assembly versions -# ================================================================================================ - +# Validates that dependency version changes do not break the DTFx NuGet packages. +# Packs DurableTask.Core and DurableTask.Emulator from source, builds a console +# app using the local packages, runs a HelloCities orchestration with the +# Emulator backend, and validates orchestration output. name: Dependency Version Validation on: push: - branches: - - main + branches: [ main ] paths: - 'Directory.Packages.props' - 'tools/DurableTask.props' @@ -40,143 +26,92 @@ jobs: name: 'Emulator Orchestration Smoke Test (NuGet Packages)' runs-on: ubuntu-latest timeout-minutes: 10 - steps: - # ---- Checkout & SDK Setup ---- - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET 8.0 SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: { dotnet-version: '8.0.x' } - # ---- Parse DTFx Versions ---- - name: Parse DTFx package versions id: version run: | set -e - - # Use MSBuild to resolve the evaluated PackageVersion/Version - # so imports, conditions, and prerelease labels are handled correctly. - get_msbuild_version() { - local project_file="$1" - local version - - version=$(dotnet msbuild "$project_file" -nologo -getProperty:PackageVersion 2>/dev/null | tail -n 1 | tr -d '\r') - if [ -z "$version" ]; then - version=$(dotnet msbuild "$project_file" -nologo -getProperty:Version 2>/dev/null | tail -n 1 | tr -d '\r') - fi - - if [ -z "$version" ]; then - echo "Failed to resolve version for $project_file" >&2 - exit 1 - fi - - echo "$version" + get_version() { + local v + v=$(dotnet msbuild "$1" -nologo -getProperty:PackageVersion 2>/dev/null | tail -n 1 | tr -d '\r') + [ -z "$v" ] && v=$(dotnet msbuild "$1" -nologo -getProperty:Version 2>/dev/null | tail -n 1 | tr -d '\r') + [ -z "$v" ] && { echo "FAIL: Cannot resolve version for $1" >&2; exit 1; } + echo "$v" } - - CORE_VERSION=$(get_msbuild_version "src/DurableTask.Core/DurableTask.Core.csproj") - echo "Core version: ${CORE_VERSION}" + CORE_VERSION=$(get_version "src/DurableTask.Core/DurableTask.Core.csproj") + EMULATOR_VERSION=$(get_version "src/DurableTask.Emulator/DurableTask.Emulator.csproj") + echo "Core: $CORE_VERSION, Emulator: $EMULATOR_VERSION" echo "core_version=${CORE_VERSION}" >> "$GITHUB_OUTPUT" - - EMULATOR_VERSION=$(get_msbuild_version "src/DurableTask.Emulator/DurableTask.Emulator.csproj") - echo "Emulator version: ${EMULATOR_VERSION}" echo "emulator_version=${EMULATOR_VERSION}" >> "$GITHUB_OUTPUT" - # ---- Pack DTFx Packages from Source ---- - - name: Pack DTFx packages from source + - name: Pack DTFx packages run: | set -e - LOCAL_PACKAGES="Test/SmokeTests/EmulatorSmokeTest/local-packages" - mkdir -p "$LOCAL_PACKAGES" - - # Build Core first (Emulator depends on it) - echo "Building DurableTask.Core..." + PKG=Test/SmokeTests/EmulatorSmokeTest/local-packages; mkdir -p "$PKG" dotnet build src/DurableTask.Core/DurableTask.Core.csproj -c Release - - echo "Building DurableTask.Emulator..." dotnet build src/DurableTask.Emulator/DurableTask.Emulator.csproj -c Release + dotnet pack src/DurableTask.Core/DurableTask.Core.csproj -c Release --no-build --output "$PKG" + dotnet pack src/DurableTask.Emulator/DurableTask.Emulator.csproj -c Release --no-build --output "$PKG" + ls -la "$PKG" - # Pack from build output - echo "Packing DurableTask.Core..." - dotnet pack src/DurableTask.Core/DurableTask.Core.csproj -c Release --no-build --output "$LOCAL_PACKAGES" - - echo "Packing DurableTask.Emulator..." - dotnet pack src/DurableTask.Emulator/DurableTask.Emulator.csproj -c Release --no-build --output "$LOCAL_PACKAGES" - - echo "Local packages:" - ls -la "$LOCAL_PACKAGES" - - # ---- Verify Packed Packages ---- - name: Verify packed packages env: CORE_VERSION: ${{ steps.version.outputs.core_version }} EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} run: | set -e - LOCAL_PACKAGES="Test/SmokeTests/EmulatorSmokeTest/local-packages" - - CORE_PKG="Microsoft.Azure.DurableTask.Core.${CORE_VERSION}.nupkg" - if [ ! -f "$LOCAL_PACKAGES/$CORE_PKG" ]; then - echo "FAIL: Missing Core package: $CORE_PKG" - echo "Available packages:" - ls -1 "$LOCAL_PACKAGES" - exit 1 - fi - echo " OK: $CORE_PKG" + PKG=Test/SmokeTests/EmulatorSmokeTest/local-packages + [ -f "$PKG/Microsoft.Azure.DurableTask.Core.${CORE_VERSION}.nupkg" ] && echo "OK: Core ${CORE_VERSION}" || { echo "FAIL: Core missing"; ls -1 "$PKG"; exit 1; } + [ -f "$PKG/Microsoft.Azure.DurableTask.Emulator.${EMULATOR_VERSION}.nupkg" ] && echo "OK: Emulator ${EMULATOR_VERSION}" || { echo "FAIL: Emulator missing"; ls -1 "$PKG"; exit 1; } - EMULATOR_PKG="Microsoft.Azure.DurableTask.Emulator.${EMULATOR_VERSION}.nupkg" - if [ ! -f "$LOCAL_PACKAGES/$EMULATOR_PKG" ]; then - echo "FAIL: Missing Emulator package: $EMULATOR_PKG" - echo "Available packages:" - ls -1 "$LOCAL_PACKAGES" - exit 1 - fi - echo " OK: $EMULATOR_PKG" - echo "PASS: DTFx packages verified." + - name: Generate NuGet.config + run: | + cat > Test/SmokeTests/EmulatorSmokeTest/NuGet.config << 'EOF' + + + + + + + + + + + + + + + EOF - # ---- Build Smoke Test App ---- - name: Build smoke test app env: CORE_VERSION: ${{ steps.version.outputs.core_version }} EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} run: | - set -e - cd Test/SmokeTests/EmulatorSmokeTest - dotnet build EmulatorSmokeTest.csproj -c Release \ + dotnet build Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj -c Release \ -p:SmokeTestCoreVersion=$CORE_VERSION \ - -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION \ - -v normal + -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION - # ---- Verify DTFx packages resolved from local-packages ---- - - name: Verify DTFx packages from local source + - name: Verify package versions in assets + env: + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} run: | set -e - echo "Verifying DTFx packages were restored from local-packages..." - ASSETS_FILE="Test/SmokeTests/EmulatorSmokeTest/obj/project.assets.json" - for pkg in "Microsoft.Azure.DurableTask.Core" "Microsoft.Azure.DurableTask.Emulator"; do - if grep -qi "$pkg" "$ASSETS_FILE"; then - echo " OK: $pkg found in project.assets.json" - else - echo " FAIL: $pkg NOT found" - exit 1 - fi - done - echo "PASS: DTFx packages verified in build output." + ASSETS=Test/SmokeTests/EmulatorSmokeTest/obj/project.assets.json + grep -qi "Microsoft.Azure.DurableTask.Core/${CORE_VERSION}" "$ASSETS" && echo "OK: Core ${CORE_VERSION}" || { echo "FAIL: Core not found"; exit 1; } + grep -qi "Microsoft.Azure.DurableTask.Emulator/${EMULATOR_VERSION}" "$ASSETS" && echo "OK: Emulator ${EMULATOR_VERSION}" || { echo "FAIL: Emulator not found"; exit 1; } - # ---- Run Smoke Test ---- - name: Run smoke test env: CORE_VERSION: ${{ steps.version.outputs.core_version }} EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} run: | - set -e - echo "Running Emulator Smoke Test..." dotnet run --project Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj \ - -c Release \ - --no-build \ + -c Release --no-build \ -p:SmokeTestCoreVersion=$CORE_VERSION \ -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION - - echo "Smoke test completed successfully." diff --git a/Test/SmokeTests/EmulatorSmokeTest/.gitignore b/Test/SmokeTests/EmulatorSmokeTest/.gitignore index dcb681c34..0272d976a 100644 --- a/Test/SmokeTests/EmulatorSmokeTest/.gitignore +++ b/Test/SmokeTests/EmulatorSmokeTest/.gitignore @@ -1 +1,2 @@ -local-packages/*.nupkg +local-packages/ +NuGet.config diff --git a/Test/SmokeTests/EmulatorSmokeTest/NuGet.config b/Test/SmokeTests/EmulatorSmokeTest/NuGet.config deleted file mode 100644 index c85dc7082..000000000 --- a/Test/SmokeTests/EmulatorSmokeTest/NuGet.config +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/Test/SmokeTests/EmulatorSmokeTest/local-packages/.gitkeep b/Test/SmokeTests/EmulatorSmokeTest/local-packages/.gitkeep deleted file mode 100644 index e69de29bb..000000000