accept JSON arrays for bake vars to preserve multiline values

Parse the vars input as a JSON array of strings, falling back to the
existing list parser when JSON parsing fails. Preserve multiline values,
whitespace, and literal quotes in each Buildx --var argument.

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-10-13 09:34:38 +02:00
parent cac84ca08e
commit a0190aa0a4
4 changed files with 90 additions and 3 deletions
+1 -1
View File
@@ -239,7 +239,7 @@ The following inputs can be used as `step.with` keys
| `set` | List | List of [targets values to override](https://docs.docker.com/engine/reference/commandline/buildx_bake/#set) (e.g., `targetpattern.key=value`) |
| `source` | String | Build source to use. Supports local path and [remote bake definition](https://docs.docker.com/build/bake/remote-definition/). With a local path, Bake runs from that directory, so all relative paths are resolved from it. See [Source semantics](#source-semantics). |
| `targets` | List/CSV | List of bake targets (`default` target used if empty) |
| `vars` | List | [Variables](https://docs.docker.com/build/bake/variables/) to set in the Bake definition as list of key-value pair |
| `vars` | List/JSON | [Variables](https://docs.docker.com/build/bake/variables/) to set in the Bake definition as a list of key-value pairs or a JSON array of strings |
| `github-token` | String | API token used to authenticate to a Git repository for [remote definitions](https://docs.docker.com/build/bake/remote-definition/) (default `${{ github.token }}`) |
### outputs
+74
View File
@@ -87,6 +87,50 @@ describe('getInputs', () => {
expect(gitContextSpy).toHaveBeenCalledTimes(1);
gitContextSpy.mockRestore();
});
describe('vars', () => {
beforeEach(() => {
setRequiredBooleanInputs();
setInput('source', '.');
});
test('preserves line-based input parsing', async () => {
setInput('vars', 'TAGS=one,two\nJSON={"value":"a,b=c"}\nQUOTED="literal"\nEMPTY=');
expect((await context.getInputs()).vars).toEqual(['TAGS=one,two', 'JSON={"value":"a,b=c"}', 'QUOTED="literal"', 'EMPTY=']);
});
test('preserves JSON entries including multiline values and whitespace', async () => {
const vars = [
'DOCKER_METADATA_OUTPUT_TAGS=example/app:latest\nexample/app:main\n',
'DOCKER_METADATA_OUTPUT_LABELS=version=1.2.3\r\ntitle=example\r\n',
'DOCKER_METADATA_OUTPUT_JSON={"tags":["example/app:latest","example/app:main"]}',
'PADDED= "one,two"=three ',
'EMPTY='
];
setInput('vars', ` \n${JSON.stringify(vars, null, 2)}\n `);
expect((await context.getInputs()).vars).toEqual(vars);
});
test('preserves duplicate JSON variable entries in order', async () => {
setInput('vars', JSON.stringify(['VERSION=default', 'OTHER=value', 'VERSION=override', 'VERSION=final']));
expect((await context.getInputs()).vars).toEqual(['VERSION=default', 'OTHER=value', 'VERSION=override', 'VERSION=final']);
});
test('accepts an empty JSON array', async () => {
setInput('vars', '[]');
expect((await context.getInputs()).vars).toEqual([]);
});
test('falls back to list parsing when JSON parsing fails', async () => {
setInput('vars', '["KEY=value"');
expect((await context.getInputs()).vars).toEqual(['["KEY=value"']);
});
test.each(['null', 'true', '1', '"KEY=value"', '{"KEY":"value"}', '["KEY=value",1]', '[null]', '[["KEY=value"]]'])('rejects invalid JSON variable lists: %s', async value => {
setInput('vars', value);
await expect(context.getInputs()).rejects.toThrow('Invalid vars input: expected a JSON array of strings.');
});
});
});
describe('getArgs', () => {
@@ -103,6 +147,36 @@ describe('getArgs', () => {
process.env = originalEnv;
});
test('passes JSON variables as individual CLI arguments without changing their values', async () => {
const vars = ['DOCKER_METADATA_OUTPUT_TAGS=example/app:latest\nexample/app:main', 'PADDED= "one,two"=three ', 'EMPTY='];
setInput('source', '.');
setInput('no-cache', 'false');
setInput('pull', 'false');
setInput('load', 'false');
setInput('push', 'false');
setInput('provenance', 'false');
setInput('vars', JSON.stringify(vars));
vi.spyOn(Buildx.prototype, 'version').mockResolvedValue('0.31.0');
const toolkit = new Toolkit();
const inputs = await context.getInputs();
const definition = await toolkit.buildxBake.getDefinition({vars: inputs.vars});
expect(await context.getArgs(inputs, definition, toolkit)).toEqual(['bake', '--allow', 'fs=*', ...vars.flatMap(value => ['--var', value]), '--metadata-file', metadataJson, '--provenance', 'false']);
});
test('requires Buildx 0.31.0 for JSON variables', async () => {
setInput('source', '.');
setInput('no-cache', 'false');
setInput('pull', 'false');
setInput('load', 'false');
setInput('push', 'false');
setInput('vars', '["KEY=value"]');
vi.spyOn(Buildx.prototype, 'version').mockResolvedValue('0.30.0');
const toolkit = new Toolkit();
const inputs = await context.getInputs();
const definition = await toolkit.buildxBake.getDefinition({vars: inputs.vars});
await expect(context.getArgs(inputs, definition, toolkit)).rejects.toThrow('Buildx >= 0.31.0 is required to use the vars input.');
});
// prettier-ignore
test.each([
[
+1 -1
View File
@@ -51,7 +51,7 @@ inputs:
description: "List of bake targets"
required: false
vars:
description: "Variables to set in the Bake definition as list of key-value pair"
description: "Variables to set in the Bake definition as a list of key-value pairs or a JSON array of strings"
required: false
github-token:
description: "API token used to authenticate to a Git repository for remote definitions"
+14 -1
View File
@@ -48,7 +48,7 @@ export async function getInputs(): Promise<Inputs> {
set: Util.getInputList('set', {ignoreComma: true, quote: false}),
source: await getBakeContext(core.getInput('source')),
targets: Util.getInputList('targets'),
vars: Util.getInputList('vars', {ignoreComma: true, quote: false}),
vars: getBakeVars(core.getInput('vars')),
'github-token': core.getInput('github-token')
};
}
@@ -171,6 +171,19 @@ async function getBakeContext(sourceInput: string): Promise<BakeContext> {
};
}
function getBakeVars(varsInput: string): string[] {
let parsed: unknown;
try {
parsed = JSON.parse(varsInput);
} catch {
return Util.getList(varsInput, {ignoreComma: true, quote: false});
}
if (!Array.isArray(parsed) || !parsed.every(value => typeof value === 'string')) {
throw new Error('Invalid vars input: expected a JSON array of strings.');
}
return parsed;
}
function noDefaultAttestations(): boolean {
if (process.env.BUILDX_NO_DEFAULT_ATTESTATIONS) {
return Util.parseBool(process.env.BUILDX_NO_DEFAULT_ATTESTATIONS);