Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(publish): honor force for no dist tag and registry version check #8054

Merged
merged 5 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,14 @@ class Publish extends BaseCommand {
// so that we send the latest and greatest thing to the registry
// note that publishConfig might have changed as well!
manifest = await this.#getManifest(spec, opts, true)

const isPreRelease = Boolean(semver.parse(manifest.version).prerelease.length)
const force = this.npm.config.get('force')
const isDefaultTag = this.npm.config.isDefault('tag') && !manifest.publishConfig?.tag

if (isPreRelease && isDefaultTag) {
throw new Error('You must specify a tag using --tag when publishing a prerelease version.')
if (!force) {
const isPreRelease = Boolean(semver.parse(manifest.version).prerelease.length)
if (isPreRelease && isDefaultTag) {
throw new Error('You must specify a tag using --tag when publishing a prerelease version.')
}
}

// If we are not in JSON mode then we show the user the contents of the tarball
Expand Down Expand Up @@ -156,11 +158,18 @@ class Publish extends BaseCommand {
}
}

const latestVersion = await this.#highestPublishedVersion(resolved, registry)
const latestSemverIsGreater = !!latestVersion && semver.gte(latestVersion, manifest.version)
if (!force) {
const { highestVersion, versions } = await this.#registryVersions(resolved, registry)
/* eslint-disable-next-line max-len */
const highestVersionIsGreater = !!highestVersion && semver.gte(highestVersion, manifest.version)

if (latestSemverIsGreater && isDefaultTag) {
throw new Error(`Cannot implicitly apply the "latest" tag because published version ${latestVersion} is higher than the new version ${manifest.version}. You must specify a tag using --tag.`)
if (versions.includes(manifest.version)) {
throw new Error(`You cannot publish over the previously published versions: ${manifest.version}.`)
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
}

if (highestVersionIsGreater && isDefaultTag) {
throw new Error(`Cannot implicitly apply the "latest" tag because previously published version ${highestVersion} is higher than the new version ${manifest.version}. You must specify a tag using --tag.`)
}
}

const access = opts.access === null ? 'default' : opts.access
Expand Down Expand Up @@ -202,15 +211,15 @@ class Publish extends BaseCommand {
}
}

async #highestPublishedVersion (spec, registry) {
async #registryVersions (spec, registry) {
try {
const packument = await pacote.packument(spec, {
...this.npm.flatOptions,
preferOnline: true,
registry,
})
if (typeof packument?.versions === 'undefined') {
return null
return { versions: [], highestVersion: null }
}
const ordered = Object.keys(packument?.versions)
.flatMap(v => {
Expand All @@ -221,9 +230,11 @@ class Publish extends BaseCommand {
return s
})
.sort((a, b) => b.compare(a))
return ordered.length >= 1 ? ordered[0].version : null
const highestVersion = ordered.length >= 1 ? ordered[0].version : null
const versions = ordered.map(v => v.version)
return { versions, highestVersion }
} catch (e) {
return null
return { versions: [], highestVersion: null }
}
}

Expand Down
20 changes: 11 additions & 9 deletions mock-registry/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,16 +359,18 @@ class MockRegistry {
}

publish (name, {
packageJson, access, noPut, putCode, manifest, packuments,
packageJson, access, noGet, noPut, putCode, manifest, packuments,
} = {}) {
// this getPackage call is used to get the latest semver version before publish
if (manifest) {
this.getPackage(name, { code: 200, resp: manifest })
} else if (packuments) {
this.getPackage(name, { code: 200, resp: this.manifest({ name, packuments }) })
} else {
// assumes the package does not exist yet and will 404 x2 from pacote.manifest
this.getPackage(name, { times: 2, code: 404 })
if (!noGet) {
// this getPackage call is used to get the latest semver version before publish
if (manifest) {
this.getPackage(name, { code: 200, resp: manifest })
} else if (packuments) {
this.getPackage(name, { code: 200, resp: this.manifest({ name, packuments }) })
} else {
// assumes the package does not exist yet and will 404 x2 from pacote.manifest
this.getPackage(name, { times: 2, code: 404 })
}
}
if (!noPut) {
this.putPackage(name, { code: putCode, packageJson, access })
Expand Down
52 changes: 51 additions & 1 deletion test/lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,28 @@ t.test('prerelease dist tag', (t) => {
await npm.exec('publish', [])
})

t.test('does not abort when prerelease and force', async t => {
const packageJson = {
...pkgJson,
version: '1.0.0-0',
publishConfig: { registry: alternateRegistry },
}
const { npm, registry } = await loadNpmWithRegistry(t, {
config: {
loglevel: 'silent',
force: true,
[`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token',
},
prefixDir: {
'package.json': JSON.stringify(packageJson, null, 2),
},
registry: alternateRegistry,
authorization: 'test-other-token',
})
registry.publish(pkg, { noGet: true, packageJson })
await npm.exec('publish', [])
})

t.end()
})

Expand Down Expand Up @@ -886,7 +908,7 @@ t.test('semver highest dist tag', async t => {
registry.publish(pkg, { noPut: true, packuments })
await t.rejects(async () => {
await npm.exec('publish', [])
}, new Error('Cannot implicitly apply the "latest" tag because published version 100.0.0 is higher than the new version 99.0.0. You must specify a tag using --tag.'))
}, new Error('Cannot implicitly apply the "latest" tag because previously published version 100.0.0 is higher than the new version 99.0.0. You must specify a tag using --tag.'))
})

await t.test('ALLOWS publish when highest is HIGHER than publishing version and flag', async t => {
Expand Down Expand Up @@ -933,4 +955,32 @@ t.test('semver highest dist tag', async t => {
registry.publish(pkg, { packuments })
await npm.exec('publish', [])
})

await t.test('PREVENTS publish when latest version is SAME AS publishing version', async t => {
const version = '100.0.0'
const { npm, registry } = await loadNpmWithRegistry(t, init({ version }))
registry.publish(pkg, { noPut: true, packuments })
await t.rejects(async () => {
await npm.exec('publish', [])
}, new Error('You cannot publish over the previously published versions: 100.0.0.'))
})

await t.test('PREVENTS publish when publishing version EXISTS ALREADY in the registry', async t => {
const version = '50.0.0'
const { npm, registry } = await loadNpmWithRegistry(t, init({ version }))
registry.publish(pkg, { noPut: true, packuments })
await t.rejects(async () => {
await npm.exec('publish', [])
}, new Error('You cannot publish over the previously published versions: 50.0.0.'))
})

await t.test('ALLOWS publish when latest is HIGHER than publishing version and flag --force', async t => {
const version = '99.0.0'
const { npm, registry } = await loadNpmWithRegistry(t, {
...init({ version }),
argv: ['--force'],
})
registry.publish(pkg, { noGet: true, packuments })
await npm.exec('publish', [])
})
})
Loading