Fix image push failing with tag:undefined under Bare
Release rolling / release (push) Successful in 9m42s

bare-querystring stringifies JS undefined as the literal "undefined", so
dockerode push options with tag: undefined made Docker look up
repo:undefined. Only pass defined push options, and fail the job when
Docker streams an error in the progress body.
This commit is contained in:
Raven Scott
2026-07-16 23:09:36 -04:00
parent abc34b39d8
commit 49794febf8
2 changed files with 89 additions and 5 deletions
+39 -5
View File
@@ -20,6 +20,25 @@ function dockerAuthOpts(authconfig) {
}
}
/**
* Build dockerode image.push() options.
*
* Never includes keys with undefined values. Under Bare, bare-querystring
* stringifies undefined as the literal "undefined", which Docker treats as a
* real tag (e.g. repo:undefined → "tag does not exist").
*
* @param {{ tag?: string|null, authconfig?: object|null }} opts
* @returns {Record<string, unknown>}
*/
export function buildImagePushOpts(opts = {}) {
const out = {}
const tag = opts.tag != null ? String(opts.tag).trim() : ''
if (tag) out.tag = tag
const auth = dockerAuthOpts(opts.authconfig || null)
if (auth) out.authconfig = auth
return out
}
export function registerImageHandlers(session) {
session.respond('listImages', async (args = {}) => {
const listOpts = { all: args.all !== false }
@@ -248,31 +267,46 @@ export function registerImageHandlers(session) {
if (!imageName) throw new Error('image name/id required')
// Optional: retag before push (repo:tag destination)
let pushRef = imageName
/** Explicit tag query only when set — never pass undefined (see below). */
let pushTag = null
if (args.repo) {
const repo = validation.sanitizeString(args.repo, 255)
const tag = validation.sanitizeString(args.tag || 'latest', 128)
if (!repo) throw new Error('repo required when tagging for push')
if (!tag) throw new Error('tag required when tagging for push')
await docker.getImage(args.id || imageName).tag({ repo, tag })
pushRef = `${repo}:${tag}`
// Name already includes the tag; omit tag query param.
pushTag = null
} else if (args.tag) {
pushTag = validation.sanitizeString(args.tag, 128) || null
}
const authconfig = resolveRegistryAuth(session, {
credentialId: args.credentialId,
auth: args.auth,
autoVault: args.autoVault,
image: pushRef,
})
const pushOpts = buildImagePushOpts({ tag: pushTag, authconfig })
const image = docker.getImage(pushRef)
const stream = await image.push({
tag: args.repo ? undefined : args.tag || undefined,
authconfig: dockerAuthOpts(authconfig),
})
const stream = await image.push(pushOpts)
// Docker may return HTTP 200 and stream { error } in the body — treat as failure.
let streamError = null
await new Promise((resolve, reject) => {
docker.modem.followProgress(
stream,
(err) => (err ? reject(err) : resolve()),
(err) => {
if (err) reject(err)
else if (streamError) reject(new Error(streamError))
else resolve()
},
(event) => {
try {
if (event?.error) {
streamError = String(event.error)
logger.debug('push layer error event', { error: event.error, image: pushRef })
}
session.push(Pushes.pushProgress, {