Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions __tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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([
[
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
112 changes: 56 additions & 56 deletions dist/index.cjs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/index.cjs.map

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
};
}
Expand Down Expand Up @@ -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);
Expand Down
Loading