chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
#requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
One-time bootstrap of stable hosted agents for the Foundry.Hosting.IntegrationTests suite.
|
||||
|
||||
.DESCRIPTION
|
||||
The IT fixture targets stable, scenario-keyed agent names (e.g. it-happy-path) and only
|
||||
manages versions on each test run. The agent itself must already exist AND its managed
|
||||
identity must hold the Foundry User role on the project scope, otherwise inbound
|
||||
inference calls fail with HTTP 500 PermissionDenied.
|
||||
|
||||
This script idempotently creates each scenario agent (with a placeholder version) and
|
||||
grants Foundry User on the project to its managed identity. Re-run it safely; existing
|
||||
agents and role assignments are left in place.
|
||||
|
||||
.PARAMETER ProjectEndpoint
|
||||
Foundry project endpoint, e.g. https://<account>.services.ai.azure.com/api/projects/<project>
|
||||
|
||||
.PARAMETER Image
|
||||
Container image reference for the placeholder version (e.g. <acr>.azurecr.io/foundry-hosting-it:<tag>).
|
||||
Use the value emitted by scripts/it-build-image.ps1.
|
||||
|
||||
.NOTES
|
||||
Per-scenario data-plane RBAC (e.g. `Search Index Data Reader` on the Azure AI Search service
|
||||
for the `azure-search-rag` scenario) is intentionally NOT performed by this script. Search,
|
||||
Cosmos, and other backing services are treated as pre-existing infrastructure. Grant the
|
||||
scenario-specific data role to the agent's managed identity manually after the first run
|
||||
(see dotnet/tests/Foundry.Hosting.IntegrationTests/README.md).
|
||||
|
||||
.EXAMPLE
|
||||
./it-bootstrap-agents.ps1 `
|
||||
-ProjectEndpoint "https://my-acct.services.ai.azure.com/api/projects/my-proj" `
|
||||
-Image "myacr.azurecr.io/foundry-hosting-it:abc123"
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $ProjectEndpoint,
|
||||
[Parameter(Mandatory)] [string] $Image
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Scenarios = @(
|
||||
'happy-path',
|
||||
'store-config',
|
||||
'tool-calling',
|
||||
'tool-calling-approval',
|
||||
'mcp-toolbox',
|
||||
'toolbox-oauth-consent',
|
||||
'custom-storage',
|
||||
'memory',
|
||||
'azure-search-rag',
|
||||
'session-files',
|
||||
'agent-skills',
|
||||
'unsupported-protocol'
|
||||
)
|
||||
|
||||
# Resolve project ARM scope from the endpoint.
|
||||
$endpointUri = [Uri]$ProjectEndpoint
|
||||
$accountName = $endpointUri.Host.Split('.')[0]
|
||||
$projectName = ($endpointUri.AbsolutePath.TrimEnd('/') -split '/')[-1]
|
||||
$accountInfo = az cognitiveservices account list --query "[?name=='$accountName'].{name:name, rg:resourceGroup, sub:id}" | ConvertFrom-Json
|
||||
if (-not $accountInfo) { throw "Could not find Cognitive Services account '$accountName'." }
|
||||
$rg = $accountInfo[0].rg
|
||||
$sub = ($accountInfo[0].sub -split '/')[2]
|
||||
$projectScope = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$accountName/projects/$projectName"
|
||||
Write-Host "Project scope: $projectScope"
|
||||
|
||||
$tok = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv
|
||||
$headers = @{
|
||||
Authorization = "Bearer $tok"
|
||||
'Foundry-Features' = 'HostedAgents=V1Preview'
|
||||
'Content-Type' = 'application/json'
|
||||
}
|
||||
|
||||
foreach ($scenario in $Scenarios) {
|
||||
$agentName = "it-$scenario"
|
||||
Write-Host ""
|
||||
Write-Host "=== $agentName ==="
|
||||
|
||||
# 1. Ensure the agent exists. Create a placeholder version if it doesn't.
|
||||
$agent = $null
|
||||
try {
|
||||
$agent = Invoke-RestMethod -Method GET -Headers $headers `
|
||||
-Uri "$ProjectEndpoint/agents/$agentName`?api-version=v1"
|
||||
Write-Host " agent exists"
|
||||
} catch {
|
||||
if ($_.Exception.Response.StatusCode -ne 404) { throw }
|
||||
}
|
||||
|
||||
if (-not $agent) {
|
||||
Write-Host " creating placeholder version..."
|
||||
$body = @{
|
||||
definition = @{
|
||||
kind = 'hosted'
|
||||
container_protocol_versions = @(@{ protocol = 'responses'; version = '2.0.0' })
|
||||
cpu = '0.25'
|
||||
memory = '0.5Gi'
|
||||
environment_variables = @{ IT_SCENARIO = $scenario }
|
||||
image = $Image
|
||||
}
|
||||
metadata = @{ enableVnextExperience = 'true' }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
Invoke-RestMethod -Method POST -Headers $headers `
|
||||
-Uri "$ProjectEndpoint/agents/$agentName/versions`?api-version=v1" `
|
||||
-Body $body | Out-Null
|
||||
Start-Sleep 5
|
||||
$agent = Invoke-RestMethod -Method GET -Headers $headers `
|
||||
-Uri "$ProjectEndpoint/agents/$agentName`?api-version=v1"
|
||||
}
|
||||
|
||||
$principalId = $agent.versions.latest.instance_identity.principal_id
|
||||
Write-Host " agent MI: $principalId"
|
||||
|
||||
# 2. PATCH the agent endpoint to route via @latest if not already configured.
|
||||
# Using @latest means each new version added by the IT fixture automatically becomes the
|
||||
# served version, no per-run PATCH needed (which is good because the strongly-typed
|
||||
# PATCH wrapper is alpha-only on Azure.AI.Projects right now).
|
||||
$hasLatestSelector = $agent.agent_endpoint -and `
|
||||
($agent.agent_endpoint.version_selector.version_selection_rules | Where-Object { $_.agent_version -eq '@latest' })
|
||||
if ($hasLatestSelector) {
|
||||
Write-Host " endpoint already routes via @latest"
|
||||
} else {
|
||||
Write-Host " patching endpoint to route via @latest..."
|
||||
$patchBody = @{
|
||||
agent_endpoint = @{
|
||||
version_selector = @{
|
||||
version_selection_rules = @(@{
|
||||
type = 'FixedRatio'
|
||||
agent_version = '@latest'
|
||||
traffic_percentage = 100
|
||||
})
|
||||
}
|
||||
protocols = @('responses')
|
||||
}
|
||||
} | ConvertTo-Json -Depth 10
|
||||
Invoke-RestMethod -Method PATCH -Headers $headers `
|
||||
-Uri "$ProjectEndpoint/agents/$agentName`?api-version=v1" `
|
||||
-Body $patchBody | Out-Null
|
||||
}
|
||||
|
||||
# 3. Grant Foundry User on the project scope to the agent MI (idempotent).
|
||||
$existing = az role assignment list --assignee $principalId --scope $projectScope `
|
||||
--query "[?roleDefinitionName=='Foundry User']" 2>$null | ConvertFrom-Json
|
||||
if ($existing) {
|
||||
Write-Host " role already assigned"
|
||||
} else {
|
||||
Write-Host " granting Foundry User..."
|
||||
$maxAttempts = 12
|
||||
$granted = $false
|
||||
for ($i = 1; $i -le $maxAttempts; $i++) {
|
||||
$output = az role assignment create `
|
||||
--assignee-object-id $principalId `
|
||||
--assignee-principal-type ServicePrincipal `
|
||||
--role 'Foundry User' `
|
||||
--scope $projectScope 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$granted = $true
|
||||
break
|
||||
}
|
||||
if ($output -match 'Cannot find user or service principal in graph') {
|
||||
Write-Host " attempt $i/$maxAttempts : MI not yet in AAD graph, retrying in 15s..."
|
||||
Start-Sleep 15
|
||||
continue
|
||||
}
|
||||
throw "az role assignment failed: $output"
|
||||
}
|
||||
if (-not $granted) {
|
||||
throw "MI '$principalId' did not appear in AAD graph after $maxAttempts attempts."
|
||||
}
|
||||
Write-Host " granted (RBAC propagation may take 1-3 minutes)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done. Wait ~3 minutes after first-time grants before running the tests."
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds and pushes the Foundry.Hosting.IntegrationTests.TestContainer image to a container registry.
|
||||
|
||||
.DESCRIPTION
|
||||
The integration tests in dotnet/tests/Foundry.Hosting.IntegrationTests provision real
|
||||
Foundry hosted agents that point at a container image. This script builds and pushes that
|
||||
image, then emits the IT_HOSTED_AGENT_IMAGE=... line that the tests read from the
|
||||
environment.
|
||||
|
||||
.PARAMETER Registry
|
||||
The container registry login server, e.g. mycompany.azurecr.io. Required. There is no
|
||||
default because every team and every dev may use a different registry.
|
||||
|
||||
.PARAMETER Repository
|
||||
Image repository name within the registry. Defaults to foundry-hosting-it.
|
||||
|
||||
.PARAMETER TestContainerProject
|
||||
Path to the test container csproj. Defaults to the in repo location.
|
||||
|
||||
.EXAMPLE
|
||||
PS> ./scripts/it-build-image.ps1 -Registry mycompany.azurecr.io
|
||||
IT_HOSTED_AGENT_IMAGE=mycompany.azurecr.io/foundry-hosting-it:abc123def456
|
||||
|
||||
.EXAMPLE
|
||||
Local dev, set the env var directly:
|
||||
PS> $env:IT_REGISTRY = "mycompany.azurecr.io"
|
||||
PS> $env:IT_HOSTED_AGENT_IMAGE = (./scripts/it-build-image.ps1 -Registry $env:IT_REGISTRY | Select-String IT_HOSTED_AGENT_IMAGE).Line.Split('=', 2)[1]
|
||||
|
||||
.EXAMPLE
|
||||
CI workflow, assumes IT_REGISTRY is set in the environment:
|
||||
- name: Build IT image
|
||||
run: pwsh ./scripts/it-build-image.ps1 -Registry $env:IT_REGISTRY | Tee-Object -FilePath $env:GITHUB_ENV
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $Registry,
|
||||
|
||||
[string] $Repository = "foundry-hosting-it",
|
||||
|
||||
[string] $TestContainerProject = "dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Resolve to the repo root regardless of the caller's PWD so all relative paths used below
|
||||
# (TestContainerProject, the framework src dirs hashed for the image tag) resolve correctly.
|
||||
# This script lives at <repoRoot>/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/.
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "../../../..")).Path
|
||||
Push-Location $RepoRoot
|
||||
try {
|
||||
|
||||
if (-not (Test-Path $TestContainerProject)) {
|
||||
throw "Test container project not found at '$TestContainerProject' (repo root '$RepoRoot')."
|
||||
}
|
||||
|
||||
# Strip any scheme/trailing slash from the registry, then derive the ACR short name.
|
||||
$Registry = $Registry -replace '^https?://', '' -replace '/+$', ''
|
||||
$registryHost = $Registry.Split('.')[0]
|
||||
if ([string]::IsNullOrWhiteSpace($registryHost)) {
|
||||
throw "Could not derive ACR short name from -Registry '$Registry'."
|
||||
}
|
||||
|
||||
# Hash the test container source content AND the source of all referenced framework projects
|
||||
# so any edit (in TestContainer OR in dotnet/src/Microsoft.Agents.AI.Foundry*/) produces a new
|
||||
# tag. The TestContainer image embeds compiled output of those projects, so a framework code
|
||||
# change must invalidate the tag for `docker push` to publish a new layer; a TestContainer-only
|
||||
# hash silently reused stale images on framework edits.
|
||||
#
|
||||
# Keep this list in sync with the `foundryHosting` paths-filter in
|
||||
# .github/workflows/dotnet-build-and-test.yml so CI gating and image tagging cover the same set.
|
||||
$hashedDirs = @(
|
||||
$TestContainerProject,
|
||||
"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"
|
||||
)
|
||||
$sourceFiles = @()
|
||||
foreach ($dir in $hashedDirs) {
|
||||
if (Test-Path $dir) {
|
||||
$sourceFiles += @(git -c core.quotepath=false ls-files -- $dir)
|
||||
}
|
||||
}
|
||||
if ($sourceFiles.Count -eq 0) {
|
||||
throw "No tracked files found under any of: $($hashedDirs -join ', ')"
|
||||
}
|
||||
$fileHashes = git hash-object -- $sourceFiles
|
||||
$shaInput = ($fileHashes -join "`n" | git hash-object --stdin).Trim()
|
||||
$tag = $shaInput.Substring(0, 12)
|
||||
$image = "$Registry/$Repository`:$tag"
|
||||
|
||||
Write-Host "Publishing $TestContainerProject ..." -ForegroundColor Cyan
|
||||
$out = Join-Path $TestContainerProject "out"
|
||||
if (Test-Path $out) {
|
||||
Remove-Item -Recurse -Force $out
|
||||
}
|
||||
|
||||
# Always tell publish to skip ProjectReference rebuilds via --no-dependencies. Publish
|
||||
# resolves TestContainer's framework lib references (Foundry, Foundry.Hosting and their
|
||||
# transitive deps) by reading the prebuilt DLLs at src/<lib>/bin/Release/net10.0/*.dll.
|
||||
# This:
|
||||
# 1) Structurally avoids the MSB3026 "file is being used by another process" race that
|
||||
# occurs when publish overwrites the same DLL paths a prior `dotnet build` produced
|
||||
# while VBCSCompiler from that build still holds file handles.
|
||||
# 2) Avoids needlessly rebuilding identical managed (RID-agnostic) library DLLs.
|
||||
# Callers MUST run `dotnet build dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj -c Release`
|
||||
# (or equivalent) first so those prebuilt DLLs exist. The CI workflow does this in the
|
||||
# preceding "Build Foundry hosted IT (and its deps)" step.
|
||||
$prebuildProbes = @(
|
||||
"dotnet/src/Microsoft.Agents.AI.Foundry/bin/Release/net10.0/Microsoft.Agents.AI.Foundry.dll",
|
||||
"dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/bin/Release/net10.0/Microsoft.Agents.AI.Foundry.Hosting.dll"
|
||||
)
|
||||
$missingPrebuilds = @($prebuildProbes | Where-Object { -not (Test-Path $_) })
|
||||
if ($missingPrebuilds.Count -gt 0) {
|
||||
$msg = @(
|
||||
"Required prebuilt outputs not found:"
|
||||
($missingPrebuilds | ForEach-Object { " - $_" })
|
||||
""
|
||||
"Publish runs with --no-dependencies and consumes prebuilt DLLs in place. Build the"
|
||||
"test project first so its ProjectReference closure populates src/<lib>/bin/Release/net10.0/:"
|
||||
" dotnet build dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj -c Release"
|
||||
) -join "`n"
|
||||
throw $msg
|
||||
}
|
||||
|
||||
dotnet publish $TestContainerProject -c Release -f net10.0 -r linux-musl-x64 --self-contained false --no-dependencies -o $out --tl:off | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet publish failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
Write-Host "Building $image ..." -ForegroundColor Cyan
|
||||
docker build -t $image -f (Join-Path $TestContainerProject "Dockerfile") $TestContainerProject | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "docker build failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
Write-Host "Pushing $image ..." -ForegroundColor Cyan
|
||||
az acr login -n $registryHost | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "az acr login failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
docker push $image | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "docker push failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
# Emit the env var line for shells / CI consumption.
|
||||
"IT_HOSTED_AGENT_IMAGE=$image"
|
||||
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
Reference in New Issue
Block a user