mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-01 12:41:16 -04:00
fix(publish): honor publishConfig access (#11746)
* fix(publish): honor publishConfig access * test(publish): cover publishConfig access * test(publish): handle invalid metadata json * test(publish): replace verdaccio-dependent access tests with unit test Verdaccio strips the top-level `access` field from publish payloads, so the metadata-fetching integration tests could never pass. Replace them with a direct unit test of the access-resolution logic in createPublishOptions. --- Written by an agent (Claude Code, claude-opus-4-7). --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
committed by
GitHub
parent
0a40f64b54
commit
64afc9233e
7
.changeset/publish-config-access.md
Normal file
7
.changeset/publish-config-access.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/releasing.commands": patch
|
||||
"@pnpm/types": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Honor `publishConfig.access` when publishing packages.
|
||||
@@ -64,6 +64,7 @@ type DevEngineKey = 'os' | 'cpu' | 'libc' | 'runtime' | 'packageManager'
|
||||
export type DevEngines = Partial<Record<DevEngineKey, EngineDependency | EngineDependency[]>>
|
||||
|
||||
export interface PublishConfig extends Record<string, unknown> {
|
||||
access?: 'public' | 'restricted'
|
||||
directory?: string
|
||||
linkDirectory?: boolean
|
||||
executableFiles?: string[]
|
||||
|
||||
@@ -145,15 +145,21 @@ function extractBundledDependencies (manifest: ExportedManifest): string[] {
|
||||
return []
|
||||
}
|
||||
|
||||
async function createPublishOptions (manifest: ExportedManifest, options: PublishPackedPkgOptions): Promise<PublishOptions> {
|
||||
/**
|
||||
* @internal Exported for unit testing of the access / registry / auth fallback rules. Not part of the package's
|
||||
* public API.
|
||||
*/
|
||||
export async function createPublishOptions (manifest: ExportedManifest, options: PublishPackedPkgOptions): Promise<PublishOptions> {
|
||||
const publishConfigRegistry = typeof manifest.publishConfig?.registry === 'string'
|
||||
? manifest.publishConfig.registry
|
||||
: undefined
|
||||
const { registry, config } = findRegistryInfo(manifest, options, publishConfigRegistry)
|
||||
const { creds, tls } = config ?? {}
|
||||
|
||||
const publishConfigAccess = manifest.publishConfig?.access
|
||||
const access = options.access ?? (isPublishAccess(publishConfigAccess) ? publishConfigAccess : undefined)
|
||||
|
||||
const {
|
||||
access,
|
||||
ci: isFromCI,
|
||||
fetchRetries,
|
||||
fetchRetryFactor,
|
||||
@@ -218,6 +224,10 @@ async function createPublishOptions (manifest: ExportedManifest, options: Publis
|
||||
return publishOptions
|
||||
}
|
||||
|
||||
function isPublishAccess (access: unknown): access is 'public' | 'restricted' {
|
||||
return access === 'public' || access === 'restricted'
|
||||
}
|
||||
|
||||
interface RegistryInfo {
|
||||
registry: NormalizedRegistryUrl
|
||||
config: RegistryConfig
|
||||
|
||||
67
releasing/commands/test/publish/publishConfigAccess.test.ts
Normal file
67
releasing/commands/test/publish/publishConfigAccess.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { afterAll, beforeAll, describe, expect, jest, test } from '@jest/globals'
|
||||
|
||||
// Mock `ci-info` so `GITHUB_ACTIONS` is false: that way `getIdToken` short-circuits in
|
||||
// `fetchTokenAndProvenanceByOidc` and `createPublishOptions` never touches the network.
|
||||
const ciInfoModule = await import('ci-info')
|
||||
const ciInfoOriginal = (ciInfoModule as { default?: Record<string, unknown> }).default ?? ciInfoModule
|
||||
jest.unstable_mockModule('ci-info', () => ({
|
||||
...ciInfoModule,
|
||||
default: { ...ciInfoOriginal, GITHUB_ACTIONS: false },
|
||||
GITHUB_ACTIONS: false,
|
||||
}))
|
||||
|
||||
const { createPublishOptions } = await import('../../src/publish/publishPackedPkg.js')
|
||||
|
||||
function baseOpts (): Parameters<typeof createPublishOptions>[1] {
|
||||
return {
|
||||
configByUri: {},
|
||||
fetchTimeout: 60_000,
|
||||
registries: { default: 'https://registry.npmjs.org/' },
|
||||
} as Parameters<typeof createPublishOptions>[1]
|
||||
}
|
||||
|
||||
// `getIdToken` honors `NPM_ID_TOKEN` regardless of which CI flag is set, so make sure
|
||||
// it isn't set during these tests — otherwise OIDC would attempt a token exchange.
|
||||
let savedNpmIdToken: string | undefined
|
||||
beforeAll(() => {
|
||||
savedNpmIdToken = process.env['NPM_ID_TOKEN']
|
||||
delete process.env['NPM_ID_TOKEN']
|
||||
})
|
||||
afterAll(() => {
|
||||
if (savedNpmIdToken === undefined) return
|
||||
process.env['NPM_ID_TOKEN'] = savedNpmIdToken
|
||||
})
|
||||
|
||||
describe('createPublishOptions: access', () => {
|
||||
test('falls back to publishConfig.access when --access is not set', async () => {
|
||||
const opts = await createPublishOptions(
|
||||
{ name: '@scope/pkg', version: '1.0.0', publishConfig: { access: 'restricted' } },
|
||||
baseOpts()
|
||||
)
|
||||
expect(opts.access).toBe('restricted')
|
||||
})
|
||||
|
||||
test('CLI --access wins over publishConfig.access', async () => {
|
||||
const opts = await createPublishOptions(
|
||||
{ name: '@scope/pkg', version: '1.0.0', publishConfig: { access: 'restricted' } },
|
||||
{ ...baseOpts(), access: 'public' }
|
||||
)
|
||||
expect(opts.access).toBe('public')
|
||||
})
|
||||
|
||||
test('access is omitted when neither CLI nor publishConfig set it', async () => {
|
||||
const opts = await createPublishOptions(
|
||||
{ name: '@scope/pkg', version: '1.0.0' },
|
||||
baseOpts()
|
||||
)
|
||||
expect(opts.access).toBeUndefined()
|
||||
})
|
||||
|
||||
test('invalid publishConfig.access values are ignored', async () => {
|
||||
const opts = await createPublishOptions(
|
||||
{ name: '@scope/pkg', version: '1.0.0', publishConfig: { access: 'bogus' as 'public' } },
|
||||
baseOpts()
|
||||
)
|
||||
expect(opts.access).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user