diff --git a/packages/js-sdk/src/template/dockerfileParser.ts b/packages/js-sdk/src/template/dockerfileParser.ts index 97d54a5727..17a3121fa8 100644 --- a/packages/js-sdk/src/template/dockerfileParser.ts +++ b/packages/js-sdk/src/template/dockerfileParser.ts @@ -220,6 +220,22 @@ function handleUserInstruction( } } +/** + * Strip surrounding double or single quotes from an ENV value. + * Docker's ENV instruction strips quotes from values like ENV KEY="value". + */ +function stripQuotes(value: string): string { + if (value.length >= 2) { + if ( + (value[0] === '"' && value[value.length - 1] === '"') || + (value[0] === "'" && value[value.length - 1] === "'") + ) { + return value.slice(1, -1) + } + } + return value +} + function handleEnvInstruction( instruction: DockerfileInstruction, templateBuilder: DockerfileParserInterface @@ -243,13 +259,13 @@ function handleEnvInstruction( const equalIndex = envString.indexOf('=') if (equalIndex > 0) { const key = envString.substring(0, equalIndex) - const value = envString.substring(equalIndex + 1) + const value = stripQuotes(envString.substring(equalIndex + 1)) envVars[key] = value } } } else { // Traditional ENV key value format - envVars[firstArg] = secondArg + envVars[firstArg] = stripQuotes(secondArg) } } else if (argumentsData.length === 1) { // ENV/ARG key=value format (single argument) or ARG key (without default) @@ -259,7 +275,7 @@ function handleEnvInstruction( const equalIndex = envString.indexOf('=') if (equalIndex > 0) { const key = envString.substring(0, equalIndex) - const value = envString.substring(equalIndex + 1) + const value = stripQuotes(envString.substring(equalIndex + 1)) envVars[key] = value } else if (keyword === 'ARG' && envString.trim()) { // ARG without default value - set as empty ENV @@ -273,7 +289,7 @@ function handleEnvInstruction( const equalIndex = envString.indexOf('=') if (equalIndex > 0) { const key = envString.substring(0, equalIndex) - const value = envString.substring(equalIndex + 1) + const value = stripQuotes(envString.substring(equalIndex + 1)) envVars[key] = value } else if (keyword === 'ARG') { // ARG without default value diff --git a/packages/js-sdk/tests/template/methods/dockerfileParserEnv.test.ts b/packages/js-sdk/tests/template/methods/dockerfileParserEnv.test.ts new file mode 100644 index 0000000000..41222f01bb --- /dev/null +++ b/packages/js-sdk/tests/template/methods/dockerfileParserEnv.test.ts @@ -0,0 +1,77 @@ +import { describe, it, assert } from 'vitest' +import { Template } from '../../../src' +import { InstructionType } from '../../../src/template/types' + +/** + * Helper to extract ENV instructions from a parsed Dockerfile. + * Returns an array of Record — one per ENV instruction. + */ +function getEnvs(dockerfileContent: string): Record[] { + const template = Template().fromDockerfile(dockerfileContent) + // @ts-expect-error - instructions is not a property of TemplateBuilder + const instructions = template.instructions as { + type: InstructionType + args: string[] + }[] + const envInstructions = instructions.filter( + (i) => i.type === InstructionType.ENV + ) + return envInstructions.map((inst) => { + const result: Record = {} + for (let i = 0; i < inst.args.length; i += 2) { + result[inst.args[i]] = inst.args[i + 1] + } + return result + }) +} + +describe('dockerfileParser ENV handling', () => { + describe('quote stripping', () => { + it('strips double quotes from ENV values', () => { + const envs = getEnvs('FROM node:24\nENV GOPATH="/go"') + assert.deepEqual(envs, [{ GOPATH: '/go' }]) + }) + + it('strips single quotes from ENV values', () => { + const envs = getEnvs("FROM node:24\nENV GOPATH='/go'") + assert.deepEqual(envs, [{ GOPATH: '/go' }]) + }) + + it('does not strip mismatched quotes', () => { + const envs = getEnvs('FROM node:24\nENV GOPATH="/go\'') + assert.deepEqual(envs, [{ GOPATH: '"/go\'' }]) + }) + + it('handles unquoted values', () => { + const envs = getEnvs('FROM node:24\nENV GOPATH=/go') + assert.deepEqual(envs, [{ GOPATH: '/go' }]) + }) + + it('preserves variable references as-is (expansion done by backend)', () => { + const envs = getEnvs( + 'FROM node:24\nENV GOPATH=/go\nENV PATH="/usr/bin:${GOPATH}/bin"' + ) + assert.deepEqual(envs, [ + { GOPATH: '/go' }, + { PATH: '/usr/bin:${GOPATH}/bin' }, + ]) + }) + + it('strips quotes from multiple key=value pairs', () => { + const envs = getEnvs('FROM node:24\nENV A="hello" B="world"') + assert.deepEqual(envs, [{ A: 'hello', B: 'world' }]) + }) + }) + + describe('ARG handling', () => { + it('ARG with default value', () => { + const envs = getEnvs('FROM node:24\nARG MY_ARG="hello"') + assert.deepEqual(envs, [{ MY_ARG: 'hello' }]) + }) + + it('ARG without default sets empty value', () => { + const envs = getEnvs('FROM node:24\nARG MY_ARG') + assert.deepEqual(envs, [{ MY_ARG: '' }]) + }) + }) +})