Conversation
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.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a GitHub Actions workflow to validate that dependency version changes don’t break the packed DTFx NuGet packages (Core + Emulator) by building and running an emulator-backed orchestration smoke test against locally-packed artifacts.
Changes:
- Added an Emulator-based smoke test console app that runs HelloCities and validates output/version info.
- Added NuGet source mapping + local package feed configuration for deterministic package resolution.
- Added a GitHub Actions workflow to pack local NuGets, build the smoke test, and run it in CI.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Test/SmokeTests/EmulatorSmokeTest/Program.cs | Implements the runtime smoke test (run orchestration + validate output + print assembly versions). |
| Test/SmokeTests/EmulatorSmokeTest/NuGet.config | Configures local feed + packageSourceMapping to force DTFx packages from the local feed. |
| Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj | Defines the net8 console app and references Core/Emulator packages via an injected version property. |
| Test/SmokeTests/EmulatorSmokeTest/.gitignore | Ignores the generated local NuGet feed directory. |
| .github/workflows/dep-version-validation.yml | Builds/packs local DTFx NuGets and runs the smoke test on dependency-related changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| string? infoVersion = asm | ||
| .GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>() | ||
| ?.InformationalVersion; |
There was a problem hiding this comment.
asm.GetCustomAttribute<T>() is an extension method from System.Reflection (CustomAttributeExtensions). With implicit usings, System.Reflection typically isn’t imported, so this may fail to compile. Fix by adding using System.Reflection; at the top, or by calling the extension method with a fully-qualified name.
| Console.WriteLine("FAIL: Orchestration did not complete successfully."); | ||
| Environment.Exit(1); | ||
| } |
There was a problem hiding this comment.
Environment.Exit(...) bypasses normal shutdown/cleanup, including disposing the loggerFactory declared with using and any other pending finalizers/flushes. Prefer setting Environment.ExitCode and returning from top-level statements (or throwing) so disposals and flushes run deterministically.
| Console.WriteLine("FAIL: Orchestration output did not contain expected greetings."); | ||
| Environment.Exit(1); | ||
| } |
There was a problem hiding this comment.
Environment.Exit(...) bypasses normal shutdown/cleanup, including disposing the loggerFactory declared with using and any other pending finalizers/flushes. Prefer setting Environment.ExitCode and returning from top-level statements (or throwing) so disposals and flushes run deterministically.
|
|
||
| Console.WriteLine(); | ||
| Console.WriteLine("PASS: HelloCities orchestration completed with expected output."); | ||
| Environment.Exit(0); |
There was a problem hiding this comment.
Environment.Exit(...) bypasses normal shutdown/cleanup, including disposing the loggerFactory declared with using and any other pending finalizers/flushes. Prefer setting Environment.ExitCode and returning from top-level statements (or throwing) so disposals and flushes run deterministically.
| # 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 |
There was a problem hiding this comment.
The workflow allows the Emulator package to have a different version (it only checks that some Microsoft.Azure.DurableTask.Emulator.*.nupkg exists), but the smoke test project pins Emulator to $(SmokeTestDtfxVersion) (same as Core). If Emulator’s packed version doesn’t match DTFX_VERSION, restore/build will fail. Fix by ensuring Emulator is packed with the same version you pass into SmokeTestDtfxVersion (e.g., bump/version both consistently), or by passing separate version properties for Core vs Emulator and wiring them in the csproj.
| # 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 | |
| EMULATOR_PKG="Microsoft.Azure.DurableTask.Emulator.${DTFX_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" |
| - 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 |
There was a problem hiding this comment.
The workflow allows the Emulator package to have a different version (it only checks that some Microsoft.Azure.DurableTask.Emulator.*.nupkg exists), but the smoke test project pins Emulator to $(SmokeTestDtfxVersion) (same as Core). If Emulator’s packed version doesn’t match DTFX_VERSION, restore/build will fail. Fix by ensuring Emulator is packed with the same version you pass into SmokeTestDtfxVersion (e.g., bump/version both consistently), or by passing separate version properties for Core vs Emulator and wiring them in the csproj.
| 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." | ||
|
|
There was a problem hiding this comment.
This check only verifies the package names appear in project.assets.json, which doesn’t prove they were restored from local-packages (or that the resolved version matches the locally packed one). A more reliable validation is to assert the resolved versions equal $DTFX_VERSION and that the package folder/path in project.assets.json points to the local feed (or that NuGet used source mapping as intended). This will make the pipeline’s failure mode clearer and prevent false positives.
| 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." | |
| env: | |
| DTFX_VERSION: ${{ steps.version.outputs.dtfx_version }} | |
| run: | | |
| set -e | |
| echo "Verifying DTFx packages were restored from local-packages..." | |
| ASSETS_FILE="Test/SmokeTests/EmulatorSmokeTest/obj/project.assets.json" | |
| LOCAL_PACKAGES="Test/SmokeTests/EmulatorSmokeTest/local-packages" | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| assets_file = os.path.abspath("Test/SmokeTests/EmulatorSmokeTest/obj/project.assets.json") | |
| local_packages = os.path.abspath("Test/SmokeTests/EmulatorSmokeTest/local-packages") | |
| dtfx_version = os.environ["DTFX_VERSION"] | |
| expected_packages = [ | |
| "Microsoft.Azure.DurableTask.Core", | |
| "Microsoft.Azure.DurableTask.Emulator", | |
| ] | |
| with open(assets_file, "r", encoding="utf-8") as f: | |
| assets = json.load(f) | |
| restore = assets.get("project", {}).get("restore", {}) | |
| sources = restore.get("sources", {}) | |
| normalized_sources = set() | |
| for source in sources.keys(): | |
| normalized = source | |
| if normalized.startswith("file://"): | |
| normalized = normalized[7:] | |
| normalized_sources.add(os.path.abspath(normalized)) | |
| if local_packages in normalized_sources: | |
| print(f" OK: local restore source found: {local_packages}") | |
| else: | |
| print(f" FAIL: local restore source not found in project.assets.json: {local_packages}") | |
| if normalized_sources: | |
| print(" Restore sources found:") | |
| for source in sorted(normalized_sources): | |
| print(f" - {source}") | |
| else: | |
| print(" No restore sources found in project.assets.json") | |
| sys.exit(1) | |
| libraries = assets.get("libraries", {}) | |
| for package in expected_packages: | |
| resolved_key = f"{package}/{dtfx_version}" | |
| if resolved_key in libraries: | |
| print(f" OK: {package} resolved to version {dtfx_version}") | |
| else: | |
| matching_versions = sorted( | |
| key.split("/", 1)[1] | |
| for key in libraries.keys() | |
| if key.startswith(package + "/") | |
| ) | |
| if matching_versions: | |
| print( | |
| f" FAIL: {package} did not resolve to version {dtfx_version}. " | |
| f"Found: {', '.join(matching_versions)}" | |
| ) | |
| else: | |
| print(f" FAIL: {package} was not resolved in project.assets.json") | |
| sys.exit(1) | |
| print("PASS: DTFx packages verified from local source with expected versions.") | |
| PY |
- 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
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).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <packageSource key="local-packages"> | ||
| <package pattern="Microsoft.Azure.DurableTask.Core" /> | ||
| <package pattern="Microsoft.Azure.DurableTask.Emulator" /> | ||
| </packageSource> | ||
| <packageSource key="nuget.org"> | ||
| <package pattern="*" /> | ||
| <package pattern="Microsoft.Azure.DurableTask.Core" /> | ||
| <package pattern="Microsoft.Azure.DurableTask.Emulator" /> | ||
| </packageSource> |
There was a problem hiding this comment.
This mapping allows the DTFx packages to be restored from either local-packages or nuget.org (both sources match), which undermines the stated goal of forcing CI to consume the locally packed nupkgs. To make the workflow effective when the package version matches an already-published version, map Microsoft.Azure.DurableTask.Core and Microsoft.Azure.DurableTask.Emulator only to local-packages in CI (e.g., remove those two patterns from the nuget.org source mapping, or use a CI-specific NuGet.config / restore argument strategy). Otherwise the restore can legally (and silently) select nuget.org/global-cache content, making the validation less reliable.
| # Parse Core version from its csproj | ||
| CORE_CSPROJ="src/DurableTask.Core/DurableTask.Core.csproj" | ||
| MAJOR=$(grep -oP '<MajorVersion>\K[^<]+' "$CORE_CSPROJ") | ||
| MINOR=$(grep -oP '<MinorVersion>\K[^<]+' "$CORE_CSPROJ") | ||
| PATCH=$(grep -oP '<PatchVersion>\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 '<Version>\K[^<]+' "$PROPS_FILE") |
There was a problem hiding this comment.
The version parsing is brittle and can produce incorrect versions:\n- Core: composing ${MAJOR}.${MINOR}.${PATCH} ignores any prerelease/labels (and will fail if version is computed via imports/conditions), which can make the later *.nupkg filename checks wrong.\n- Emulator: grepping <Version> can return multiple matches if the props file contains more than one <Version> element.\nA more robust approach is to ask MSBuild for the evaluated Version/PackageVersion (e.g., dotnet msbuild ... -getProperty:PackageVersion / Version) so imports/conditions/prerelease labels are handled correctly.
| # Parse Core version from its csproj | |
| CORE_CSPROJ="src/DurableTask.Core/DurableTask.Core.csproj" | |
| MAJOR=$(grep -oP '<MajorVersion>\K[^<]+' "$CORE_CSPROJ") | |
| MINOR=$(grep -oP '<MinorVersion>\K[^<]+' "$CORE_CSPROJ") | |
| PATCH=$(grep -oP '<PatchVersion>\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 '<Version>\K[^<]+' "$PROPS_FILE") | |
| get_msbuild_version() { | |
| local project_file="$1" | |
| local version | |
| version=$(dotnet msbuild "$project_file" -nologo -getProperty:PackageVersion | tail -n 1 | tr -d '\r') | |
| if [ -z "$version" ]; then | |
| version=$(dotnet msbuild "$project_file" -nologo -getProperty:Version | 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" | |
| } | |
| # Resolve Core version from evaluated MSBuild properties | |
| CORE_CSPROJ="src/DurableTask.Core/DurableTask.Core.csproj" | |
| CORE_VERSION=$(get_msbuild_version "$CORE_CSPROJ") | |
| echo "Core version: ${CORE_VERSION}" | |
| echo "core_version=${CORE_VERSION}" >> "$GITHUB_OUTPUT" | |
| # Resolve Emulator version from evaluated MSBuild properties | |
| EMULATOR_CSPROJ="src/DurableTask.Emulator/DurableTask.Emulator.csproj" | |
| EMULATOR_VERSION=$(get_msbuild_version "$EMULATOR_CSPROJ") |
| // 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<System.Reflection.AssemblyInformationalVersionAttribute>() | ||
| ?.InformationalVersion; | ||
| if (infoVersion != null && infoVersion.Contains('+')) | ||
| { | ||
| infoVersion = infoVersion[..infoVersion.IndexOf('+')]; | ||
| } | ||
| Console.WriteLine($" {name} = {infoVersion ?? asm.GetName().Version?.ToString() ?? "unknown"}"); | ||
| } | ||
| } |
There was a problem hiding this comment.
This prints the currently loaded assemblies before the code first touches any DTFx types (the first concrete usage is later at new LocalOrchestrationService() / new TaskHubWorker(...)). On .NET, referenced assemblies are often loaded lazily, so this can end up printing nothing (or an incomplete list), reducing the usefulness of the validation output. Consider moving this block to after the worker/client are created (or after the orchestration run), or explicitly forcing load via known types (e.g., typeof(TaskHubClient).Assembly) before enumerating.
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.
…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)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Wait for completion | ||
| OrchestrationState result = await client.WaitForOrchestrationAsync( | ||
| instance, TimeSpan.FromSeconds(30), CancellationToken.None); | ||
|
|
There was a problem hiding this comment.
WaitForOrchestrationAsync can return null on timeout in some DTFx versions/implementations; the current code will then throw a NullReferenceException when accessing result.OrchestrationStatus / result.Output. Handle the timeout case explicitly (e.g., check for null and treat it as a FAIL with a clear message) before dereferencing result.
| if (result == null) | |
| { | |
| Console.WriteLine("FAIL: Orchestration did not complete within the timeout."); | |
| await worker.StopAsync(true); | |
| return 1; | |
| } |
| 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); | ||
|
|
There was a problem hiding this comment.
If anything throws between StartAsync() and StopAsync(true) (e.g., timeout/transport exceptions), the worker won’t be stopped, which can cause noisy teardown and occasionally hangs in CI depending on background threads/timers. Wrap the worker lifecycle in try/finally (or use an async disposal pattern if supported) to guarantee StopAsync(true) always runs.
| 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); | |
| worker | |
| .AddTaskOrchestrations(typeof(HelloCitiesOrchestration)) | |
| .AddTaskActivities(typeof(SayHelloActivity)); | |
| await worker.StartAsync(); | |
| OrchestrationState result = null!; | |
| try | |
| { | |
| // 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 | |
| result = await client.WaitForOrchestrationAsync( | |
| instance, TimeSpan.FromSeconds(30), CancellationToken.None); | |
| Console.WriteLine($"Orchestration status: {result.OrchestrationStatus}"); | |
| Console.WriteLine($"Orchestration output: {result.Output}"); | |
| } | |
| finally | |
| { | |
| await worker.StopAsync(true); | |
| } |
| <packageSourceMapping> | ||
| <packageSource key="local-packages"> | ||
| <package pattern="Microsoft.Azure.DurableTask.Core" /> | ||
| <package pattern="Microsoft.Azure.DurableTask.Emulator" /> | ||
| </packageSource> | ||
| <packageSource key="nuget.org"> | ||
| <package pattern="*" /> | ||
| <package pattern="Microsoft.Azure.DurableTask.Core" /> | ||
| <package pattern="Microsoft.Azure.DurableTask.Emulator" /> | ||
| </packageSource> | ||
| </packageSourceMapping> |
There was a problem hiding this comment.
With the DTFx package IDs mapped to both local-packages and nuget.org, NuGet is allowed to resolve those packages from nuget.org even during CI, which undermines the workflow’s goal of validating the locally packed artifacts. To make this deterministic, map the DTFx package IDs only to local-packages (and keep nuget.org mapped to * for other dependencies), or use a CI-only NuGet.config that enforces local-only for the two DTFx packages.
| 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 | ||
| 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." | ||
|
|
There was a problem hiding this comment.
This check only verifies that the package IDs appear in project.assets.json, not that they were actually restored from local-packages. As written, it will pass even if the packages were pulled from nuget.org. To make this a true validation, enforce local-only via packageSourceMapping/CI NuGet.config (preferred), or add a verification step that inspects restore output/logs for local-packages being the source for the two DTFx packages.
| 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 | |
| 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." | |
| RESTORE_LOG="$RUNNER_TEMP/emulator-smoke-restore.log" | |
| dotnet restore EmulatorSmokeTest.csproj \ | |
| -p:SmokeTestCoreVersion=$CORE_VERSION \ | |
| -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION \ | |
| -v diag 2>&1 | tee "$RESTORE_LOG" | |
| dotnet build EmulatorSmokeTest.csproj -c Release --no-restore \ | |
| -p:SmokeTestCoreVersion=$CORE_VERSION \ | |
| -p:SmokeTestEmulatorVersion=$EMULATOR_VERSION \ | |
| -v normal | |
| # ---- Verify DTFx packages resolved from local-packages ---- | |
| - name: Verify DTFx packages from local source | |
| 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..." | |
| RESTORE_LOG="$RUNNER_TEMP/emulator-smoke-restore.log" | |
| LOCAL_PACKAGES="$(realpath Test/SmokeTests/EmulatorSmokeTest/local-packages)" | |
| if [ ! -f "$RESTORE_LOG" ]; then | |
| echo "FAIL: Restore log not found: $RESTORE_LOG" | |
| exit 1 | |
| fi | |
| check_restore_source() { | |
| pkg="$1" | |
| version="$2" | |
| if awk -v pkg="$pkg" -v version="$version" -v src="$LOCAL_PACKAGES" 'index($0, pkg) && index($0, version) && index($0, src) { found=1 } END { exit found ? 0 : 1 }' "$RESTORE_LOG"; then | |
| echo " OK: $pkg $version restored from $LOCAL_PACKAGES" | |
| else | |
| echo " FAIL: $pkg $version was not verified as restored from $LOCAL_PACKAGES" | |
| echo " Relevant restore log lines:" | |
| grep -F "$pkg" "$RESTORE_LOG" || true | |
| exit 1 | |
| fi | |
| } | |
| check_restore_source "Microsoft.Azure.DurableTask.Core" "$CORE_VERSION" | |
| check_restore_source "Microsoft.Azure.DurableTask.Emulator" "$EMULATOR_VERSION" | |
| echo "PASS: DTFx packages verified as restored from local-packages." |
| 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 |
There was a problem hiding this comment.
The script suppresses MSBuild stderr (2>/dev/null), which makes version-resolution failures hard to diagnose in CI. Also, because the MSBuild call is inside a pipeline, set -e alone won’t reliably fail the step on MSBuild errors unless pipefail is enabled. Consider using set -euo pipefail and avoiding stderr suppression (or capture and print stderr on failure).
Summary
Adds a GitHub Actions workflow (dep-version-validation.yml) 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.
What this pipeline does
Trigger paths
Motivation
DTFx Core is consumed downstream by:
The existing CI (\�ng/ci/public-build.yml) runs unit tests, which validates DTFx internally. However, there was no validation that the packed NuGet packages resolve correctly and function at runtime after dependency changes. A dep version change that introduces assembly binding issues would only be caught when downstream consumers update their references.
This pipeline catches those issues at the source. Mirrors the SDK-PR-Validation pipeline pattern from AAPT-DTMB.