diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 2d079f488e..95842e703a 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -7,13 +7,6 @@ name: dotnet-build-and-test on: workflow_dispatch: - workflow_call: - inputs: - checkout-ref: - description: "Git ref to checkout (e.g., a commit SHA from a PR)" - required: false - type: string - default: "" pull_request: branches: ["main", "feature*"] merge_group: @@ -46,8 +39,6 @@ jobs: cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }} steps: - uses: actions/checkout@v6 - with: - ref: ${{ inputs.checkout-ref }} - uses: dorny/paths-filter@v3 id: filter with: @@ -85,7 +76,6 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{ inputs.checkout-ref }} persist-credentials: false sparse-checkout: | . diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml new file mode 100644 index 0000000000..029ec5151d --- /dev/null +++ b/.github/workflows/dotnet-integration-tests.yml @@ -0,0 +1,102 @@ +# +# Dedicated .NET integration tests workflow, called from the manual integration test orchestrator. +# Only runs integration test matrix entries (net10.0 and net472). +# + +name: dotnet-integration-tests + +on: + workflow_call: + inputs: + checkout-ref: + description: "Git ref to checkout (e.g., refs/pull/123/head)" + required: true + type: string + +permissions: + contents: read + id-token: write + +jobs: + dotnet-integration-tests: + strategy: + fail-fast: false + matrix: + include: + - { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release } + - { targetFramework: "net472", os: "windows-latest", configuration: Release } + runs-on: ${{ matrix.os }} + environment: integration + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.checkout-ref }} + persist-credentials: false + sparse-checkout: | + . + .github + dotnet + python + workflow-samples + + - name: Start Azure Cosmos DB Emulator + if: runner.os == 'Windows' + shell: pwsh + run: | + Write-Host "Launching Azure Cosmos DB Emulator" + Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator" + Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" + echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV + + - name: Setup dotnet + uses: actions/setup-dotnet@v5.1.0 + with: + global-json-file: ${{ github.workspace }}/dotnet/global.json + + - name: Build dotnet solutions + shell: bash + run: | + export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ') + for solution in $SOLUTIONS; do + dotnet build $solution -c ${{ matrix.configuration }} --warnaserror + done + + - name: Azure CLI Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Set up Durable Task and Azure Functions Integration Test Emulators + if: matrix.os == 'ubuntu-latest' + uses: ./.github/actions/azure-functions-integration-setup + + - name: Run Integration Tests + shell: bash + run: | + export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ') + for project in $INTEGRATION_TEST_PROJECTS; do + target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r') + if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then + dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --filter "Category!=IntegrationDisabled" + else + echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)" + fi + done + env: + COSMOSDB_ENDPOINT: https://localhost:8081 + COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }} + OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }} + OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }} + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }} + AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }} + AzureAI__Endpoint: ${{ secrets.AZUREAI__ENDPOINT }} + AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }} + AzureAI__BingConnectionId: ${{ vars.AZUREAI__BINGCONECTIONID }} + FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }} + FOUNDRY_MEDIA_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MEDIA_DEPLOYMENT_NAME }} + FOUNDRY_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL_DEPLOYMENT_NAME }} + FOUNDRY_CONNECTION_GROUNDING_TOOL: ${{ vars.FOUNDRY_CONNECTION_GROUNDING_TOOL }} diff --git a/.github/workflows/integration-tests-manual.yml b/.github/workflows/integration-tests-manual.yml index eb7c9859b4..d3d617fa68 100644 --- a/.github/workflows/integration-tests-manual.yml +++ b/.github/workflows/integration-tests-manual.yml @@ -2,8 +2,9 @@ # This workflow allows manually running integration tests against an open PR or a branch. # Go to Actions → "Integration Tests (Manual)" → Run workflow → enter a PR number or branch name. # -# It reuses the existing dotnet-build-and-test and python-merge-tests workflows, +# It calls dedicated integration-only workflows (dotnet-integration-tests and python-integration-tests), # passing a ref so they check out and test the correct code. +# Changed paths are detected here so only the relevant test suites run. # name: Integration Tests (Manual) @@ -37,6 +38,8 @@ jobs: runs-on: ubuntu-latest outputs: checkout-ref: ${{ steps.resolve.outputs.checkout-ref }} + dotnet-changes: ${{ steps.detect-changes.outputs.dotnet }} + python-changes: ${{ steps.detect-changes.outputs.python }} steps: - name: Resolve checkout ref id: resolve @@ -45,7 +48,6 @@ jobs: PR_NUMBER: ${{ github.event.inputs.pr-number }} BRANCH: ${{ github.event.inputs.branch }} REPO: ${{ github.repository }} - REPO_OWNER: ${{ github.repository_owner }} run: | if [ -n "$PR_NUMBER" ] && [ -n "$BRANCH" ]; then echo "::error::Please provide either a PR number or a branch name, not both." @@ -63,20 +65,14 @@ jobs: exit 1 fi - PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state,headRepository,headRepositoryOwner) + PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state) PR_STATE=$(echo "$PR_DATA" | jq -r '.state') - HEAD_OWNER=$(echo "$PR_DATA" | jq -r '.headRepositoryOwner.login') if [ "$PR_STATE" != "OPEN" ]; then echo "::error::PR #$PR_NUMBER is not open (state: $PR_STATE)" exit 1 fi - if [ "$HEAD_OWNER" != "$REPO_OWNER" ]; then - echo "::error::PR #$PR_NUMBER is from a fork ($HEAD_OWNER). Running integration tests against fork PRs is not allowed for security reasons." - exit 1 - fi - echo "checkout-ref=refs/pull/$PR_NUMBER/head" >> "$GITHUB_OUTPUT" echo "Running integration tests for PR #$PR_NUMBER" else @@ -89,10 +85,41 @@ jobs: echo "Running integration tests for branch $BRANCH" fi + - name: Detect changed paths + id: detect-changes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.inputs.pr-number }} + BRANCH: ${{ github.event.inputs.branch }} + REPO: ${{ github.repository }} + run: | + if [ -n "$PR_NUMBER" ]; then + CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only) + else + # For branches, compare against main using the GitHub API + CHANGED_FILES=$(gh api "repos/$REPO/compare/main...$BRANCH" --jq '.files[].filename') + fi + + DOTNET_CHANGES=false + PYTHON_CHANGES=false + + if echo "$CHANGED_FILES" | grep -q '^dotnet/'; then + DOTNET_CHANGES=true + fi + + if echo "$CHANGED_FILES" | grep -q '^python/'; then + PYTHON_CHANGES=true + fi + + echo "dotnet=$DOTNET_CHANGES" >> "$GITHUB_OUTPUT" + echo "python=$PYTHON_CHANGES" >> "$GITHUB_OUTPUT" + echo "Detected changes — dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES" + dotnet-integration-tests: name: .NET Integration Tests needs: resolve-ref - uses: ./.github/workflows/dotnet-build-and-test.yml + if: needs.resolve-ref.outputs.dotnet-changes == 'true' + uses: ./.github/workflows/dotnet-integration-tests.yml with: checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }} secrets: inherit @@ -100,7 +127,8 @@ jobs: python-integration-tests: name: Python Integration Tests needs: resolve-ref - uses: ./.github/workflows/python-merge-tests.yml + if: needs.resolve-ref.outputs.python-changes == 'true' + uses: ./.github/workflows/python-integration-tests.yml with: checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }} secrets: inherit diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml new file mode 100644 index 0000000000..22af38d9c2 --- /dev/null +++ b/.github/workflows/python-integration-tests.yml @@ -0,0 +1,134 @@ +# +# Dedicated Python integration tests workflow, called from the manual integration test orchestrator. +# Runs all tests (unit + integration). +# + +name: python-integration-tests + +on: + workflow_call: + inputs: + checkout-ref: + description: "Git ref to checkout (e.g., refs/pull/123/head)" + required: true + type: string + +permissions: + contents: read + id-token: write + +env: + UV_CACHE_DIR: /tmp/.uv-cache + RUN_INTEGRATION_TESTS: "true" + +jobs: + python-tests-core: + name: Python Integration Tests - Core + runs-on: ubuntu-latest + environment: integration + timeout-minutes: 60 + env: + UV_PYTHON: "3.10" + OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} + OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} + OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }} + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }} + AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }} + AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }} + LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }} + FUNCTIONS_WORKER_RUNTIME: "python" + DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + AzureWebJobsStorage: "UseDevelopmentStorage=true" + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.checkout-ref }} + persist-credentials: false + + - name: Set up python and install the project + id: python-setup + uses: ./.github/actions/python-setup + with: + python-version: "3.10" + os: ${{ runner.os }} + env: + UV_CACHE_DIR: /tmp/.uv-cache + + - name: Azure CLI Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Set up Azure Functions Integration Test Emulators + uses: ./.github/actions/azure-functions-integration-setup + id: azure-functions-setup + + - name: Test with pytest + run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 + + python-tests-azure-ai: + name: Python Integration Tests - Azure AI + runs-on: ubuntu-latest + environment: integration + timeout-minutes: 60 + env: + UV_PYTHON: "3.10" + AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }} + AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }} + LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }} + defaults: + run: + working-directory: python + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.checkout-ref }} + persist-credentials: false + + - name: Set up python and install the project + id: python-setup + uses: ./.github/actions/python-setup + with: + python-version: "3.10" + os: ${{ runner.os }} + env: + UV_CACHE_DIR: /tmp/.uv-cache + + - name: Azure CLI Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Test with pytest + timeout-minutes: 15 + run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 + + python-integration-tests-check: + if: always() + runs-on: ubuntu-latest + needs: + [ + python-tests-core, + python-tests-azure-ai + ] + steps: + - name: Fail workflow if tests failed + if: contains(join(needs.*.result, ','), 'failure') + uses: actions/github-script@v8 + with: + script: core.setFailed('Integration Tests Failed!') + + - name: Fail workflow if tests cancelled + if: contains(join(needs.*.result, ','), 'cancelled') + uses: actions/github-script@v8 + with: + script: core.setFailed('Integration Tests Cancelled!') diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index 8704ec56c1..8c0a0189c1 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -2,13 +2,6 @@ name: Python - Merge - Tests on: workflow_dispatch: - workflow_call: - inputs: - checkout-ref: - description: "Git ref to checkout (e.g., a commit SHA from a PR)" - required: false - type: string - default: "" pull_request: branches: ["main"] merge_group: @@ -17,7 +10,7 @@ on: - cron: "0 0 * * *" # Run at midnight UTC daily permissions: - contents: write + contents: read id-token: write env: @@ -33,11 +26,9 @@ jobs: contents: read pull-requests: read outputs: - pythonChanges: ${{ steps.filter.outputs.python}} + pythonChanges: ${{ steps.filter.outputs.python }} steps: - uses: actions/checkout@v6 - with: - ref: ${{ inputs.checkout-ref }} - uses: dorny/paths-filter@v3 id: filter with: @@ -85,8 +76,6 @@ jobs: working-directory: python steps: - uses: actions/checkout@v6 - with: - ref: ${{ inputs.checkout-ref }} - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -146,8 +135,6 @@ jobs: working-directory: python steps: - uses: actions/checkout@v6 - with: - ref: ${{ inputs.checkout-ref }} - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup