diff --git a/.github/workflows/dep-version-validation.yml b/.github/workflows/dep-version-validation.yml new file mode 100644 index 000000000..e7623d0f6 --- /dev/null +++ b/.github/workflows/dep-version-validation.yml @@ -0,0 +1,117 @@ +# 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 ] + 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: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: { dotnet-version: '8.0.x' } + + - name: Parse DTFx package versions + id: version + run: | + set -e + 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_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" + echo "emulator_version=${EMULATOR_VERSION}" >> "$GITHUB_OUTPUT" + + - name: Pack DTFx packages + run: | + set -e + PKG=Test/SmokeTests/EmulatorSmokeTest/local-packages; mkdir -p "$PKG" + dotnet build src/DurableTask.Core/DurableTask.Core.csproj -c Release + 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" + + - name: Verify packed packages + env: + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} + run: | + set -e + 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; } + + - name: Generate NuGet.config + run: | + cat > Test/SmokeTests/EmulatorSmokeTest/NuGet.config << 'EOF' + + + + + + + + + + + + + + + EOF + + - name: Build smoke test app + env: + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} + run: | + dotnet build Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj -c Release \ + -p:SmokeTestCoreVersion=$CORE_VERSION \ + -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION + + - name: Verify package versions in assets + env: + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} + run: | + set -e + 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; } + + - name: Run smoke test + env: + CORE_VERSION: ${{ steps.version.outputs.core_version }} + EMULATOR_VERSION: ${{ steps.version.outputs.emulator_version }} + run: | + dotnet run --project Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj \ + -c Release --no-build \ + -p:SmokeTestCoreVersion=$CORE_VERSION \ + -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION diff --git a/Test/SmokeTests/EmulatorSmokeTest/.gitignore b/Test/SmokeTests/EmulatorSmokeTest/.gitignore new file mode 100644 index 000000000..0272d976a --- /dev/null +++ b/Test/SmokeTests/EmulatorSmokeTest/.gitignore @@ -0,0 +1,2 @@ +local-packages/ +NuGet.config diff --git a/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj new file mode 100644 index 000000000..15b18255e --- /dev/null +++ b/Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + enable + enable + false + false + false + + 3.7.1 + 2.6.0 + + + + + + + + + + diff --git a/Test/SmokeTests/EmulatorSmokeTest/Program.cs b/Test/SmokeTests/EmulatorSmokeTest/Program.cs new file mode 100644 index 000000000..3e76ee27f --- /dev/null +++ b/Test/SmokeTests/EmulatorSmokeTest/Program.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; +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); +}); + +// 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); + +// 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) +{ + Console.WriteLine("FAIL: Orchestration did not complete successfully."); + return 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."); + return 1; +} + +Console.WriteLine(); +Console.WriteLine("PASS: HelloCities orchestration completed with expected output."); +return 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}!"; + } +}