feat(plugin): add registry name handling and release age exclusion for pnpm workspace
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / node 22.19 (push) Has been cancelled
CI / node 26 (push) Has been cancelled
CI / python 3.10 / keyless SDK (push) Has been cancelled
CI / windows node 24 / wine blocking (push) Has been cancelled
CI / wine apt cache (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Has been cancelled
Release (vendor) / Pack npm tarballs (push) Has been cancelled
Release (dsh) / Pack npm tarballs (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Release (vendor) / Publish to npm (push) Has been cancelled
Release (dsh) / Publish to npm (push) Has been cancelled
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / node 22.19 (push) Has been cancelled
CI / node 26 (push) Has been cancelled
CI / python 3.10 / keyless SDK (push) Has been cancelled
CI / windows node 24 / wine blocking (push) Has been cancelled
CI / wine apt cache (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Has been cancelled
Release (vendor) / Pack npm tarballs (push) Has been cancelled
Release (dsh) / Pack npm tarballs (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Release (vendor) / Publish to npm (push) Has been cancelled
Release (dsh) / Publish to npm (push) Has been cancelled
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
runPnpmRemove,
|
||||
uninstallBundle,
|
||||
writeAllowBuilds,
|
||||
writeReleaseAgeExclude,
|
||||
} from './install.ts'
|
||||
import {
|
||||
fetchMarketplaceCatalog,
|
||||
@@ -253,8 +254,12 @@ export class PluginInventoryGateway extends TypertRemoteService {
|
||||
writeAllowBuilds(profileDir, spec.consentBuilds)
|
||||
}
|
||||
// A registry-name spec participates in the registry fallback loop and the
|
||||
// minimum-release-age exemption; a git, tarball, or path spec runs once.
|
||||
// minimum-release-age exemption; a git, tarball, or path spec runs once. The
|
||||
// release-age exemption is written into pnpm-workspace.yaml (not passed as a
|
||||
// CLI flag) so an older pnpm ignores the setting instead of aborting on an
|
||||
// unknown option.
|
||||
const registryName = registryPackageName(spec.spec)
|
||||
if (registryName !== undefined) writeReleaseAgeExclude(profileDir, [registryName])
|
||||
const result = registryName === undefined
|
||||
? runPnpmInstall({
|
||||
binName: 'dsh',
|
||||
@@ -273,7 +278,6 @@ export class PluginInventoryGateway extends TypertRemoteService {
|
||||
pnpmCjs: pnpm.pnpmCjs,
|
||||
spec: spec.spec,
|
||||
before,
|
||||
minimumReleaseAgeExclude: registryName,
|
||||
})
|
||||
if (result.pendingBuilds !== undefined) {
|
||||
return { ok: true, restartRequired: false, pendingBuilds: result.pendingBuilds }
|
||||
|
||||
@@ -150,8 +150,6 @@ export interface PnpmInstallOptions {
|
||||
readonly before: ProfileManifest
|
||||
/** An npm registry to install from (`pnpm add --registry`); defaults to pnpm's configured one. */
|
||||
readonly registry?: string
|
||||
/** A bare registry package name to exempt from pnpm's minimum-release-age check. */
|
||||
readonly minimumReleaseAgeExclude?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,15 +162,9 @@ export interface PnpmInstallOptions {
|
||||
* @returns `pendingBuilds` when pnpm blocked build scripts, else an empty result.
|
||||
*/
|
||||
export function runPnpmInstall(options: PnpmInstallOptions): PnpmAddResult {
|
||||
const { binName, profileDir, installAnchor, nodeBin, pnpmCjs, spec, before, registry, minimumReleaseAgeExclude } = options
|
||||
const { binName, profileDir, installAnchor, nodeBin, pnpmCjs, spec, before, registry } = options
|
||||
const args = ['add', spec]
|
||||
if (registry !== undefined) args.push('--registry', registry)
|
||||
// `--minimum-release-age-exclude` is understood only by pnpm ≥10.7. It is safe
|
||||
// with the vendored pnpm (pinned 11.7); a PATH-pnpm fallback in a development
|
||||
// checkout may be older and reject the option, so skip it there.
|
||||
if (minimumReleaseAgeExclude !== undefined && nodeBin !== undefined) {
|
||||
args.push(`--minimum-release-age-exclude=${minimumReleaseAgeExclude}`)
|
||||
}
|
||||
const result = nodeBin === undefined ? spawn(pnpmCjs, args, profileDir) : spawn(nodeBin, [pnpmCjs, ...args], profileDir)
|
||||
if (result.exitCode !== 0) {
|
||||
const pendingBuilds = parseBlockedBuilds(result.output)
|
||||
@@ -264,6 +256,37 @@ export function writeAllowBuilds(profileDir: string, names: readonly string[]):
|
||||
writeFileSync(workspacePath, dump(doc))
|
||||
}
|
||||
|
||||
/**
|
||||
* Exempt the given registry package names from pnpm's `minimumReleaseAge` check
|
||||
* by writing the `minimumReleaseAgeExclude` setting into the profile's
|
||||
* `pnpm-workspace.yaml`. Writing the config directly is the robust way to beat
|
||||
* the release-age check: the setting is honored by pnpm ≥10.16 (the documented
|
||||
* mechanism, which pnpm reads only from `pnpm-workspace.yaml`, not `.npmrc`),
|
||||
* and — unlike the `--minimum-release-age-exclude` CLI flag — an unknown-key pnpm
|
||||
* ignores it instead of aborting with `Unknown option`. This lets a plugin
|
||||
* published minutes ago install at its latest version on whichever pnpm the
|
||||
* runtime resolves. Existing workspace settings are preserved and the exclusion
|
||||
* list is merged.
|
||||
* @param profileDir - the writable profile directory.
|
||||
* @param names - the registry package names to exempt from the release-age check.
|
||||
*/
|
||||
export function writeReleaseAgeExclude(profileDir: string, names: readonly string[]): void {
|
||||
const workspacePath = join(profileDir, 'pnpm-workspace.yaml')
|
||||
let doc: Record<string, unknown>
|
||||
try {
|
||||
const parsed = load(readFileSync(workspacePath, 'utf8'))
|
||||
doc = parsed !== null && typeof parsed === 'object'
|
||||
? parsed as Record<string, unknown>
|
||||
: { ...PROFILE_WORKSPACE_BASE }
|
||||
} catch {
|
||||
doc = { ...PROFILE_WORKSPACE_BASE }
|
||||
}
|
||||
const excluded = new Set<string>((doc.minimumReleaseAgeExclude ?? []) as string[])
|
||||
for (const name of names) excluded.add(name)
|
||||
if (excluded.size > 0) doc.minimumReleaseAgeExclude = [...excluded]
|
||||
writeFileSync(workspacePath, dump(doc))
|
||||
}
|
||||
|
||||
/** Options for removing one plugin dependency. */
|
||||
export interface PnpmRemoveOptions {
|
||||
readonly binName: string
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { load } from 'js-yaml'
|
||||
import { readProfileManifest } from '@deepseek-ai/dsh-app-boot'
|
||||
import {
|
||||
composeOfflineBundle, INSTALL_REGISTRIES, parseBlockedBuilds, registryPackageName,
|
||||
resolvePnpm, resolvePnpmCommand, runPnpmInstall, writeAllowBuilds,
|
||||
writeReleaseAgeExclude,
|
||||
runPnpmInstallWithRegistries, runPnpmRemove, uninstallBundle,
|
||||
} from '../src/install.ts'
|
||||
|
||||
@@ -67,24 +69,6 @@ function makeBlockedBuildPnpm(dir: string): string {
|
||||
return file
|
||||
}
|
||||
|
||||
/**
|
||||
* A self-executable fake pnpm (shebang + executable bit) that records its argv
|
||||
* and exits 0 — simulates a `pnpm` command invoked directly off PATH, without a
|
||||
* `node` prefix (as the PATH-pnpm fallback does).
|
||||
*/
|
||||
function makeExecutablePnpm(dir: string): string {
|
||||
const file = join(dir, 'pnpm-path')
|
||||
writeFileSync(file, [
|
||||
'#!/usr/bin/env node',
|
||||
"const fs = require('fs')",
|
||||
'const args = process.argv.slice(2)',
|
||||
'fs.writeFileSync(process.env.RECORD, JSON.stringify(args))',
|
||||
'process.exit(0)',
|
||||
].join('\n'))
|
||||
chmodSync(file, 0o755)
|
||||
return file
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake pnpm that, for `remove <name>`, drops the named dependency from the
|
||||
* profile's package.json (as pnpm does) and exits 0.
|
||||
@@ -171,7 +155,7 @@ describe('runPnpmInstall', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('passes minimum-release-age-exclude for a registry name', () => {
|
||||
it('never passes a release-age CLI flag (the exemption is a workspace setting, not a flag)', () => {
|
||||
const dir = makeProfile()
|
||||
const record = join(dir, 'record.json')
|
||||
const pnpm = makeRecordingPnpm(dir)
|
||||
@@ -181,32 +165,28 @@ describe('runPnpmInstall', () => {
|
||||
runPnpmInstall({
|
||||
binName: 'dsh', profileDir: dir, installAnchor: join(dir, 'package.json'),
|
||||
nodeBin: process.execPath, pnpmCjs: pnpm, spec: 'x',
|
||||
before: readProfileManifest('dsh', dir), minimumReleaseAgeExclude: 'x',
|
||||
before: readProfileManifest('dsh', dir),
|
||||
})
|
||||
const args = JSON.parse(readFileSync(record, 'utf8')) as string[]
|
||||
expect(args).toContain('--minimum-release-age-exclude=x')
|
||||
expect(args).toEqual(['add', 'x'])
|
||||
expect(args.some(arg => arg.includes('minimum-release-age'))).toBe(false)
|
||||
} finally {
|
||||
delete process.env.RECORD
|
||||
delete process.env.EXIT
|
||||
}
|
||||
})
|
||||
|
||||
it('omits minimum-release-age-exclude for a PATH-pnpm fallback (may be an older pnpm)', () => {
|
||||
it('writes the minimum-release-age exemption into pnpm-workspace.yaml, preserving existing settings', () => {
|
||||
const dir = makeProfile()
|
||||
const record = join(dir, 'record.json')
|
||||
const pnpm = makeExecutablePnpm(dir)
|
||||
process.env.RECORD = record
|
||||
try {
|
||||
runPnpmInstall({
|
||||
binName: 'dsh', profileDir: dir, installAnchor: join(dir, 'package.json'),
|
||||
nodeBin: undefined, pnpmCjs: pnpm, spec: 'x',
|
||||
before: readProfileManifest('dsh', dir), minimumReleaseAgeExclude: 'x',
|
||||
})
|
||||
const args = JSON.parse(readFileSync(record, 'utf8')) as string[]
|
||||
expect(args).toEqual(['add', 'x'])
|
||||
} finally {
|
||||
delete process.env.RECORD
|
||||
}
|
||||
const workspacePath = join(dir, 'pnpm-workspace.yaml')
|
||||
writeFileSync(workspacePath, 'autoInstallPeers: false\nallowBuilds:\n node-pty: true\n')
|
||||
writeReleaseAgeExclude(dir, ['dsh-theme-plugin'])
|
||||
writeReleaseAgeExclude(dir, ['@scope/pkg'])
|
||||
const doc = JSON.parse(JSON.stringify(load(readFileSync(workspacePath, 'utf8')))) as Record<string, unknown>
|
||||
expect(doc.minimumReleaseAgeExclude).toEqual(['dsh-theme-plugin', '@scope/pkg'])
|
||||
// Existing settings survive.
|
||||
expect((doc.allowBuilds as Record<string, unknown>).node_pty ?? (doc.allowBuilds as Record<string, unknown>)['node-pty']).toBe(true)
|
||||
expect(doc.autoInstallPeers).toBe(false)
|
||||
})
|
||||
|
||||
it('returns pendingBuilds when pnpm blocks build scripts', () => {
|
||||
|
||||
Reference in New Issue
Block a user