name: ".NET SDK Tests" on: workflow_dispatch: workflow_call: permissions: contents: read jobs: validate: name: ".NET SDK Build and Format" if: github.event.repository.fork == false runs-on: ubuntu-latest timeout-minutes: 20 defaults: run: shell: bash working-directory: ./dotnet steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 with: dotnet-version: "10.0.x" - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: "22" - name: Restore .NET dependencies run: dotnet restore - name: Run dotnet format check run: | if ! dotnet format --no-restore --verify-no-changes; then echo "❌ dotnet format produced changes. Please run 'dotnet format' in dotnet" exit 1 fi echo "✅ dotnet format produced no changes" # Build the whole solution once to validate every SDK target framework. # Matrix cells below only need to build frameworks consumed by the tests. - name: Build SDK run: dotnet build --no-restore test: name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }}, ${{ matrix.backend }}, ${{ matrix.shard }})" if: github.event.repository.fork == false env: POWERSHELL_UPDATECHECK: Off COPILOT_SDK_E2E_BACKEND: ${{ matrix.backend }} DOTNET_TEST_FILTER: ${{ matrix.test-filter }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] transport: ["default", "inprocess"] backend: [capi] shard: [full] # TODO: Re-enable after fixing in-process sqlite file locking on shutdown on Windows. exclude: - os: windows-latest transport: "inprocess" - os: windows-latest transport: default shard: full # TODO(cli-1.0.81-2): CLI 1.0.81-5 still stops completing in-process # CAPI model turns, causing repeated per-test timeouts until the # 30-minute job limit. Stdio CAPI and in-process BYOK remain enabled. - os: ubuntu-latest transport: inprocess - os: macos-latest transport: inprocess # The macOS default/capi host runs the whole suite on the smallest # runner in the matrix (3 vCPU / 7 GB vs ubuntu's 4 / 16). Since the # 1.0.81-2 bump it stopped finishing: the job ran 50+ minutes until # the runner lost communication with the server, and a runner that # dies that way uploads no logs at all, so the failure could not be # diagnosed from CI output. Split it across isolated hosts so one # poisoned runtime cannot consume the entire job without diagnostics. - os: macos-latest transport: default shard: full include: # Keep xUnit serial within each process, but split the slow Windows # default-transport suite across two isolated test hosts. Keep both # target frameworks in each shard: separate framework jobs did not # shorten the critical path and doubled the Windows job count. - os: windows-latest transport: default backend: capi shard: "1" - os: windows-latest transport: default backend: capi shard: "2" - os: macos-latest transport: default backend: capi shard: "1" - os: macos-latest transport: default backend: capi shard: "2a" - os: macos-latest transport: default backend: capi shard: "2b-pending" - os: macos-latest transport: default backend: capi shard: "2b-permission" - os: macos-latest transport: default backend: capi shard: "2b-auth" - os: macos-latest transport: default backend: capi shard: "2b-hooks" - os: macos-latest transport: default backend: capi shard: "2b-unit-p" - os: macos-latest transport: default backend: capi shard: "2b-provider" - os: macos-latest transport: default backend: capi shard: "2b-rpc-additional" - os: macos-latest transport: default backend: capi shard: "2b-rpc-agent" - os: macos-latest transport: default backend: capi shard: "2b-rpc-event-log" - os: macos-latest transport: default backend: capi shard: "2b-rpc-event-effects" - os: macos-latest transport: default backend: capi shard: "2b-rpc-mcp-skills" - os: macos-latest transport: default backend: capi shard: "2b-rpc-mcp-config" - os: macos-latest transport: default backend: capi shard: "2b-rpc-mcp-lifecycle" - os: macos-latest transport: default backend: capi shard: "2b-rpc-q-z" # Standalone extension tests require the legacy JavaScript entrypoint. # Keep its subprocess tree isolated from the rest of the O/P/R shard. - os: macos-latest transport: default backend: capi shard: extensions - os: macos-latest transport: default backend: capi shard: "2c" - os: ubuntu-latest transport: inprocess backend: anthropic-messages shard: full test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" - os: ubuntu-latest transport: inprocess backend: openai-responses shard: full test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" - os: ubuntu-latest transport: inprocess backend: openai-completions shard: full test-filter: "FullyQualifiedName~GitHub.Copilot.Test.E2E&E2EBackend!=SelfConfiguredBackend&E2EBackend!=CapiOnly" runs-on: ${{ matrix.os }} # A hung test used to run until the runner died (~50 min) and the dying # runner never uploaded its logs, so the failures were undiagnosable. # Every healthy cell finishes well under 15 min. timeout-minutes: 20 defaults: run: shell: bash working-directory: ./dotnet steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 with: dotnet-version: "10.0.x" - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: "22" cache: "npm" cache-dependency-path: | ./nodejs/package-lock.json ./test/harness/package-lock.json - name: Install Node.js dependencies working-directory: ./nodejs run: npm ci --ignore-scripts - name: Restore .NET dependencies run: dotnet restore test/GitHub.Copilot.SDK.Test.csproj - name: Install test harness dependencies working-directory: ./test/harness run: npm ci --ignore-scripts - name: Warm up PowerShell if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - name: Select inprocess transport if: matrix.transport == 'inprocess' run: echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV" - name: Run .NET SDK tests env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} DOTNET_TEST_SHARD: ${{ matrix.shard }} run: | # --blame-hang names the offending test instead of letting it wedge # the runner. No single test legitimately runs for 10 minutes; the # whole suite normally finishes in about five. # The validation job performs the full analyzer-enabled SDK build. # Build only test-consumed frameworks here and do not repeat analyzers. args=( --no-restore -v n --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type none --logger "trx;LogFilePrefix=test-results" --results-directory "$GITHUB_WORKSPACE/dotnet/TestResults" -p:RunAnalyzers=false ) filter="$DOTNET_TEST_FILTER" if [[ "$DOTNET_TEST_SHARD" != "full" ]]; then case "$DOTNET_TEST_SHARD" in 1) # M moved here after recent runs showed shard 2 was about two # minutes slower; this keeps Windows and macOS balanced. initials=(A C D H I J K L M N Q S U W Y) shard_filter="FullyQualifiedName~GitHub.Copilot.Test.ConnectionToken" ;; 2) initials=(B E F G O P R T V X Z) shard_filter="" ;; 2a) initials=(B E F G) shard_filter="" ;; 2b-pending) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PendingWorkResumeE2ETests" ;; 2b-permission) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PermissionE2ETests" ;; 2b-auth) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PerSessionAuthE2ETests" ;; 2b-hooks) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PreMcpToolCallHookE2ETests" ;; 2b-unit-p) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.Unit.P" ;; 2b-provider) initials=(O) shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.ProviderEndpointE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RewindE2ETests|FullyQualifiedName~GitHub.Copilot.Test.Unit.R" ;; 2b-rpc-additional) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAdditionalEdgeCasesE2ETests" ;; 2b-rpc-agent) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAgentE2ETests" ;; 2b-rpc-event-log) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventLogE2ETests" ;; 2b-rpc-event-effects) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventSideEffectsE2ETests" ;; 2b-rpc-mcp-skills) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpAndSkillsE2ETests" ;; 2b-rpc-mcp-config) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpConfigE2ETests" ;; 2b-rpc-mcp-lifecycle) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpLifecycleE2ETests" ;; 2b-rpc-q-z) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcQueueE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcRemoteE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcScheduleE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerMiscE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerPluginsE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerRemoteControlE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateExtrasE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellAndFleetE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellEdgeCaseE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellUserRequestedE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcTasksAndHandlersE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcUiEphemeralQueryE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcWorkspaceCheckpointsE2ETests" ;; extensions) initials=() shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcExtensionsLoadedE2ETests" ;; 2c) initials=(T V X Z) shard_filter="" ;; esac for namespace in E2E Unit; do for initial in "${initials[@]}"; do clause="FullyQualifiedName~GitHub.Copilot.Test.${namespace}.${initial}" shard_filter="${shard_filter:+${shard_filter}|}${clause}" done done filter="${filter:+(${filter})&}(${shard_filter})" fi if [[ -n "$filter" ]]; then args+=(--filter "$filter") fi dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" - name: Upload .NET test diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dotnet-test-diagnostics-${{ matrix.os }}-${{ matrix.transport }}-${{ matrix.backend }}-${{ matrix.shard }}-${{ github.run_attempt }} path: dotnet/TestResults/ if-no-files-found: warn retention-days: 7 # JavaScript actions use a glibc-linked Node runtime, so Alpine runs through Docker. alpine-arm64: name: ".NET SDK Tests (Alpine ARM64, ${{ matrix.transport }}, CAPI)" if: github.event.repository.fork == false runs-on: ubuntu-24.04-arm timeout-minutes: 20 env: COPILOT_SDK_E2E_BACKEND: capi strategy: fail-fast: false matrix: transport: ["default", "inprocess"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run ${{ matrix.transport }} tests on ARM64 Alpine uses: ./.github/actions/run-alpine-tests env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} with: image: mcr.microsoft.com/dotnet/sdk:10.0-alpine workdir: /workspace transport: ${{ matrix.transport }} command: | dotnet --info | grep -Eq "RID:[[:space:]]+linux-musl-arm64" apk add --no-cache nodejs npm (cd nodejs && npm ci --ignore-scripts) (cd test/harness && npm ci --ignore-scripts) export DOTNET_ROLL_FORWARD=Major dotnet restore dotnet/test/GitHub.Copilot.SDK.Test.csproj dotnet test dotnet/test/GitHub.Copilot.SDK.Test.csproj \ --no-restore \ --framework net8.0 \ -v n \ --blame-hang \ --blame-hang-timeout 10m \ --blame-hang-dump-type none \ --logger "trx;LogFilePrefix=test-results" \ --results-directory /workspace/dotnet/TestResults \ -p:RunAnalyzers=false - name: Upload .NET test diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dotnet-test-diagnostics-alpine-arm64-${{ matrix.transport }}-capi-${{ github.run_attempt }} path: dotnet/TestResults/ if-no-files-found: warn retention-days: 7