Skip to content

Commit

Permalink
fix: stop retrying on 409 conflict
Browse files Browse the repository at this point in the history
BREAKING CHANGE: libnpmpublish will no longer attempt a single automatic
retry on 409 responses during publish.
  • Loading branch information
wraithgar authored and lukekarrys committed Jul 26, 2023
1 parent d2c3712 commit 0a71ebb
Show file tree
Hide file tree
Showing 2 changed files with 9 additions and 341 deletions.
89 changes: 9 additions & 80 deletions workspaces/libnpmpublish/lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,42 +50,16 @@ Remove the 'private' field from the package.json to publish it.`),
opts
)

try {
const res = await npmFetch(spec.escapedName, {
...opts,
method: 'PUT',
body: metadata,
ignoreBody: true,
})
if (transparencyLogUrl) {
res.transparencyLogUrl = transparencyLogUrl
}
return res
} catch (err) {
if (err.code !== 'E409') {
throw err
}
// if E409, we attempt exactly ONE retry, to protect us
// against malicious activity like trying to publish
// a bunch of new versions of a package at the same time
// and/or spamming the registry
const current = await npmFetch.json(spec.escapedName, {
...opts,
query: { write: true },
})
const newMetadata = patchMetadata(current, metadata)
const res = await npmFetch(spec.escapedName, {
...opts,
method: 'PUT',
body: newMetadata,
ignoreBody: true,
})
/* istanbul ignore next */
if (transparencyLogUrl) {
res.transparencyLogUrl = transparencyLogUrl
}
return res
const res = await npmFetch(spec.escapedName, {
...opts,
method: 'PUT',
body: metadata,
ignoreBody: true,
})
if (transparencyLogUrl) {
res.transparencyLogUrl = transparencyLogUrl
}
return res
}

const patchManifest = (_manifest, opts) => {
Expand Down Expand Up @@ -195,51 +169,6 @@ const buildMetadata = async (registry, manifest, tarballData, spec, opts) => {
}
}

const patchMetadata = (current, newData) => {
const curVers = Object.keys(current.versions || {})
.map(v => semver.clean(v, true))
.concat(Object.keys(current.time || {})
.map(v => semver.valid(v, true) && semver.clean(v, true))
.filter(v => v))

const newVersion = Object.keys(newData.versions)[0]

if (curVers.indexOf(newVersion) !== -1) {
const { name: pkgid, version } = newData
throw Object.assign(
new Error(
`Cannot publish ${pkgid}@${version} over existing version.`
), {
code: 'EPUBLISHCONFLICT',
pkgid,
version,
})
}

current.versions = current.versions || {}
current.versions[newVersion] = newData.versions[newVersion]
for (const i in newData) {
switch (i) {
// objects that copy over the new stuffs
case 'dist-tags':
case 'versions':
case '_attachments':
for (const j in newData[i]) {
current[i] = current[i] || {}
current[i][j] = newData[i][j]
}
break

// copy
default:
current[i] = newData[i]
break
}
}

return current
}

// Check that all the prereqs are met for provenance generation
const ensureProvenanceGeneration = async (registry, spec, opts) => {
if (ciInfo.GITHUB_ACTIONS) {
Expand Down
261 changes: 0 additions & 261 deletions workspaces/libnpmpublish/test/publish.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict'

const cloneDeep = require('lodash.clonedeep')
const crypto = require('crypto')
const fs = require('fs')
const npa = require('npm-package-arg')
Expand Down Expand Up @@ -180,266 +179,6 @@ t.test('scoped publish - restricted access', async t => {
t.ok(ret, 'publish succeeded')
})

t.test('retry after a conflict', async t => {
const { publish } = t.mock('..')
const registry = new MockRegistry({
tap: t,
registry: opts.registry,
authorization: token,
})
const REV = '72-47f2986bfd8e8b55068b204588bbf484'
const manifest = {
name: 'libnpmpublish',
version: '1.0.0',
description: 'some stuff',
}

const basePackument = {
name: 'libnpmpublish',
description: 'some stuff',
access: 'public',
_id: 'libnpmpublish',
'dist-tags': {},
versions: {},
_attachments: {},
}
const currentPackument = cloneDeep({
...basePackument,
time: {
modified: new Date().toISOString(),
created: new Date().toISOString(),
'1.0.1': new Date().toISOString(),
},
'dist-tags': { latest: '1.0.1' },
versions: {
'1.0.1': {
_id: 'libnpmpublish@1.0.1',
_nodeVersion: process.versions.node,
_npmVersion: opts.npmVersion,
name: 'libnpmpublish',
version: '1.0.1',
description: 'some stuff',
dist: {
shasum,
integrity: integrity.toString(),
tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.1.tgz',
},
},
},
_attachments: {
'libnpmpublish-1.0.1.tgz': {
content_type: 'application/octet-stream',
data: tarData.toString('base64'),
length: tarData.length,
},
},
})
const newPackument = cloneDeep({
...basePackument,
'dist-tags': { latest: '1.0.0' },
versions: {
'1.0.0': {
_id: 'libnpmpublish@1.0.0',
_nodeVersion: process.versions.node,
_npmVersion: opts.npmVersion,
name: 'libnpmpublish',
version: '1.0.0',
description: 'some stuff',
dist: {
shasum,
integrity: integrity.toString(),
tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
},
},
},
_attachments: {
'libnpmpublish-1.0.0.tgz': {
content_type: 'application/octet-stream',
data: tarData.toString('base64'),
length: tarData.length,
},
},
})
const mergedPackument = cloneDeep({
...basePackument,
time: currentPackument.time,
'dist-tags': { latest: '1.0.0' },
versions: { ...currentPackument.versions, ...newPackument.versions },
_attachments: { ...currentPackument._attachments, ...newPackument._attachments },
})

registry.nock.put('/libnpmpublish', body => {
t.notOk(body._rev, 'no _rev in initial post')
t.same(body, newPackument, 'got conflicting packument')
return true
}).reply(409, { error: 'gimme _rev plz' })

registry.nock.get('/libnpmpublish?write=true').reply(200, {
_rev: REV,
...currentPackument,
})

registry.nock.put('/libnpmpublish', body => {
t.same(body, {
_rev: REV,
...mergedPackument,
}, 'posted packument includes _rev and a merged version')
return true
}).reply(201, {})

const ret = await publish(manifest, tarData, opts)
t.ok(ret, 'publish succeeded')
})

t.test('retry after a conflict -- no versions on remote', async t => {
const { publish } = t.mock('..')
const registry = new MockRegistry({
tap: t,
registry: opts.registry,
authorization: token,
})
const REV = '72-47f2986bfd8e8b55068b204588bbf484'
const manifest = {
name: 'libnpmpublish',
version: '1.0.0',
description: 'some stuff',
}

const basePackument = {
name: 'libnpmpublish',
description: 'some stuff',
access: 'public',
_id: 'libnpmpublish',
}
const currentPackument = { ...basePackument }
const newPackument = cloneDeep({
...basePackument,
'dist-tags': { latest: '1.0.0' },
versions: {
'1.0.0': {
_id: 'libnpmpublish@1.0.0',
_nodeVersion: process.versions.node,
_npmVersion: opts.npmVersion,
name: 'libnpmpublish',
version: '1.0.0',
description: 'some stuff',
dist: {
shasum,
integrity: integrity.toString(),
tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
},
},
},
_attachments: {
'libnpmpublish-1.0.0.tgz': {
content_type: 'application/octet-stream',
data: tarData.toString('base64'),
length: tarData.length,
},
},
})
const mergedPackument = cloneDeep({
...basePackument,
'dist-tags': { latest: '1.0.0' },
versions: { ...newPackument.versions },
_attachments: { ...newPackument._attachments },
})

registry.nock.put('/libnpmpublish', body => {
t.notOk(body._rev, 'no _rev in initial post')
t.same(body, newPackument, 'got conflicting packument')
return true
}).reply(409, { error: 'gimme _rev plz' })

registry.nock.get('/libnpmpublish?write=true').reply(200, {
_rev: REV,
...currentPackument,
})

registry.nock.put('/libnpmpublish', body => {
t.same(body, {
_rev: REV,
...mergedPackument,
}, 'posted packument includes _rev and a merged version')
return true
}).reply(201, {})

const ret = await publish(manifest, tarData, opts)

t.ok(ret, 'publish succeeded')
})

t.test('version conflict', async t => {
const { publish } = t.mock('..')
const registry = new MockRegistry({
tap: t,
registry: opts.registry,
authorization: token,
})
const REV = '72-47f2986bfd8e8b55068b204588bbf484'
const manifest = {
name: 'libnpmpublish',
version: '1.0.0',
description: 'some stuff',
}

const basePackument = {
name: 'libnpmpublish',
description: 'some stuff',
access: 'public',
_id: 'libnpmpublish',
'dist-tags': {},
versions: {},
_attachments: {},
}
const newPackument = cloneDeep(Object.assign({}, basePackument, {
'dist-tags': { latest: '1.0.0' },
versions: {
'1.0.0': {
_id: 'libnpmpublish@1.0.0',
_nodeVersion: process.versions.node,
_npmVersion: '6.13.7',
name: 'libnpmpublish',
version: '1.0.0',
description: 'some stuff',
dist: {
shasum,
integrity: integrity.toString(),
tarball: 'http://mock.reg/libnpmpublish/-/libnpmpublish-1.0.0.tgz',
},
},
},
_attachments: {
'libnpmpublish-1.0.0.tgz': {
content_type: 'application/octet-stream',
data: tarData.toString('base64'),
length: tarData.length,
},
},
}))

registry.nock.put('/libnpmpublish', body => {
t.notOk(body._rev, 'no _rev in initial post')
t.same(body, newPackument, 'got conflicting packument')
return true
}).reply(409, { error: 'gimme _rev plz' })

registry.nock.get('/libnpmpublish?write=true').reply(200, {
_rev: REV,
...newPackument,
})

try {
await publish(manifest, tarData, {
...opts,
npmVersion: '6.13.7',
token: 'deadbeef',
})
} catch (err) {
t.equal(err.code, 'EPUBLISHCONFLICT', 'got publish conflict code')
}
})

t.test('refuse if package marked private', async t => {
const { publish } = t.mock('..')
const registry = new MockRegistry({
Expand Down

0 comments on commit 0a71ebb

Please sign in to comment.