mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
ci: set compatibility toolcache after runner environment export
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-06-node-compatibility-selfhosted.md
|
||||
2026-09-06-node-compatibility-selfhosted.md: e62be31b4383d9d13548565dce6371a4b250857d
|
||||
2026-09-06-node-compatibility-selfhosted.zh.md: ca8457d160dfd5f48010eb80ee03984425e29392
|
||||
2026-09-06-node-compatibility-selfhosted.md: 44015255a464e75b481ed41ac22f60421eed6059
|
||||
2026-09-06-node-compatibility-selfhosted.zh.md: 2794b47e02537341bae4d1fd2b434ebf3ba0ff3c
|
||||
|
||||
@@ -12,7 +12,7 @@ The Node 22.19, 24.9, and 26 compatibility jobs consume hosted Linux minutes eve
|
||||
|
||||
[CI](../../../../.github/workflows/ci.yml) applies the Linux failover variable to these three jobs, requiring a non-Dependabot author and a non-fork head repository matching the current repository. The standard hosted fallback remains available. [Runner documentation](../../../../docs/ci-compatible-selfhosted.md) owns setup and cleanup behavior.
|
||||
|
||||
The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching.
|
||||
The temporary tool cache trades repeated Node downloads for isolation across concurrent runners and Node versions. A setup-node-only [ESM preload](../../../../scripts/ci-compatible-toolcache.mjs) assigns the cache inside the action process: the Actions runner overwrites reserved environment variables after reading step configuration. An executed path check rejects installations outside runner temp; compatibility processes do not inherit the preload. pnpm keeps its existing private setup destination and persistent content-addressed store. Compile caches and node-gyp headers use runner temp before the first pnpm invocation. No global Node symlink or system package changes are introduced. Hosted jobs retain their tool and package caching.
|
||||
|
||||
The [failover runbook](2026-07-26-ci-failover-runbook.md) remains the owner of repository trust and pool switching. The [serial reference decision](2026-07-21-serial-cross-platform-ci-reference.md) remains the owner of master scheduling. Neither decision is superseded beyond the compatibility jobs' runner selection; both remain active.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
[CI](../../../../.github/workflows/ci.yml) 将 Linux 故障切换变量应用于这三个作业,要求作者不是 Dependabot,且非 fork 的头部仓库与当前仓库相同。标准托管回退仍然可用。[运行器文档](../../../../docs/ci-compatible-selfhosted.zh.md) 拥有安装与清理行为的说明。
|
||||
|
||||
临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存。
|
||||
临时工具缓存以重复下载 Node 为代价,换取并发运行器与 Node 版本之间的隔离。仅用于 setup-node 的 [ESM 预加载模块](../../../../scripts/ci-compatible-toolcache.mjs) 在 action 进程内指定缓存:Actions 运行器在读取步骤配置后会覆盖保留的环境变量。实际执行的路径检查拒绝运行器临时目录之外的安装;兼容性进程不继承预加载设置。pnpm 保留现有的私有安装目录和持久化内容寻址 store。编译缓存与 node-gyp 头文件在首次调用 pnpm 前就使用运行器临时目录。不引入全局 Node 符号链接或系统软件包变更。托管作业保留其工具与软件包缓存。
|
||||
|
||||
[故障切换手册](2026-07-26-ci-failover-runbook.zh.md) 仍拥有仓库信任与池切换规则。[串行参考决策](2026-07-21-serial-cross-platform-ci-reference.zh.md) 仍拥有 master 调度规则。除兼容性作业的运行器选择外,这两个决策都未被取代;两者均保持活跃。
|
||||
|
||||
|
||||
@@ -344,12 +344,23 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: ${{ runner.environment == 'self-hosted' && format('{0}/node-compat-toolcache', runner.temp) || runner.tool_cache }}
|
||||
# The runner overwrites RUNNER_* step env before starting JavaScript actions.
|
||||
NODE_OPTIONS: ${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: ${{ runner.environment == 'github-hosted' && 'pnpm' || '' }}
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Verify isolated Node installation
|
||||
if: runner.environment == 'self-hosted'
|
||||
run: |
|
||||
node_path=$(node -p process.execPath)
|
||||
echo "Node executable: $node_path"
|
||||
case "$node_path" in
|
||||
"$RUNNER_TEMP/node-compat-toolcache/"*) ;;
|
||||
*) echo "::error::Node compatibility installation is outside runner temp"; exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Install (immutable)
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/ci-compatible-selfhosted.md
|
||||
ci-compatible-selfhosted.md: b8f4efd31449c83d11c1db9802dfe1255743edb1
|
||||
ci-compatible-selfhosted.zh.md: 592f9d6b467315b3ed88b81df9fbd90a206f7f8b
|
||||
ci-compatible-selfhosted.md: 5cddb6fabf3b38f82463b7248683a22b7257930a
|
||||
ci-compatible-selfhosted.zh.md: c93e8c2eb38bc7edfc35382b0a2dacb01d637d5c
|
||||
|
||||
@@ -24,7 +24,7 @@ Each matrix entry runs one repository gate at a time. The matrix retains indepen
|
||||
|
||||
## Installation and cleanup
|
||||
|
||||
Self-hosted Node installations use a tool cache beneath `runner.temp`. pnpm setup uses its runner-and-run-private destination. Node compile caches and node-gyp headers also stay beneath runner temp; the pnpm content-addressed store remains persistent. Hosted jobs retain their normal tool cache and pnpm caching. Self-hosted jobs do not restore or upload hosted package caches.
|
||||
Self-hosted Node installations use a tool cache beneath `runner.temp`. A setup-node-only ESM preload sets the path inside the action process because the Actions runner overwrites reserved `RUNNER_*` step variables. The following step rejects a Node executable outside that temporary installation; later compatibility processes do not inherit the preload. pnpm setup uses its runner-and-run-private destination. Node compile caches and node-gyp headers also stay beneath runner temp; the pnpm content-addressed store remains persistent. Hosted jobs retain their normal tool cache and pnpm caching. Self-hosted jobs do not restore or upload hosted package caches.
|
||||
|
||||
The runner owns temporary-directory cleanup between jobs. These jobs do not install system packages or change global Node symlinks. The shared image must already provide the compiler and Python dependencies needed by native npm packages. A cold temporary Node cache requires downloading the selected runtime again.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Node 22.19、24.9 和 26 仅在 `DSH_CI_FAILOVER_LINUX=selfhosted`,且 PR(Pu
|
||||
|
||||
## 安装与清理
|
||||
|
||||
自托管 Node 安装使用 `runner.temp` 下的工具缓存。pnpm 设置使用运行器和运行私有的目标目录。Node 编译缓存和 node-gyp 头文件也保留在运行器临时目录下;pnpm 内容寻址 store 保持持久化。托管作业保留其常规工具缓存和 pnpm 缓存。自托管作业不恢复或上传托管软件包缓存。
|
||||
自托管 Node 安装使用 `runner.temp` 下的工具缓存。仅用于 setup-node 的 ESM 预加载模块在 action 进程内设置路径,因为 Actions 运行器会覆盖保留的 `RUNNER_*` 步骤变量。后续步骤拒绝位于该临时安装之外的 Node 可执行文件;之后的兼容性进程不继承预加载设置。pnpm 设置使用运行器和运行私有的目标目录。Node 编译缓存和 node-gyp 头文件也保留在运行器临时目录下;pnpm 内容寻址 store 保持持久化。托管作业保留其常规工具缓存和 pnpm 缓存。自托管作业不恢复或上传托管软件包缓存。
|
||||
|
||||
运行器负责作业之间的临时目录清理。这些作业不安装系统软件包,也不修改全局 Node 符号链接。共享镜像必须已提供原生 npm 软件包所需的编译器和 Python 依赖。冷的临时 Node 缓存需要重新下载所选运行时。
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const labels = ['self-hosted', 'linux', 'x64', 'vm-backup']
|
||||
function evaluate(expression: string, context: Record<string, unknown>): unknown {
|
||||
const body = expression.trim().slice(3, -2)
|
||||
return runInNewContext(body, {
|
||||
...context, fromJSON: JSON.parse, format: (template: string, value: string) => template.replace('{0}', value),
|
||||
...context, fromJSON: JSON.parse,
|
||||
}, { timeout: 1000 }) as unknown
|
||||
}
|
||||
|
||||
@@ -81,21 +81,68 @@ describe('Node compatibility self-hosted routing', () => {
|
||||
it('isolates version installs and enables hosted package caching only on hosted runners', () => {
|
||||
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
|
||||
expect(setup.env).toEqual({
|
||||
RUNNER_TOOL_CACHE: "${{ runner.environment == 'self-hosted' && format('{0}/node-compat-toolcache', runner.temp) || runner.tool_cache }}",
|
||||
NODE_OPTIONS: "${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}",
|
||||
})
|
||||
expect(setup.with?.['node-version']).toBe('${{ matrix.node }}')
|
||||
expect(setup.with?.['package-manager-cache']).toBe(false)
|
||||
for (const [environment, cache] of [['github-hosted', 'pnpm'], ['self-hosted', '']]) {
|
||||
const context = { runner: { environment, temp: '/runner/temp', tool_cache: '/runner/toolcache' } }
|
||||
expect(evaluate(setup.with?.cache as string, context)).toBe(cache)
|
||||
expect(evaluate(setup.env!.RUNNER_TOOL_CACHE!, context)).toBe(
|
||||
environment === 'self-hosted' ? '/runner/temp/node-compat-toolcache' : '/runner/toolcache',
|
||||
expect(evaluate(setup.env!.NODE_OPTIONS!, context)).toBe(
|
||||
environment === 'self-hosted' ? '--import=./scripts/ci-compatible-toolcache.mjs' : '',
|
||||
)
|
||||
}
|
||||
expect(job.steps[0]?.with).toEqual({ 'persist-credentials': false })
|
||||
expect(job.steps.some(step => step.uses?.startsWith('actions/cache/'))).toBe(false)
|
||||
})
|
||||
|
||||
it('overrides runner exports inside setup-node without affecting later Node processes', () => {
|
||||
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
|
||||
const nodeOptions = evaluate(setup.env!.NODE_OPTIONS!, { runner: { environment: 'self-hosted' } }) as string
|
||||
const root = mkdtempSync(join(tmpdir(), 'ci-compatible preload-'))
|
||||
try {
|
||||
const env = { PATH: process.env.PATH, RUNNER_TEMP: root, RUNNER_TOOL_CACHE: join(root, 'persistent') }
|
||||
const probe = (options: Record<string, string | undefined>) => {
|
||||
const child = spawnSync(process.execPath, ['-p', 'process.env.RUNNER_TOOL_CACHE'], {
|
||||
cwd: resolve(import.meta.dirname, '..'), env: options, encoding: 'utf8', timeout: 10_000,
|
||||
})
|
||||
expect(child.error).toBeUndefined()
|
||||
expect(child.signal).toBeNull()
|
||||
return child
|
||||
}
|
||||
const setupChild = probe({ ...env, NODE_OPTIONS: nodeOptions })
|
||||
expect(setupChild.status, setupChild.stderr).toBe(0)
|
||||
expect(setupChild.stdout.trim()).toBe(join(root, 'node-compat-toolcache'))
|
||||
const normalChild = probe(env)
|
||||
expect(normalChild.status, normalChild.stderr).toBe(0)
|
||||
expect(normalChild.stdout.trim()).toBe(env.RUNNER_TOOL_CACHE)
|
||||
const missingTemp = probe({ ...env, RUNNER_TEMP: undefined, NODE_OPTIONS: nodeOptions })
|
||||
expect(missingTemp.status).not.toBe(0)
|
||||
expect(missingTemp.stderr).toContain('requires an absolute RUNNER_TEMP')
|
||||
expect(job.env).not.toHaveProperty('NODE_OPTIONS')
|
||||
expect(job.steps.filter(step => step.env?.NODE_OPTIONS)).toEqual([setup])
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')('rejects a Node executable outside its runner temporary installation', () => {
|
||||
const step = job.steps.find(candidate => candidate.name === 'Verify isolated Node installation')!
|
||||
expect(step.if).toBe("runner.environment == 'self-hosted'")
|
||||
for (const [executable, status] of [
|
||||
['/runner temp/node-compat-toolcache/node/24.9.0/x64/bin/node', 0],
|
||||
['/shared/toolcache/node/24.9.0/x64/bin/node', 1],
|
||||
['/runner temp/node-compat-toolcache-other/node', 1],
|
||||
] as const) {
|
||||
const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', 'node() { printf "%s" "$TEST_EXECUTABLE"; }; ' + step.run!], {
|
||||
env: { PATH: process.env.PATH, RUNNER_TEMP: '/runner temp', TEST_EXECUTABLE: executable }, encoding: 'utf8', timeout: 10_000,
|
||||
})
|
||||
expect(child.error).toBeUndefined()
|
||||
expect(child.signal).toBeNull()
|
||||
expect(child.status, child.stderr).toBe(status)
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')('configures generated caches before pnpm without changing HOME or global links', () => {
|
||||
const index = job.steps.findIndex(step => step.name === 'Isolate compatibility caches')
|
||||
const step = job.steps[index]!
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { isAbsolute, join } from 'node:path'
|
||||
|
||||
// The Actions runner exports RUNNER_TOOL_CACHE after step env. Run inside the
|
||||
// setup-node process so version installs use runner temp rather than shared state.
|
||||
const temp = process.env.RUNNER_TEMP
|
||||
assert(temp && isAbsolute(temp), 'Node compatibility setup requires an absolute RUNNER_TEMP')
|
||||
process.env.RUNNER_TOOL_CACHE = join(temp, 'node-compat-toolcache')
|
||||
Reference in New Issue
Block a user