From f1e4b71d1d726e11269212f23167811a8c1854d8 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 15 Oct 2026 09:12:13 -0400 Subject: [PATCH 1/6] fix(ci): select integration test events explicitly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7d5f1773-4610-4dc8-8e7a-6cd710cfda0e --- .github/tests/package-lock.json | 37 +++++ .github/tests/package.json | 8 + .github/tests/test_integration_events.js | 127 +++++++++++++++ .github/workflows/dotnet-build-and-test.yml | 64 +++++--- .../workflows/dotnet-integration-tests.yml | 1 + .github/workflows/github-automation-tests.yml | 9 ++ .../workflows/python-integration-tests.yml | 153 ++++++++++-------- CONTRIBUTING.md | 3 + 8 files changed, 313 insertions(+), 89 deletions(-) create mode 100644 .github/tests/package-lock.json create mode 100644 .github/tests/package.json create mode 100644 .github/tests/test_integration_events.js diff --git a/.github/tests/package-lock.json b/.github/tests/package-lock.json new file mode 100644 index 000000000000..8eb3dbb685c6 --- /dev/null +++ b/.github/tests/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@actions/expressions": "0.3.61", + "yaml": "2.9.1" + } + }, + "node_modules/@actions/expressions": { + "version": "0.3.61", + "resolved": "https://registry.npmjs.org/@actions/expressions/-/expressions-0.3.61.tgz", + "integrity": "sha1-ppjvCTXpGf/TP4GgFAh4j68t7yk=", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/.github/tests/package.json b/.github/tests/package.json new file mode 100644 index 000000000000..b270c15ca949 --- /dev/null +++ b/.github/tests/package.json @@ -0,0 +1,8 @@ +{ + "private": true, + "description": "Offline tests for GitHub automation", + "dependencies": { + "@actions/expressions": "0.3.61", + "yaml": "2.9.1" + } +} diff --git a/.github/tests/test_integration_events.js b/.github/tests/test_integration_events.js new file mode 100644 index 000000000000..9ae4fb9325db --- /dev/null +++ b/.github/tests/test_integration_events.js @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { readFileSync } = require('node:fs'); +const path = require('node:path'); +const YAML = require('yaml'); + +const workflows = [ + 'python-integration-tests.yml', + 'dotnet-build-and-test.yml', + 'dotnet-integration-tests.yml', +].map((name) => ({ + name, + workflow: YAML.parse(readFileSync(path.join(__dirname, '..', 'workflows', name), 'utf8')), +})); + +async function evaluate(condition, context) { + if (condition === undefined) return true; + if (typeof condition === 'boolean') return condition; + const { Lexer, Parser, Evaluator, data } = await import('@actions/expressions'); + const { truthy } = await import('@actions/expressions/result'); + const always = { name: 'always', minArgs: 0, maxArgs: 0, call: () => new data.BooleanData(true) }; + const convert = (value) => { + if (typeof value === 'boolean') return new data.BooleanData(value); + if (typeof value === 'string') return new data.StringData(value); + return new data.Dictionary( + ...Object.entries(value).map(([key, entry]) => ({ key, value: convert(entry) })), + ); + }; + const expression = String(condition).replace(/^\s*\$\{\{|\}\}\s*$/g, '').trim(); + const tokens = new Lexer(expression).lex().tokens; + const parsed = new Parser(tokens, Object.keys(context), [always]).parse(); + return truthy(new Evaluator(parsed, convert(context), new Map([['always', always]])).evaluate()); +} + +function contextFor(event, ref) { + return { + github: { event_name: event, ref }, + matrix: { 'integration-tests': true, os: 'ubuntu-latest' }, + steps: { filter: { outputs: { python: 'true', dotnet: 'true' } } }, + needs: { + 'paths-filter': { outputs: { pythonChanges: 'true', dotnetChanges: 'true' } }, + }, + }; +} + +function containsSecrets(value) { + return JSON.stringify(value ?? {}).includes('secrets.'); +} + +describe('Integration workflow event selection', () => { + for (const { name, workflow } of workflows) { + for (const [event, ref] of [ + ['pull_request', 'refs/pull/42/merge'], + ['merge_group', 'refs/heads/gh-readonly-queue/main/pr-42'], + ['merge_group', 'refs/heads/main'], + ['workflow_dispatch', 'refs/heads/topic'], + ['repository_dispatch', 'refs/heads/main'], + ]) { + it(`${name} does not run integration setup or tests for ${event} on ${ref}`, async () => { + const context = contextFor(event, ref); + for (const [jobId, job] of Object.entries(workflow.jobs)) { + if (!await evaluate(job.if, context)) continue; + const prefix = `${name}: ${jobId}`; + assert.equal(containsSecrets(workflow.env), false, `${prefix} inherits integration settings`); + assert.notEqual( + job.permissions?.['id-token'] ?? workflow.permissions?.['id-token'], + 'write', + `${prefix} requests integration setup permissions`, + ); + assert.equal(containsSecrets(job.env), false, `${prefix} has integration settings`); + assert.notEqual(job.environment, 'integration', `${prefix} selects the integration environment`); + for (const step of job.steps ?? []) { + if (!await evaluate(step.if, context)) continue; + assert.equal(containsSecrets(step), false, `${prefix}: ${step.name ?? step.uses}`); + assert.doesNotMatch( + step.uses ?? '', + /^(azure\/login|google-github-actions\/auth|aws-actions\/configure-aws-credentials)@/, + `${prefix}: ${step.name}`, + ); + } + } + }); + } + } + + it('keeps main-branch manual integration tests enabled', async () => { + for (const { name, workflow } of workflows) { + const context = contextFor('workflow_dispatch', 'refs/heads/main'); + let found = false; + for (const [jobId, job] of Object.entries(workflow.jobs)) { + if (jobId === 'python-merge-gate-ollama') continue; + const tests = (job.steps ?? []).filter((step) => /Integration Tests/.test(step.name ?? '')); + if (tests.length === 0) continue; + assert.equal(await evaluate(job.if, context), true, `${name}: ${jobId} is disabled`); + for (const step of tests) { + assert.equal(await evaluate(step.if, context), true, `${name}: ${jobId}: ${step.name}`); + assert.equal(containsSecrets(step.env), true, `${name}: ${jobId} has no integration settings`); + found = true; + } + } + assert.equal(found, true, `${name} has no manual integration tests`); + } + }); + + it('keeps scheduled Python integration tests enabled on main', async () => { + const { workflow } = workflows.find(({ name }) => name === 'python-integration-tests.yml'); + assert.equal( + await evaluate(workflow.jobs['python-integration-tests'].if, contextFor('schedule', 'refs/heads/main')), + true, + ); + }); + + it('keeps .NET build and unit tests enabled for pull requests and the merge queue', async () => { + const { workflow } = workflows.find(({ name }) => name === 'dotnet-build-and-test.yml'); + const job = workflow.jobs['dotnet-build-and-test']; + for (const event of ['pull_request', 'merge_group']) { + const context = contextFor(event, 'refs/heads/gh-readonly-queue/main/pr-42'); + assert.equal(await evaluate(job.if, context), true); + assert.equal( + await evaluate(job.steps.find((step) => step.name === 'Run Unit Tests').if, context), + true, + ); + } + }); +}); diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index e4d9a962ced0..00d7146cc897 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -21,7 +21,6 @@ concurrency: permissions: contents: read - id-token: "write" jobs: paths-filter: @@ -60,14 +59,11 @@ jobs: dotnet: "10.0", os: "ubuntu-latest", configuration: Release, - integration-tests: true, - environment: "integration", } - { dotnet: "10.0", os: "windows-latest", configuration: Debug } - { dotnet: "9.0", os: "windows-latest", configuration: Release } runs-on: ${{ matrix.os }} - environment: ${{ matrix.environment }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -124,8 +120,43 @@ jobs: shell: pwsh run: .github/workflows/test-aot-compatibility.ps1 ${{ matrix.dotnet }} + # Generate test reports and check coverage + - name: Generate test reports + uses: danielpalme/ReportGenerator-GitHub-Action@18e0cd4c1bebd0c8b3978b380a6a4ea61c51178e # 5.4.15 + with: + reports: "./TestResults/Coverage/**/coverage.cobertura.xml" + targetdir: "./TestResults/Reports" + reporttypes: "HtmlInline;JsonSummary" + + - name: Upload coverage report artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: CoverageReport-${{ matrix.os }}-${{ matrix.dotnet }}-${{ matrix.configuration }} # Artifact name + path: ./TestResults/Reports # Directory containing files to upload + + - name: Check coverage + shell: pwsh + run: .github/workflows/check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD + + dotnet-integration-tests: + needs: paths-filter + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.dotnetChanges == 'true' + runs-on: ubuntu-latest + environment: "integration" + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup dotnet + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 + with: + global-json-file: ${{ github.workspace }}/dotnet/global.json + - name: Azure CLI Login - if: github.event_name != 'pull_request' && matrix.integration-tests uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -134,11 +165,10 @@ jobs: - name: Run Integration Tests shell: bash - if: github.event_name != 'pull_request' && matrix.integration-tests run: | export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | grep -v "Experimental.Orchestration.Flow.IntegrationTests.csproj" | grep -v "VectorDataIntegrationTests.csproj" | tr '\n' ' ') for project in $INTEGRATION_TEST_PROJECTS; do - dotnet test -f net10.0 -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx + dotnet test -f net10.0 -c Release $project -v Normal --logger trx done env: # Azure OpenAI Deployments @@ -182,29 +212,11 @@ jobs: AzureAI__ConnectionString: ${{ secrets.AZUREAI__CONNECTIONSTRING }} AzureAI__ChatModelId: ${{ vars.AZUREAI__CHATMODELID }} - # Generate test reports and check coverage - - name: Generate test reports - uses: danielpalme/ReportGenerator-GitHub-Action@18e0cd4c1bebd0c8b3978b380a6a4ea61c51178e # 5.4.15 - with: - reports: "./TestResults/Coverage/**/coverage.cobertura.xml" - targetdir: "./TestResults/Reports" - reporttypes: "HtmlInline;JsonSummary" - - - name: Upload coverage report artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: CoverageReport-${{ matrix.os }}-${{ matrix.dotnet }}-${{ matrix.configuration }} # Artifact name - path: ./TestResults/Reports # Directory containing files to upload - - - name: Check coverage - shell: pwsh - run: .github/workflows/check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD - # This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed dotnet-build-and-test-check: if: always() runs-on: ubuntu-latest - needs: [dotnet-build-and-test] + needs: [dotnet-build-and-test, dotnet-integration-tests] steps: - name: Get Date shell: bash diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml index a3b325453d55..d6beecf104cb 100644 --- a/.github/workflows/dotnet-integration-tests.yml +++ b/.github/workflows/dotnet-integration-tests.yml @@ -16,6 +16,7 @@ permissions: jobs: integration-tests: + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' strategy: matrix: os: [ubuntu-latest] diff --git a/.github/workflows/github-automation-tests.yml b/.github/workflows/github-automation-tests.yml index a7b3e003f38d..ff11885a139e 100644 --- a/.github/workflows/github-automation-tests.yml +++ b/.github/workflows/github-automation-tests.yml @@ -6,6 +6,9 @@ on: - ".github/actions/**" - ".github/scripts/check_team_membership.js" - ".github/tests/**" + - ".github/workflows/dotnet-build-and-test.yml" + - ".github/workflows/dotnet-integration-tests.yml" + - ".github/workflows/python-integration-tests.yml" - ".github/workflows/devflow-pr-review.yml" - ".github/workflows/github-automation-tests.yml" - ".github/workflows/label-issues.yml" @@ -17,6 +20,9 @@ on: - ".github/actions/**" - ".github/scripts/check_team_membership.js" - ".github/tests/**" + - ".github/workflows/dotnet-build-and-test.yml" + - ".github/workflows/dotnet-integration-tests.yml" + - ".github/workflows/python-integration-tests.yml" - ".github/workflows/devflow-pr-review.yml" - ".github/workflows/github-automation-tests.yml" - ".github/workflows/label-issues.yml" @@ -35,5 +41,8 @@ jobs: with: node-version: "22" + - name: Install test dependencies + run: npm ci --prefix .github/tests --ignore-scripts + - name: Run JavaScript tests run: node --test .github/tests/*.js diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index e29fe50b9a79..255d601539ff 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -15,68 +15,12 @@ on: permissions: contents: read - id-token: "write" env: # Configure a constant location for the uv cache UV_CACHE_DIR: /tmp/.uv-cache Python_Integration_Tests: Python_Integration_Tests INTEGRATION_TEST_SERVICE_SETUP_EXCEPTION: ${{ true }} - AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002 - AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }} - AZURE_OPENAI_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_DEPLOYMENT_NAME }} - AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME }} - AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME }} - AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME }} - AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION }} - AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} - AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT: ${{ secrets.AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT }} - AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT: ${{ secrets.AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT }} - AZURE_AI_AGENT_ENDPOINT: ${{ secrets.AZURE_AI_AGENT_ENDPOINT }} - AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME: ${{ secrets.AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME }} - AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME }} - BING_API_KEY: ${{ secrets.BING_API_KEY }} - OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI_RESPONSES_MODEL_ID }} - OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }} - OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }} - OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} - OPENAI_AUDIO_TO_TEXT_MODEL_ID: ${{ vars.OPENAI_AUDIO_TO_TEXT_MODEL_ID }} - OPENAI_TEXT_TO_AUDIO_MODEL_ID: ${{ vars.OPENAI_TEXT_TO_AUDIO_MODEL_ID }} - OPENAI_TEXT_TO_IMAGE_MODEL_ID: ${{ vars.OPENAI_TEXT_TO_IMAGE_MODEL_ID }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }} - POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}} - POSTGRES_MAX_POOL: ${{ vars.POSTGRES_MAX_POOL }} - AZURE_AI_SEARCH_API_KEY: ${{secrets.AZURE_AI_SEARCH_API_KEY}} - AZURE_AI_SEARCH_ENDPOINT: ${{secrets.AZURE_AI_SEARCH_ENDPOINT}} - MONGODB_ATLAS_CONNECTION_STRING: ${{secrets.MONGODB_ATLAS_CONNECTION_STRING}} - AZURE_KEY_VAULT_ENDPOINT: ${{secrets.AZURE_KEY_VAULT_ENDPOINT}} - AZURE_KEY_VAULT_CLIENT_ID: ${{secrets.AZURE_KEY_VAULT_CLIENT_ID}} - AZURE_KEY_VAULT_CLIENT_SECRET: ${{secrets.AZURE_KEY_VAULT_CLIENT_SECRET}} - ACA_POOL_MANAGEMENT_ENDPOINT: ${{secrets.ACA_POOL_MANAGEMENT_ENDPOINT}} - MISTRALAI_API_KEY: ${{secrets.MISTRALAI_API_KEY}} - MISTRALAI_CHAT_MODEL_ID: ${{ vars.MISTRALAI_CHAT_MODEL_ID }} - MISTRALAI_EMBEDDING_MODEL_ID: ${{ vars.MISTRALAI_EMBEDDING_MODEL_ID }} - ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}} - ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }} - OLLAMA_CHAT_MODEL_ID: "${{ vars.OLLAMA_CHAT_MODEL_ID || '' }}" # llama3.2:1b - OLLAMA_CHAT_MODEL_ID_IMAGE: "${{ vars.OLLAMA_CHAT_MODEL_ID_IMAGE || '' }}" # moondream - OLLAMA_CHAT_MODEL_ID_TOOL_CALL: "${{ vars.OLLAMA_CHAT_MODEL_ID_TOOL_CALL || '' }}" # llama3.2:1b - OLLAMA_TEXT_MODEL_ID: "${{ vars.OLLAMA_TEXT_MODEL_ID || '' }}" # llama3.2:1b - OLLAMA_EMBEDDING_MODEL_ID: "${{ vars.OLLAMA_EMBEDDING_MODEL_ID || '' }}" # nomic-embed-text - GOOGLE_AI_GEMINI_MODEL_ID: ${{ vars.GOOGLE_AI_GEMINI_MODEL_ID }} - GOOGLE_AI_EMBEDDING_MODEL_ID: ${{ vars.GOOGLE_AI_EMBEDDING_MODEL_ID }} - GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} - GOOGLE_AI_CLOUD_PROJECT_ID: ${{ vars.GOOGLE_AI_CLOUD_PROJECT_ID }} - GOOGLE_AI_CLOUD_REGION: ${{ vars.GOOGLE_AI_CLOUD_REGION }} - VERTEX_AI_PROJECT_ID: ${{ vars.VERTEX_AI_PROJECT_ID }} - VERTEX_AI_GEMINI_MODEL_ID: ${{ vars.VERTEX_AI_GEMINI_MODEL_ID }} - VERTEX_AI_EMBEDDING_MODEL_ID: ${{ vars.VERTEX_AI_EMBEDDING_MODEL_ID }} - REDIS_CONNECTION_STRING: ${{ vars.REDIS_CONNECTION_STRING }} - AZURE_COSMOS_DB_NO_SQL_URL: ${{ vars.AZURE_COSMOS_DB_NO_SQL_URL }} - AZURE_COSMOS_DB_NO_SQL_KEY: ${{ secrets.AZURE_COSMOS_DB_NO_SQL_KEY }} - BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN: ${{ secrets.BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN }} - BEDROCK_AGENT_FOUNDATION_MODEL: ${{ vars.BEDROCK_AGENT_FOUNDATION_MODEL }} jobs: paths-filter: @@ -103,7 +47,10 @@ jobs: python-merge-gate-ai-services: name: Python Pre-Merge Integration Tests - AI Services (incl samples using those) needs: paths-filter - if: github.event_name != 'pull_request' && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + permissions: + contents: read + id-token: write strategy: max-parallel: 1 fail-fast: false @@ -154,6 +101,62 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run Integration Tests id: run_tests_ai_services + env: &integration-env + AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002 + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }} + AZURE_OPENAI_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_DEPLOYMENT_NAME }} + AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_AUDIO_TO_TEXT_DEPLOYMENT_NAME }} + AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_TO_AUDIO_DEPLOYMENT_NAME }} + AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME }} + AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT: ${{ secrets.AZURE_OPENAI_AUDIO_TO_TEXT_ENDPOINT }} + AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT: ${{ secrets.AZURE_OPENAI_TEXT_TO_AUDIO_ENDPOINT }} + AZURE_AI_AGENT_ENDPOINT: ${{ secrets.AZURE_AI_AGENT_ENDPOINT }} + AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME: ${{ secrets.AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME }} + AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME }} + BING_API_KEY: ${{ secrets.BING_API_KEY }} + OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI_RESPONSES_MODEL_ID }} + OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }} + OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }} + OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} + OPENAI_AUDIO_TO_TEXT_MODEL_ID: ${{ vars.OPENAI_AUDIO_TO_TEXT_MODEL_ID }} + OPENAI_TEXT_TO_AUDIO_MODEL_ID: ${{ vars.OPENAI_TEXT_TO_AUDIO_MODEL_ID }} + OPENAI_TEXT_TO_IMAGE_MODEL_ID: ${{ vars.OPENAI_TEXT_TO_IMAGE_MODEL_ID }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }} + POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}} + POSTGRES_MAX_POOL: ${{ vars.POSTGRES_MAX_POOL }} + AZURE_AI_SEARCH_API_KEY: ${{secrets.AZURE_AI_SEARCH_API_KEY}} + AZURE_AI_SEARCH_ENDPOINT: ${{secrets.AZURE_AI_SEARCH_ENDPOINT}} + MONGODB_ATLAS_CONNECTION_STRING: ${{secrets.MONGODB_ATLAS_CONNECTION_STRING}} + AZURE_KEY_VAULT_ENDPOINT: ${{secrets.AZURE_KEY_VAULT_ENDPOINT}} + AZURE_KEY_VAULT_CLIENT_ID: ${{secrets.AZURE_KEY_VAULT_CLIENT_ID}} + AZURE_KEY_VAULT_CLIENT_SECRET: ${{secrets.AZURE_KEY_VAULT_CLIENT_SECRET}} + ACA_POOL_MANAGEMENT_ENDPOINT: ${{secrets.ACA_POOL_MANAGEMENT_ENDPOINT}} + MISTRALAI_API_KEY: ${{secrets.MISTRALAI_API_KEY}} + MISTRALAI_CHAT_MODEL_ID: ${{ vars.MISTRALAI_CHAT_MODEL_ID }} + MISTRALAI_EMBEDDING_MODEL_ID: ${{ vars.MISTRALAI_EMBEDDING_MODEL_ID }} + ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}} + ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }} + OLLAMA_CHAT_MODEL_ID: "${{ vars.OLLAMA_CHAT_MODEL_ID || '' }}" # llama3.2:1b + OLLAMA_CHAT_MODEL_ID_IMAGE: "${{ vars.OLLAMA_CHAT_MODEL_ID_IMAGE || '' }}" # moondream + OLLAMA_CHAT_MODEL_ID_TOOL_CALL: "${{ vars.OLLAMA_CHAT_MODEL_ID_TOOL_CALL || '' }}" # llama3.2:1b + OLLAMA_TEXT_MODEL_ID: "${{ vars.OLLAMA_TEXT_MODEL_ID || '' }}" # llama3.2:1b + OLLAMA_EMBEDDING_MODEL_ID: "${{ vars.OLLAMA_EMBEDDING_MODEL_ID || '' }}" # nomic-embed-text + GOOGLE_AI_GEMINI_MODEL_ID: ${{ vars.GOOGLE_AI_GEMINI_MODEL_ID }} + GOOGLE_AI_EMBEDDING_MODEL_ID: ${{ vars.GOOGLE_AI_EMBEDDING_MODEL_ID }} + GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} + GOOGLE_AI_CLOUD_PROJECT_ID: ${{ vars.GOOGLE_AI_CLOUD_PROJECT_ID }} + GOOGLE_AI_CLOUD_REGION: ${{ vars.GOOGLE_AI_CLOUD_REGION }} + VERTEX_AI_PROJECT_ID: ${{ vars.VERTEX_AI_PROJECT_ID }} + VERTEX_AI_GEMINI_MODEL_ID: ${{ vars.VERTEX_AI_GEMINI_MODEL_ID }} + VERTEX_AI_EMBEDDING_MODEL_ID: ${{ vars.VERTEX_AI_EMBEDDING_MODEL_ID }} + REDIS_CONNECTION_STRING: ${{ vars.REDIS_CONNECTION_STRING }} + AZURE_COSMOS_DB_NO_SQL_URL: ${{ vars.AZURE_COSMOS_DB_NO_SQL_URL }} + AZURE_COSMOS_DB_NO_SQL_KEY: ${{ secrets.AZURE_COSMOS_DB_NO_SQL_KEY }} + BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN: ${{ secrets.BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN }} + BEDROCK_AGENT_FOUNDATION_MODEL: ${{ vars.BEDROCK_AGENT_FOUNDATION_MODEL }} shell: bash run: | uv run pytest -v --log-cli-level=INFO --durations=20 -n logical --dist loadfile --dist worksteal -m "not ollama" -k "not bedrock" ./tests/integration/completions ./tests/integration/embeddings ./tests/samples ./tests/integration/cross_language @@ -161,7 +164,10 @@ jobs: python-merge-gate-multi-modality: name: Python Pre-Merge Integration Tests - Multi-Modality needs: paths-filter - if: github.event_name != 'pull_request' && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + permissions: + contents: read + id-token: write strategy: max-parallel: 1 fail-fast: false @@ -195,6 +201,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run Integration Tests id: run_tests_multi_modality + env: *integration-env shell: bash run: | uv run pytest -v --log-cli-level=INFO --durations=20 -n logical --dist loadfile --dist worksteal ./tests/integration/audio_to_text ./tests/integration/text_to_audio ./tests/integration/text_to_image @@ -202,7 +209,10 @@ jobs: python-merge-gate-agents: name: Python Pre-Merge Integration Tests - Agents needs: paths-filter - if: github.event_name != 'pull_request' && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + permissions: + contents: read + id-token: write strategy: max-parallel: 1 fail-fast: false @@ -244,6 +254,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run Integration Tests id: run_tests_agents + env: *integration-env shell: bash run: | uv run pytest -v --log-cli-level=INFO --durations=20 -n logical --dist loadfile --dist worksteal -k "not bedrock" ./tests/integration/agents @@ -252,7 +263,7 @@ jobs: name: Python Pre-Merge Integration Tests - Ollama needs: paths-filter # Ollama tests are very unstable at the moment. It often fails to pull models from the Ollama server. Thus, this job is disabled for now. - if: false && github.event_name != 'pull_request' && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: false && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' strategy: max-parallel: 1 fail-fast: false @@ -297,13 +308,17 @@ jobs: - name: Run Integration Tests id: run_tests_ai_services shell: bash + env: *integration-env run: | uv run pytest -v --log-cli-level=INFO --durations=0 -n logical --dist loadfile --dist worksteal -m ollama --timeout=300 ./tests/integration/completions ./tests/integration/embeddings python-merge-gate-memory: name: Python Pre-Merge Integration Tests - Memory (incl samples using those) needs: paths-filter - if: github.event_name != 'pull_request' && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + permissions: + contents: read + id-token: write strategy: max-parallel: 1 fail-fast: false @@ -353,6 +368,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run Integration Tests id: run_tests_memory + env: *integration-env timeout-minutes: 15 shell: bash run: | @@ -361,7 +377,10 @@ jobs: python-integration-tests: name: Python Integration Tests - Scheduled run needs: paths-filter - if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + permissions: + contents: read + id-token: write strategy: max-parallel: 1 fail-fast: false @@ -448,6 +467,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run Integration Tests - Completions id: run_tests_completions + env: *integration-env timeout-minutes: 10 shell: bash # Ollama tests are very unstable at the moment. It often fails to pull models from the Ollama server. Thus, Ollama is disabled for now. @@ -455,6 +475,7 @@ jobs: uv run pytest -v -n logical --dist loadfile --dist worksteal -m "not ollama" -k "not bedrock" ./tests/integration/completions - name: Run Integration Tests - Embeddings id: run_tests_embeddings + env: *integration-env timeout-minutes: 5 shell: bash # Ollama tests are very unstable at the moment. It often fails to pull models from the Ollama server. Thus, Ollama is disabled for now. @@ -463,23 +484,27 @@ jobs: - name: Run Integration Tests - Memory id: run_tests_memory timeout-minutes: 5 + env: *integration-env shell: bash run: | uv run pytest -v -n logical --dist loadfile --dist worksteal ./tests/integration/memory - name: Run Integration Tests - Cross Language id: run_tests_cross_language + env: *integration-env timeout-minutes: 5 shell: bash run: | uv run pytest -v -n logical --dist loadfile --dist worksteal ./tests/integration/cross_language - name: Run Integration Tests - Planning id: run_tests_planning + env: *integration-env timeout-minutes: 5 shell: bash run: | uv run pytest -v -n logical --dist loadfile --dist worksteal ./tests/integration/planning - name: Run Integration Tests - Samples id: run_tests_samples + env: *integration-env timeout-minutes: 5 shell: bash run: | @@ -487,12 +512,14 @@ jobs: - name: Run Integration Tests - Agents id: run_tests_agents timeout-minutes: 10 + env: *integration-env shell: bash run: | uv run pytest -v -n logical --dist loadfile --dist worksteal -k "not bedrock" ./tests/integration/agents - name: Run Integration Tests - Multi-Modality id: run_tests_multi_modality timeout-minutes: 5 + env: *integration-env shell: bash run: | uv run pytest -v -n logical --dist loadfile --dist worksteal ./tests/integration/audio_to_text ./tests/integration/text_to_audio ./tests/integration/text_to_image @@ -553,7 +580,7 @@ jobs: - name: Microsoft Teams Notification uses: skitionek/notify-microsoft-teams@216b8b28967e7e89b0b64b81dc94aeae90e642ab # v1.3.0 - if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + if: github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') with: webhook_url: ${{ secrets.MSTEAMS_WEBHOOK }} dry_run: ${{ env.run_type != 'Daily' && env.run_type != 'Manual'}} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14cbb9be650a..787f2041c6d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -110,6 +110,9 @@ The scripts below are used to build, test, and lint within the project. - Typescript: - Build/Test: `yarn build` - Linting (auto-fix): `yarn lint:fix` +- GitHub automation: + - Install test dependencies: `npm ci --prefix .github/tests --ignore-scripts` + - Test: `node --test .github/tests/*.js` ### Adding Plugins and Memory Connectors From ecf4577a7d6c6671614043cebdbe45154f9bae8a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 15 Oct 2026 10:11:33 -0400 Subject: [PATCH 2/6] fix(ci): remove automation unit test suite Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7d5f1773-4610-4dc8-8e7a-6cd710cfda0e --- .github/tests/package-lock.json | 37 --- .github/tests/package.json | 8 - .github/tests/test_check_team_membership.js | 117 --------- .github/tests/test_create_github_app_token.js | 226 ------------------ .github/tests/test_integration_events.js | 127 ---------- .github/workflows/github-automation-tests.yml | 48 ---- CONTRIBUTING.md | 3 - 7 files changed, 566 deletions(-) delete mode 100644 .github/tests/package-lock.json delete mode 100644 .github/tests/package.json delete mode 100644 .github/tests/test_check_team_membership.js delete mode 100644 .github/tests/test_create_github_app_token.js delete mode 100644 .github/tests/test_integration_events.js delete mode 100644 .github/workflows/github-automation-tests.yml diff --git a/.github/tests/package-lock.json b/.github/tests/package-lock.json deleted file mode 100644 index 8eb3dbb685c6..000000000000 --- a/.github/tests/package-lock.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "tests", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@actions/expressions": "0.3.61", - "yaml": "2.9.1" - } - }, - "node_modules/@actions/expressions": { - "version": "0.3.61", - "resolved": "https://registry.npmjs.org/@actions/expressions/-/expressions-0.3.61.tgz", - "integrity": "sha1-ppjvCTXpGf/TP4GgFAh4j68t7yk=", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/yaml": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", - "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - } - } -} diff --git a/.github/tests/package.json b/.github/tests/package.json deleted file mode 100644 index b270c15ca949..000000000000 --- a/.github/tests/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "private": true, - "description": "Offline tests for GitHub automation", - "dependencies": { - "@actions/expressions": "0.3.61", - "yaml": "2.9.1" - } -} diff --git a/.github/tests/test_check_team_membership.js b/.github/tests/test_check_team_membership.js deleted file mode 100644 index 6b6863c20098..000000000000 --- a/.github/tests/test_check_team_membership.js +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -const { describe, it } = require('node:test'); -const assert = require('node:assert/strict'); - -const checkTeamMembership = require('../scripts/check_team_membership.js'); - -const context = { repo: { owner: 'microsoft', repo: 'semantic-kernel' } }; - -function githubWithMembership(membership, username = 'contributor') { - return { - rest: { - pulls: { - get: async (params) => { - assert.deepEqual(params, { ...context.repo, pull_number: 42 }); - return { data: { user: { login: 'contributor' } } }; - }, - }, - teams: { - getByName: async (params) => { - assert.deepEqual(params, { org: 'microsoft', team_slug: 'developers' }); - }, - getMembershipForUserInOrg: async (params) => { - assert.deepEqual(params, { - org: 'microsoft', - team_slug: 'developers', - username, - }); - if (membership instanceof Error) throw membership; - return { data: { state: membership } }; - }, - }, - }, - }; -} - -describe('Review requester team membership', () => { - const options = { context, teamSlug: 'developers', prNumber: '42' }; - - it('uses the PR author from the event on automatic runs', async () => { - const github = githubWithMembership('active'); - github.rest.pulls.get = async () => { - throw new Error('Should not look up the PR when the event includes its author'); - }; - assert.deepEqual( - await checkTeamMembership({ - ...options, - context: { ...context, payload: { pull_request: { user: { login: 'contributor' } } } }, - github, - }), - { author: 'contributor', isTeamMember: true }, - ); - }); - - it('uses the initiator on manual runs, even if the PR author is external', async () => { - const github = githubWithMembership('active', 'maintainer'); - github.rest.pulls.get = async () => { - throw new Error('Should not look up the PR author on manual runs'); - }; - assert.deepEqual( - await checkTeamMembership({ ...options, github, username: 'maintainer' }), - { author: 'maintainer', isTeamMember: true }, - ); - assert.deepEqual( - await checkTeamMembership({ - ...options, - github: githubWithMembership(Object.assign(new Error('Not Found'), { status: 404 }), 'outsider'), - username: 'outsider', - }), - { author: 'outsider', isTeamMember: false }, - ); - }); - - it('accepts an active team member', async () => { - assert.deepEqual( - await checkTeamMembership({ ...options, github: githubWithMembership('active') }), - { author: 'contributor', isTeamMember: true }, - ); - }); - - it('rejects a pending team member and an outsider', async () => { - for (const membership of ['pending', Object.assign(new Error('Not Found'), { status: 404 })]) { - assert.deepEqual( - await checkTeamMembership({ ...options, github: githubWithMembership(membership) }), - { author: 'contributor', isTeamMember: false }, - ); - } - }); - - it('does not treat API errors or a missing team as nonmembership', async () => { - await assert.rejects( - checkTeamMembership({ - ...options, - github: githubWithMembership(Object.assign(new Error('Forbidden'), { status: 403 })), - }), - /Forbidden/, - ); - const github = githubWithMembership('active'); - github.rest.teams.getByName = async () => { - throw Object.assign(new Error('Team not found'), { status: 404 }); - }; - await assert.rejects(checkTeamMembership({ ...options, github }), /Team not found/); - }); - - it('rejects missing configuration and invalid PR numbers', async () => { - await assert.rejects( - checkTeamMembership({ ...options, github: githubWithMembership('active'), teamSlug: '' }), - /DEVELOPER_TEAM/, - ); - for (const prNumber of ['0', '1e2', '9007199254740992']) { - await assert.rejects( - checkTeamMembership({ ...options, github: githubWithMembership('active'), prNumber }), - /valid PR number/, - ); - } - }); -}); diff --git a/.github/tests/test_create_github_app_token.js b/.github/tests/test_create_github_app_token.js deleted file mode 100644 index 7508dcdc694b..000000000000 --- a/.github/tests/test_create_github_app_token.js +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -const { describe, it } = require('node:test'); -const assert = require('node:assert/strict'); - -const { - base64ToBase64Url, - createInstallationToken, - createJwtSigningInput, - readConfig, -} = require('../actions/github-app-token/create-token.js'); - -const CONFIG = { - azureSubscriptionId: 'subscription-id', - keyVaultName: 'vault-name', - keyName: 'key-name', - githubAppClientId: 'client-id', - githubAppInstallationId: '12345', - targetRepository: 'microsoft/semantic-kernel', - permissionProfile: 'devflow', -}; - -describe('GitHub App token creation', () => { - it('creates a short-lived GitHub App JWT', () => { - const signingInput = createJwtSigningInput('client-id', 1_000); - const [encodedHeader, encodedPayload] = signingInput.split('.'); - const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString()); - const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString()); - - assert.deepEqual(header, { alg: 'RS256', typ: 'JWT' }); - assert.deepEqual(payload, { iat: 940, exp: 1_540, iss: 'client-id' }); - }); - - it('converts Key Vault signatures to unpadded base64url', () => { - assert.equal(base64ToBase64Url('+/8='), '-_8'); - }); - - it('requests a repository-scoped installation token', async () => { - let request; - const token = await createInstallationToken(CONFIG, { - nowSeconds: 1_000, - execute: (command, args) => { - assert.equal(command, 'az'); - assert.ok(args.includes('RS256')); - return '+/8=\n'; - }, - fetch: async (url, options) => { - request = { url, options }; - return { - ok: true, - json: async () => ({ token: 'installation-token' }), - }; - }, - }); - - assert.equal(token, 'installation-token'); - assert.equal(request.url, 'https://api.github.com/app/installations/12345/access_tokens'); - assert.match(request.options.headers.Authorization, /^Bearer [^.]+\.[^.]+\.-_8$/); - assert.deepEqual(JSON.parse(request.options.body), { - repositories: ['semantic-kernel'], - permissions: { - contents: 'read', - issues: 'write', - pull_requests: 'write', - }, - }); - }); - - it('limits issue-label tokens to issue writes', async () => { - let request; - await createInstallationToken( - { ...CONFIG, permissionProfile: 'issues' }, - { - execute: () => '+/8=\n', - fetch: async (_url, options) => { - request = options; - return { - ok: true, - json: async () => ({ token: 'installation-token' }), - }; - }, - }, - ); - - assert.deepEqual(JSON.parse(request.body).permissions, { - issues: 'write', - }); - }); - - it('limits pull-request label tokens to contents and pull requests', async () => { - let request; - await createInstallationToken( - { ...CONFIG, permissionProfile: 'pull-requests' }, - { - execute: () => '+/8=\n', - fetch: async (_url, options) => { - request = options; - return { - ok: true, - json: async () => ({ token: 'installation-token' }), - }; - }, - }, - ); - - assert.deepEqual(JSON.parse(request.body).permissions, { - contents: 'read', - pull_requests: 'write', - }); - }); - - it('limits team-check tokens to member and pull-request reads', async () => { - let request; - await createInstallationToken( - { ...CONFIG, permissionProfile: 'team-check' }, - { - execute: () => '+/8=\n', - fetch: async (_url, options) => { - request = options; - return { - ok: true, - json: async () => ({ token: 'installation-token' }), - }; - }, - }, - ); - - assert.deepEqual(JSON.parse(request.body).permissions, { - members: 'read', - pull_requests: 'read', - }); - }); - - it('rejects unknown permission profiles before signing', async () => { - let signed = false; - - await assert.rejects( - createInstallationToken( - { ...CONFIG, permissionProfile: 'everything' }, - { - execute: () => { - signed = true; - return '+/8=\n'; - }, - }, - ), - /PERMISSION_PROFILE must be issues, pull-requests, devflow, or team-check/, - ); - assert.equal(signed, false); - }); - - it('rejects inherited object properties as permission profiles before signing', async () => { - let signed = false; - - await assert.rejects( - createInstallationToken( - { ...CONFIG, permissionProfile: 'toString' }, - { - execute: () => { - signed = true; - return '+/8=\n'; - }, - }, - ), - /PERMISSION_PROFILE must be issues, pull-requests, devflow, or team-check/, - ); - assert.equal(signed, false); - }); - - it('rejects incomplete configuration', () => { - assert.throws( - () => readConfig({}), - /Required GitHub App authentication configuration is missing/, - ); - }); - - it('rejects repository values with extra path segments before signing', async () => { - let signed = false; - - await assert.rejects( - createInstallationToken( - { ...CONFIG, targetRepository: 'microsoft/semantic-kernel/extra' }, - { - execute: () => { - signed = true; - return '+/8=\n'; - }, - }, - ), - /TARGET_REPOSITORY must use the owner\/repository format/, - ); - assert.equal(signed, false); - }); - - it('rejects an empty Key Vault signature', async () => { - await assert.rejects( - createInstallationToken(CONFIG, { - execute: () => '\n', - }), - /Key Vault returned an empty signature/, - ); - }); - - it('rejects a failed GitHub token request', async () => { - await assert.rejects( - createInstallationToken(CONFIG, { - execute: () => '+/8=\n', - fetch: async () => ({ ok: false, status: 403 }), - }), - /GitHub installation token request failed with HTTP 403/, - ); - }); - - it('rejects an empty GitHub installation token', async () => { - await assert.rejects( - createInstallationToken(CONFIG, { - execute: () => '+/8=\n', - fetch: async () => ({ - ok: true, - json: async () => ({ token: '' }), - }), - }), - /GitHub returned an empty installation token/, - ); - }); -}); diff --git a/.github/tests/test_integration_events.js b/.github/tests/test_integration_events.js deleted file mode 100644 index 9ae4fb9325db..000000000000 --- a/.github/tests/test_integration_events.js +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -const { describe, it } = require('node:test'); -const assert = require('node:assert/strict'); -const { readFileSync } = require('node:fs'); -const path = require('node:path'); -const YAML = require('yaml'); - -const workflows = [ - 'python-integration-tests.yml', - 'dotnet-build-and-test.yml', - 'dotnet-integration-tests.yml', -].map((name) => ({ - name, - workflow: YAML.parse(readFileSync(path.join(__dirname, '..', 'workflows', name), 'utf8')), -})); - -async function evaluate(condition, context) { - if (condition === undefined) return true; - if (typeof condition === 'boolean') return condition; - const { Lexer, Parser, Evaluator, data } = await import('@actions/expressions'); - const { truthy } = await import('@actions/expressions/result'); - const always = { name: 'always', minArgs: 0, maxArgs: 0, call: () => new data.BooleanData(true) }; - const convert = (value) => { - if (typeof value === 'boolean') return new data.BooleanData(value); - if (typeof value === 'string') return new data.StringData(value); - return new data.Dictionary( - ...Object.entries(value).map(([key, entry]) => ({ key, value: convert(entry) })), - ); - }; - const expression = String(condition).replace(/^\s*\$\{\{|\}\}\s*$/g, '').trim(); - const tokens = new Lexer(expression).lex().tokens; - const parsed = new Parser(tokens, Object.keys(context), [always]).parse(); - return truthy(new Evaluator(parsed, convert(context), new Map([['always', always]])).evaluate()); -} - -function contextFor(event, ref) { - return { - github: { event_name: event, ref }, - matrix: { 'integration-tests': true, os: 'ubuntu-latest' }, - steps: { filter: { outputs: { python: 'true', dotnet: 'true' } } }, - needs: { - 'paths-filter': { outputs: { pythonChanges: 'true', dotnetChanges: 'true' } }, - }, - }; -} - -function containsSecrets(value) { - return JSON.stringify(value ?? {}).includes('secrets.'); -} - -describe('Integration workflow event selection', () => { - for (const { name, workflow } of workflows) { - for (const [event, ref] of [ - ['pull_request', 'refs/pull/42/merge'], - ['merge_group', 'refs/heads/gh-readonly-queue/main/pr-42'], - ['merge_group', 'refs/heads/main'], - ['workflow_dispatch', 'refs/heads/topic'], - ['repository_dispatch', 'refs/heads/main'], - ]) { - it(`${name} does not run integration setup or tests for ${event} on ${ref}`, async () => { - const context = contextFor(event, ref); - for (const [jobId, job] of Object.entries(workflow.jobs)) { - if (!await evaluate(job.if, context)) continue; - const prefix = `${name}: ${jobId}`; - assert.equal(containsSecrets(workflow.env), false, `${prefix} inherits integration settings`); - assert.notEqual( - job.permissions?.['id-token'] ?? workflow.permissions?.['id-token'], - 'write', - `${prefix} requests integration setup permissions`, - ); - assert.equal(containsSecrets(job.env), false, `${prefix} has integration settings`); - assert.notEqual(job.environment, 'integration', `${prefix} selects the integration environment`); - for (const step of job.steps ?? []) { - if (!await evaluate(step.if, context)) continue; - assert.equal(containsSecrets(step), false, `${prefix}: ${step.name ?? step.uses}`); - assert.doesNotMatch( - step.uses ?? '', - /^(azure\/login|google-github-actions\/auth|aws-actions\/configure-aws-credentials)@/, - `${prefix}: ${step.name}`, - ); - } - } - }); - } - } - - it('keeps main-branch manual integration tests enabled', async () => { - for (const { name, workflow } of workflows) { - const context = contextFor('workflow_dispatch', 'refs/heads/main'); - let found = false; - for (const [jobId, job] of Object.entries(workflow.jobs)) { - if (jobId === 'python-merge-gate-ollama') continue; - const tests = (job.steps ?? []).filter((step) => /Integration Tests/.test(step.name ?? '')); - if (tests.length === 0) continue; - assert.equal(await evaluate(job.if, context), true, `${name}: ${jobId} is disabled`); - for (const step of tests) { - assert.equal(await evaluate(step.if, context), true, `${name}: ${jobId}: ${step.name}`); - assert.equal(containsSecrets(step.env), true, `${name}: ${jobId} has no integration settings`); - found = true; - } - } - assert.equal(found, true, `${name} has no manual integration tests`); - } - }); - - it('keeps scheduled Python integration tests enabled on main', async () => { - const { workflow } = workflows.find(({ name }) => name === 'python-integration-tests.yml'); - assert.equal( - await evaluate(workflow.jobs['python-integration-tests'].if, contextFor('schedule', 'refs/heads/main')), - true, - ); - }); - - it('keeps .NET build and unit tests enabled for pull requests and the merge queue', async () => { - const { workflow } = workflows.find(({ name }) => name === 'dotnet-build-and-test.yml'); - const job = workflow.jobs['dotnet-build-and-test']; - for (const event of ['pull_request', 'merge_group']) { - const context = contextFor(event, 'refs/heads/gh-readonly-queue/main/pr-42'); - assert.equal(await evaluate(job.if, context), true); - assert.equal( - await evaluate(job.steps.find((step) => step.name === 'Run Unit Tests').if, context), - true, - ); - } - }); -}); diff --git a/.github/workflows/github-automation-tests.yml b/.github/workflows/github-automation-tests.yml deleted file mode 100644 index ff11885a139e..000000000000 --- a/.github/workflows/github-automation-tests.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: GitHub automation tests - -on: - pull_request: - paths: - - ".github/actions/**" - - ".github/scripts/check_team_membership.js" - - ".github/tests/**" - - ".github/workflows/dotnet-build-and-test.yml" - - ".github/workflows/dotnet-integration-tests.yml" - - ".github/workflows/python-integration-tests.yml" - - ".github/workflows/devflow-pr-review.yml" - - ".github/workflows/github-automation-tests.yml" - - ".github/workflows/label-issues.yml" - - ".github/workflows/label-pr.yml" - push: - branches: - - main - paths: - - ".github/actions/**" - - ".github/scripts/check_team_membership.js" - - ".github/tests/**" - - ".github/workflows/dotnet-build-and-test.yml" - - ".github/workflows/dotnet-integration-tests.yml" - - ".github/workflows/python-integration-tests.yml" - - ".github/workflows/devflow-pr-review.yml" - - ".github/workflows/github-automation-tests.yml" - - ".github/workflows/label-issues.yml" - - ".github/workflows/label-pr.yml" - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "22" - - - name: Install test dependencies - run: npm ci --prefix .github/tests --ignore-scripts - - - name: Run JavaScript tests - run: node --test .github/tests/*.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 787f2041c6d6..14cbb9be650a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -110,9 +110,6 @@ The scripts below are used to build, test, and lint within the project. - Typescript: - Build/Test: `yarn build` - Linting (auto-fix): `yarn lint:fix` -- GitHub automation: - - Install test dependencies: `npm ci --prefix .github/tests --ignore-scripts` - - Test: `node --test .github/tests/*.js` ### Adding Plugins and Memory Connectors From 8f688cf4b3af1ff43ac86e73437978c2425687fe Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 15 Oct 2026 10:14:13 -0400 Subject: [PATCH 3/6] chore: reverts removes files from main --- .github/tests/test_check_team_membership.js | 117 +++++++++ .github/tests/test_create_github_app_token.js | 226 ++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 .github/tests/test_check_team_membership.js create mode 100644 .github/tests/test_create_github_app_token.js diff --git a/.github/tests/test_check_team_membership.js b/.github/tests/test_check_team_membership.js new file mode 100644 index 000000000000..6b6863c20098 --- /dev/null +++ b/.github/tests/test_check_team_membership.js @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const checkTeamMembership = require('../scripts/check_team_membership.js'); + +const context = { repo: { owner: 'microsoft', repo: 'semantic-kernel' } }; + +function githubWithMembership(membership, username = 'contributor') { + return { + rest: { + pulls: { + get: async (params) => { + assert.deepEqual(params, { ...context.repo, pull_number: 42 }); + return { data: { user: { login: 'contributor' } } }; + }, + }, + teams: { + getByName: async (params) => { + assert.deepEqual(params, { org: 'microsoft', team_slug: 'developers' }); + }, + getMembershipForUserInOrg: async (params) => { + assert.deepEqual(params, { + org: 'microsoft', + team_slug: 'developers', + username, + }); + if (membership instanceof Error) throw membership; + return { data: { state: membership } }; + }, + }, + }, + }; +} + +describe('Review requester team membership', () => { + const options = { context, teamSlug: 'developers', prNumber: '42' }; + + it('uses the PR author from the event on automatic runs', async () => { + const github = githubWithMembership('active'); + github.rest.pulls.get = async () => { + throw new Error('Should not look up the PR when the event includes its author'); + }; + assert.deepEqual( + await checkTeamMembership({ + ...options, + context: { ...context, payload: { pull_request: { user: { login: 'contributor' } } } }, + github, + }), + { author: 'contributor', isTeamMember: true }, + ); + }); + + it('uses the initiator on manual runs, even if the PR author is external', async () => { + const github = githubWithMembership('active', 'maintainer'); + github.rest.pulls.get = async () => { + throw new Error('Should not look up the PR author on manual runs'); + }; + assert.deepEqual( + await checkTeamMembership({ ...options, github, username: 'maintainer' }), + { author: 'maintainer', isTeamMember: true }, + ); + assert.deepEqual( + await checkTeamMembership({ + ...options, + github: githubWithMembership(Object.assign(new Error('Not Found'), { status: 404 }), 'outsider'), + username: 'outsider', + }), + { author: 'outsider', isTeamMember: false }, + ); + }); + + it('accepts an active team member', async () => { + assert.deepEqual( + await checkTeamMembership({ ...options, github: githubWithMembership('active') }), + { author: 'contributor', isTeamMember: true }, + ); + }); + + it('rejects a pending team member and an outsider', async () => { + for (const membership of ['pending', Object.assign(new Error('Not Found'), { status: 404 })]) { + assert.deepEqual( + await checkTeamMembership({ ...options, github: githubWithMembership(membership) }), + { author: 'contributor', isTeamMember: false }, + ); + } + }); + + it('does not treat API errors or a missing team as nonmembership', async () => { + await assert.rejects( + checkTeamMembership({ + ...options, + github: githubWithMembership(Object.assign(new Error('Forbidden'), { status: 403 })), + }), + /Forbidden/, + ); + const github = githubWithMembership('active'); + github.rest.teams.getByName = async () => { + throw Object.assign(new Error('Team not found'), { status: 404 }); + }; + await assert.rejects(checkTeamMembership({ ...options, github }), /Team not found/); + }); + + it('rejects missing configuration and invalid PR numbers', async () => { + await assert.rejects( + checkTeamMembership({ ...options, github: githubWithMembership('active'), teamSlug: '' }), + /DEVELOPER_TEAM/, + ); + for (const prNumber of ['0', '1e2', '9007199254740992']) { + await assert.rejects( + checkTeamMembership({ ...options, github: githubWithMembership('active'), prNumber }), + /valid PR number/, + ); + } + }); +}); diff --git a/.github/tests/test_create_github_app_token.js b/.github/tests/test_create_github_app_token.js new file mode 100644 index 000000000000..7508dcdc694b --- /dev/null +++ b/.github/tests/test_create_github_app_token.js @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft. All rights reserved. + +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + base64ToBase64Url, + createInstallationToken, + createJwtSigningInput, + readConfig, +} = require('../actions/github-app-token/create-token.js'); + +const CONFIG = { + azureSubscriptionId: 'subscription-id', + keyVaultName: 'vault-name', + keyName: 'key-name', + githubAppClientId: 'client-id', + githubAppInstallationId: '12345', + targetRepository: 'microsoft/semantic-kernel', + permissionProfile: 'devflow', +}; + +describe('GitHub App token creation', () => { + it('creates a short-lived GitHub App JWT', () => { + const signingInput = createJwtSigningInput('client-id', 1_000); + const [encodedHeader, encodedPayload] = signingInput.split('.'); + const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString()); + const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString()); + + assert.deepEqual(header, { alg: 'RS256', typ: 'JWT' }); + assert.deepEqual(payload, { iat: 940, exp: 1_540, iss: 'client-id' }); + }); + + it('converts Key Vault signatures to unpadded base64url', () => { + assert.equal(base64ToBase64Url('+/8='), '-_8'); + }); + + it('requests a repository-scoped installation token', async () => { + let request; + const token = await createInstallationToken(CONFIG, { + nowSeconds: 1_000, + execute: (command, args) => { + assert.equal(command, 'az'); + assert.ok(args.includes('RS256')); + return '+/8=\n'; + }, + fetch: async (url, options) => { + request = { url, options }; + return { + ok: true, + json: async () => ({ token: 'installation-token' }), + }; + }, + }); + + assert.equal(token, 'installation-token'); + assert.equal(request.url, 'https://api.github.com/app/installations/12345/access_tokens'); + assert.match(request.options.headers.Authorization, /^Bearer [^.]+\.[^.]+\.-_8$/); + assert.deepEqual(JSON.parse(request.options.body), { + repositories: ['semantic-kernel'], + permissions: { + contents: 'read', + issues: 'write', + pull_requests: 'write', + }, + }); + }); + + it('limits issue-label tokens to issue writes', async () => { + let request; + await createInstallationToken( + { ...CONFIG, permissionProfile: 'issues' }, + { + execute: () => '+/8=\n', + fetch: async (_url, options) => { + request = options; + return { + ok: true, + json: async () => ({ token: 'installation-token' }), + }; + }, + }, + ); + + assert.deepEqual(JSON.parse(request.body).permissions, { + issues: 'write', + }); + }); + + it('limits pull-request label tokens to contents and pull requests', async () => { + let request; + await createInstallationToken( + { ...CONFIG, permissionProfile: 'pull-requests' }, + { + execute: () => '+/8=\n', + fetch: async (_url, options) => { + request = options; + return { + ok: true, + json: async () => ({ token: 'installation-token' }), + }; + }, + }, + ); + + assert.deepEqual(JSON.parse(request.body).permissions, { + contents: 'read', + pull_requests: 'write', + }); + }); + + it('limits team-check tokens to member and pull-request reads', async () => { + let request; + await createInstallationToken( + { ...CONFIG, permissionProfile: 'team-check' }, + { + execute: () => '+/8=\n', + fetch: async (_url, options) => { + request = options; + return { + ok: true, + json: async () => ({ token: 'installation-token' }), + }; + }, + }, + ); + + assert.deepEqual(JSON.parse(request.body).permissions, { + members: 'read', + pull_requests: 'read', + }); + }); + + it('rejects unknown permission profiles before signing', async () => { + let signed = false; + + await assert.rejects( + createInstallationToken( + { ...CONFIG, permissionProfile: 'everything' }, + { + execute: () => { + signed = true; + return '+/8=\n'; + }, + }, + ), + /PERMISSION_PROFILE must be issues, pull-requests, devflow, or team-check/, + ); + assert.equal(signed, false); + }); + + it('rejects inherited object properties as permission profiles before signing', async () => { + let signed = false; + + await assert.rejects( + createInstallationToken( + { ...CONFIG, permissionProfile: 'toString' }, + { + execute: () => { + signed = true; + return '+/8=\n'; + }, + }, + ), + /PERMISSION_PROFILE must be issues, pull-requests, devflow, or team-check/, + ); + assert.equal(signed, false); + }); + + it('rejects incomplete configuration', () => { + assert.throws( + () => readConfig({}), + /Required GitHub App authentication configuration is missing/, + ); + }); + + it('rejects repository values with extra path segments before signing', async () => { + let signed = false; + + await assert.rejects( + createInstallationToken( + { ...CONFIG, targetRepository: 'microsoft/semantic-kernel/extra' }, + { + execute: () => { + signed = true; + return '+/8=\n'; + }, + }, + ), + /TARGET_REPOSITORY must use the owner\/repository format/, + ); + assert.equal(signed, false); + }); + + it('rejects an empty Key Vault signature', async () => { + await assert.rejects( + createInstallationToken(CONFIG, { + execute: () => '\n', + }), + /Key Vault returned an empty signature/, + ); + }); + + it('rejects a failed GitHub token request', async () => { + await assert.rejects( + createInstallationToken(CONFIG, { + execute: () => '+/8=\n', + fetch: async () => ({ ok: false, status: 403 }), + }), + /GitHub installation token request failed with HTTP 403/, + ); + }); + + it('rejects an empty GitHub installation token', async () => { + await assert.rejects( + createInstallationToken(CONFIG, { + execute: () => '+/8=\n', + fetch: async () => ({ + ok: true, + json: async () => ({ token: '' }), + }), + }), + /GitHub returned an empty installation token/, + ); + }); +}); From d69a4e5f8845576f72a958f8d7bcb30617473c25 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 15 Oct 2026 10:22:45 -0400 Subject: [PATCH 4/6] chore: more restauration --- .github/workflows/github-automation-tests.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/github-automation-tests.yml diff --git a/.github/workflows/github-automation-tests.yml b/.github/workflows/github-automation-tests.yml new file mode 100644 index 000000000000..a7b3e003f38d --- /dev/null +++ b/.github/workflows/github-automation-tests.yml @@ -0,0 +1,39 @@ +name: GitHub automation tests + +on: + pull_request: + paths: + - ".github/actions/**" + - ".github/scripts/check_team_membership.js" + - ".github/tests/**" + - ".github/workflows/devflow-pr-review.yml" + - ".github/workflows/github-automation-tests.yml" + - ".github/workflows/label-issues.yml" + - ".github/workflows/label-pr.yml" + push: + branches: + - main + paths: + - ".github/actions/**" + - ".github/scripts/check_team_membership.js" + - ".github/tests/**" + - ".github/workflows/devflow-pr-review.yml" + - ".github/workflows/github-automation-tests.yml" + - ".github/workflows/label-issues.yml" + - ".github/workflows/label-pr.yml" + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + + - name: Run JavaScript tests + run: node --test .github/tests/*.js From d909fd7775df01efbff9e8b896cb68a39620c038 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 15 Oct 2026 10:40:57 -0400 Subject: [PATCH 5/6] fix(ci): allow repository-local integration events Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7d5f1773-4610-4dc8-8e7a-6cd710cfda0e --- .github/workflows/dotnet-build-and-test.yml | 2 +- .github/workflows/dotnet-integration-tests.yml | 8 ++------ .github/workflows/python-integration-tests.yml | 17 ++++++----------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 00d7146cc897..cd3a15033571 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -140,7 +140,7 @@ jobs: dotnet-integration-tests: needs: paths-filter - if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.dotnetChanges == 'true' + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.paths-filter.outputs.dotnetChanges == 'true' runs-on: ubuntu-latest environment: "integration" permissions: diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml index d6beecf104cb..b5aa61df0487 100644 --- a/.github/workflows/dotnet-integration-tests.yml +++ b/.github/workflows/dotnet-integration-tests.yml @@ -16,7 +16,7 @@ permissions: jobs: integration-tests: - if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository strategy: matrix: os: [ubuntu-latest] @@ -24,25 +24,21 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: ${{ github.event_name != 'pull_request' }} with: clean: true persist-credentials: false - name: Setup .NET uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 - if: ${{ github.event_name != 'pull_request' }} with: dotnet-version: 10.0.x - name: Find projects shell: bash - if: ${{ github.event_name != 'pull_request' }} run: echo "projects=$(find ./dotnet -type f -name "*Tests.csproj" | tr '\n' ' ')" >> $GITHUB_ENV - name: Integration Tests shell: bash - if: ${{ github.event_name != 'pull_request' }} env: # Set Azure credentials secret as an input AzureOpenAI__Label: azure-text-davinci-003 AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002 @@ -65,4 +61,4 @@ jobs: with: name: dotnet-testresults-${{ matrix.configuration }} path: ./TestResults - if: ${{ github.event_name != 'pull_request' && always() }} + if: ${{ always() }} diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 255d601539ff..f27da3ab3a28 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -47,7 +47,7 @@ jobs: python-merge-gate-ai-services: name: Python Pre-Merge Integration Tests - AI Services (incl samples using those) needs: paths-filter - if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -93,7 +93,6 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ vars.AWS_REGION }} - name: Azure CLI Login - if: github.event_name != 'pull_request' uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -164,7 +163,7 @@ jobs: python-merge-gate-multi-modality: name: Python Pre-Merge Integration Tests - Multi-Modality needs: paths-filter - if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -193,7 +192,6 @@ jobs: run: | uv sync --all-extras --dev - name: Azure CLI Login - if: github.event_name != 'pull_request' uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -209,7 +207,7 @@ jobs: python-merge-gate-agents: name: Python Pre-Merge Integration Tests - Agents needs: paths-filter - if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -246,7 +244,6 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ vars.AWS_REGION }} - name: Azure CLI Login - if: github.event_name != 'pull_request' uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -263,7 +260,7 @@ jobs: name: Python Pre-Merge Integration Tests - Ollama needs: paths-filter # Ollama tests are very unstable at the moment. It often fails to pull models from the Ollama server. Thus, this job is disabled for now. - if: false && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' strategy: max-parallel: 1 fail-fast: false @@ -315,7 +312,7 @@ jobs: python-merge-gate-memory: name: Python Pre-Merge Integration Tests - Memory (incl samples using those) needs: paths-filter - if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -360,7 +357,6 @@ jobs: run: | uv sync --all-extras --dev - name: Azure CLI Login - if: github.event_name != 'pull_request' uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -377,7 +373,7 @@ jobs: python-integration-tests: name: Python Integration Tests - Scheduled run needs: paths-filter - if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -459,7 +455,6 @@ jobs: Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator" Start-CosmosDbEmulator - name: Azure CLI Login - if: github.event_name != 'pull_request' uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} From 804a3aed55ab49a4d56bcdcee09868b449503788 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 15 Oct 2026 10:58:53 -0400 Subject: [PATCH 6/6] fix(ci): skip integration jobs for Dependabot pull requests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7d5f1773-4610-4dc8-8e7a-6cd710cfda0e --- .github/workflows/dotnet-build-and-test.yml | 2 +- .github/workflows/dotnet-integration-tests.yml | 2 +- .github/workflows/python-integration-tests.yml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index cd3a15033571..cb9daffc05df 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -140,7 +140,7 @@ jobs: dotnet-integration-tests: needs: paths-filter - if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && needs.paths-filter.outputs.dotnetChanges == 'true' + if: (github.event_name != 'pull_request' || (github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository)) && needs.paths-filter.outputs.dotnetChanges == 'true' runs-on: ubuntu-latest environment: "integration" permissions: diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml index b5aa61df0487..b4ca98a921fc 100644 --- a/.github/workflows/dotnet-integration-tests.yml +++ b/.github/workflows/dotnet-integration-tests.yml @@ -16,7 +16,7 @@ permissions: jobs: integration-tests: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + if: github.event_name != 'pull_request' || (github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository) strategy: matrix: os: [ubuntu-latest] diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index f27da3ab3a28..8d57877c5ff1 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -47,7 +47,7 @@ jobs: python-merge-gate-ai-services: name: Python Pre-Merge Integration Tests - AI Services (incl samples using those) needs: paths-filter - if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || (github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository)) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -163,7 +163,7 @@ jobs: python-merge-gate-multi-modality: name: Python Pre-Merge Integration Tests - Multi-Modality needs: paths-filter - if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || (github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository)) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -207,7 +207,7 @@ jobs: python-merge-gate-agents: name: Python Pre-Merge Integration Tests - Agents needs: paths-filter - if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || (github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository)) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write @@ -260,7 +260,7 @@ jobs: name: Python Pre-Merge Integration Tests - Ollama needs: paths-filter # Ollama tests are very unstable at the moment. It often fails to pull models from the Ollama server. Thus, this job is disabled for now. - if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: false && (github.event_name != 'pull_request' || (github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository)) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' strategy: max-parallel: 1 fail-fast: false @@ -312,7 +312,7 @@ jobs: python-merge-gate-memory: name: Python Pre-Merge Integration Tests - Memory (incl samples using those) needs: paths-filter - if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' + if: (github.event_name != 'pull_request' || (github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository)) && github.event_name != 'schedule' && needs.paths-filter.outputs.pythonChanges == 'true' permissions: contents: read id-token: write