Skip to content

Python: Separate message texts with a newline in AgentResponse.text #32204

Python: Separate message texts with a newline in AgentResponse.text

Python: Separate message texts with a newline in AgentResponse.text #32204

#
# This workflow will build all .slnx files in the dotnet folder, and run all unit tests and integration tests using dotnet docker containers,
# each targeting a single version of the dotnet SDK.
#
name: dotnet-build-and-test
on:
workflow_dispatch:
pull_request:
branches: ["main", "feature*"]
merge_group:
branches: ["main", "feature*"]
push:
branches: ["main", "feature*"]
schedule:
- cron: "0 0 * * *" # Run at midnight UTC daily
env:
COVERAGE_THRESHOLD: 80
COVERAGE_FRAMEWORK: net10.0 # framework target for which we run/report code coverage
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
paths-filter:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
azureStorageChanges: ${{ steps.filter.outputs.azurestorage }}
foundryHostingChanges: ${{ steps.filter.outputs.foundryHosting }}
coreChanged: ${{ steps.filter.outputs.core }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
filters: |
dotnet:
- 'dotnet/**'
- '.github/workflows/dotnet-build-and-test.yml'
cosmosdb:
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
azurestorage:
- 'dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureStorage.IntegrationTests/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureStorage.UnitTests/**'
- 'dotnet/Directory.Packages.props'
- '.github/workflows/dotnet-build-and-test.yml'
# The Foundry hosted-agent IT is costly (builds a container, pushes to ACR,
# provisions live agents). Only run it when the project under test, its
# dependency chain, the test container, the test fixture, or their tooling
# changed. Keep this list in sync with $hashedDirs in scripts/it-build-image.ps1.
foundryHosting:
- 'dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/**'
- 'dotnet/src/Microsoft.Agents.AI.Foundry/**'
- 'dotnet/src/Microsoft.Agents.AI/**'
- 'dotnet/src/Microsoft.Agents.AI.Abstractions/**'
- 'dotnet/src/Microsoft.Agents.AI.Workflows/**'
- 'dotnet/tests/Foundry.Hosting.IntegrationTests/**'
- 'dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/**'
- 'dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/**'
- 'dotnet/Directory.Packages.props'
- 'dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-build-image.ps1'
- '.github/workflows/dotnet-build-and-test.yml'
core:
- 'dotnet/src/Microsoft.Agents.AI/**'
- 'dotnet/src/Microsoft.Agents.AI.Abstractions/**'
- 'dotnet/src/Microsoft.Agents.AI.OpenAI/**'
- 'dotnet/src/Microsoft.Agents.AI.Workflows/**'
- 'dotnet/src/Microsoft.Agents.AI.Workflows.Generators/**'
- 'dotnet/eng/scripts/New-FilteredSolution.ps1'
- 'dotnet/tests/Directory.Build.props'
- 'dotnet/Directory.Packages.props'
- 'dotnet/global.json'
- '.github/workflows/dotnet-build-and-test.yml'
# run only if 'dotnet' files were changed
- name: dotnet tests
if: steps.filter.outputs.dotnet == 'true'
run: echo "Dotnet file"
- name: dotnet CosmosDB tests
if: steps.filter.outputs.cosmosdb == 'true'
run: echo "Dotnet CosmosDB changes"
# run only if not 'dotnet' files were changed
- name: not dotnet tests
if: steps.filter.outputs.dotnet != 'true'
run: echo "NOT dotnet file"
# Build the full solution (including samples) on all TFMs. No tests.
dotnet-build:
needs: paths-filter
if: needs.paths-filter.outputs.dotnetChanges == 'true'
strategy:
fail-fast: false
matrix:
include:
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release }
- { targetFramework: "net9.0", os: "windows-latest", configuration: Debug }
- { targetFramework: "net8.0", os: "ubuntu-latest", configuration: Release }
- { targetFramework: "net472", os: "windows-latest", configuration: Release }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
python
declarative-agents
- name: Free runner disk space
uses: ./.github/actions/free-runner-disk-space
- name: Setup dotnet
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Build dotnet solutions
shell: bash
run: |
export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ')
for solution in $SOLUTIONS; do
dotnet build $solution -c ${{ matrix.configuration }} --warnaserror
done
- name: Package install check
shell: bash
# All frameworks are only built for the release configuration, so we only run this step for the release configuration
# and dotnet new doesn't support net472
if: matrix.configuration == 'Release' && matrix.targetFramework != 'net472'
run: |
TEMP_DIR=$(mktemp -d)
export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ')
for solution in $SOLUTIONS; do
dotnet pack $solution /property:TargetFrameworks=${{ matrix.targetFramework }} -c ${{ matrix.configuration }} --no-build --no-restore --output "$TEMP_DIR/artifacts"
done
pushd "$TEMP_DIR"
# Create a new console app to test the package installation
dotnet new console -f ${{ matrix.targetFramework }} --name packcheck --output consoleapp
# Create minimal nuget.config and use only dotnet nuget commands
echo '<?xml version="1.0" encoding="utf-8"?><configuration><packageSources><clear /></packageSources></configuration>' > consoleapp/nuget.config
# Add sources with local first using dotnet nuget commands
dotnet nuget add source ../artifacts --name local --configfile consoleapp/nuget.config
dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org --configfile consoleapp/nuget.config
# Change to project directory to ensure local nuget.config is used
pushd consoleapp
dotnet add packcheck.csproj package Microsoft.Agents.AI --prerelease
dotnet add packcheck.csproj package Microsoft.Agents.AI.LocalCodeAct --prerelease
dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj
# Clean up
popd
popd
rm -rf "$TEMP_DIR"
# Unit and integration jobs share steps but run with separate permissions and environments.
dotnet-test:
needs: paths-filter
if: needs.paths-filter.outputs.dotnetChanges == 'true'
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: false }
- { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: false }
runs-on: ${{ matrix.os }}
steps: &dotnet-test-steps
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
docs/specs
python
declarative-agents
- name: Free runner disk space
uses: ./.github/actions/free-runner-disk-space
# Branch and merge-queue unit checks always use the emulator; direct PR checks are path-selected.
- name: Start Azure Cosmos DB Emulator
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || matrix.integration-tests || github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') }}
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$emulatorPath = "$env:ProgramFiles\Azure Cosmos DB Emulator\Microsoft.Azure.Cosmos.Emulator.exe"
$emulatorKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
$maxRetries = 3
$timeout = 600 # 10 minutes instead of default 4 minutes
$retryCount = 0
$started = $false
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Starting Azure Cosmos DB Emulator" -ForegroundColor Cyan
Write-Host "Timeout: $timeout seconds, Max retries: $maxRetries" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
while (-not $started -and $retryCount -lt $maxRetries) {
$retryCount++
if ($retryCount -gt 1) {
Write-Host "`n--- Retry attempt $retryCount of $maxRetries ---" -ForegroundColor Yellow
# Stop and cleanup before retry
Write-Host "Stopping existing emulator instances..." -ForegroundColor Yellow
Stop-CosmosDbEmulator -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
# Force kill if still running
Get-Process -Name "Microsoft.Azure.Cosmos.Emulator" -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 3
# Cleanup old data
$dataPath = Join-Path $env:LOCALAPPDATA "CosmosDbEmulator"
if (Test-Path $dataPath) {
Write-Host "Cleaning up emulator data..." -ForegroundColor Yellow
Remove-Item -Recurse -Force $dataPath -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
}
}
try {
Write-Host "`nStarting emulator (attempt $retryCount)..." -ForegroundColor Cyan
Start-CosmosDbEmulator -NoUI -Key $emulatorKey -AllowNetworkAccess -NoFirewall
# Wait for emulator to be ready with extended timeout
$startTime = [DateTimeOffset]::Now
$stopTime = $startTime.AddSeconds($timeout)
$lastStatus = -1
Write-Host "Waiting for emulator to be ready..." -ForegroundColor Yellow
do {
Start-Sleep -Seconds 5
$elapsed = [int](([DateTimeOffset]::Now - $startTime).TotalSeconds)
# Check status
$statusProcess = Start-Process $emulatorPath -ArgumentList "/getstatus" -PassThru -Wait -NoNewWindow -ErrorAction SilentlyContinue
$statusCode = if ($statusProcess) { $statusProcess.ExitCode } else { -1 }
# Log status changes
if ($statusCode -ne $lastStatus) {
$statusMsg = switch ($statusCode) {
1 { "Emulator is starting..." }
2 { "Emulator is running!"; $started = $true }
3 { "Emulator is stopped" }
default { "Unknown status (code: $statusCode)" }
}
Write-Host "[${elapsed}s] $statusMsg" -ForegroundColor $(if ($statusCode -eq 2) { "Green" } else { "Yellow" })
$lastStatus = $statusCode
}
if ($started) { break }
# Check if process crashed
$processRunning = Get-Process -Name "Microsoft.Azure.Cosmos.Emulator" -ErrorAction SilentlyContinue
if (-not $processRunning -and $statusCode -ne 2) {
Write-Warning "Emulator process is not running"
break
}
} while ([DateTimeOffset]::Now -lt $stopTime)
if (-not $started) {
Write-Error "Emulator failed to start within $timeout seconds"
}
}
catch {
Write-Warning "Error on attempt ${retryCount}: $_"
if ($retryCount -ge $maxRetries) { throw }
}
}
if ($started) {
Write-Host "`n======================================" -ForegroundColor Green
Write-Host "Cosmos DB Emulator started successfully!" -ForegroundColor Green
Write-Host "======================================`n" -ForegroundColor Green
# Verify connectivity
try {
$response = Invoke-WebRequest -Uri "https://localhost:8081/" -UseBasicParsing -SkipCertificateCheck -TimeoutSec 10 -ErrorAction Stop
Write-Host "Emulator is responding (Status: $($response.StatusCode))" -ForegroundColor Green
}
catch {
Write-Warning "Emulator may not be fully ready: $_"
}
echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
}
else {
Write-Host "`n======================================" -ForegroundColor Red
Write-Host "Failed to start emulator after $maxRetries attempts" -ForegroundColor Red
Write-Host "======================================" -ForegroundColor Red
# Show diagnostic info
$logPath = Join-Path $env:LOCALAPPDATA "CosmosDbEmulator\cosmosdb.log"
if (Test-Path $logPath) {
Write-Host "`nLast 30 lines of emulator log:" -ForegroundColor Yellow
Get-Content $logPath -Tail 30
}
throw "Failed to start Cosmos DB Emulator"
}
- name: Start Azurite Blob service
if: ${{ runner.os == 'Linux' && (needs.paths-filter.outputs.azureStorageChanges == 'true' || matrix.integration-tests || github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') }}
shell: bash
run: |
docker run --detach --rm \
--name azurite \
--publish 10000:10000 \
mcr.microsoft.com/azure-storage/azurite:3.35.0@sha256:647c63a91102a9d8e8000aab803436e1fc85fbb285e7ce830a82ee5d6661cf37 \
azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck
for attempt in {1..30}; do
if (echo > /dev/tcp/127.0.0.1/10000) > /dev/null 2>&1; then
echo "AZURITE_AVAILABLE=true" >> "$GITHUB_ENV"
exit 0
fi
sleep 1
done
docker logs azurite
exit 1
- name: Setup dotnet
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Generate test solution (no samples)
shell: pwsh
run: |
./dotnet/eng/scripts/New-FilteredSolution.ps1 `
-Solution dotnet/agent-framework-dotnet.slnx `
-TargetFramework ${{ matrix.targetFramework }} `
-Configuration ${{ matrix.configuration }} `
-ExcludeSamples `
-OutputPath dotnet/filtered.slnx `
-Verbose
- name: Build src and tests
shell: bash
run: dotnet build dotnet/filtered.slnx -c ${{ matrix.configuration }} -f ${{ matrix.targetFramework }} --warnaserror
- name: Generate test-type filtered solutions
shell: pwsh
run: |
$commonArgs = @{
Solution = "dotnet/filtered.slnx"
TargetFramework = "${{ matrix.targetFramework }}"
Configuration = "${{ matrix.configuration }}"
Verbose = $true
}
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
-TestProjectNameIncludeFilter "*UnitTests*" `
-OutputPath dotnet/filtered-unit.slnx
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
-TestProjectNameIncludeFilter "*IntegrationTests*" `
-OutputPath dotnet/filtered-integration.slnx
- name: Run Unit Tests
if: ${{ !matrix.integration-tests }}
shell: pwsh
working-directory: dotnet
run: |
$coverageSettings = Join-Path $PWD "tests/coverage.runsettings"
$coverageArgs = @()
if ("${{ matrix.targetFramework }}" -eq "${{ env.COVERAGE_FRAMEWORK }}") {
$coverageArgs = @(
"--coverage",
"--coverage-output-format", "cobertura",
"--coverage-settings", $coverageSettings,
"--results-directory", "../TestResults/Coverage/"
)
}
dotnet test --solution ./filtered-unit.slnx `
-f ${{ matrix.targetFramework }} `
-c ${{ matrix.configuration }} `
--no-build -v Normal `
--report-xunit-trx `
--ignore-exit-code 8 `
@coverageArgs
env:
# Cosmos DB Emulator connection settings
COSMOSDB_ENDPOINT: https://localhost:8081
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
- name: Log event name and matrix integration-tests
shell: bash
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
- name: Azure CLI Login
if: matrix.integration-tests
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Run Integration Tests
shell: pwsh
working-directory: dotnet
if: matrix.integration-tests
run: |
dotnet test --solution ./filtered-integration.slnx `
-f ${{ matrix.targetFramework }} `
-c ${{ matrix.configuration }} `
--no-build -v Normal `
--report-xunit-trx `
--report-xunit-junit `
--results-directory ../IntegrationTestResults/ `
--ignore-exit-code 8 `
--filter-not-trait "Category=IntegrationDisabled" `
--filter-not-trait "Category=FoundryHostedAgents" `
--parallel-algorithm aggressive `
--max-threads 2.0x
env:
# Cosmos DB Emulator connection settings
COSMOSDB_ENDPOINT: https://localhost:8081
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
# OpenAI Models
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# Azure OpenAI Models
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Microsoft Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
# Anthropic Models
# Disable Anthropic tests by not providing environment vars until 404 failure is resolved
# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# ANTHROPIC_CHAT_MODEL_NAME: ${{ vars.ANTHROPIC_CHAT_MODEL_NAME }}
# ANTHROPIC_REASONING_MODEL_NAME: ${{ vars.ANTHROPIC_REASONING_MODEL_NAME }}
# Generate test reports and check coverage
- name: Generate test reports
if: ${{ !matrix.integration-tests && matrix.targetFramework == env.COVERAGE_FRAMEWORK }}
uses: danielpalme/ReportGenerator-GitHub-Action@861674232075b2c6bd1405a04c4bb9b150653e81 # 5.5.12
with:
reports: "./TestResults/Coverage/**/*.cobertura.xml"
targetdir: "./TestResults/Reports"
reporttypes: "HtmlInline;JsonSummary;Cobertura"
- name: Upload coverage report artifact
if: ${{ !matrix.integration-tests && matrix.targetFramework == env.COVERAGE_FRAMEWORK }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
path: ./TestResults/Reports # Directory containing files to upload
- name: Check coverage
if: ${{ !matrix.integration-tests && matrix.targetFramework == env.COVERAGE_FRAMEWORK }}
shell: pwsh
run: ./dotnet/eng/scripts/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
- name: Upload integration test results
if: always() && matrix.integration-tests
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dotnet-test-results-${{ matrix.targetFramework }}-${{ matrix.os }}
path: |
IntegrationTestResults/**/*.junit
IntegrationTestResults/**/*.junit.xml
if-no-files-found: ignore
# Repository PRs and reviewer-approved merge groups can run integration checks before merging.
dotnet-integration-test:
needs: paths-filter
if: needs.paths-filter.outputs.dotnetChanges == 'true' && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'))
permissions:
contents: read
id-token: "write"
strategy:
fail-fast: false
matrix:
include:
- { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true }
- { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true }
runs-on: ${{ matrix.os }}
environment: integration
steps: *dotnet-test-steps
dotnet-upload-coverage:
needs: [paths-filter, dotnet-test]
if: needs.paths-filter.outputs.dotnetChanges == 'true' && github.event_name != 'merge_group' && ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') || (github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch))
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
code-quality: write
pull-requests: read
steps:
- name: Download coverage report artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: CoverageReport-ubuntu-latest-${{ env.COVERAGE_FRAMEWORK }}-Release
path: ./TestResults/Reports
- name: Upload coverage report to GitHub
uses: actions/upload-code-coverage@2b21a77928be8d5168c2b9581a67f2adbebacc52 # v1.4.4
with:
file: ./TestResults/Reports/Cobertura.xml
language: CSharp
label: code-coverage/dotnet
fail-on-error: false
- name: Summarize GitHub coverage report
run: echo ".NET coverage is uploaded to GitHub's integrated coverage report with label \`code-coverage/dotnet\`." >> "$GITHUB_STEP_SUMMARY"
# The Foundry hosted-agent IT is costly (it builds a container, pushes to ACR, and provisions
# live agents on a separate Foundry project). Running it in its own job keeps the overall
# workflow time roughly flat: it executes in parallel to dotnet-build and dotnet-test and is
# gated on paths-filter.outputs.foundryHostingChanges so unrelated edits skip the work.
dotnet-foundry-hosted-it:
needs: paths-filter
if: needs.paths-filter.outputs.foundryHostingChanges == 'true' && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'))
permissions:
contents: read
id-token: "write"
runs-on: ubuntu-latest
environment: integration
# Scenario agents share @latest endpoints across PRs and branch runs.
concurrency:
group: dotnet-foundry-hosted-integration
cancel-in-progress: false
queue: max
env:
configuration: Release
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
python
- name: Free runner disk space
uses: ./.github/actions/free-runner-disk-space
- name: Setup dotnet
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
# Build the test csproj directly instead of a filtered slnx + -f override.
# The test project pins TargetFrameworks=net10.0 and its ProjectReference closure
# gives MSBuild a single-rooted graph, so each multi-targeted dependency is invoked
# exactly once for net10.0. This avoids the MSB3026/MSB3491/MSB4018/MSB3883 file-lock
# collisions caused by parallel inner-builds racing on shared bin/obj output paths
# under the previous slnx + global TFM override approach.
- name: Build Foundry hosted IT (and its deps)
shell: bash
run: dotnet build dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj -c "$configuration" --warnaserror
- name: Azure CLI Login
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# We rebuild and push the test container image on every IT run so framework code changes
# are picked up; the image tag is content-hashed across the test container source AND its
# framework project references, so identical content is a no-op push.
#
# The script always passes --no-dependencies to dotnet publish so publish never re-touches
# the framework lib DLLs the prior "Build Foundry hosted IT (and its deps)" step produced.
# This structurally eliminates the MSB3026 collision that VBCSCompiler from the prebuild
# would otherwise cause by holding file handles to those DLLs. Do not remove the prebuild
# step: the subsequent `dotnet test --no-build` step and the publish's ProjectReference
# resolution both depend on the prebuilt outputs being present.
- name: Build and push Foundry Hosted Agents test container
id: build-foundry-hosted-image
shell: pwsh
working-directory: ${{ github.workspace }}
run: |
$registry = "${{ vars.IT_HOSTED_AGENT_REGISTRY }}"
if ([string]::IsNullOrWhiteSpace($registry)) {
throw "IT_HOSTED_AGENT_REGISTRY not set in the integration environment."
}
& "${{ github.workspace }}/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-build-image.ps1" -Registry $registry | Tee-Object -FilePath $env:GITHUB_ENV -Append
- name: Run Foundry Hosted Agents Integration Tests
shell: pwsh
working-directory: dotnet
run: |
dotnet test --project tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj `
-c $env:configuration `
--no-build -v Normal `
--report-xunit-trx `
--ignore-exit-code 8 `
--filter-trait "Category=FoundryHostedAgents"
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.IT_HOSTED_AGENT_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.IT_HOSTED_AGENT_MODEL_DEPLOYMENT_NAME }}
# Use the dedicated hosted-agent Search index for both Search scenarios.
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
AZURE_SEARCH_INDEX_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_SEARCH_INDEX_NAME }}
# IT_HOSTED_AGENT_IMAGE was exported into $GITHUB_ENV by the previous step.
# This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed
dotnet-build-and-test-check:
if: always()
runs-on: ubuntu-latest
needs: [dotnet-build, dotnet-test, dotnet-integration-test, dotnet-foundry-hosted-it]
steps:
- name: Get Date
shell: bash
run: |
echo "date=$(date +'%m/%d/%Y %H:%M:%S')" >> "$GITHUB_ENV"
- name: Run Type is Daily
if: ${{ github.event_name == 'schedule' }}
shell: bash
run: |
echo "run_type=Daily" >> "$GITHUB_ENV"
- name: Run Type is Manual
if: ${{ github.event_name == 'workflow_dispatch' }}
shell: bash
run: |
echo "run_type=Manual" >> "$GITHUB_ENV"
- name: Run Type is ${{ github.event_name }}
if: ${{ github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'}}
shell: bash
run: |
echo "run_type=${{ github.event_name }}" >> "$GITHUB_ENV"
- name: Fail workflow if tests failed
id: check_tests_failed
if: contains(join(needs.*.result, ','), 'failure')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: core.setFailed('Integration Tests Failed!')
- name: Fail workflow if tests cancelled
id: check_tests_cancelled
if: contains(join(needs.*.result, ','), 'cancelled')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: core.setFailed('Integration Tests Cancelled!')
# Integration test trend report (aggregates JUnit XML results from dotnet test jobs)
dotnet-integration-test-report:
name: Integration Test Report
if: >
always() &&
(needs.dotnet-integration-test.result == 'success' ||
needs.dotnet-integration-test.result == 'failure')
needs: [dotnet-integration-test]
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
.github/actions/python-setup
python
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: "3.13"
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: dotnet-test-results-*
path: dotnet-test-results/
- name: Restore report history cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
restore-keys: |
dotnet-integration-report-history-
- name: Generate trend report
run: >
uv run python scripts/integration_test_report/aggregate.py
../dotnet-test-results/
dotnet-integration-report-history.json
dotnet-integration-test-report.md
- name: Post to Job Summary
if: always()
run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
- name: Upload trend report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dotnet-integration-test-report
path: |
python/dotnet-integration-test-report.md
python/dotnet-integration-report-history.json