Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
182 changes: 182 additions & 0 deletions .github/workflows/dep-version-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# ================================================================================================
# 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 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
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.

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_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
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:
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"

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:
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: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."

Comment on lines +103 to +108
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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."

Copilot uses AI. Check for mistakes.
# ---- 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 \
-p:SmokeTestCoreVersion=$CORE_VERSION \
-p:SmokeTestEmulatorVersion=$EMULATOR_VERSION

echo "Smoke test completed successfully."
1 change: 1 addition & 0 deletions Test/SmokeTests/EmulatorSmokeTest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
local-packages/*.nupkg
28 changes: 28 additions & 0 deletions Test/SmokeTests/EmulatorSmokeTest/EmulatorSmokeTest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>false</IsTestProject>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<!-- Default versions for standalone restore (e.g. Dependency Submission workflow).
These are overridden by -p:SmokeTestCoreVersion / -p:SmokeTestEmulatorVersion in the CI pipeline. -->
<SmokeTestCoreVersion Condition="'$(SmokeTestCoreVersion)' == ''">3.7.1</SmokeTestCoreVersion>
<SmokeTestEmulatorVersion Condition="'$(SmokeTestEmulatorVersion)' == ''">2.6.0</SmokeTestEmulatorVersion>
</PropertyGroup>

<ItemGroup>
<!--
DTFx packages under test (from local-packages feed).
Core and Emulator use different version schemes, so they have separate properties.
Injected via -p:SmokeTestCoreVersion=... -p:SmokeTestEmulatorVersion=... at build time.
-->
<PackageReference Include="Microsoft.Azure.DurableTask.Core" Version="$(SmokeTestCoreVersion)" />
<PackageReference Include="Microsoft.Azure.DurableTask.Emulator" Version="$(SmokeTestEmulatorVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions Test/SmokeTests/EmulatorSmokeTest/NuGet.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local-packages" value="./local-packages" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>

<!--
Package source mapping ensures that the DTFx packages under test are ONLY
resolved from the local-packages feed during CI builds. When local-packages
is empty (standalone restore), NuGet will fall through to nuget.org since
both sources map the patterns.
-->
<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>
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
</packageSourceMapping>
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
</configuration>
111 changes: 111 additions & 0 deletions Test/SmokeTests/EmulatorSmokeTest/Program.cs
Original file line number Diff line number Diff line change
@@ -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);

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (result == null)
{
Console.WriteLine("FAIL: Orchestration did not complete within the timeout.");
await worker.StopAsync(true);
return 1;
}

Copilot uses AI. Check for mistakes.
Console.WriteLine($"Orchestration status: {result.OrchestrationStatus}");
Console.WriteLine($"Orchestration output: {result.Output}");

await worker.StopAsync(true);

Comment on lines +23 to +43
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
// 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<AssemblyInformationalVersionAttribute>()
?.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;
}
Comment on lines +66 to +68
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

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;
}
Comment on lines +75 to +77
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Console.WriteLine();
Console.WriteLine("PASS: HelloCities orchestration completed with expected output.");
return 0;

// ---- Orchestration ----

/// <summary>
/// A simple function chaining orchestration that calls SayHello for three cities.
/// </summary>
public class HelloCitiesOrchestration : TaskOrchestration<string, object>
{
public override async Task<string> RunTask(OrchestrationContext context, object input)
{
string result = "";
result += await context.ScheduleTask<string>(typeof(SayHelloActivity), "Tokyo") + " ";
result += await context.ScheduleTask<string>(typeof(SayHelloActivity), "London") + " ";
result += await context.ScheduleTask<string>(typeof(SayHelloActivity), "Seattle");
return result;
}
}

// ---- Activity ----

/// <summary>
/// A simple activity that returns a greeting for the given city name.
/// </summary>
public class SayHelloActivity : TaskActivity<string, string>
{
protected override string Execute(TaskContext context, string cityName)
{
return $"Hello, {cityName}!";
}
}
Empty file.
Loading