From 74af49030a66735ed1041a3515e67f79cd885914 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Wed, 28 Jan 2026 11:46:48 -0800 Subject: [PATCH 1/8] fix: Correct YAML syntax in update-e2e-status workflow --- .github/workflows/update-e2e-status.yml | 126 +++++++++++++++--------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/.github/workflows/update-e2e-status.yml b/.github/workflows/update-e2e-status.yml index d20dac05..a02fb7a4 100644 --- a/.github/workflows/update-e2e-status.yml +++ b/.github/workflows/update-e2e-status.yml @@ -22,12 +22,11 @@ jobs: with: ref: main - - name: Get job statuses and update README + - name: Get job statuses + id: get-status uses: actions/github-script@v7 with: script: | - const fs = require('fs'); - // Get the triggering workflow run or latest run let runId; if (context.payload.workflow_run) { @@ -43,12 +42,15 @@ jobs: }); if (runs.data.workflow_runs.length === 0) { console.log('No workflow runs found'); + core.setOutput('found', 'false'); return; } runId = runs.data.workflow_runs[0].id; } console.log(`Processing workflow run: ${runId}`); + core.setOutput('run-id', runId); + core.setOutput('run-url', `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`); // Get jobs for this run const jobs = await github.rest.actions.listJobsForWorkflowRun({ @@ -57,60 +59,90 @@ jobs: run_id: runId }); - // Map job names to their status badges + // Map job names to their status const jobStatusMap = { - 'Python OpenAI Agent': { key: 'python-openai', label: 'Python OpenAI' }, - 'Node.js OpenAI Agent': { key: 'nodejs-openai', label: 'Node.js OpenAI' }, - '.NET Semantic Kernel Agent': { key: 'dotnet-sk', label: '.NET Semantic Kernel' }, - '.NET Agent Framework Agent': { key: 'dotnet-af', label: '.NET Agent Framework' } + 'Python OpenAI Agent': 'python-openai', + 'Node.js OpenAI Agent': 'nodejs-openai', + '.NET Semantic Kernel Agent': 'dotnet-sk', + '.NET Agent Framework Agent': 'dotnet-af' }; - const statuses = {}; for (const job of jobs.data.jobs) { - const mapping = jobStatusMap[job.name]; - if (mapping) { - const conclusion = job.conclusion || job.status; - let badge; - if (conclusion === 'success') { - badge = `![${mapping.label}](https://img.shields.io/badge/${encodeURIComponent(mapping.label)}-passing-brightgreen)`; - } else if (conclusion === 'failure') { - badge = `![${mapping.label}](https://img.shields.io/badge/${encodeURIComponent(mapping.label)}-failing-red)`; - } else if (conclusion === 'in_progress' || job.status === 'in_progress') { - badge = `![${mapping.label}](https://img.shields.io/badge/${encodeURIComponent(mapping.label)}-running-yellow)`; - } else { - badge = `![${mapping.label}](https://img.shields.io/badge/${encodeURIComponent(mapping.label)}-pending-lightgrey)`; - } - statuses[mapping.key] = badge; - console.log(`${job.name}: ${conclusion} -> ${badge}`); + const key = jobStatusMap[job.name]; + if (key) { + const conclusion = job.conclusion || job.status || 'unknown'; + core.setOutput(key, conclusion); + console.log(`${job.name}: ${conclusion}`); } } - - // Read current README - let readme = fs.readFileSync('README.md', 'utf8'); - - // Generate new status table - const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - const newTable = `## E2E Test Status + core.setOutput('found', 'true'); + core.setOutput('date', new Date().toISOString().split('T')[0]); -| Sample | Status | -|--------|--------| -| Python OpenAI | ${statuses['python-openai'] || '![Python OpenAI](https://img.shields.io/badge/Python%20OpenAI-unknown-lightgrey)'} | -| Node.js OpenAI | ${statuses['nodejs-openai'] || '![Node.js OpenAI](https://img.shields.io/badge/Node.js%20OpenAI-unknown-lightgrey)'} | -| .NET Semantic Kernel | ${statuses['dotnet-sk'] || '![.NET SK](https://img.shields.io/badge/.NET%20SK-unknown-lightgrey)'} | -| .NET Agent Framework | ${statuses['dotnet-af'] || '![.NET AF](https://img.shields.io/badge/.NET%20AF-unknown-lightgrey)'} | + - name: Update README + if: steps.get-status.outputs.found == 'true' + env: + PYTHON_STATUS: ${{ steps.get-status.outputs.python-openai }} + NODEJS_STATUS: ${{ steps.get-status.outputs.nodejs-openai }} + DOTNET_SK_STATUS: ${{ steps.get-status.outputs.dotnet-sk }} + DOTNET_AF_STATUS: ${{ steps.get-status.outputs.dotnet-af }} + RUN_URL: ${{ steps.get-status.outputs.run-url }} + UPDATE_DATE: ${{ steps.get-status.outputs.date }} + run: | + python3 << 'EOF' + import os + import re + + def create_badge(name, status): + encoded_name = name.replace(' ', '%20').replace('.', '%2E') + if status == 'success': + return f'![{name}](https://img.shields.io/badge/{encoded_name}-passing-brightgreen)' + elif status == 'failure': + return f'![{name}](https://img.shields.io/badge/{encoded_name}-failing-red)' + elif status == 'in_progress': + return f'![{name}](https://img.shields.io/badge/{encoded_name}-running-yellow)' + else: + return f'![{name}](https://img.shields.io/badge/{encoded_name}-unknown-lightgrey)' + + python_badge = create_badge('Python OpenAI', os.environ.get('PYTHON_STATUS', 'unknown')) + nodejs_badge = create_badge('Node.js OpenAI', os.environ.get('NODEJS_STATUS', 'unknown')) + dotnet_sk_badge = create_badge('.NET SK', os.environ.get('DOTNET_SK_STATUS', 'unknown')) + dotnet_af_badge = create_badge('.NET AF', os.environ.get('DOTNET_AF_STATUS', 'unknown')) + run_url = os.environ.get('RUN_URL', '#') + update_date = os.environ.get('UPDATE_DATE', 'unknown') + + new_section = f'''## E2E Test Status -*Last updated: ${new Date().toISOString().split('T')[0]} | [View Run](${runUrl})*`; - - // Replace the status section - const statusRegex = /## E2E Test Status[\s\S]*?\n(?=\n>|$)/; - if (statusRegex.test(readme)) { - readme = readme.replace(statusRegex, newTable + '\n'); - } - - fs.writeFileSync('README.md', readme); - console.log('README updated successfully'); + | Sample | Status | + |--------|--------| + | Python OpenAI | {python_badge} | + | Node.js OpenAI | {nodejs_badge} | + | .NET Semantic Kernel | {dotnet_sk_badge} | + | .NET Agent Framework | {dotnet_af_badge} | + + *Last updated: {update_date} | [View Run]({run_url})* + + ''' + + # Remove leading spaces from heredoc + new_section = '\n'.join(line.lstrip() for line in new_section.split('\n')) + + with open('README.md', 'r') as f: + content = f.read() + + # Pattern to match the E2E Test Status section until the next section + pattern = r'## E2E Test Status\n.*?(?=\n> ####|\n## [^E]|\Z)' + + if re.search(pattern, content, re.DOTALL): + content = re.sub(pattern, new_section, content, flags=re.DOTALL) + with open('README.md', 'w') as f: + f.write(content) + print('README updated successfully') + else: + print('E2E Test Status section not found') + EOF - name: Commit and push changes + if: steps.get-status.outputs.found == 'true' run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" From da196518fa195079c7abfa84b6d25b90e06a3167 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Wed, 28 Jan 2026 12:38:29 -0800 Subject: [PATCH 2/8] fix: Apply Copilot review suggestions - proper URL encoding, empty string handling, textwrap.dedent, and job count tracking --- .github/workflows/update-e2e-status.yml | 34 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/update-e2e-status.yml b/.github/workflows/update-e2e-status.yml index a02fb7a4..8df80a4c 100644 --- a/.github/workflows/update-e2e-status.yml +++ b/.github/workflows/update-e2e-status.yml @@ -67,16 +67,24 @@ jobs: '.NET Agent Framework Agent': 'dotnet-af' }; + let jobsFound = 0; for (const job of jobs.data.jobs) { const key = jobStatusMap[job.name]; if (key) { const conclusion = job.conclusion || job.status || 'unknown'; core.setOutput(key, conclusion); console.log(`${job.name}: ${conclusion}`); + jobsFound++; } } - core.setOutput('found', 'true'); - core.setOutput('date', new Date().toISOString().split('T')[0]); + + if (jobsFound > 0) { + core.setOutput('found', 'true'); + core.setOutput('date', new Date().toISOString().split('T')[0]); + } else { + console.log('No matching jobs found'); + core.setOutput('found', 'false'); + } - name: Update README if: steps.get-status.outputs.found == 'true' @@ -91,9 +99,11 @@ jobs: python3 << 'EOF' import os import re + import textwrap + from urllib.parse import quote def create_badge(name, status): - encoded_name = name.replace(' ', '%20').replace('.', '%2E') + encoded_name = quote(name, safe='') if status == 'success': return f'![{name}](https://img.shields.io/badge/{encoded_name}-passing-brightgreen)' elif status == 'failure': @@ -103,12 +113,12 @@ jobs: else: return f'![{name}](https://img.shields.io/badge/{encoded_name}-unknown-lightgrey)' - python_badge = create_badge('Python OpenAI', os.environ.get('PYTHON_STATUS', 'unknown')) - nodejs_badge = create_badge('Node.js OpenAI', os.environ.get('NODEJS_STATUS', 'unknown')) - dotnet_sk_badge = create_badge('.NET SK', os.environ.get('DOTNET_SK_STATUS', 'unknown')) - dotnet_af_badge = create_badge('.NET AF', os.environ.get('DOTNET_AF_STATUS', 'unknown')) - run_url = os.environ.get('RUN_URL', '#') - update_date = os.environ.get('UPDATE_DATE', 'unknown') + python_badge = create_badge('Python OpenAI', os.environ.get('PYTHON_STATUS') or 'unknown') + nodejs_badge = create_badge('Node.js OpenAI', os.environ.get('NODEJS_STATUS') or 'unknown') + dotnet_sk_badge = create_badge('.NET SK', os.environ.get('DOTNET_SK_STATUS') or 'unknown') + dotnet_af_badge = create_badge('.NET AF', os.environ.get('DOTNET_AF_STATUS') or 'unknown') + run_url = os.environ.get('RUN_URL') or '#' + update_date = os.environ.get('UPDATE_DATE') or 'unknown' new_section = f'''## E2E Test Status @@ -123,14 +133,14 @@ jobs: ''' - # Remove leading spaces from heredoc - new_section = '\n'.join(line.lstrip() for line in new_section.split('\n')) + # Remove leading spaces from heredoc while preserving blank lines + new_section = textwrap.dedent(new_section) with open('README.md', 'r') as f: content = f.read() # Pattern to match the E2E Test Status section until the next section - pattern = r'## E2E Test Status\n.*?(?=\n> ####|\n## [^E]|\Z)' + pattern = r'## E2E Test Status\n.*?(?=\n> ####|\n## (?!E2E Test Status)|\Z)' if re.search(pattern, content, re.DOTALL): content = re.sub(pattern, new_section, content, flags=re.DOTALL) From e4c3ae36dccb8bc105b676297bc1fddc5c60d79f Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 29 Jan 2026 15:12:43 -0800 Subject: [PATCH 3/8] Split into multiple workflows --- .github/workflows/e2e-agent-samples.yml | 44 +--- .../workflows/e2e-dotnet-agent-framework.yml | 155 ++++++++++++++ .../workflows/e2e-dotnet-semantic-kernel.yml | 155 ++++++++++++++ .github/workflows/e2e-nodejs-openai.yml | 170 ++++++++++++++++ .github/workflows/e2e-orchestrator.yml | 81 ++++++++ .github/workflows/e2e-python-openai.yml | 189 ++++++++++++++++++ README.md | 10 +- 7 files changed, 766 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/e2e-dotnet-agent-framework.yml create mode 100644 .github/workflows/e2e-dotnet-semantic-kernel.yml create mode 100644 .github/workflows/e2e-nodejs-openai.yml create mode 100644 .github/workflows/e2e-orchestrator.yml create mode 100644 .github/workflows/e2e-python-openai.yml diff --git a/.github/workflows/e2e-agent-samples.yml b/.github/workflows/e2e-agent-samples.yml index 7a80f456..4d2b8d08 100644 --- a/.github/workflows/e2e-agent-samples.yml +++ b/.github/workflows/e2e-agent-samples.yml @@ -2,49 +2,27 @@ # Licensed under the MIT License. # ============================================================================= -# E2E Tests: Agent Samples +# E2E Tests: Agent Samples (DEPRECATED - Use e2e-orchestrator.yml instead) # ============================================================================= -# Runs E2E integration tests for all agent samples -# Uses external PowerShell scripts for maintainability +# This workflow has been replaced by individual E2E workflows: +# - e2e-python-openai.yml +# - e2e-nodejs-openai.yml +# - e2e-dotnet-semantic-kernel.yml +# - e2e-dotnet-agent-framework.yml +# - e2e-orchestrator.yml (triggers all of the above) +# +# This file is kept for reference but will not run. # ============================================================================= -name: E2E Agent Samples +name: E2E Agent Samples (Deprecated) on: - # Run nightly at 6 AM UTC - schedule: - - cron: '0 6 * * *' - push: - branches: - - main - - 'feature/*' - paths: - - 'python/**' - - 'nodejs/**' - - 'dotnet/**' - - 'tests/e2e/**' - - '.github/workflows/e2e-agent-samples.yml' - - 'scripts/e2e/**' - pull_request: - branches: - - main - paths: - - 'python/**' - - 'nodejs/**' - - 'dotnet/**' - - 'tests/e2e/**' - - '.github/workflows/e2e-agent-samples.yml' - - 'scripts/e2e/**' workflow_dispatch: inputs: sample: - description: 'Specific sample to test (leave empty for all)' + description: 'This workflow is deprecated. Use e2e-orchestrator.yml instead' required: false default: '' - debug: - description: 'Enable debug logging' - required: false - default: 'false' permissions: contents: read diff --git a/.github/workflows/e2e-dotnet-agent-framework.yml b/.github/workflows/e2e-dotnet-agent-framework.yml new file mode 100644 index 00000000..4f86a493 --- /dev/null +++ b/.github/workflows/e2e-dotnet-agent-framework.yml @@ -0,0 +1,155 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: E2E - .NET Agent Framework + +on: + push: + branches: [main] + paths: + - 'dotnet/agent-framework/**' + - 'scripts/e2e/**' + - '.github/workflows/e2e-dotnet-agent-framework.yml' + pull_request: + branches: [main] + paths: + - 'dotnet/agent-framework/**' + - 'scripts/e2e/**' + workflow_dispatch: + +permissions: + contents: read + +env: + SAMPLE_PATH: dotnet/agent-framework/sample-agent + AGENT_PORT: 3978 + SCRIPTS_PATH: scripts/e2e + E2E_TESTS_PATH: tests/e2e + +jobs: + dotnet-agent-framework: + name: .NET Agent Framework Agent + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + working-directory: ${{ env.SAMPLE_PATH }} + run: dotnet restore + + - name: Build + working-directory: ${{ env.SAMPLE_PATH }} + run: dotnet build --no-restore + + - name: Acquire Bearer Token (ROPC) + id: token + shell: pwsh + run: | + $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` + -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` + -TenantId "${{ secrets.MCP_TENANT_ID }}" ` + -Username "${{ secrets.MCP_TEST_USERNAME }}" ` + -Password "${{ secrets.MCP_TEST_PASSWORD }}" + echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT + echo "::add-mask::$token" + + - name: Copy ToolingManifest.json + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" + + - name: Generate appsettings.json + shell: pwsh + run: | + $configMappings = @{ + "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_ENDPOINT }}" + "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_API_KEY }}" + "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_DEPLOYMENT }}" + "TokenValidation:Enabled" = "false" + "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" + "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_AF_CLIENT_ID }}" + "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_AF_CLIENT_SECRET }}" + "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" + "Connections:ServiceConnection:Settings:AuthorityEndpoint" = "https://login.microsoftonline.com/${{ secrets.TENANT_ID }}" + "Connections:ServiceConnection:Settings:Scopes:0" = "5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default" + } + & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` + -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` + -ConfigMappings $configMappings + + - name: Start Agent + id: start-agent + shell: pwsh + run: | + $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` + -AgentPath "${{ env.SAMPLE_PATH }}" ` + -StartCommand "dotnet run --no-build" ` + -Port ${{ env.AGENT_PORT }} ` + -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` + -Environment "Development" ` + -Runtime "dotnet" + echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT + + - name: Verify Agent Running + shell: pwsh + run: | + $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" + if ($agentPid) { + try { + $proc = Get-Process -Id $agentPid -ErrorAction Stop + Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green + } catch { + Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red + throw "Agent process has stopped" + } + } + $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue + Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green + + - name: Restore E2E Test Dependencies + run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" + + - name: Run .NET E2E Tests + shell: pwsh + run: | + dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` + --no-restore ` + --verbosity normal ` + --logger "console;verbosity=detailed" ` + --logger "trx;LogFileName=test-results-dotnet-af.trx" ` + --filter "FullyQualifiedName~BasicConversation|FullyQualifiedName~Notification" + + - name: Capture Agent Logs + if: always() + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` + -AgentPath "${{ env.SAMPLE_PATH }}" ` + -OutputPath "${{ env.E2E_TESTS_PATH }}/TestResults" ` + -TestName "dotnet-af" + + - name: Stop Agent Process + if: always() + shell: pwsh + run: | + $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" + if ($agentPid) { + & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -ProcessId $agentPid + } + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-dotnet-af + path: | + ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx + ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt + retention-days: 7 diff --git a/.github/workflows/e2e-dotnet-semantic-kernel.yml b/.github/workflows/e2e-dotnet-semantic-kernel.yml new file mode 100644 index 00000000..02deae6e --- /dev/null +++ b/.github/workflows/e2e-dotnet-semantic-kernel.yml @@ -0,0 +1,155 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: E2E - .NET Semantic Kernel + +on: + push: + branches: [main] + paths: + - 'dotnet/semantic-kernel/**' + - 'scripts/e2e/**' + - '.github/workflows/e2e-dotnet-semantic-kernel.yml' + pull_request: + branches: [main] + paths: + - 'dotnet/semantic-kernel/**' + - 'scripts/e2e/**' + workflow_dispatch: + +permissions: + contents: read + +env: + SAMPLE_PATH: dotnet/semantic-kernel/sample-agent + AGENT_PORT: 3978 + SCRIPTS_PATH: scripts/e2e + E2E_TESTS_PATH: tests/e2e + +jobs: + dotnet-semantic-kernel: + name: .NET Semantic Kernel Agent + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + working-directory: ${{ env.SAMPLE_PATH }} + run: dotnet restore + + - name: Build + working-directory: ${{ env.SAMPLE_PATH }} + run: dotnet build --no-restore + + - name: Acquire Bearer Token (ROPC) + id: token + shell: pwsh + run: | + $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` + -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` + -TenantId "${{ secrets.MCP_TENANT_ID }}" ` + -Username "${{ secrets.MCP_TEST_USERNAME }}" ` + -Password "${{ secrets.MCP_TEST_PASSWORD }}" + echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT + echo "::add-mask::$token" + + - name: Copy ToolingManifest.json + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" + + - name: Generate appsettings.json + shell: pwsh + run: | + $configMappings = @{ + "AIServices:UseAzureOpenAI" = "true" + "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_ENDPOINT }}" + "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_API_KEY }}" + "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_DEPLOYMENT }}" + "TokenValidation:Enabled" = "false" + "TokenValidation:TenantId" = "${{ secrets.TENANT_ID }}" + "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" + "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_SK_CLIENT_ID }}" + "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_SK_CLIENT_SECRET }}" + "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" + } + & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` + -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` + -ConfigMappings $configMappings + + - name: Start Agent + id: start-agent + shell: pwsh + run: | + $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` + -AgentPath "${{ env.SAMPLE_PATH }}" ` + -StartCommand "dotnet run --no-build" ` + -Port ${{ env.AGENT_PORT }} ` + -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` + -Environment "Development" ` + -Runtime "dotnet" + echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT + + - name: Verify Agent Running + shell: pwsh + run: | + $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" + if ($agentPid) { + try { + $proc = Get-Process -Id $agentPid -ErrorAction Stop + Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green + } catch { + Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red + throw "Agent process has stopped" + } + } + $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue + Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green + + - name: Restore E2E Test Dependencies + run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" + + - name: Run .NET E2E Tests + shell: pwsh + run: | + dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` + --no-restore ` + --verbosity normal ` + --logger "console;verbosity=detailed" ` + --logger "trx;LogFileName=test-results-dotnet-sk.trx" ` + --filter "FullyQualifiedName~BasicConversation|FullyQualifiedName~Notification" + + - name: Capture Agent Logs + if: always() + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` + -AgentPath "${{ env.SAMPLE_PATH }}" ` + -OutputPath "${{ env.E2E_TESTS_PATH }}/TestResults" ` + -TestName "dotnet-sk" + + - name: Stop Agent Process + if: always() + shell: pwsh + run: | + $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" + if ($agentPid) { + & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -ProcessId $agentPid + } + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-dotnet-sk + path: | + ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx + ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt + retention-days: 7 diff --git a/.github/workflows/e2e-nodejs-openai.yml b/.github/workflows/e2e-nodejs-openai.yml new file mode 100644 index 00000000..cb5af497 --- /dev/null +++ b/.github/workflows/e2e-nodejs-openai.yml @@ -0,0 +1,170 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: E2E - Node.js OpenAI + +on: + push: + branches: [main] + paths: + - 'nodejs/openai/**' + - 'scripts/e2e/**' + - '.github/workflows/e2e-nodejs-openai.yml' + pull_request: + branches: [main] + paths: + - 'nodejs/openai/**' + - 'scripts/e2e/**' + workflow_dispatch: + +permissions: + contents: read + +env: + SAMPLE_PATH: nodejs/openai/sample-agent + AGENT_PORT: 3979 + SCRIPTS_PATH: scripts/e2e + E2E_TESTS_PATH: tests/e2e + +jobs: + nodejs-openai: + name: Node.js OpenAI Agent + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + working-directory: ${{ env.SAMPLE_PATH }} + run: | + if (Test-Path "package-lock.json") { + npm ci + } else { + npm install + } + if (Test-Path "tsconfig.json") { + npm run build + } + shell: pwsh + + - name: Acquire Bearer Token (ROPC) + id: token + shell: pwsh + run: | + $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` + -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` + -TenantId "${{ secrets.MCP_TENANT_ID }}" ` + -Username "${{ secrets.MCP_TEST_USERNAME }}" ` + -Password "${{ secrets.MCP_TEST_PASSWORD }}" + echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT + echo "::add-mask::$token" + + - name: Copy ToolingManifest.json + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" + + - name: Generate .env configuration + shell: pwsh + run: | + $configMappings = @{ + "NODE_ENV" = "development" + "AZURE_OPENAI_API_KEY" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_API_KEY }}" + "AZURE_OPENAI_ENDPOINT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_ENDPOINT }}" + "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" + "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" + "connections__service_connection__settings__authType" = "ClientSecret" + "connections__service_connection__settings__clientId" = "${{ secrets.NODEJS_OPENAI_AGENT_ID }}" + "connections__service_connection__settings__clientSecret" = "${{ secrets.NODEJS_OPENAI_CLIENT_SECRET }}" + "connections__service_connection__settings__tenantId" = "${{ secrets.TENANT_ID }}" + "connectionsMap__0__serviceUrl" = "*" + "connectionsMap__0__connection" = "service_connection" + "agentic_scopes" = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" + } + & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` + -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` + -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` + -Port ${{ env.AGENT_PORT }} ` + -ConfigMappings $configMappings + + - name: Start Agent + id: start-agent + shell: pwsh + run: | + $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` + -AgentPath "${{ env.SAMPLE_PATH }}" ` + -StartCommand "node dist/index.js" ` + -Port ${{ env.AGENT_PORT }} ` + -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` + -Environment "Development" ` + -Runtime "nodejs" + echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT + + - name: Verify Agent Running + shell: pwsh + run: | + $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" + if ($agentPid) { + try { + $proc = Get-Process -Id $agentPid -ErrorAction Stop + Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green + } catch { + Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red + throw "Agent process has stopped" + } + } + $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" + $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue + Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green + + - name: Restore E2E Test Dependencies + run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" + + - name: Run .NET E2E Tests + shell: pwsh + run: | + dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` + --no-restore ` + --verbosity normal ` + --logger "console;verbosity=detailed" ` + --logger "trx;LogFileName=test-results-nodejs-openai.trx" ` + --filter "FullyQualifiedName~BasicConversation|FullyQualifiedName~Notification" + + - name: Capture Agent Logs + if: always() + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` + -AgentPath "${{ env.SAMPLE_PATH }}" ` + -OutputPath "${{ env.E2E_TESTS_PATH }}/TestResults" ` + -TestName "nodejs-openai" + + - name: Stop Agent Process + if: always() + shell: pwsh + run: | + $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" + if ($agentPid) { + & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -ProcessId $agentPid + } + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-nodejs-openai + path: | + ${{ env.E2E_TESTS_PATH }}/TestResults/**/*.trx + ${{ env.E2E_TESTS_PATH }}/TestResults/**/*-logs.txt + retention-days: 7 diff --git a/.github/workflows/e2e-orchestrator.yml b/.github/workflows/e2e-orchestrator.yml new file mode 100644 index 00000000..94686984 --- /dev/null +++ b/.github/workflows/e2e-orchestrator.yml @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: E2E Test Orchestrator + +on: + schedule: + # Run every 6 hours + - cron: '0 */6 * * *' + push: + branches: [main] + paths: + - 'python/openai/**' + - 'nodejs/openai/**' + - 'dotnet/semantic-kernel/**' + - 'dotnet/agent-framework/**' + - '.github/workflows/e2e-*.yml' + - 'scripts/e2e/**' + pull_request: + branches: [main] + paths: + - 'python/openai/**' + - 'nodejs/openai/**' + - 'dotnet/semantic-kernel/**' + - 'dotnet/agent-framework/**' + - '.github/workflows/e2e-*.yml' + - 'scripts/e2e/**' + workflow_dispatch: + inputs: + sample: + description: 'Sample to test (leave empty for all)' + required: false + type: choice + options: + - '' + - 'python-openai' + - 'nodejs-openai' + - 'dotnet-sk' + - 'dotnet-af' + +permissions: + contents: read + +jobs: + trigger-e2e-tests: + name: Trigger E2E Tests + runs-on: ubuntu-latest + strategy: + matrix: + sample: + - python-openai + - nodejs-openai + - dotnet-sk + - dotnet-af + steps: + - name: Trigger ${{ matrix.sample }} E2E Test + if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == matrix.sample }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const workflowMap = { + 'python-openai': 'e2e-python-openai.yml', + 'nodejs-openai': 'e2e-nodejs-openai.yml', + 'dotnet-sk': 'e2e-dotnet-sk.yml', + 'dotnet-af': 'e2e-dotnet-af.yml' + }; + + const sample = '${{ matrix.sample }}'; + const workflow = workflowMap[sample]; + + console.log(`Triggering ${workflow} for ${sample}...`); + + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflow, + ref: context.ref + }); + + console.log(`Successfully triggered ${workflow}`); diff --git a/.github/workflows/e2e-python-openai.yml b/.github/workflows/e2e-python-openai.yml new file mode 100644 index 00000000..9f6c5ba0 --- /dev/null +++ b/.github/workflows/e2e-python-openai.yml @@ -0,0 +1,189 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: E2E - Python OpenAI + +on: + push: + branches: [main] + paths: + - 'python/openai/**' + - 'scripts/e2e/**' + - '.github/workflows/e2e-python-openai.yml' + pull_request: + branches: [main] + paths: + - 'python/openai/**' + - 'scripts/e2e/**' + workflow_dispatch: + # Allow orchestrator to trigger this + repository_dispatch: + types: [e2e-python-openai] + +permissions: + contents: read + +env: + SAMPLE_PATH: python/openai/sample-agent + AGENT_PORT: 3979 + SCRIPTS_PATH: scripts/e2e + E2E_TESTS_PATH: tests/e2e + +jobs: + python-openai: + name: Python OpenAI Agent + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv package manager + run: pip install uv + + - name: Install dependencies + working-directory: ${{ env.SAMPLE_PATH }} + run: uv sync + + - name: Acquire Bearer Token (ROPC) + id: token + shell: pwsh + run: | + $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` + -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` + -TenantId "${{ secrets.MCP_TENANT_ID }}" ` + -Username "${{ secrets.MCP_TEST_USERNAME }}" ` + -Password "${{ secrets.MCP_TEST_PASSWORD }}" + echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT + echo "::add-mask::$token" + + - name: Copy ToolingManifest.json + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" + + - name: Generate .env configuration + shell: pwsh + run: | + $configMappings = @{ + "AZURE_OPENAI_API_KEY" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_API_KEY }}" + "AZURE_OPENAI_ENDPOINT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_ENDPOINT }}" + "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" + "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" + "USE_AGENTIC_AUTH" = "false" + "MCP_PLATFORM_ENDPOINT" = "https://agent365.svc.cloud.microsoft" + "AGENT_ID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHTYPE" = "ClientSecret" + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET" = "${{ secrets.PYTHON_OPENAI_CLIENT_SECRET }}" + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID" = "${{ secrets.TENANT_ID }}" + "CONNECTIONSMAP__0__SERVICEURL" = "*" + "CONNECTIONSMAP__0__CONNECTION" = "SERVICE_CONNECTION" + "ENABLE_OBSERVABILITY" = "true" + } + & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` + -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` + -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` + -Port ${{ env.AGENT_PORT }} ` + -ConfigMappings $configMappings + + - name: Start Agent + id: start-agent + shell: pwsh + run: | + $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` + -AgentPath "${{ env.SAMPLE_PATH }}" ` + -StartCommand "uv run python start_with_generic_host.py" ` + -Port ${{ env.AGENT_PORT }} ` + -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` + -Environment "Development" ` + -Runtime "python" + echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT + + - name: Verify Agent Running + shell: pwsh + run: | + Write-Host "=== Verifying Agent ===" -ForegroundColor Cyan + + $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" + Write-Host "Checking agent process (PID: $agentPid)..." -ForegroundColor Gray + + if ($agentPid) { + try { + $proc = Get-Process -Id $agentPid -ErrorAction Stop + Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green + } catch { + Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red + + $logFile = "${{ env.SAMPLE_PATH }}/agent.log" + if (Test-Path $logFile) { + Write-Host "Agent logs:" -ForegroundColor Yellow + Get-Content $logFile + } + throw "Agent process has stopped" + } + } + + $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" + Write-Host "Verifying agent at $agentUrl..." -ForegroundColor Gray + $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue + Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green + + - name: Restore E2E Test Dependencies + shell: pwsh + run: | + dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" + + - name: Run .NET E2E Tests + shell: pwsh + run: | + dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` + --no-restore ` + --filter "Category=HTTP" ` + --logger "trx;LogFileName=test-results.trx" ` + --results-directory "${{ runner.temp }}/TestResults" + env: + AGENT_PORT: ${{ env.AGENT_PORT }} + AGENT_URL: http://localhost:${{ env.AGENT_PORT }} + TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-python-openai + path: ${{ runner.temp }}/TestResults/ + retention-days: 30 + + - name: Upload Test Conversations + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-conversations-python-openai + path: ${{ runner.temp }}/TestConversations/ + retention-days: 30 + continue-on-error: true + + - name: Capture Agent Logs + if: always() + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" + + - name: Cleanup Agent Process + if: always() + shell: pwsh + run: | + & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` + -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` + -Port ${{ env.AGENT_PORT }} diff --git a/README.md b/README.md index b6281d03..72eaca34 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Microsoft Agent 365 SDK Samples and Prompts -[![E2E Tests](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-agent-samples.yml?branch=main&label=E2E%20Agent%20Samples)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-agent-samples.yml) +[![E2E Tests](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-orchestrator.yml?branch=main&label=E2E%20All%20Samples)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-orchestrator.yml) This repository contains sample agents and prompts for building with the Microsoft Agent 365 SDK. The Microsoft Agent 365 SDK extends the Microsoft 365 Agents SDK with enterprise-grade capabilities for building sophisticated agents. It provides comprehensive tooling for observability, notifications, runtime utilities, and development tools that help developers create production-ready agents for platforms including M365, Teams, Copilot Studio, and Webchat. @@ -11,10 +11,10 @@ This repository contains sample agents and prompts for building with the Microso | Sample | Status | |--------|--------| -| Python OpenAI | [![Python OpenAI](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-agent-samples.yml?branch=main&label=E2E&job=python-openai)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-agent-samples.yml) | -| Node.js OpenAI | [![Node.js OpenAI](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-agent-samples.yml?branch=main&label=E2E&job=nodejs-openai)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-agent-samples.yml) | -| .NET Semantic Kernel | [![.NET SK](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-agent-samples.yml?branch=main&label=E2E&job=dotnet-semantic-kernel)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-agent-samples.yml) | -| .NET Agent Framework | [![.NET AF](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-agent-samples.yml?branch=main&label=E2E&job=dotnet-agent-framework)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-agent-samples.yml) | +| Python OpenAI | [![Python OpenAI](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-python-openai.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-python-openai.yml) | +| Node.js OpenAI | [![Node.js OpenAI](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-nodejs-openai.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-nodejs-openai.yml) | +| .NET Semantic Kernel | [![.NET SK](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-dotnet-semantic-kernel.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-dotnet-semantic-kernel.yml) | +| .NET Agent Framework | [![.NET AF](https://img.shields.io/github/actions/workflow/status/microsoft/Agent365-Samples/e2e-dotnet-agent-framework.yml?branch=main&label=E2E)](https://github.com/microsoft/Agent365-Samples/actions/workflows/e2e-dotnet-agent-framework.yml) | > #### Note: > Use the information in this README to contribute to this open-source project. To learn about using this SDK in your projects, refer to the [Microsoft Agent 365 Developer documentation](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/). From 35330e1fdc10b0fcf81972ea08706915abaaf6c3 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 29 Jan 2026 15:20:32 -0800 Subject: [PATCH 4/8] refactor: Use workflow_call instead of workflow_dispatch for orchestrator - removes need for actions:write permission --- .../workflows/e2e-dotnet-agent-framework.yml | 3 +- .../workflows/e2e-dotnet-semantic-kernel.yml | 3 +- .github/workflows/e2e-nodejs-openai.yml | 3 +- .github/workflows/e2e-orchestrator.yml | 60 +++++++------------ .github/workflows/e2e-python-openai.yml | 6 +- 5 files changed, 31 insertions(+), 44 deletions(-) diff --git a/.github/workflows/e2e-dotnet-agent-framework.yml b/.github/workflows/e2e-dotnet-agent-framework.yml index 4f86a493..777f5e1d 100644 --- a/.github/workflows/e2e-dotnet-agent-framework.yml +++ b/.github/workflows/e2e-dotnet-agent-framework.yml @@ -4,6 +4,8 @@ name: E2E - .NET Agent Framework on: + workflow_call: + workflow_dispatch: push: branches: [main] paths: @@ -15,7 +17,6 @@ on: paths: - 'dotnet/agent-framework/**' - 'scripts/e2e/**' - workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/e2e-dotnet-semantic-kernel.yml b/.github/workflows/e2e-dotnet-semantic-kernel.yml index 02deae6e..4ba67153 100644 --- a/.github/workflows/e2e-dotnet-semantic-kernel.yml +++ b/.github/workflows/e2e-dotnet-semantic-kernel.yml @@ -4,6 +4,8 @@ name: E2E - .NET Semantic Kernel on: + workflow_call: + workflow_dispatch: push: branches: [main] paths: @@ -15,7 +17,6 @@ on: paths: - 'dotnet/semantic-kernel/**' - 'scripts/e2e/**' - workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/e2e-nodejs-openai.yml b/.github/workflows/e2e-nodejs-openai.yml index cb5af497..b2a0ec7f 100644 --- a/.github/workflows/e2e-nodejs-openai.yml +++ b/.github/workflows/e2e-nodejs-openai.yml @@ -4,6 +4,8 @@ name: E2E - Node.js OpenAI on: + workflow_call: # Allow orchestrator to call this workflow + workflow_dispatch: # Allow manual triggering push: branches: [main] paths: @@ -15,7 +17,6 @@ on: paths: - 'nodejs/openai/**' - 'scripts/e2e/**' - workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/e2e-orchestrator.yml b/.github/workflows/e2e-orchestrator.yml index 94686984..1cee986c 100644 --- a/.github/workflows/e2e-orchestrator.yml +++ b/.github/workflows/e2e-orchestrator.yml @@ -42,40 +42,26 @@ permissions: contents: read jobs: - trigger-e2e-tests: - name: Trigger E2E Tests - runs-on: ubuntu-latest - strategy: - matrix: - sample: - - python-openai - - nodejs-openai - - dotnet-sk - - dotnet-af - steps: - - name: Trigger ${{ matrix.sample }} E2E Test - if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == matrix.sample }} - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const workflowMap = { - 'python-openai': 'e2e-python-openai.yml', - 'nodejs-openai': 'e2e-nodejs-openai.yml', - 'dotnet-sk': 'e2e-dotnet-sk.yml', - 'dotnet-af': 'e2e-dotnet-af.yml' - }; - - const sample = '${{ matrix.sample }}'; - const workflow = workflowMap[sample]; - - console.log(`Triggering ${workflow} for ${sample}...`); - - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: workflow, - ref: context.ref - }); - - console.log(`Successfully triggered ${workflow}`); + python-openai: + name: Python OpenAI E2E + if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'python-openai' }} + uses: ./.github/workflows/e2e-python-openai.yml + secrets: inherit + + nodejs-openai: + name: Node.js OpenAI E2E + if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'nodejs-openai' }} + uses: ./.github/workflows/e2e-nodejs-openai.yml + secrets: inherit + + dotnet-sk: + name: .NET Semantic Kernel E2E + if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'dotnet-sk' }} + uses: ./.github/workflows/e2e-dotnet-semantic-kernel.yml + secrets: inherit + + dotnet-af: + name: .NET Agent Framework E2E + if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'dotnet-af' }} + uses: ./.github/workflows/e2e-dotnet-agent-framework.yml + secrets: inherit diff --git a/.github/workflows/e2e-python-openai.yml b/.github/workflows/e2e-python-openai.yml index 9f6c5ba0..668f4e8d 100644 --- a/.github/workflows/e2e-python-openai.yml +++ b/.github/workflows/e2e-python-openai.yml @@ -4,6 +4,8 @@ name: E2E - Python OpenAI on: + workflow_call: # Allow orchestrator to call this workflow + workflow_dispatch: # Allow manual triggering push: branches: [main] paths: @@ -15,10 +17,6 @@ on: paths: - 'python/openai/**' - 'scripts/e2e/**' - workflow_dispatch: - # Allow orchestrator to trigger this - repository_dispatch: - types: [e2e-python-openai] permissions: contents: read From 1a2d25b46692c9cdb785d0168f7c3fc7933d1f08 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 29 Jan 2026 15:36:45 -0800 Subject: [PATCH 5/8] fix: Correct PowerShell parameter names - use -AgentPID instead of -ProcessId, remove invalid -OutputPath and -TestName parameters --- .github/workflows/e2e-dotnet-agent-framework.yml | 6 ++---- .github/workflows/e2e-dotnet-semantic-kernel.yml | 6 ++---- .github/workflows/e2e-nodejs-openai.yml | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e-dotnet-agent-framework.yml b/.github/workflows/e2e-dotnet-agent-framework.yml index 777f5e1d..aa73fa6a 100644 --- a/.github/workflows/e2e-dotnet-agent-framework.yml +++ b/.github/workflows/e2e-dotnet-agent-framework.yml @@ -132,9 +132,7 @@ jobs: shell: pwsh run: | & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -OutputPath "${{ env.E2E_TESTS_PATH }}/TestResults" ` - -TestName "dotnet-af" + -AgentPath "${{ env.SAMPLE_PATH }}" - name: Stop Agent Process if: always() @@ -142,7 +140,7 @@ jobs: run: | $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -ProcessId $agentPid + & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid } - name: Upload Test Results diff --git a/.github/workflows/e2e-dotnet-semantic-kernel.yml b/.github/workflows/e2e-dotnet-semantic-kernel.yml index 4ba67153..c286c348 100644 --- a/.github/workflows/e2e-dotnet-semantic-kernel.yml +++ b/.github/workflows/e2e-dotnet-semantic-kernel.yml @@ -132,9 +132,7 @@ jobs: shell: pwsh run: | & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -OutputPath "${{ env.E2E_TESTS_PATH }}/TestResults" ` - -TestName "dotnet-sk" + -AgentPath "${{ env.SAMPLE_PATH }}" - name: Stop Agent Process if: always() @@ -142,7 +140,7 @@ jobs: run: | $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -ProcessId $agentPid + & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid } - name: Upload Test Results diff --git a/.github/workflows/e2e-nodejs-openai.yml b/.github/workflows/e2e-nodejs-openai.yml index b2a0ec7f..8c207dd4 100644 --- a/.github/workflows/e2e-nodejs-openai.yml +++ b/.github/workflows/e2e-nodejs-openai.yml @@ -147,9 +147,7 @@ jobs: shell: pwsh run: | & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -OutputPath "${{ env.E2E_TESTS_PATH }}/TestResults" ` - -TestName "nodejs-openai" + -AgentPath "${{ env.SAMPLE_PATH }}" - name: Stop Agent Process if: always() @@ -157,7 +155,7 @@ jobs: run: | $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" if ($agentPid) { - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -ProcessId $agentPid + & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" -AgentPID $agentPid } - name: Upload Test Results From 9fcfc10ea3fe1c89a63ef35f3c7d16daac28af88 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 29 Jan 2026 15:46:53 -0800 Subject: [PATCH 6/8] fix: Remove invalid test filter - test names BasicConversation and Notification don't exist --- .github/workflows/e2e-dotnet-agent-framework.yml | 3 +-- .github/workflows/e2e-dotnet-semantic-kernel.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-dotnet-agent-framework.yml b/.github/workflows/e2e-dotnet-agent-framework.yml index aa73fa6a..8e5b9379 100644 --- a/.github/workflows/e2e-dotnet-agent-framework.yml +++ b/.github/workflows/e2e-dotnet-agent-framework.yml @@ -124,8 +124,7 @@ jobs: --no-restore ` --verbosity normal ` --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-dotnet-af.trx" ` - --filter "FullyQualifiedName~BasicConversation|FullyQualifiedName~Notification" + --logger "trx;LogFileName=test-results-dotnet-af.trx" - name: Capture Agent Logs if: always() diff --git a/.github/workflows/e2e-dotnet-semantic-kernel.yml b/.github/workflows/e2e-dotnet-semantic-kernel.yml index c286c348..583ed034 100644 --- a/.github/workflows/e2e-dotnet-semantic-kernel.yml +++ b/.github/workflows/e2e-dotnet-semantic-kernel.yml @@ -124,8 +124,7 @@ jobs: --no-restore ` --verbosity normal ` --logger "console;verbosity=detailed" ` - --logger "trx;LogFileName=test-results-dotnet-sk.trx" ` - --filter "FullyQualifiedName~BasicConversation|FullyQualifiedName~Notification" + --logger "trx;LogFileName=test-results-dotnet-sk.trx" - name: Capture Agent Logs if: always() From 8d8a297f9ff8c2243445547f193c7b84bb43c861 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 29 Jan 2026 16:16:20 -0800 Subject: [PATCH 7/8] chore: Remove deprecated monolithic E2E workflow --- .github/workflows/e2e-agent-samples.yml | 644 ------------------------ 1 file changed, 644 deletions(-) delete mode 100644 .github/workflows/e2e-agent-samples.yml diff --git a/.github/workflows/e2e-agent-samples.yml b/.github/workflows/e2e-agent-samples.yml deleted file mode 100644 index 4d2b8d08..00000000 --- a/.github/workflows/e2e-agent-samples.yml +++ /dev/null @@ -1,644 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# ============================================================================= -# E2E Tests: Agent Samples (DEPRECATED - Use e2e-orchestrator.yml instead) -# ============================================================================= -# This workflow has been replaced by individual E2E workflows: -# - e2e-python-openai.yml -# - e2e-nodejs-openai.yml -# - e2e-dotnet-semantic-kernel.yml -# - e2e-dotnet-agent-framework.yml -# - e2e-orchestrator.yml (triggers all of the above) -# -# This file is kept for reference but will not run. -# ============================================================================= - -name: E2E Agent Samples (Deprecated) - -on: - workflow_dispatch: - inputs: - sample: - description: 'This workflow is deprecated. Use e2e-orchestrator.yml instead' - required: false - default: '' - -permissions: - contents: read - -env: - # MCP Authentication - Required for all samples - MCP_CLIENT_ID: ${{ secrets.MCP_CLIENT_ID }} - MCP_TENANT_ID: ${{ secrets.MCP_TENANT_ID }} - MCP_TEST_USERNAME: ${{ secrets.MCP_TEST_USERNAME }} - MCP_TEST_PASSWORD: ${{ secrets.MCP_TEST_PASSWORD }} - MCP_ENVIRONMENT: 'Development' - - # Common settings - SCRIPTS_PATH: scripts/e2e - - # E2E Integration Tests - located in this repository - E2E_TESTS_PATH: 'tests/e2e' - -jobs: - # =========================================================================== - # Python OpenAI Sample - # =========================================================================== - python-openai: - name: Python OpenAI Agent - runs-on: windows-latest - if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'python-openai' }} - - env: - SAMPLE_PATH: python/openai/sample-agent - AGENT_PORT: 3979 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install uv package manager - run: pip install uv - - - name: Install dependencies - working-directory: ${{ env.SAMPLE_PATH }} - run: uv sync - - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "AZURE_OPENAI_API_KEY" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.PYTHON_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "USE_AGENTIC_AUTH" = "false" - "MCP_PLATFORM_ENDPOINT" = "https://agent365.svc.cloud.microsoft" - "AGENT_ID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__AUTHTYPE" = "ClientSecret" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID" = "${{ secrets.PYTHON_OPENAI_AGENT_ID }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET" = "${{ secrets.PYTHON_OPENAI_CLIENT_SECRET }}" - "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID" = "${{ secrets.TENANT_ID }}" - "CONNECTIONSMAP__0__SERVICEURL" = "*" - "CONNECTIONSMAP__0__CONNECTION" = "SERVICE_CONNECTION" - "ENABLE_OBSERVABILITY" = "true" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "uv run python start_with_generic_host.py" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "python" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - Write-Host "=== Verifying Agent ===" -ForegroundColor Cyan - - # Verify agent process is still running - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - Write-Host "Checking agent process (PID: $agentPid)..." -ForegroundColor Gray - - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - - # Show logs - $logFile = "${{ env.SAMPLE_PATH }}/agent.log" - if (Test-Path $logFile) { - Write-Host "Agent logs:" -ForegroundColor Yellow - Get-Content $logFile - } - throw "Agent process has stopped" - } - } - - # Quick health check before running tests - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - Write-Host "Verifying agent at $agentUrl..." -ForegroundColor Gray - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - shell: pwsh - run: | - dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-python-openai - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - - name: Upload Test Conversations - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-conversations-python-openai - path: ${{ runner.temp }}/TestConversations/ - retention-days: 30 - continue-on-error: true - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup Agent Process - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} - - # =========================================================================== - # Node.js OpenAI Sample - # =========================================================================== - nodejs-openai: - name: Node.js OpenAI Agent - runs-on: windows-latest - if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'nodejs-openai' }} - - env: - SAMPLE_PATH: nodejs/openai/sample-agent - AGENT_PORT: 3979 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install dependencies - working-directory: ${{ env.SAMPLE_PATH }} - run: | - if (Test-Path "package-lock.json") { - npm ci - } else { - npm install - } - if (Test-Path "tsconfig.json") { - npm run build - } - shell: pwsh - - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate .env configuration - shell: pwsh - run: | - $configMappings = @{ - "NODE_ENV" = "development" - "AZURE_OPENAI_API_KEY" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_API_KEY }}" - "AZURE_OPENAI_ENDPOINT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_ENDPOINT }}" - "AZURE_OPENAI_DEPLOYMENT" = "${{ secrets.NODEJS_OPENAI_AZURE_OPENAI_DEPLOYMENT }}" - "AZURE_OPENAI_API_VERSION" = "2024-12-01-preview" - "connections__service_connection__settings__authType" = "ClientSecret" - "connections__service_connection__settings__clientId" = "${{ secrets.NODEJS_OPENAI_AGENT_ID }}" - "connections__service_connection__settings__clientSecret" = "${{ secrets.NODEJS_OPENAI_CLIENT_SECRET }}" - "connections__service_connection__settings__tenantId" = "${{ secrets.TENANT_ID }}" - "connectionsMap__0__serviceUrl" = "*" - "connectionsMap__0__connection" = "service_connection" - "agentic_scopes" = "ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-EnvConfig.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/.env" ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Port ${{ env.AGENT_PORT }} ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "node dist/index.js" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "nodejs" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $agentUrl = "http://localhost:${{ env.AGENT_PORT }}" - $healthResponse = Invoke-WebRequest -Uri "$agentUrl/api/health" -Method GET -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health check: $($healthResponse.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-nodejs-openai - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - - name: Capture Agent Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} - - # =========================================================================== - # .NET Semantic Kernel Sample - # =========================================================================== - dotnet-semantic-kernel: - name: .NET Semantic Kernel Agent - runs-on: windows-latest - if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'dotnet-semantic-kernel' }} - - env: - SAMPLE_PATH: dotnet/semantic-kernel/sample-agent - AGENT_PORT: 3978 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Restore dependencies - working-directory: ${{ env.SAMPLE_PATH }} - run: dotnet restore - - - name: Build - working-directory: ${{ env.SAMPLE_PATH }} - run: dotnet build --no-restore - - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate appsettings.json - shell: pwsh - run: | - $configMappings = @{ - "AIServices:UseAzureOpenAI" = "true" - "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_ENDPOINT }}" - "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_API_KEY }}" - "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_SK_AZURE_OPENAI_DEPLOYMENT }}" - "TokenValidation:Enabled" = "false" - "TokenValidation:TenantId" = "${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" - "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_SK_CLIENT_ID }}" - "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_SK_CLIENT_SECRET }}" - "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" - } - & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "dotnet run --no-build" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "dotnet" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-dotnet-sk - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - - name: Capture Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} - - # =========================================================================== - # .NET Agent Framework Sample - # =========================================================================== - dotnet-agent-framework: - name: .NET Agent Framework Agent - runs-on: windows-latest - if: ${{ github.event.inputs.sample == '' || github.event.inputs.sample == 'dotnet-agent-framework' }} - - env: - SAMPLE_PATH: dotnet/agent-framework/sample-agent - AGENT_PORT: 3978 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Restore dependencies - working-directory: ${{ env.SAMPLE_PATH }} - run: dotnet restore - - - name: Build - working-directory: ${{ env.SAMPLE_PATH }} - run: dotnet build --no-restore - - - name: Acquire Bearer Token (ROPC) - id: token - shell: pwsh - run: | - $token = & "${{ env.SCRIPTS_PATH }}/Acquire-BearerToken.ps1" ` - -ClientId "${{ secrets.MCP_CLIENT_ID }}" ` - -TenantId "${{ secrets.MCP_TENANT_ID }}" ` - -Username "${{ secrets.MCP_TEST_USERNAME }}" ` - -Password "${{ secrets.MCP_TEST_PASSWORD }}" - echo "BEARER_TOKEN=$token" >> $env:GITHUB_OUTPUT - echo "::add-mask::$token" - - - name: Copy ToolingManifest.json - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Copy-ToolingManifest.ps1" -TargetPath "${{ env.SAMPLE_PATH }}" - - - name: Generate appsettings.json - shell: pwsh - run: | - $configMappings = @{ - "AIServices:AzureOpenAI:Endpoint" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_ENDPOINT }}" - "AIServices:AzureOpenAI:ApiKey" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_API_KEY }}" - "AIServices:AzureOpenAI:DeploymentName" = "${{ secrets.DOTNET_AF_AZURE_OPENAI_DEPLOYMENT }}" - "TokenValidation:Enabled" = "false" - "Connections:ServiceConnection:Settings:AuthType" = "ClientSecret" - "Connections:ServiceConnection:Settings:ClientId" = "${{ secrets.DOTNET_AF_CLIENT_ID }}" - "Connections:ServiceConnection:Settings:ClientSecret" = "${{ secrets.DOTNET_AF_CLIENT_SECRET }}" - "Connections:ServiceConnection:Settings:TenantId" = "${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:AuthorityEndpoint" = "https://login.microsoftonline.com/${{ secrets.TENANT_ID }}" - "Connections:ServiceConnection:Settings:Scopes:0" = "5a807f24-c9de-44ee-a3a7-329e88a00ffc/.default" - } - & "${{ env.SCRIPTS_PATH }}/Generate-AppSettings.ps1" ` - -OutputPath "${{ env.SAMPLE_PATH }}/appsettings.json" ` - -ConfigMappings $configMappings - - - name: Start Agent - id: start-agent - shell: pwsh - run: | - $agentPid = & "${{ env.SCRIPTS_PATH }}/Start-Agent.ps1" ` - -AgentPath "${{ env.SAMPLE_PATH }}" ` - -StartCommand "dotnet run --no-build" ` - -Port ${{ env.AGENT_PORT }} ` - -BearerToken "${{ steps.token.outputs.BEARER_TOKEN }}" ` - -Environment "Development" ` - -Runtime "dotnet" - echo "AGENT_PID=$agentPid" >> $env:GITHUB_OUTPUT - - - name: Verify Agent Running - shell: pwsh - run: | - $agentPid = "${{ steps.start-agent.outputs.AGENT_PID }}" - if ($agentPid) { - try { - $proc = Get-Process -Id $agentPid -ErrorAction Stop - Write-Host "Agent process (PID: $agentPid) is running: $($proc.ProcessName)" -ForegroundColor Green - } catch { - Write-Host "ERROR: Agent process (PID: $agentPid) is NOT running!" -ForegroundColor Red - throw "Agent process has stopped" - } - } - $response = Invoke-WebRequest -Uri "http://localhost:${{ env.AGENT_PORT }}/api/health" -UseBasicParsing -ErrorAction SilentlyContinue - Write-Host "Health: $($response.StatusCode)" -ForegroundColor Green - - - name: Restore E2E Test Dependencies - run: dotnet restore "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" - - - name: Run .NET E2E Tests - shell: pwsh - run: | - dotnet test "${{ env.E2E_TESTS_PATH }}/Agent365.E2E.Tests.csproj" ` - --no-restore ` - --filter "Category=HTTP" ` - --logger "trx;LogFileName=test-results.trx" ` - --results-directory "${{ runner.temp }}/TestResults" - env: - AGENT_PORT: ${{ env.AGENT_PORT }} - AGENT_URL: http://localhost:${{ env.AGENT_PORT }} - TEST_RESULTS_DIR: ${{ runner.temp }}/TestConversations - - - name: Upload Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-dotnet-af - path: ${{ runner.temp }}/TestResults/ - retention-days: 30 - - - name: Capture Logs - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Capture-AgentLogs.ps1" -AgentPath "${{ env.SAMPLE_PATH }}" - - - name: Cleanup - if: always() - shell: pwsh - run: | - & "${{ env.SCRIPTS_PATH }}/Stop-AgentProcess.ps1" ` - -AgentPID "${{ steps.start-agent.outputs.AGENT_PID }}" ` - -Port ${{ env.AGENT_PORT }} - - # =========================================================================== - # Summary - # =========================================================================== - summary: - name: Test Summary - runs-on: ubuntu-latest - needs: [python-openai, nodejs-openai, dotnet-semantic-kernel, dotnet-agent-framework] - if: always() - - steps: - - name: Generate Summary - run: | - echo "## E2E Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Sample | Status |" >> $GITHUB_STEP_SUMMARY - echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Python OpenAI | ${{ needs.python-openai.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Node.js OpenAI | ${{ needs.nodejs-openai.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| .NET Semantic Kernel | ${{ needs.dotnet-semantic-kernel.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| .NET Agent Framework | ${{ needs.dotnet-agent-framework.result }} |" >> $GITHUB_STEP_SUMMARY From 70c0607c88219ce5bb4f06260f14130d36b34af9 Mon Sep 17 00:00:00 2001 From: Rahul Devikar Date: Thu, 29 Jan 2026 16:17:03 -0800 Subject: [PATCH 8/8] chore: Remove deprecated monolithic E2E workflow --- .github/workflows/update-e2e-status.yml | 160 ------------------------ 1 file changed, 160 deletions(-) delete mode 100644 .github/workflows/update-e2e-status.yml diff --git a/.github/workflows/update-e2e-status.yml b/.github/workflows/update-e2e-status.yml deleted file mode 100644 index 8df80a4c..00000000 --- a/.github/workflows/update-e2e-status.yml +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -name: Update E2E Status - -on: - workflow_run: - workflows: ["E2E Agent Samples"] - types: - - completed - workflow_dispatch: - -permissions: - contents: write - -jobs: - update-status: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: main - - - name: Get job statuses - id: get-status - uses: actions/github-script@v7 - with: - script: | - // Get the triggering workflow run or latest run - let runId; - if (context.payload.workflow_run) { - runId = context.payload.workflow_run.id; - } else { - // Manual trigger - get latest run - const runs = await github.rest.actions.listWorkflowRuns({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'e2e-agent-samples.yml', - branch: 'main', - per_page: 1 - }); - if (runs.data.workflow_runs.length === 0) { - console.log('No workflow runs found'); - core.setOutput('found', 'false'); - return; - } - runId = runs.data.workflow_runs[0].id; - } - - console.log(`Processing workflow run: ${runId}`); - core.setOutput('run-id', runId); - core.setOutput('run-url', `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`); - - // Get jobs for this run - const jobs = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId - }); - - // Map job names to their status - const jobStatusMap = { - 'Python OpenAI Agent': 'python-openai', - 'Node.js OpenAI Agent': 'nodejs-openai', - '.NET Semantic Kernel Agent': 'dotnet-sk', - '.NET Agent Framework Agent': 'dotnet-af' - }; - - let jobsFound = 0; - for (const job of jobs.data.jobs) { - const key = jobStatusMap[job.name]; - if (key) { - const conclusion = job.conclusion || job.status || 'unknown'; - core.setOutput(key, conclusion); - console.log(`${job.name}: ${conclusion}`); - jobsFound++; - } - } - - if (jobsFound > 0) { - core.setOutput('found', 'true'); - core.setOutput('date', new Date().toISOString().split('T')[0]); - } else { - console.log('No matching jobs found'); - core.setOutput('found', 'false'); - } - - - name: Update README - if: steps.get-status.outputs.found == 'true' - env: - PYTHON_STATUS: ${{ steps.get-status.outputs.python-openai }} - NODEJS_STATUS: ${{ steps.get-status.outputs.nodejs-openai }} - DOTNET_SK_STATUS: ${{ steps.get-status.outputs.dotnet-sk }} - DOTNET_AF_STATUS: ${{ steps.get-status.outputs.dotnet-af }} - RUN_URL: ${{ steps.get-status.outputs.run-url }} - UPDATE_DATE: ${{ steps.get-status.outputs.date }} - run: | - python3 << 'EOF' - import os - import re - import textwrap - from urllib.parse import quote - - def create_badge(name, status): - encoded_name = quote(name, safe='') - if status == 'success': - return f'![{name}](https://img.shields.io/badge/{encoded_name}-passing-brightgreen)' - elif status == 'failure': - return f'![{name}](https://img.shields.io/badge/{encoded_name}-failing-red)' - elif status == 'in_progress': - return f'![{name}](https://img.shields.io/badge/{encoded_name}-running-yellow)' - else: - return f'![{name}](https://img.shields.io/badge/{encoded_name}-unknown-lightgrey)' - - python_badge = create_badge('Python OpenAI', os.environ.get('PYTHON_STATUS') or 'unknown') - nodejs_badge = create_badge('Node.js OpenAI', os.environ.get('NODEJS_STATUS') or 'unknown') - dotnet_sk_badge = create_badge('.NET SK', os.environ.get('DOTNET_SK_STATUS') or 'unknown') - dotnet_af_badge = create_badge('.NET AF', os.environ.get('DOTNET_AF_STATUS') or 'unknown') - run_url = os.environ.get('RUN_URL') or '#' - update_date = os.environ.get('UPDATE_DATE') or 'unknown' - - new_section = f'''## E2E Test Status - - | Sample | Status | - |--------|--------| - | Python OpenAI | {python_badge} | - | Node.js OpenAI | {nodejs_badge} | - | .NET Semantic Kernel | {dotnet_sk_badge} | - | .NET Agent Framework | {dotnet_af_badge} | - - *Last updated: {update_date} | [View Run]({run_url})* - - ''' - - # Remove leading spaces from heredoc while preserving blank lines - new_section = textwrap.dedent(new_section) - - with open('README.md', 'r') as f: - content = f.read() - - # Pattern to match the E2E Test Status section until the next section - pattern = r'## E2E Test Status\n.*?(?=\n> ####|\n## (?!E2E Test Status)|\Z)' - - if re.search(pattern, content, re.DOTALL): - content = re.sub(pattern, new_section, content, flags=re.DOTALL) - with open('README.md', 'w') as f: - f.write(content) - print('README updated successfully') - else: - print('E2E Test Status section not found') - EOF - - - name: Commit and push changes - if: steps.get-status.outputs.found == 'true' - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add README.md - git diff --staged --quiet || (git commit -m "chore: Update E2E test status [skip ci]" && git push)