mirror of
https://github.com/docker/bake-action.git
synced 2026-10-14 19:30:27 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user