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:
Nicolas Beaussart
2026-05-19 22:49:42 +02:00
committed by GitHub
parent 0a40f64b54
commit 64afc9233e
4 changed files with 87 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/releasing.commands": patch
"@pnpm/types": patch
"pnpm": patch
---
Honor `publishConfig.access` when publishing packages.

View File

@@ -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[]

View File

@@ -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

View 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()
})
})