test(ci): exercise locked toolchain build consumers

This commit is contained in:
ethernet
2026-09-08 13:02:14 -04:00
parent b676997d2d
commit 7df50e8ab9
9 changed files with 81 additions and 31 deletions

View File

@@ -63,17 +63,12 @@ jobs:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Set up locked Node and npm
id: node
- name: Set up the locked site toolchain
uses: ./.github/actions/setup-pm
with:
toolchain: node
toolchain: all
node-cache-dependency-path: website/package-lock.json
- uses: ./.github/actions/setup-pm
with:
cache-python: true
- name: Install PyYAML for skill extraction
uses: ./.github/actions/retry
with:

View File

@@ -13,11 +13,10 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up locked Node and npm
id: node
- name: Set up the locked site toolchain
uses: ./.github/actions/setup-pm
with:
toolchain: node
toolchain: all
node-cache-dependency-path: website/package-lock.json
- name: Install website dependencies
@@ -26,10 +25,6 @@ jobs:
command: npm ci
working-directory: website
- uses: ./.github/actions/setup-pm
with:
cache-python: true
- name: Install ascii-guard
uses: ./.github/actions/retry
with:

View File

@@ -39,12 +39,12 @@ jobs:
libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 \
xdg-utils libatspi2.0-0 libdrm2 libgbm1 libasound2t64
# ── Node ───────────────────────────────────────────────────────────
- name: Set up locked Node and npm
id: node
- name: Set up the locked desktop toolchain
uses: ./.github/actions/setup-pm
with:
toolchain: node
toolchain: all
extras: '["all", "dev"]'
prune-python-cache: true
# Full npm ci (not --ignore-scripts): electron's postinstall
# downloads the binary we launch, and node-pty's native build is
@@ -53,13 +53,6 @@ jobs:
with:
command: npm ci
# ── Python (for the hermes serve backend) ──────────────────────────
- name: Set up locked Python and backend dependencies
uses: ./.github/actions/setup-pm
with:
extras: '["all", "dev"]'
prune-python-cache: true
# ── Build desktop app ─────────────────────────────────────────────
# The Playwright step below runs `npm run build` before testing so
# dist/ is always fresh — no separate build step needed here.

View File

@@ -79,7 +79,7 @@ jobs:
flags=()
if [ "$EXPECT_WARM" = true ]; then flags+=(--offline); fi
npm ci --workspace tests-js --include-workspace-root --include=dev --ignore-scripts --no-audit --no-fund ${flags[@]+"${flags[@]}"}
node node_modules/vitest/vitest.mjs run --root tests-js setup-pm-post.test.mjs
node node_modules/vitest/vitest.mjs run --root tests-js setup-pm-post.test.mjs generate-icons.test.mjs
- name: Run the PM and action contracts
shell: bash

View File

@@ -39,3 +39,27 @@ jobs:
prune-python-cache: true
cache-suffix: smoke-prune-${{ github.run_id }}-${{ github.run_attempt }}
- run: python -c 'import pytest; print(pytest.__version__)'
build-consumers:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup-pm
with:
toolchain: all
cache-suffix: smoke-consumers-${{ github.run_id }}-${{ github.run_attempt }}
- name: Install the docs tools into the command environment
run: |
uv pip install --python "$HERMES_PYTHON" ascii-guard==2.3.0 pyyaml==6.0.3 httpx==0.28.1
python3 -c 'import yaml,httpx; print(yaml.__version__, httpx.__version__)'
- name: Install the locked desktop and test workspaces
run: npm ci --workspace apps/desktop --workspace tests-js --include-workspace-root --include=dev --ignore-scripts --no-audit --no-fund
- name: Exercise the actual after-pack relocation hook
run: node node_modules/vitest/vitest.mjs run --root tests-js after-pack-toolchain.test.mjs generate-icons.test.mjs setup-pm-post.test.mjs
- name: Exercise the payload and icon build entrypoints
run: |
npm run payload --workspace apps/desktop -- --help
node scripts/generate-icons.mjs
node scripts/generate-icons.mjs --check

View File

@@ -34,7 +34,7 @@
"builder": "cross-env NODE_OPTIONS=--max-old-space-size=16384 node scripts/run-electron-builder.mjs",
"pack": "npm run build && npm run builder -- --dir --publish never",
"dist": "npm run build && npm run builder",
"payload": "uv run --no-project --python 3.11 python ../../scripts/bundles/stage.py --out build/agent-payload",
"payload": "uv run --no-project python ../../scripts/bundles/stage.py --out build/agent-payload",
"dist:bundled": "npm run payload && cross-env HERMES_DESKTOP_VARIANT=bundled npm run dist",
"dist:mac": "npm run build && npm run builder -- --mac",
"dist:mac:dmg": "npm run build && npm run builder -- --mac dmg",

View File

@@ -24,7 +24,7 @@ export function generateIcons(args = [], { root = repoRoot, run = spawnSync, env
delete childEnv.PYTHONPATH
delete childEnv.PYTHONHOME
const result = run('uv', [
'run', '--isolated', '--locked', '--only-group', 'icon-build', '--python', '3.14',
'run', '--isolated', '--locked', '--only-group', 'icon-build',
// PM copies its wheel cache into payloads. Keep build wheels outside it.
'--cache-dir', path.join(root, '.cache', 'icon-build'),
'python', path.join(root, 'scripts', 'generate_icons.py'), ...args

View File

@@ -0,0 +1,43 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { expect, test } from 'vitest'
import afterPack from '../apps/desktop/scripts/after-pack.mjs'
// This is the Linux afterPack path on a real Linux host, not a fake host flag.
// macOS adds Developer ID signing; its native release lane owns that proof.
test.runIf(process.platform === 'linux')('afterPack uses the provisioned Python to repair actual payload links', async () => {
const directory = mkdtempSync(path.join(tmpdir(), 'after-pack-toolchain-'))
const payload = path.join(directory, 'resources', 'agent-payload')
const store = path.join(payload, 'tools', 'python', 'bin')
const venv = path.join(payload, 'venv', 'bin')
const python = execFileSync('python3', ['-c', 'import sys; print(sys.executable)'], { encoding: 'utf8' }).trim()
const previous = { UV_PYTHON: process.env.UV_PYTHON, UV_PYTHON_DOWNLOADS: process.env.UV_PYTHON_DOWNLOADS, PYTHONPATH: process.env.PYTHONPATH }
const observed = path.join(directory, 'interpreter.json')
const identity = 'import json,os,sys; print(json.dumps(os.path.realpath(sys.executable)))'
const expected = JSON.parse(execFileSync(python, ['-c', identity], { encoding: 'utf8' }))
try {
// Record the interpreter that actually executes the relocation script.
writeFileSync(path.join(directory, 'sitecustomize.py'), `import json,os,sys\nfrom pathlib import Path\nPath(${JSON.stringify(observed)}).write_text(json.dumps(os.path.realpath(sys.executable)), encoding="utf-8")\n`)
process.env.PYTHONPATH = directory
process.env.UV_PYTHON = python
process.env.UV_PYTHON_DOWNLOADS = 'never'
mkdirSync(store, { recursive: true })
mkdirSync(venv, { recursive: true })
writeFileSync(path.join(payload, 'manifest.json'), '{}')
writeFileSync(path.join(store, 'python3'), 'payload interpreter link target')
symlinkSync('/builder/tools/python/bin/python3', path.join(venv, 'python'))
symlinkSync('python', path.join(venv, 'python3'))
await afterPack({ electronPlatformName: process.platform, appOutDir: directory })
expect(JSON.parse(readFileSync(observed, 'utf8'))).toBe(expected)
expect(readlinkSync(path.join(venv, 'python'))).toBe('../../tools/python/bin/python3')
expect(readFileSync(path.join(venv, 'python3'), 'utf8')).toBe('payload interpreter link target')
} finally {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
rmSync(directory, { recursive: true, force: true })
}
})

View File

@@ -5,13 +5,13 @@ import { generateIcons } from '../scripts/generate-icons.mjs'
test('icon builds use only the locked build group outside every application venv', () => {
const run = vi.fn(() => ({ status: 0 }))
const root = path.resolve('icon-build-fixture')
const env = { PATH: 'tools', VIRTUAL_ENV: 'runtime-venv', PYTHONPATH: 'payload-libraries', PYTHONHOME: 'payload-python' }
const env = { PATH: 'tools', UV_PYTHON: 'pm-locked-python', VIRTUAL_ENV: 'runtime-venv', PYTHONPATH: 'payload-libraries', PYTHONHOME: 'payload-python' }
expect(generateIcons(['--check'], { root, run, env })).toBe(0)
expect(run).toHaveBeenCalledExactlyOnceWith('uv', [
'run', '--isolated', '--locked', '--only-group', 'icon-build', '--python', '3.11',
'run', '--isolated', '--locked', '--only-group', 'icon-build',
'--cache-dir', path.join(root, '.cache', 'icon-build'),
'python', path.join(root, 'scripts', 'generate_icons.py'), '--check'
], { cwd: root, stdio: 'inherit', windowsHide: true, env: { PATH: 'tools', VIRTUAL_ENV: 'runtime-venv' } })
], { cwd: root, stdio: 'inherit', windowsHide: true, env: { PATH: 'tools', UV_PYTHON: env.UV_PYTHON, VIRTUAL_ENV: 'runtime-venv' } })
expect(env.PYTHONPATH).toBe('payload-libraries')
})