Преглед на файлове

revert(ci): run Windows Python runtime CI on GitHub-hosted Windows

Windows x64 runtime builds resolve their hosted matrix.runner unconditionally
again (windows-2025 for pull-request CI). Remove the DSH_CI_FAILOVER_WINDOWS
selector, job-private Python toolchain, self-hosted dependency install and
post-step cleanup, the private setup script, and the routing spec introduced
in #3629. The Windows failover switch again covers only the native Windows
jobs in ci.yml.
Tianyi Cui преди 3 седмици
родител
ревизия
faa33c0ffa
променени са 3 файла, в които са добавени 3 реда и са изтрити 278 реда
  1. 3 54
      .github/workflows/build-exe-for-python-sdk.yml
  2. 0 167
      scripts/python-runtime-selfhosted.spec.ts
  3. 0 57
      scripts/setup-python-runtime-windows.ps1

+ 3 - 54
.github/workflows/build-exe-for-python-sdk.yml

@@ -150,19 +150,7 @@ jobs:
   build:
     needs: [plan, sdk-wheel]
     name: ${{ matrix.target }}
-    # Release and manual builds retain disposable hosted images. Only trusted CI
-    # may use the persistent Windows host; Linux requires an unavailable Docker daemon.
-    runs-on: >-
-      ${{ inputs.ci && !inputs.release
-          && github.repository == 'deepseek-harness/deepseek-harness'
-          && github.event_name == 'pull_request'
-          && github.event.pull_request.head.repo.full_name == github.repository
-          && !github.event.pull_request.head.repo.fork
-          && github.event.pull_request.user.login != 'dependabot[bot]'
-          && matrix.target == 'node24-win-x64'
-          && vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
-          && fromJSON('["self-hosted", "dsh-win-ci", "windows", "x64"]')
-          || matrix.runner }}
+    runs-on: ${{ matrix.runner }}
     timeout-minutes: 45
     strategy:
       fail-fast: false
@@ -170,21 +158,13 @@ jobs:
         include: ${{ fromJSON(needs.plan.outputs.matrix) }}
     steps:
       - uses: actions/checkout@v6
-        with:
-          persist-credentials: false
-
-      - name: Prepare private Windows Python toolchain
-        id: private-windows
-        if: runner.os == 'Windows' && runner.environment == 'self-hosted'
-        shell: pwsh
-        run: ./scripts/setup-python-runtime-windows.ps1
 
       - uses: pnpm/action-setup@v4
         with:
           dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
 
       - name: Enable Windows Developer Mode (symlink support)
-        if: runner.os == 'Windows' && runner.environment != 'self-hosted'
+        if: runner.os == 'Windows'
         shell: pwsh
         run: >-
           reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
@@ -195,22 +175,18 @@ jobs:
       - uses: actions/setup-node@v6
         with:
           node-version: 24
-          cache: ${{ runner.environment != 'self-hosted' && 'pnpm' || '' }}
-          package-manager-cache: false
+          cache: pnpm
 
       - uses: actions/setup-python@v6.3.0
-        if: runner.environment != 'self-hosted'
         with:
           python-version: '3.10'
 
       - name: Install Python build tooling
-        if: runner.environment != 'self-hosted'
         run: python -m pip install uv==0.11.23
 
       # Cache pkg's target Node binary; lockfile changes roll the
       # exact key while the restore prefix can seed its replacement.
       - uses: actions/cache@v4
-        if: runner.environment != 'self-hosted'
         with:
           path: ~/.pkg-cache
           key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
@@ -218,16 +194,8 @@ jobs:
             pkg-fetch-${{ matrix.target }}-
 
       - name: Install (immutable)
-        if: runner.environment != 'self-hosted'
         run: pnpm install --frozen-lockfile
 
-      - name: Install private Windows dependencies (immutable)
-        if: runner.os == 'Windows' && runner.environment == 'self-hosted'
-        shell: pwsh
-        run: |
-          pnpm install --frozen-lockfile --package-import-method=copy
-          if ($LASTEXITCODE -ne 0) { throw 'Private Windows dependency installation failed.' }
-
       - name: Rebuild Linux node-pty against manylinux 2.28
         if: runner.os == 'Linux'
         env:
@@ -512,22 +480,3 @@ jobs:
           path: dist-python/${{ steps.runtime-posix.outputs.wheel || steps.runtime-windows.outputs.wheel }}
           if-no-files-found: error
           retention-days: 7
-
-      # Node action posts consume temp/compile-cache paths. pnpm post skips
-      # pruning without run_install; no Python/pkg/npm subprocess runs after cleanup.
-      - name: Remove private Windows toolchain and test directories
-        if: always() && steps.private-windows.outputs.root != ''
-        shell: pwsh
-        env:
-          PRIVATE_ROOT: ${{ steps.private-windows.outputs.root }}
-        run: |
-          Set-Location $env:GITHUB_WORKSPACE
-          $env:TMP = $env:RUNNER_TEMP
-          $env:TEMP = $env:RUNNER_TEMP
-          Remove-Item Env:NODE_COMPILE_CACHE -ErrorAction SilentlyContinue
-          "NODE_COMPILE_CACHE=" >> $env:GITHUB_ENV
-          "TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
-          "TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
-          node -e "const fs = require('node:fs'); const root = process.env.PRIVATE_ROOT; if (fs.lstatSync(root, { throwIfNoEntry: false })?.isSymbolicLink()) fs.unlinkSync(root); else fs.rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })"
-          if ($LASTEXITCODE -ne 0) { throw 'Private Windows job directory removal failed.' }
-          if (Test-Path -LiteralPath $env:PRIVATE_ROOT) { throw 'Private Windows job directory survived cleanup.' }

+ 0 - 167
scripts/python-runtime-selfhosted.spec.ts

@@ -1,167 +0,0 @@
-import { spawnSync } from 'node:child_process'
-import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
-import { tmpdir } from 'node:os'
-import { resolve } from 'node:path'
-import { runInNewContext } from 'node:vm'
-import * as yaml from 'js-yaml'
-import { describe, expect, it } from 'vitest'
-
-const root = resolve(import.meta.dirname, '..')
-const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/build-exe-for-python-sdk.yml'), 'utf8')) as {
-  jobs: Record<string, { 'runs-on': string; steps: Array<{ name?: string; id?: string; uses?: string; if?: string; shell?: string; run?: string; with?: Record<string, unknown> }> }>
-}
-const build = workflow.jobs.build!
-const selector = build['runs-on'].slice(3, -2).trim()
-const windows = ['self-hosted', 'dsh-win-ci', 'windows', 'x64']
-
-function context() {
-  return {
-    inputs: { ci: true, release: false },
-    github: {
-      repository: 'deepseek-harness/deepseek-harness',
-      event_name: 'pull_request',
-      ref: 'refs/pull/42/merge',
-      event: { pull_request: {
-        head: { repo: { full_name: 'deepseek-harness/deepseek-harness', fork: false } },
-        user: { login: 'contributor' },
-      } },
-    },
-    matrix: { target: 'node24-win-x64', runner: 'windows-2025' },
-    vars: { DSH_CI_FAILOVER_WINDOWS: 'selfhosted' },
-    fromJSON: JSON.parse,
-  }
-}
-
-function route(value: ReturnType<typeof context>, expression = selector): unknown {
-  // These canonical-case fixtures share JS/Actions comparison results; Actions also ignores string case.
-  // This evaluates the selected syntax, not GitHub's complete expression language.
-  return runInNewContext(expression, value, { timeout: 1000 })
-}
-
-describe('Python runtime self-hosted routing', () => {
-  it('routes same-repository member PRs to native x64 Windows', () => {
-    expect(route(context())).toEqual(windows)
-  })
-
-  it.each([
-    ['release caller', (value: ReturnType<typeof context>) => { value.inputs.release = true }],
-    ['non-CI caller', (value: ReturnType<typeof context>) => { value.inputs.ci = false }],
-    ['manual dispatch', (value: ReturnType<typeof context>) => { value.github.event_name = 'workflow_dispatch' }],
-    ['pull_request_target', (value: ReturnType<typeof context>) => { value.github.event_name = 'pull_request_target' }],
-    ['unknown event', (value: ReturnType<typeof context>) => { value.github.event_name = '' }],
-    ['fork', (value: ReturnType<typeof context>) => { value.github.event.pull_request.head.repo.fork = true }],
-    ['different repository head', (value: ReturnType<typeof context>) => { value.github.event.pull_request.head.repo.full_name = 'someone/fork' }],
-    ['different caller repository', (value: ReturnType<typeof context>) => { value.github.repository = 'someone/fork' }],
-    ['Dependabot author', (value: ReturnType<typeof context>) => { value.github.event.pull_request.user.login = 'dependabot[bot]' }],
-    ['disabled failover', (value: ReturnType<typeof context>) => { value.vars.DSH_CI_FAILOVER_WINDOWS = '' }],
-    ['unknown failover value', (value: ReturnType<typeof context>) => { value.vars.DSH_CI_FAILOVER_WINDOWS = 'hosted' }],
-    ['master push', (value: ReturnType<typeof context>) => { value.github.event_name = 'push'; value.github.ref = 'refs/heads/master' }],
-    ['branch push', (value: ReturnType<typeof context>) => { value.github.event_name = 'push'; value.github.ref = 'refs/heads/topic' }],
-    ['tag push', (value: ReturnType<typeof context>) => { value.github.event_name = 'push'; value.github.ref = 'refs/tags/python-v1' }],
-  ] as const)('keeps %s on the hosted fallback', (_name, change) => {
-    const value = context()
-    change(value)
-    expect(route(value)).toBe('windows-2025')
-  })
-
-  it.each([
-    ['node24-linux-x64', 'ubuntu-latest'],
-    ['node24-linux-arm64', 'ubuntu-24.04-arm'],
-    ['node24-macos-arm64', 'macos-latest'],
-    ['node24-macos-x64', 'macos-15-intel'],
-  ])('keeps %s hosted even with failover enabled', (target, runner) => {
-    const value = context()
-    value.matrix = { target, runner }
-    expect(route(value)).toBe(runner)
-  })
-
-  it('keeps setup helper jobs on hosted images', () => {
-    expect(workflow.jobs.plan!['runs-on']).toBe('ubuntu-latest')
-    expect(workflow.jobs['sdk-wheel']!['runs-on']).toBe('ubuntu-latest')
-  })
-
-  it('isolates setup before pnpm and excludes shared installers and cache archives', () => {
-    const privateSetup = build.steps.findIndex(step => step.id === 'private-windows')
-    expect(privateSetup).toBeGreaterThan(0)
-    expect(privateSetup).toBeLessThan(build.steps.findIndex(step => step.uses?.startsWith('pnpm/action-setup@')))
-    for (const step of build.steps.filter(step => step.uses?.startsWith('actions/setup-python@') || step.uses?.startsWith('actions/cache@') || step.name === 'Install Python build tooling')) {
-      expect(step.if).toBe("runner.environment != 'self-hosted'")
-    }
-    expect(build.steps.find(step => step.name?.startsWith('Enable Windows'))?.if).toBe("runner.os == 'Windows' && runner.environment != 'self-hosted'")
-    expect(build.steps.find(step => step.uses?.startsWith('actions/setup-node@'))?.with?.cache).toContain("runner.environment != 'self-hosted'")
-    expect(build.steps.at(-1)).toMatchObject({ if: "always() && steps.private-windows.outputs.root != ''", shell: 'pwsh' })
-    expect(build.steps.find(step => step.uses?.startsWith('actions/setup-node@'))?.with?.['package-manager-cache']).toBe(false)
-    expect(build.steps.find(step => step.name === 'Install (immutable)')?.if).toBe("runner.environment != 'self-hosted'")
-    expect(build.steps.find(step => step.name === 'Install private Windows dependencies (immutable)')).toMatchObject({
-      if: "runner.os == 'Windows' && runner.environment == 'self-hosted'",
-      shell: 'pwsh',
-    })
-    expect(build.steps.find(step => step.name === 'Install private Windows dependencies (immutable)')?.run).toContain('pnpm install --frozen-lockfile --package-import-method=copy')
-    const cleanup = build.steps.at(-1)!.run!
-    expect(cleanup).toContain('"NODE_COMPILE_CACHE=" >> $env:GITHUB_ENV')
-    expect(cleanup).toContain('"TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV')
-    expect(cleanup).toContain('"TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV')
-    expect(cleanup).toContain('maxRetries: 10, retryDelay: 100')
-  })
-
-  it.each(['root', 'nested', 'absent'] as const)('cleans a %s job directory without deleting another target', (location) => {
-    const temp = mkdtempSync(resolve(tmpdir(), 'python-runtime-cleanup-'))
-    try {
-      const target = resolve(temp, 'other-job')
-      const owned = resolve(temp, 'owned')
-      mkdirSync(target)
-      writeFileSync(resolve(target, 'sentinel'), 'preserve')
-      if (location === 'nested') mkdirSync(owned)
-      if (location !== 'absent') symlinkSync(target, location === 'root' ? owned : resolve(owned, 'link'), 'junction')
-      const command = /node -e "([^"\n]+)"/.exec(build.steps.at(-1)!.run!)?.[1]
-      expect(command).toBeDefined()
-      const result = spawnSync(process.execPath, ['-e', command!], {
-        env: { ...process.env, PRIVATE_ROOT: owned, NODE_COMPILE_CACHE: '' },
-        encoding: 'utf8',
-        timeout: 10000,
-      })
-      expect(result.error).toBeUndefined()
-      expect(result.signal).toBeNull()
-      expect(result.status, result.stderr).toBe(0)
-      expect(existsSync(owned)).toBe(false)
-      expect(readFileSync(resolve(target, 'sentinel'), 'utf8')).toBe('preserve')
-    } finally {
-      rmSync(temp, { recursive: true, force: true })
-    }
-  })
-
-  it('reads UTF-8 Session JSONL independently of the host locale', () => {
-    const setup = readFileSync(resolve(root, 'scripts/setup-python-runtime-windows.ps1'), 'utf8')
-    const utf8 = /PYTHONUTF8 = '([^']+)'/.exec(setup)?.[1]
-    expect(utf8).toBe('1')
-    const result = spawnSync(process.platform === 'win32' ? 'python' : 'python3', ['-c', [
-      'import pathlib, tempfile, sys',
-      'assert sys.flags.utf8_mode == 1',
-      'with tempfile.TemporaryDirectory(prefix="python-runtime-encoding-") as root:',
-      '    log = pathlib.Path(root) / "session.jsonl"',
-      '    text = chr(0x2014) + chr(0x4e2d)',
-      '    log.write_bytes(text.encode("utf-8"))',
-      '    assert log.read_text() == text',
-    ].join('\n')], {
-      env: { ...process.env, LC_ALL: 'C', LANG: 'C', PYTHONCOERCECLOCALE: '0', PYTHONUTF8: utf8 },
-      encoding: 'utf8',
-      timeout: 10000,
-    })
-    expect(result.error).toBeUndefined()
-    expect(result.signal).toBeNull()
-    expect(result.status, result.stderr).toBe(0)
-  })
-
-  it('pins portable Python without registry or shared cache writes', () => {
-    const setup = readFileSync(resolve(root, 'scripts/setup-python-runtime-windows.ps1'), 'utf8')
-    expect(setup).toContain('--no-bin --no-registry 3.10')
-    expect(setup).toContain('--managed-python --no-python-downloads --seed')
-    expect(setup).toContain('UV_PYTHON_INSTALL_REGISTRY')
-    expect(setup).toContain('PNPM_CONFIG_STORE_DIR')
-    expect(setup).toContain('PKG_CACHE_PATH')
-    expect(setup.indexOf('$bootstrapScripts >> $env:GITHUB_PATH')).toBeLessThan(setup.indexOf('$toolingScripts >> $env:GITHUB_PATH'))
-    expect(setup).toContain('AllowDevelopmentWithoutDevLicense -ErrorAction SilentlyContinue')
-    expect(setup).toContain('$null -eq $devMode -or')
-    expect(setup).not.toMatch(/reg add|Set-ItemProperty|InstallAllUsers/)
-  })
-})

+ 0 - 57
scripts/setup-python-runtime-windows.ps1

@@ -1,57 +0,0 @@
-# Prepare a job-private Python 3.10 toolchain without Windows installer or registry writes.
-$ErrorActionPreference = 'Stop'
-$root = Join-Path $env:RUNNER_TEMP ("python-runtime-" + [guid]::NewGuid().ToString('N'))
-New-Item -ItemType Directory -Path $root | Out-Null
-"root=$root" >> $env:GITHUB_OUTPUT
-
-$privateEnvironment = @{
-  # Session JSONL and SDK pipes use UTF-8, including on Chinese Windows images.
-  PYTHONUTF8 = '1'
-  PYTHONIOENCODING = 'utf-8'
-  TMP = $root
-  TEMP = $root
-  UV_CACHE_DIR = (Join-Path $root 'uv-cache')
-  UV_PYTHON_INSTALL_DIR = (Join-Path $root 'python')
-  UV_PYTHON_INSTALL_BIN = '0'
-  UV_PYTHON_INSTALL_REGISTRY = '0'
-  UV_NO_CONFIG = '1'
-  PIP_CACHE_DIR = (Join-Path $root 'pip-cache')
-  npm_config_cache = (Join-Path $root 'npm-cache')
-  npm_config_devdir = (Join-Path $root 'node-gyp')
-  PNPM_CONFIG_PACKAGE_IMPORT_METHOD = 'copy'
-  PKG_CACHE_PATH = (Join-Path $root 'pkg-cache')
-  PNPM_CONFIG_STORE_DIR = (Join-Path $root 'pnpm-store')
-  NODE_COMPILE_CACHE = (Join-Path $root 'node-compile-cache')
-}
-foreach ($entry in $privateEnvironment.GetEnumerator()) {
-  [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process')
-  "$($entry.Key)=$($entry.Value)" >> $env:GITHUB_ENV
-}
-
-if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne 'X64') {
-  throw 'Python runtime CI requires a native x64 Windows host.'
-}
-$devMode = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' -Name AllowDevelopmentWithoutDevLicense -ErrorAction SilentlyContinue
-if ($null -eq $devMode -or $devMode.AllowDevelopmentWithoutDevLicense -ne 1) {
-  throw 'The self-hosted Windows image must enable Developer Mode before CI.'
-}
-
-$bootstrap = Join-Path $root 'bootstrap'
-python -m venv $bootstrap
-if ($LASTEXITCODE -ne 0) { throw 'The self-hosted Windows image requires Python with venv and ensurepip.' }
-$bootstrapScripts = Join-Path $bootstrap 'Scripts'
-& (Join-Path $bootstrapScripts 'python.exe') -m pip --isolated --disable-pip-version-check --no-cache-dir install uv==0.11.23
-if ($LASTEXITCODE -ne 0) { throw 'Job-private uv installation failed.' }
-$uv = Join-Path $bootstrapScripts 'uv.exe'
-& $uv python install --install-dir $env:UV_PYTHON_INSTALL_DIR --no-bin --no-registry 3.10
-if ($LASTEXITCODE -ne 0) { throw 'Job-private Python 3.10 download failed.' }
-$tooling = Join-Path $root 'tooling'
-& $uv venv --python 3.10 --managed-python --no-python-downloads --seed $tooling
-if ($LASTEXITCODE -ne 0) { throw 'Job-private Python 3.10 environment creation failed.' }
-$toolingScripts = Join-Path $tooling 'Scripts'
-$python = Join-Path $toolingScripts 'python.exe'
-& $python -c 'import platform, sys; assert sys.version_info[:2] == (3, 10); assert platform.machine() == "AMD64"; print(sys.version); print(sys.executable)'
-if ($LASTEXITCODE -ne 0) { throw 'Job-private Python version or architecture is incorrect.' }
-# Actions prepends each entry: the last appended directory wins Python lookup.
-$bootstrapScripts >> $env:GITHUB_PATH
-$toolingScripts >> $env:GITHUB_PATH