diff --git a/pipelines/azure-benchmarks.yml b/pipelines/azure-benchmarks.yml index e2f9ab13a..5c67d6dd4 100644 --- a/pipelines/azure-benchmarks.yml +++ b/pipelines/azure-benchmarks.yml @@ -19,10 +19,10 @@ stages: - job: RunBenchmark displayName: 'Run Copilot Benchmarks' steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.12' - inputs: - versionSpec: '3.12' + - template: /pipelines/templates/steps/install-uv.yml + + - pwsh: uv python install 3.12 && uv python pin 3.12 + displayName: 'Install Python 3.12 with uv' - task: PipAuthenticate@1 displayName: 'Authenticate pip with MicrosoftSweBench feed' @@ -36,4 +36,4 @@ stages: addSpnToEnvironment: true scriptType: 'pscore' scriptLocation: 'scriptPath' - scriptPath: $(Build.SourcesDirectory)/pipelines/scripts/Invoke-CopilotBenchmarks.ps1 + scriptPath: $(Build.SourcesDirectory)/pipelines/scripts/Invoke-CopilotBenchmarks.ps1 \ No newline at end of file diff --git a/pipelines/scripts/Activate-Venv.ps1 b/pipelines/scripts/Activate-Venv.ps1 new file mode 100644 index 000000000..1db307b3f --- /dev/null +++ b/pipelines/scripts/Activate-Venv.ps1 @@ -0,0 +1,53 @@ +<#! +.SYNOPSIS +Activates a virtual environment for a CI machine. Any further usages of "python" will utilize this virtual environment. + +.DESCRIPTION +When activating a virtual environment, only a few things are actually functionally changed on the machine. + +# 1. PATH = path to the bin directory of the virtual env. "Scripts" on windows machines +# 2. VIRTUAL_ENV = path to root of the virtual env +# 3. VIRTUAL_ENV_PROMPT = the prompt that is displayed next to the CLI cursor when the virtual env is active +# within a CI machine, we only need the PATH and VIRTUAL_ENV variables to be set. +# 4. (optional and inconsistently) _OLD_VIRTUAL_PATH = the PATH before the virtual env was activated. This is not set in this script. + +.PARAMETER VenvName +The name of the virtual environment to activate. + +.PARAMETER RepoRoot +The root of the repository. +#> +param ( + [string]$VenvName = "venv" +) + +Set-StrictMode -Version 4 +$ErrorActionPreference = "Stop" + +$repoRoot = Join-Path $PSScriptRoot ".." ".." -Resolve +$venvPath = Join-Path $repoRoot $VenvName +$pipelineRun = $env:TF_BUILD -eq "True" + +if (-not (Test-Path $venvPath)) { + Write-Error "Virtual environment '$venvPath' does not exist at $venvPath" + exit 1 +} + +Write-Host "Activating virtual environment '$VenvName' via VIRTUAL_ENV variable at $venvPath.'" +$env:VIRTUAL_ENV = $venvPath +if ($pipelineRun) { + Write-Host "##vso[task.setvariable variable=VIRTUAL_ENV]$($env:VIRTUAL_ENV)" +} + +$venvBinPath = $IsWindows ? (Join-Path $venvPath "Scripts") : (Join-Path $venvPath "bin") + +Write-Host "Prepending path with $venvBinPath" +$env:PATH = $IsWindows ? "$venvBinPath;$($env:PATH)" : "$venvBinPath`:$($env:PATH)" +if ($pipelineRun) { + Write-Host "##vso[task.prependpath]$venvBinPath" +} + +if ($pipelineRun) { + Write-Host "Unset of PYTHONHOME" + Write-Host "##vso[task.setvariable variable=PYTHONHOME]" +} diff --git a/pipelines/scripts/Create-Venv.ps1 b/pipelines/scripts/Create-Venv.ps1 new file mode 100644 index 000000000..0743b4c39 --- /dev/null +++ b/pipelines/scripts/Create-Venv.ps1 @@ -0,0 +1,40 @@ +<#! +.SYNOPSIS +Creates a virtual environment for a CI machine. + +.DESCRIPTION +If the virtual environment directory already exists, it will skip the creation. The location of the virtual environment will be stored in a variable +named _LOCATION. The location will be RepoRoot + VenvName. + +.PARAMETER VenvName +The name of the virtual environment which will be created. + +.PARAMETER RepoRoot +The root of the repository. +#> +param( + [string] $VenvName = "venv" +) + +$repoRoot = Join-Path $PSScriptRoot ".." ".." -Resolve +$venvPath = Join-Path $repoRoot $VenvName + +if (!(Test-Path $venvPath)) { + if (Get-Command uv -ErrorAction SilentlyContinue) { + Write-Host "Creating virtual environment '$VenvName' using uv." + uv venv $venvPath --verbose + } + else { + $invokingPython = (Get-Command "python").Source + + Write-Host "Creating virtual environment '$VenvName' using virtualenv and python located at '$invokingPython'." + python -m pip install virtualenv==20.25.1 + python -m virtualenv $venvPath + } + $pythonVersion = python --version + Write-Host "Virtual environment '$VenvName' created at directory path '$venvPath' utilizing python version $pythonVersion." + Write-Host "##vso[task.setvariable variable=$($VenvName)_LOCATION]$venvPath" +} +else { + Write-Host "Virtual environment '$VenvName' already exists. Skipping creation." +} \ No newline at end of file diff --git a/pipelines/scripts/Invoke-CopilotBenchmarks.ps1 b/pipelines/scripts/Invoke-CopilotBenchmarks.ps1 index 1cf03886e..cef5108a5 100644 --- a/pipelines/scripts/Invoke-CopilotBenchmarks.ps1 +++ b/pipelines/scripts/Invoke-CopilotBenchmarks.ps1 @@ -24,97 +24,104 @@ .LINK https://github.com/devdiv-microsoft/MicrosoftSweBench/wiki #> +param( + [string]$Benchmark = "azure", + [string]$Model = "claude-sonnet-4.5-autodev-test", + [switch]$NoWait +) - param( - [string]$Benchmark = "azure", - [string]$Model = "claude-sonnet-4.5-autodev-test", - [switch]$NoWait - ) +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" - Set-StrictMode -Version Latest - $ErrorActionPreference = "Stop" +if (!$Benchmark) { + throw "Benchmark parameter is required." +} - if (!$Benchmark) { - throw "Benchmark parameter is required." - } - - if (!$Model) { - throw "Model parameter is required." - } - - $vaultName = "kv-msbench-eval-azuremcp" - $secretName = "azure-eval-gh-pat" +if (!$Model) { + throw "Model parameter is required." +} - Write-Host "Benchmark: $Benchmark" - Write-Host "Model: $Model" - Write-Host "NoWait: $NoWait" +$repoRoot = Join-Path $PSScriptRoot ".." ".." -Resolve +$vaultName = "kv-msbench-eval-azuremcp" +$secretName = "azure-eval-gh-pat" - $pipelineRun = $env:TF_BUILD -eq "True" +Write-Host "Benchmark: $Benchmark" +Write-Host "Model: $Model" +Write-Host "NoWait: $NoWait" - # --- Retrieve GitHub PAT from KeyVault --- - try { - Write-Host "Retrieving GitHub PAT from KeyVault $vaultName secret $secretName" - $pat = az keyvault secret show --vault-name $vaultName --name $secretName --query value -o tsv +$pipelineRun = $env:TF_BUILD -eq "True" - if (!$pat) { - throw "Secret $secretName not found in KeyVault $vaultName." - } +. "$PSScriptRoot/Create-Venv.ps1" -VenvName "venv" -RepoRoot $repoRoot +. "$PSScriptRoot/Activate-Venv.ps1" -VenvName "venv" -RepoRoot $repoRoot - $env:GITHUB_MCP_SERVER_TOKEN = $pat - - # Log the PAT as a secret variable to avoid exposing it in logs - if ($pipelineRun) { - Write-Host "##vso[task.setsecret]$pat" - } - } - catch { - throw "Failed to retrieve GitHub PAT from KeyVault: $_" - } +# --- Retrieve GitHub PAT from KeyVault --- +try { + Write-Host "Retrieving GitHub PAT from KeyVault $vaultName secret $secretName" + $pat = az keyvault secret show --vault-name $vaultName --name $secretName --query value -o tsv - # --- Feed auth is handled by the PipAuthenticate@1 pipeline task --- - # PipAuthenticate sets PIP_EXTRA_INDEX_URL for the azure-sdk/internal/MicrosoftSweBench feed. - if ($env:PIP_EXTRA_INDEX_URL) { - Write-Host "PIP_EXTRA_INDEX_URL is set (feed auth configured by PipAuthenticate task)" - } else { - Write-Warning "PIP_EXTRA_INDEX_URL is not set. Feed authentication may fail. Ensure PipAuthenticate@1 runs before this script." + if (!$pat) { + throw "Secret $secretName not found in KeyVault $vaultName." } - $pythonCommand = Get-Command python - Write-Host "Using python from: $($pythonCommand.Path). Version: $(python --version 2>&1)" - - Write-Host "Install/upgrade pip" - python -m pip install --upgrade pip - if ($LASTEXITCODE -ne 0) { - throw "pip install/upgrade failed with exit code $LASTEXITCODE" + $env:GITHUB_MCP_SERVER_TOKEN = $pat + + # Log the PAT as a secret variable to avoid exposing it in logs + if ($pipelineRun) { + Write-Host "##vso[task.setsecret]$pat" } - - Write-Host "Installing/upgrading MSBench CLI" - python -m pip install msbench-cli --no-input - if ($LASTEXITCODE -ne 0) { - throw "pip install msbench-cli failed with exit code $LASTEXITCODE" +} +catch { + throw "Failed to retrieve GitHub PAT from KeyVault: $_" +} + +# --- Feed authentication --- +# In CI, PipAuthenticate@1 sets PIP_EXTRA_INDEX_URL automatically. +# For local runs, fall back to az CLI token acquisition. +if ($env:PIP_EXTRA_INDEX_URL) { + Write-Host "PIP_EXTRA_INDEX_URL is set (feed auth configured by PipAuthenticate task). Forwarding to UV_EXTRA_INDEX_URL for MSBench CLI." + $env:UV_EXTRA_INDEX_URL = $env:PIP_EXTRA_INDEX_URL +} else { + Write-Host "PIP_EXTRA_INDEX_URL not set — acquiring Azure DevOps AAD token for local feed auth" + $feedUrl = "https://pkgs.dev.azure.com/azure-sdk/internal/_packaging/MicrosoftSweBench/pypi/simple/" + $adoResourceId = "499b84ac-1321-427f-aa17-267ca6975798" + $adoAccessToken = az account get-access-token --resource $adoResourceId --query accessToken -o tsv + + if (!$adoAccessToken) { + throw "Failed to acquire Azure DevOps AAD token. Run 'az login' first." } - Write-Host "MSBench CLI version" - & 'msbench-cli' version - if ($LASTEXITCODE -ne 0) { - throw "msbench-cli version failed with exit code $LASTEXITCODE" - } - - $runArgs = @( - "run", - "--agent", "github-copilot-cli", - "--benchmark", $Benchmark, - "--model", $Model, - "--env", "GITHUB_MCP_SERVER_TOKEN" - ) - - if ($NoWait) { - $runArgs += "--no-wait" - } - - Write-Host "Running: msbench-cli $($runArgs -join ' ')" - & 'msbench-cli' @runArgs - - if ($LASTEXITCODE -ne 0) { - throw "msbench-cli run failed with exit code $LASTEXITCODE" - } + $encodedToken = [System.Uri]::EscapeDataString($adoAccessToken) + $env:UV_EXTRA_INDEX_URL = $feedUrl -replace "https://", "https://vsts:$encodedToken@" + Write-Host "UV_EXTRA_INDEX_URL set via az CLI token" +} + + +Write-Host "`n> uv pip install msbench-cli" +& uv pip install msbench-cli +if ($LASTEXITCODE -ne 0) { + throw "uv pip install msbench-cli failed with exit code $LASTEXITCODE" +} + +Write-Host "`n> uv run 'msbench-cli' version" +uv run 'msbench-cli' version +if ($LASTEXITCODE -ne 0) { + throw "uv run msbench-cli failed with exit code $LASTEXITCODE" +} + +$runArgs = @( + "run", + "--agent", "github-copilot-cli", + "--benchmark", $Benchmark, + "--model", $Model, + "--env", "GITHUB_MCP_SERVER_TOKEN" +) + +if ($NoWait) { + $runArgs += "--no-wait" +} + +Write-Host "`n> msbench-cli $($runArgs -join ' ')" +uv run 'msbench-cli' @runArgs +if ($LASTEXITCODE -ne 0) { + throw "msbench-cli run failed with exit code $LASTEXITCODE" +} diff --git a/pipelines/templates/steps/install-uv.yml b/pipelines/templates/steps/install-uv.yml new file mode 100644 index 000000000..3f3993dbf --- /dev/null +++ b/pipelines/templates/steps/install-uv.yml @@ -0,0 +1,25 @@ +steps: + - task: Bash@3 + displayName: 'Install uv (Linux/macOS)' + inputs: + targetType: inline + script: | + curl -LsSf https://astral.sh/uv/install.sh | sh + condition: or(eq(variables['Agent.OS'], 'Linux'), eq(variables['Agent.OS'], 'Darwin')) + + - task: Bash@3 + inputs: + targetType: inline + script: | + echo "##vso[task.prependpath]$HOME/.local/bin" + displayName: 'Prepend path for MacOS' + condition: eq(variables['Agent.OS'], 'Darwin') + + - task: PowerShell@2 + displayName: 'Install uv (Windows)' + inputs: + targetType: inline + script: | + iex (irm https://astral.sh/uv/install.ps1) + Write-Host "##vso[task.prependpath]$env:USERPROFILE\.local\bin" + condition: eq(variables['Agent.OS'], 'Windows_NT') \ No newline at end of file