From 7df50e8ab9dd53238361ef54ba70959d6c37fc84 Mon Sep 17 00:00:00 2001 From: ethernet Date: Tue, 8 Sep 2026 13:02:14 -0400 Subject: [PATCH] test(ci): exercise locked toolchain build consumers --- .github/workflows/deploy-site.yml | 9 ++--- .github/workflows/docs-site-checks.yml | 9 ++--- .github/workflows/e2e-desktop.yml | 15 +++------ .github/workflows/pm-toolchain-smoke.yml | 2 +- .github/workflows/pm-toolchain.yml | 24 +++++++++++++ apps/desktop/package.json | 2 +- scripts/generate-icons.mjs | 2 +- tests-js/after-pack-toolchain.test.mjs | 43 ++++++++++++++++++++++++ tests-js/generate-icons.test.mjs | 6 ++-- 9 files changed, 81 insertions(+), 31 deletions(-) create mode 100644 tests-js/after-pack-toolchain.test.mjs diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 20b16a7516..ccb0c84b02 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -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: diff --git a/.github/workflows/docs-site-checks.yml b/.github/workflows/docs-site-checks.yml index 2bd8d0e044..1e7ced3ce2 100644 --- a/.github/workflows/docs-site-checks.yml +++ b/.github/workflows/docs-site-checks.yml @@ -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: diff --git a/.github/workflows/e2e-desktop.yml b/.github/workflows/e2e-desktop.yml index 24ba09d893..473060b7a6 100644 --- a/.github/workflows/e2e-desktop.yml +++ b/.github/workflows/e2e-desktop.yml @@ -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. diff --git a/.github/workflows/pm-toolchain-smoke.yml b/.github/workflows/pm-toolchain-smoke.yml index 214a1693a4..9432f640af 100644 --- a/.github/workflows/pm-toolchain-smoke.yml +++ b/.github/workflows/pm-toolchain-smoke.yml @@ -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 diff --git a/.github/workflows/pm-toolchain.yml b/.github/workflows/pm-toolchain.yml index e9913f8b10..776f8ab0ed 100644 --- a/.github/workflows/pm-toolchain.yml +++ b/.github/workflows/pm-toolchain.yml @@ -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 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 55ca9c26ea..b41681b849 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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", diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs index 58ba2efc83..77becb17ac 100644 --- a/scripts/generate-icons.mjs +++ b/scripts/generate-icons.mjs @@ -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 diff --git a/tests-js/after-pack-toolchain.test.mjs b/tests-js/after-pack-toolchain.test.mjs new file mode 100644 index 0000000000..ad7719999d --- /dev/null +++ b/tests-js/after-pack-toolchain.test.mjs @@ -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 }) + } +}) diff --git a/tests-js/generate-icons.test.mjs b/tests-js/generate-icons.test.mjs index b29b845d8e..90a4905637 100644 --- a/tests-js/generate-icons.test.mjs +++ b/tests-js/generate-icons.test.mjs @@ -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') })