mirror of
https://github.com/caprover/caprover
synced 2026-08-23 07:56:28 +00:00
Merge pull request #2465 from caprover/release
Run build / build (push) Canceled after 0s
Run formatter / check-code-formatting (push) Canceled after 0s
Run lint / run-lint (push) Canceled after 0s
Build and push the edge image / run-pre-checks (push) Canceled after 0s
Build and push the edge image / build-publish-docker-hub (push) Canceled after 0s
Run build / build (push) Canceled after 0s
Run formatter / check-code-formatting (push) Canceled after 0s
Run lint / run-lint (push) Canceled after 0s
Build and push the edge image / run-pre-checks (push) Canceled after 0s
Build and push the edge image / build-publish-docker-hub (push) Canceled after 0s
Release back merge
This commit is contained in:
@@ -4,6 +4,11 @@ Available as `edge`
|
||||
|
||||
- Improved: Updated Node.js to version 24 and removed unsupported ARMv7 images [PR-2448](https://github.com/caprover/caprover/pull/2448)
|
||||
|
||||
## [1.15.3] - 2026-08-20
|
||||
|
||||
- Fixed: Restored build output when using the BuildKit builder [Issue-2461](https://github.com/caprover/caprover/issues/2461)
|
||||
- Fixed: Tar file deployments failing from the dashboard [Issue-2463](https://github.com/caprover/caprover/issues/2463)
|
||||
|
||||
## [1.15.2] - 2026-08-14
|
||||
|
||||
- Fixed: Legacy custom Captain NGINX templates failing after upgrading to v1.15.0 [Issue-2455](https://github.com/caprover/caprover/issues/2455)
|
||||
|
||||
@@ -44,7 +44,7 @@ echo $IMAGE_NAME:$CAPROVER_VERSION
|
||||
echo "**************************************"
|
||||
echo "**************************************"
|
||||
|
||||
FRONTEND_COMMIT_HASH=5db8b202774fb50c9ecdddc42020bb454d64c42d
|
||||
FRONTEND_COMMIT_HASH=35dccc3e099c06f295f78c50d131fc5def91f995
|
||||
|
||||
## Building frontend app
|
||||
ORIG_DIR=$(pwd)
|
||||
|
||||
+63
-72
@@ -11,6 +11,11 @@ import {
|
||||
import { DockerAuthObj, DockerRegistryConfig } from '../models/DockerAuthObj'
|
||||
import { DockerSecret } from '../models/DockerSecret'
|
||||
import DockerService from '../models/DockerService'
|
||||
import {
|
||||
DockerBuildOutputDecoder,
|
||||
DockerJsonMessage,
|
||||
DockerJsonStreamParser,
|
||||
} from './DockerBuildOutput'
|
||||
import { IHashMapGeneric } from '../models/ICacheGeneric'
|
||||
import {
|
||||
IDockerApiPort,
|
||||
@@ -30,35 +35,6 @@ import dockerodeUtils = require('dockerode/lib/util')
|
||||
const Base64 = Base64Provider.Base64
|
||||
const CAPROVER_MANAGED_SERVICE_LABEL = 'com.caprover.managed'
|
||||
|
||||
function safeParseChunk(chunk: string): {
|
||||
stream?: string
|
||||
error?: any
|
||||
errorDetail?: any
|
||||
}[] {
|
||||
chunk = `${chunk}`.trim()
|
||||
try {
|
||||
// See https://github.com/caprover/caprover/issues/570
|
||||
// This appears to be bug either in Docker or dockerone:
|
||||
// Sometimes chunk appears as two JSON objects, like
|
||||
// ```
|
||||
// {"stream":"something......"}
|
||||
// {"stream":"another line of things"}
|
||||
// ```
|
||||
const chunks = chunk.split('\n')
|
||||
const returnVal = [] as any[]
|
||||
chunks.forEach((chk) => {
|
||||
returnVal.push(JSON.parse(chk))
|
||||
})
|
||||
return returnVal
|
||||
} catch (ignore) {
|
||||
return [
|
||||
{
|
||||
stream: `Cannot parse ${chunk}`,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class IDockerUpdateOrders {
|
||||
public static readonly AUTO = 'auto'
|
||||
public static readonly STOP_FIRST = 'stopFirst'
|
||||
@@ -374,13 +350,11 @@ class DockerApi {
|
||||
.then(function (stream) {
|
||||
return new Promise<void>(function (resolve, reject) {
|
||||
let errorMessage = ''
|
||||
const streamParser = new DockerJsonStreamParser()
|
||||
const outputDecoder = new DockerBuildOutputDecoder()
|
||||
|
||||
stream.setEncoding('utf8')
|
||||
|
||||
// THIS BLOCK HAS TO BE HERE. "end" EVENT WON'T GET CALLED OTHERWISE.
|
||||
stream.on('data', function (chunkRaw) {
|
||||
Logger.dev(`stream data ${chunkRaw}`)
|
||||
safeParseChunk(chunkRaw).forEach((chunk) => {
|
||||
function processMessage(message: DockerJsonMessage) {
|
||||
outputDecoder.decode(message).forEach((chunk) => {
|
||||
const chuckStream = chunk.stream
|
||||
if (chuckStream) {
|
||||
// Logger.dev('stream data ' + chuckStream);
|
||||
@@ -401,6 +375,14 @@ class DockerApi {
|
||||
errorMessage += chunk.error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stream.setEncoding('utf8')
|
||||
|
||||
// THIS BLOCK HAS TO BE HERE. "end" EVENT WON'T GET CALLED OTHERWISE.
|
||||
stream.on('data', function (chunkRaw) {
|
||||
Logger.dev(`stream data ${chunkRaw}`)
|
||||
streamParser.push(chunkRaw).forEach(processMessage)
|
||||
})
|
||||
|
||||
// stream.pipe(process.stdout, {end: true});
|
||||
@@ -408,6 +390,7 @@ class DockerApi {
|
||||
// https://nodejs.org/api/stream.html#stream_event_end
|
||||
|
||||
stream.on('end', function () {
|
||||
streamParser.flush().forEach(processMessage)
|
||||
if (errorMessage) {
|
||||
reject(errorMessage)
|
||||
return
|
||||
@@ -452,32 +435,35 @@ class DockerApi {
|
||||
return new Promise<void>(function (resolve, reject) {
|
||||
let errorMessage = ''
|
||||
const logsBeforeError: string[] = []
|
||||
const streamParser = new DockerJsonStreamParser()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
logsBeforeError.push('')
|
||||
}
|
||||
|
||||
function processMessage(chunk: DockerJsonMessage) {
|
||||
const chuckStream = chunk.stream
|
||||
if (chuckStream) {
|
||||
// Logger.dev('stream data ' + chuckStream);
|
||||
logsBeforeError.shift()
|
||||
logsBeforeError.push(chuckStream)
|
||||
}
|
||||
|
||||
if (chunk.error) {
|
||||
Logger.e(chunk.error)
|
||||
Logger.e(JSON.stringify(chunk.errorDetail))
|
||||
errorMessage += '\n [truncated] \n'
|
||||
errorMessage += logsBeforeError.join('')
|
||||
errorMessage += '\n'
|
||||
errorMessage += chunk.error
|
||||
}
|
||||
}
|
||||
|
||||
stream.setEncoding('utf8')
|
||||
|
||||
// THIS BLOCK HAS TO BE HERE. "end" EVENT WON'T GET CALLED OTHERWISE.
|
||||
stream.on('data', function (chunkRaw) {
|
||||
Logger.dev(`stream data ${chunkRaw}`)
|
||||
safeParseChunk(chunkRaw).forEach((chunk) => {
|
||||
const chuckStream = chunk.stream
|
||||
if (chuckStream) {
|
||||
// Logger.dev('stream data ' + chuckStream);
|
||||
logsBeforeError.shift()
|
||||
logsBeforeError.push(chuckStream)
|
||||
}
|
||||
|
||||
if (chunk.error) {
|
||||
Logger.e(chunk.error)
|
||||
Logger.e(JSON.stringify(chunk.errorDetail))
|
||||
errorMessage += '\n [truncated] \n'
|
||||
errorMessage += logsBeforeError.join('')
|
||||
errorMessage += '\n'
|
||||
errorMessage += chunk.error
|
||||
}
|
||||
})
|
||||
streamParser.push(chunkRaw).forEach(processMessage)
|
||||
})
|
||||
|
||||
// stream.pipe(process.stdout, {end: true});
|
||||
@@ -485,6 +471,7 @@ class DockerApi {
|
||||
// https://nodejs.org/api/stream.html#stream_event_end
|
||||
|
||||
stream.on('end', function () {
|
||||
streamParser.flush().forEach(processMessage)
|
||||
if (errorMessage) {
|
||||
reject(errorMessage)
|
||||
return
|
||||
@@ -742,32 +729,35 @@ class DockerApi {
|
||||
.then(function (stream) {
|
||||
return new Promise<void>(function (resolve, reject) {
|
||||
let errorMessage = ''
|
||||
const streamParser = new DockerJsonStreamParser()
|
||||
|
||||
function processMessage(chunk: DockerJsonMessage) {
|
||||
const chuckStream = chunk.stream
|
||||
if (chuckStream) {
|
||||
// Logger.dev('stream data ' + chuckStream);
|
||||
buildLogs.log(chuckStream)
|
||||
}
|
||||
|
||||
if (chunk.error) {
|
||||
Logger.e(chunk.error)
|
||||
const errorDetails = JSON.stringify(
|
||||
chunk.errorDetail
|
||||
)
|
||||
Logger.e(errorDetails)
|
||||
buildLogs.log(errorDetails)
|
||||
buildLogs.log(chunk.error)
|
||||
errorMessage += '\n'
|
||||
errorMessage += errorDetails
|
||||
errorMessage += chunk.error
|
||||
}
|
||||
}
|
||||
|
||||
stream.setEncoding('utf8')
|
||||
|
||||
// THIS BLOCK HAS TO BE HERE. "end" EVENT WON'T GET CALLED OTHERWISE.
|
||||
stream.on('data', function (chunkRaw) {
|
||||
Logger.dev(`stream data ${chunkRaw}`)
|
||||
safeParseChunk(chunkRaw).forEach((chunk) => {
|
||||
const chuckStream = chunk.stream
|
||||
if (chuckStream) {
|
||||
// Logger.dev('stream data ' + chuckStream);
|
||||
buildLogs.log(chuckStream)
|
||||
}
|
||||
|
||||
if (chunk.error) {
|
||||
Logger.e(chunk.error)
|
||||
const errorDetails = JSON.stringify(
|
||||
chunk.errorDetail
|
||||
)
|
||||
Logger.e(errorDetails)
|
||||
buildLogs.log(errorDetails)
|
||||
buildLogs.log(chunk.error)
|
||||
errorMessage += '\n'
|
||||
errorMessage += errorDetails
|
||||
errorMessage += chunk.error
|
||||
}
|
||||
})
|
||||
streamParser.push(chunkRaw).forEach(processMessage)
|
||||
})
|
||||
|
||||
// stream.pipe(process.stdout, {end: true});
|
||||
@@ -775,6 +765,7 @@ class DockerApi {
|
||||
// https://nodejs.org/api/stream.html#stream_event_end
|
||||
|
||||
stream.on('end', function () {
|
||||
streamParser.flush().forEach(processMessage)
|
||||
if (errorMessage) {
|
||||
buildLogs.log('Push failed...')
|
||||
reject(errorMessage)
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
export interface DockerJsonMessage {
|
||||
stream?: string
|
||||
error?: any
|
||||
errorDetail?: any
|
||||
id?: string
|
||||
aux?: unknown
|
||||
}
|
||||
|
||||
interface ProtobufValue {
|
||||
value: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
interface ProtobufBytes {
|
||||
value: Buffer
|
||||
offset: number
|
||||
}
|
||||
|
||||
interface BuildKitVertex {
|
||||
digest: string
|
||||
name: string
|
||||
error: string
|
||||
}
|
||||
|
||||
interface BuildKitStatus {
|
||||
vertices: BuildKitVertex[]
|
||||
logs: Buffer[]
|
||||
warnings: Buffer[]
|
||||
}
|
||||
|
||||
export class DockerJsonStreamParser {
|
||||
private pending = ''
|
||||
|
||||
push(chunk: string): DockerJsonMessage[] {
|
||||
this.pending += chunk
|
||||
const messages: DockerJsonMessage[] = []
|
||||
let newlineIndex = this.pending.indexOf('\n')
|
||||
|
||||
while (newlineIndex >= 0) {
|
||||
const line = this.pending.slice(0, newlineIndex)
|
||||
this.pending = this.pending.slice(newlineIndex + 1)
|
||||
const message = this.parseLine(line)
|
||||
if (message) {
|
||||
messages.push(message)
|
||||
}
|
||||
newlineIndex = this.pending.indexOf('\n')
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
flush(): DockerJsonMessage[] {
|
||||
const line = this.pending
|
||||
this.pending = ''
|
||||
const message = this.parseLine(line)
|
||||
return message ? [message] : []
|
||||
}
|
||||
|
||||
private parseLine(line: string): DockerJsonMessage | undefined {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed) as DockerJsonMessage
|
||||
} catch (ignore) {
|
||||
return {
|
||||
stream: `Cannot parse ${trimmed}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DockerBuildOutputDecoder {
|
||||
private displayedVertices = new Set<string>()
|
||||
private displayedErrors = new Set<string>()
|
||||
|
||||
decode(message: DockerJsonMessage): DockerJsonMessage[] {
|
||||
const output: DockerJsonMessage[] = []
|
||||
|
||||
if (message.stream || message.error) {
|
||||
output.push(message)
|
||||
}
|
||||
|
||||
if (
|
||||
message.id !== 'moby.buildkit.trace' ||
|
||||
typeof message.aux !== 'string'
|
||||
) {
|
||||
return output
|
||||
}
|
||||
|
||||
try {
|
||||
const status = decodeBuildKitStatus(
|
||||
Buffer.from(message.aux, 'base64')
|
||||
)
|
||||
|
||||
status.vertices.forEach((vertex) => {
|
||||
const vertexId = vertex.digest || vertex.name
|
||||
if (
|
||||
vertex.name &&
|
||||
vertexId &&
|
||||
!this.displayedVertices.has(vertexId)
|
||||
) {
|
||||
this.displayedVertices.add(vertexId)
|
||||
output.push({ stream: `${vertex.name}\n` })
|
||||
}
|
||||
|
||||
if (vertex.error && !this.displayedErrors.has(vertex.error)) {
|
||||
this.displayedErrors.add(vertex.error)
|
||||
output.push({
|
||||
error: vertex.error,
|
||||
errorDetail: { message: vertex.error },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
status.logs.forEach((log) => {
|
||||
const text = log.toString('utf8')
|
||||
if (text) {
|
||||
output.push({ stream: text })
|
||||
}
|
||||
})
|
||||
|
||||
status.warnings.forEach((warning) => {
|
||||
const text = warning.toString('utf8').trim()
|
||||
if (text) {
|
||||
output.push({ stream: `WARNING: ${text}\n` })
|
||||
}
|
||||
})
|
||||
} catch (ignore) {
|
||||
output.push({ stream: 'Cannot parse BuildKit build output\n' })
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
function decodeBuildKitStatus(buffer: Buffer): BuildKitStatus {
|
||||
const status: BuildKitStatus = {
|
||||
vertices: [],
|
||||
logs: [],
|
||||
warnings: [],
|
||||
}
|
||||
let offset = 0
|
||||
|
||||
while (offset < buffer.length) {
|
||||
const tag = readVarint(buffer, offset)
|
||||
offset = tag.offset
|
||||
const fieldNumber = Math.floor(tag.value / 8)
|
||||
const wireType = tag.value % 8
|
||||
|
||||
if (wireType === 2 && fieldNumber === 1) {
|
||||
const value = readBytes(buffer, offset)
|
||||
offset = value.offset
|
||||
status.vertices.push(decodeVertex(value.value))
|
||||
} else if (wireType === 2 && fieldNumber === 3) {
|
||||
const value = readBytes(buffer, offset)
|
||||
offset = value.offset
|
||||
const log = decodeVertexLog(value.value)
|
||||
if (log) {
|
||||
status.logs.push(log)
|
||||
}
|
||||
} else if (wireType === 2 && fieldNumber === 4) {
|
||||
const value = readBytes(buffer, offset)
|
||||
offset = value.offset
|
||||
const warning = decodeVertexWarning(value.value)
|
||||
if (warning) {
|
||||
status.warnings.push(warning)
|
||||
}
|
||||
} else {
|
||||
offset = skipField(buffer, offset, wireType)
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
function decodeVertex(buffer: Buffer): BuildKitVertex {
|
||||
const vertex: BuildKitVertex = { digest: '', name: '', error: '' }
|
||||
let offset = 0
|
||||
|
||||
while (offset < buffer.length) {
|
||||
const tag = readVarint(buffer, offset)
|
||||
offset = tag.offset
|
||||
const fieldNumber = Math.floor(tag.value / 8)
|
||||
const wireType = tag.value % 8
|
||||
|
||||
if (wireType === 2 && [1, 3, 7].includes(fieldNumber)) {
|
||||
const value = readBytes(buffer, offset)
|
||||
offset = value.offset
|
||||
const text = value.value.toString('utf8')
|
||||
if (fieldNumber === 1) vertex.digest = text
|
||||
if (fieldNumber === 3) vertex.name = text
|
||||
if (fieldNumber === 7) vertex.error = text
|
||||
} else {
|
||||
offset = skipField(buffer, offset, wireType)
|
||||
}
|
||||
}
|
||||
|
||||
return vertex
|
||||
}
|
||||
|
||||
function decodeVertexLog(buffer: Buffer): Buffer | undefined {
|
||||
return decodeBytesField(buffer, 4)
|
||||
}
|
||||
|
||||
function decodeVertexWarning(buffer: Buffer): Buffer | undefined {
|
||||
return decodeBytesField(buffer, 3)
|
||||
}
|
||||
|
||||
function decodeBytesField(
|
||||
buffer: Buffer,
|
||||
expectedFieldNumber: number
|
||||
): Buffer | undefined {
|
||||
let offset = 0
|
||||
|
||||
while (offset < buffer.length) {
|
||||
const tag = readVarint(buffer, offset)
|
||||
offset = tag.offset
|
||||
const fieldNumber = Math.floor(tag.value / 8)
|
||||
const wireType = tag.value % 8
|
||||
|
||||
if (wireType === 2 && fieldNumber === expectedFieldNumber) {
|
||||
return readBytes(buffer, offset).value
|
||||
}
|
||||
|
||||
offset = skipField(buffer, offset, wireType)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function readVarint(buffer: Buffer, startOffset: number): ProtobufValue {
|
||||
let value = 0
|
||||
let multiplier = 1
|
||||
let offset = startOffset
|
||||
|
||||
for (let byteCount = 0; byteCount < 10; byteCount++) {
|
||||
if (offset >= buffer.length) {
|
||||
throw new Error('Invalid protobuf varint')
|
||||
}
|
||||
|
||||
const byte = buffer[offset++]
|
||||
if (multiplier <= 2 ** 49) {
|
||||
value += (byte & 0x7f) * multiplier
|
||||
}
|
||||
if ((byte & 0x80) === 0) {
|
||||
return { value, offset }
|
||||
}
|
||||
multiplier *= 128
|
||||
}
|
||||
|
||||
throw new Error('Invalid protobuf varint')
|
||||
}
|
||||
|
||||
function readBytes(buffer: Buffer, startOffset: number): ProtobufBytes {
|
||||
const length = readVarint(buffer, startOffset)
|
||||
const endOffset = length.offset + length.value
|
||||
if (endOffset > buffer.length) {
|
||||
throw new Error('Invalid protobuf field length')
|
||||
}
|
||||
|
||||
return {
|
||||
value: buffer.subarray(length.offset, endOffset),
|
||||
offset: endOffset,
|
||||
}
|
||||
}
|
||||
|
||||
function skipField(
|
||||
buffer: Buffer,
|
||||
startOffset: number,
|
||||
wireType: number
|
||||
): number {
|
||||
if (wireType === 0) {
|
||||
return readVarint(buffer, startOffset).offset
|
||||
}
|
||||
if (wireType === 1) {
|
||||
return ensureOffset(buffer, startOffset + 8)
|
||||
}
|
||||
if (wireType === 2) {
|
||||
return readBytes(buffer, startOffset).offset
|
||||
}
|
||||
if (wireType === 5) {
|
||||
return ensureOffset(buffer, startOffset + 4)
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported protobuf wire type ${wireType}`)
|
||||
}
|
||||
|
||||
function ensureOffset(buffer: Buffer, offset: number): number {
|
||||
if (offset > buffer.length) {
|
||||
throw new Error('Invalid protobuf field')
|
||||
}
|
||||
return offset
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const CONSTANT_FILE_OVERRIDE_USER =
|
||||
const configs = {
|
||||
publishedNameOnDockerHub: 'caprover/caprover',
|
||||
|
||||
version: '1.15.2',
|
||||
version: '1.15.3',
|
||||
|
||||
defaultMaxLogSize: '512m',
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
DockerBuildOutputDecoder,
|
||||
DockerJsonStreamParser,
|
||||
} from '../src/docker/DockerBuildOutput'
|
||||
|
||||
function varint(value: number): Buffer {
|
||||
const bytes: number[] = []
|
||||
do {
|
||||
const remaining = Math.floor(value / 128)
|
||||
bytes.push(value % 128 | (remaining ? 0x80 : 0))
|
||||
value = remaining
|
||||
} while (value)
|
||||
return Buffer.from(bytes)
|
||||
}
|
||||
|
||||
function bytesField(fieldNumber: number, value: string | Buffer): Buffer {
|
||||
const data = Buffer.isBuffer(value) ? value : Buffer.from(value)
|
||||
return Buffer.concat([
|
||||
varint(fieldNumber * 8 + 2),
|
||||
varint(data.length),
|
||||
data,
|
||||
])
|
||||
}
|
||||
|
||||
test('buffers split JSON messages and parses multiple messages', () => {
|
||||
const parser = new DockerJsonStreamParser()
|
||||
|
||||
expect(parser.push('{"stream":"hel')).toEqual([])
|
||||
expect(parser.push('lo"}\n{"error":"failed"}\n')).toEqual([
|
||||
{ stream: 'hello' },
|
||||
{ error: 'failed' },
|
||||
])
|
||||
})
|
||||
|
||||
test('keeps buffers isolated and flushes the final message', () => {
|
||||
const first = new DockerJsonStreamParser()
|
||||
const second = new DockerJsonStreamParser()
|
||||
|
||||
first.push('{"stream":"first"')
|
||||
second.push('{"stream":"second"}')
|
||||
|
||||
expect(first.push('}\n')).toEqual([{ stream: 'first' }])
|
||||
expect(second.flush()).toEqual([{ stream: 'second' }])
|
||||
})
|
||||
|
||||
test('reports malformed JSON without interrupting later messages', () => {
|
||||
const parser = new DockerJsonStreamParser()
|
||||
|
||||
expect(parser.push('not-json\n{"stream":"still running"}\n')).toEqual([
|
||||
{ stream: 'Cannot parse not-json' },
|
||||
{ stream: 'still running' },
|
||||
])
|
||||
})
|
||||
|
||||
test('preserves traditional builder output', () => {
|
||||
const decoder = new DockerBuildOutputDecoder()
|
||||
const message = { stream: 'Step 1/1\n' }
|
||||
|
||||
expect(decoder.decode(message)).toEqual([message])
|
||||
})
|
||||
|
||||
test('decodes BuildKit vertices, logs, and warnings', () => {
|
||||
const vertex = Buffer.concat([
|
||||
bytesField(1, 'sha256:vertex'),
|
||||
bytesField(3, '[1/1] RUN npm test'),
|
||||
])
|
||||
const log = Buffer.concat([
|
||||
bytesField(1, 'sha256:vertex'),
|
||||
bytesField(4, 'tests passed\n'),
|
||||
])
|
||||
const warning = bytesField(3, 'deprecated instruction')
|
||||
const status = Buffer.concat([
|
||||
bytesField(1, vertex),
|
||||
bytesField(3, log),
|
||||
bytesField(4, warning),
|
||||
])
|
||||
const decoder = new DockerBuildOutputDecoder()
|
||||
|
||||
expect(
|
||||
decoder.decode({
|
||||
id: 'moby.buildkit.trace',
|
||||
aux: status.toString('base64'),
|
||||
})
|
||||
).toEqual([
|
||||
{ stream: '[1/1] RUN npm test\n' },
|
||||
{ stream: 'tests passed\n' },
|
||||
{ stream: 'WARNING: deprecated instruction\n' },
|
||||
])
|
||||
|
||||
expect(
|
||||
decoder.decode({
|
||||
id: 'moby.buildkit.trace',
|
||||
aux: bytesField(1, vertex).toString('base64'),
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test('decodes BuildKit vertex errors once with error details', () => {
|
||||
const vertex = Buffer.concat([
|
||||
bytesField(1, 'sha256:failed-vertex'),
|
||||
bytesField(3, '[1/1] RUN exit 1'),
|
||||
bytesField(7, 'process exited with code 1'),
|
||||
])
|
||||
const message = {
|
||||
id: 'moby.buildkit.trace',
|
||||
aux: bytesField(1, vertex).toString('base64'),
|
||||
}
|
||||
const decoder = new DockerBuildOutputDecoder()
|
||||
|
||||
expect(decoder.decode(message)).toEqual([
|
||||
{ stream: '[1/1] RUN exit 1\n' },
|
||||
{
|
||||
error: 'process exited with code 1',
|
||||
errorDetail: { message: 'process exited with code 1' },
|
||||
},
|
||||
])
|
||||
expect(decoder.decode(message)).toEqual([])
|
||||
})
|
||||
|
||||
test('reports malformed BuildKit protobuf output', () => {
|
||||
const decoder = new DockerBuildOutputDecoder()
|
||||
|
||||
expect(
|
||||
decoder.decode({
|
||||
id: 'moby.buildkit.trace',
|
||||
aux: Buffer.from([0x0a, 0x05, 0x01]).toString('base64'),
|
||||
})
|
||||
).toEqual([{ stream: 'Cannot parse BuildKit build output\n' }])
|
||||
})
|
||||
|
||||
test('ignores unrelated aux messages', () => {
|
||||
const decoder = new DockerBuildOutputDecoder()
|
||||
|
||||
expect(
|
||||
decoder.decode({ id: 'other.message', aux: 'not-protobuf' })
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test('skips 10-byte protobuf varints without dropping later output', () => {
|
||||
const fullWidthVarint = Buffer.from([
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
|
||||
])
|
||||
const log = Buffer.concat([
|
||||
Buffer.from([0x08]),
|
||||
fullWidthVarint,
|
||||
bytesField(4, 'still visible\n'),
|
||||
])
|
||||
const status = bytesField(3, log)
|
||||
const decoder = new DockerBuildOutputDecoder()
|
||||
|
||||
expect(
|
||||
decoder.decode({
|
||||
id: 'moby.buildkit.trace',
|
||||
aux: status.toString('base64'),
|
||||
})
|
||||
).toEqual([{ stream: 'still visible\n' }])
|
||||
})
|
||||
Reference in New Issue
Block a user