mirror of
https://github.com/docker/build-push-action.git
synced 2026-03-31 18:09:56 +00:00
Merge pull request #131 from useblacksmith/pr-130-fixed
*: add estargz compression support
This commit is contained in:
commit
30c71162f1
32
README.md
32
README.md
@ -100,6 +100,37 @@ jobs:
|
|||||||
tags: user/app:latest
|
tags: user/app:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### eStargz compression for faster pulls
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: blacksmith
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Build and push with eStargz
|
||||||
|
uses: useblacksmith/build-push-action@v1
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: user/app:latest
|
||||||
|
estargz: true
|
||||||
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
* [Multi-platform image](https://docs.docker.com/build/ci/github-actions/multi-platform/)
|
* [Multi-platform image](https://docs.docker.com/build/ci/github-actions/multi-platform/)
|
||||||
@ -190,6 +221,7 @@ The following inputs can be used as `step.with` keys:
|
|||||||
| `target` | String | Sets the target stage to build |
|
| `target` | String | Sets the target stage to build |
|
||||||
| `ulimit` | List | [Ulimit](https://docs.docker.com/engine/reference/commandline/buildx_build/#ulimit) options (e.g., `nofile=1024:1024`) |
|
| `ulimit` | List | [Ulimit](https://docs.docker.com/engine/reference/commandline/buildx_build/#ulimit) options (e.g., `nofile=1024:1024`) |
|
||||||
| `github-token` | String | GitHub Token used to authenticate against a repository for [Git context](#git-context) (default `${{ github.token }}`) |
|
| `github-token` | String | GitHub Token used to authenticate against a repository for [Git context](#git-context) (default `${{ github.token }}`) |
|
||||||
|
| `estargz` | Bool | Enable [eStargz compression](https://github.com/containerd/stargz-snapshotter/blob/main/docs/estargz.md) for faster image pulls (requires `push: true`) (default `false`) |
|
||||||
|
|
||||||
### outputs
|
### outputs
|
||||||
|
|
||||||
|
|||||||
@ -108,6 +108,10 @@ inputs:
|
|||||||
description: "GitHub Token used to authenticate against a repository for Git context"
|
description: "GitHub Token used to authenticate against a repository for Git context"
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
required: false
|
required: false
|
||||||
|
estargz:
|
||||||
|
description: "Enable eStargz compression for faster image pulls (requires push: true)"
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
imageid:
|
imageid:
|
||||||
|
|||||||
2
dist/index.js
generated
vendored
2
dist/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
33
package-lock.json
generated
33
package-lock.json
generated
@ -8,7 +8,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@buf/blacksmith_vm-agent.connectrpc_es": "^1.6.1-20250304023716-e8d233d92eac.2",
|
"@buf/blacksmith_vm-agent.connectrpc_es": "^1.6.1-20251002224722-c44b45f26c5e.2",
|
||||||
"@connectrpc/connect": "^1.6.1",
|
"@connectrpc/connect": "^1.6.1",
|
||||||
"@connectrpc/connect-node": "^1.6.1",
|
"@connectrpc/connect-node": "^1.6.1",
|
||||||
"@docker/actions-toolkit": "0.37.1",
|
"@docker/actions-toolkit": "0.37.1",
|
||||||
@ -2379,38 +2379,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@buf/blacksmith_vm-agent.bufbuild_es": {
|
"node_modules/@buf/blacksmith_vm-agent.bufbuild_es": {
|
||||||
"version": "1.10.0-20250304023716-e8d233d92eac.1",
|
"version": "1.10.0-20251002224722-c44b45f26c5e.1",
|
||||||
"resolved": "https://buf.build/gen/npm/v1/@buf/blacksmith_vm-agent.bufbuild_es/-/blacksmith_vm-agent.bufbuild_es-1.10.0-20250304023716-e8d233d92eac.1.tgz",
|
"resolved": "https://buf.build/gen/npm/v1/@buf/blacksmith_vm-agent.bufbuild_es/-/blacksmith_vm-agent.bufbuild_es-1.10.0-20251002224722-c44b45f26c5e.1.tgz",
|
||||||
"dependencies": {
|
|
||||||
"@buf/googleapis_googleapis.bufbuild_es": "1.10.0-20250203201857-83c0f6c19b2f.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@bufbuild/protobuf": "^1.10.0"
|
"@bufbuild/protobuf": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@buf/blacksmith_vm-agent.connectrpc_es": {
|
"node_modules/@buf/blacksmith_vm-agent.connectrpc_es": {
|
||||||
"version": "1.6.1-20250304023716-e8d233d92eac.2",
|
"version": "1.6.1-20251002224722-c44b45f26c5e.2",
|
||||||
"resolved": "https://buf.build/gen/npm/v1/@buf/blacksmith_vm-agent.connectrpc_es/-/blacksmith_vm-agent.connectrpc_es-1.6.1-20250304023716-e8d233d92eac.2.tgz",
|
"resolved": "https://buf.build/gen/npm/v1/@buf/blacksmith_vm-agent.connectrpc_es/-/blacksmith_vm-agent.connectrpc_es-1.6.1-20251002224722-c44b45f26c5e.2.tgz",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@buf/blacksmith_vm-agent.bufbuild_es": "1.10.0-20250304023716-e8d233d92eac.1",
|
"@buf/blacksmith_vm-agent.bufbuild_es": "1.10.0-20251002224722-c44b45f26c5e.1"
|
||||||
"@buf/googleapis_googleapis.connectrpc_es": "1.6.1-20250203201857-83c0f6c19b2f.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@connectrpc/connect": "^1.6.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@buf/googleapis_googleapis.bufbuild_es": {
|
|
||||||
"version": "1.10.0-20250203201857-83c0f6c19b2f.1",
|
|
||||||
"resolved": "https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.bufbuild_es/-/googleapis_googleapis.bufbuild_es-1.10.0-20250203201857-83c0f6c19b2f.1.tgz",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@bufbuild/protobuf": "^1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@buf/googleapis_googleapis.connectrpc_es": {
|
|
||||||
"version": "1.6.1-20250203201857-83c0f6c19b2f.2",
|
|
||||||
"resolved": "https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.connectrpc_es/-/googleapis_googleapis.connectrpc_es-1.6.1-20250203201857-83c0f6c19b2f.2.tgz",
|
|
||||||
"dependencies": {
|
|
||||||
"@buf/googleapis_googleapis.bufbuild_es": "1.10.0-20250203201857-83c0f6c19b2f.1"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@connectrpc/connect": "^1.6.1"
|
"@connectrpc/connect": "^1.6.1"
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
"packageManager": "yarn@3.6.3",
|
"packageManager": "yarn@3.6.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@buf/blacksmith_vm-agent.connectrpc_es": "^1.6.1-20250304023716-e8d233d92eac.2",
|
"@buf/blacksmith_vm-agent.connectrpc_es": "^1.6.1-20251002224722-c44b45f26c5e.2",
|
||||||
"@connectrpc/connect": "^1.6.1",
|
"@connectrpc/connect": "^1.6.1",
|
||||||
"@connectrpc/connect-node": "^1.6.1",
|
"@connectrpc/connect-node": "^1.6.1",
|
||||||
"@docker/actions-toolkit": "0.37.1",
|
"@docker/actions-toolkit": "0.37.1",
|
||||||
|
|||||||
189
src/__tests__/estargz.test.ts
Normal file
189
src/__tests__/estargz.test.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import * as core from '@actions/core';
|
||||||
|
import {getArgs, Inputs} from '../context';
|
||||||
|
import {Toolkit} from '@docker/actions-toolkit/lib/toolkit';
|
||||||
|
|
||||||
|
jest.mock('@actions/core');
|
||||||
|
|
||||||
|
// Mock the Toolkit.
|
||||||
|
jest.mock('@docker/actions-toolkit/lib/toolkit');
|
||||||
|
|
||||||
|
describe('eStargz compression', () => {
|
||||||
|
let mockToolkit: jest.Mocked<Toolkit>;
|
||||||
|
let baseInputs: Inputs;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Create a mock toolkit with all necessary methods.
|
||||||
|
mockToolkit = {
|
||||||
|
buildx: {
|
||||||
|
versionSatisfies: jest.fn(),
|
||||||
|
getCommand: jest.fn(),
|
||||||
|
printVersion: jest.fn(),
|
||||||
|
isAvailable: jest.fn()
|
||||||
|
},
|
||||||
|
buildxBuild: {
|
||||||
|
getImageIDFilePath: jest.fn().mockReturnValue('/tmp/iidfile'),
|
||||||
|
getMetadataFilePath: jest.fn().mockReturnValue('/tmp/metadata'),
|
||||||
|
resolveImageID: jest.fn(),
|
||||||
|
resolveMetadata: jest.fn(),
|
||||||
|
resolveDigest: jest.fn(),
|
||||||
|
resolveWarnings: jest.fn(),
|
||||||
|
resolveRef: jest.fn()
|
||||||
|
},
|
||||||
|
builder: {
|
||||||
|
inspect: jest.fn().mockResolvedValue({
|
||||||
|
name: 'default',
|
||||||
|
driver: 'docker-container',
|
||||||
|
nodes: []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
buildkit: {
|
||||||
|
versionSatisfies: jest.fn().mockResolvedValue(false)
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Base inputs for testing.
|
||||||
|
baseInputs = {
|
||||||
|
'add-hosts': [],
|
||||||
|
allow: [],
|
||||||
|
annotations: [],
|
||||||
|
attests: [],
|
||||||
|
'build-args': [],
|
||||||
|
'build-contexts': [],
|
||||||
|
builder: '',
|
||||||
|
'cache-from': [],
|
||||||
|
'cache-to': [],
|
||||||
|
'cgroup-parent': '',
|
||||||
|
context: '.',
|
||||||
|
file: '',
|
||||||
|
labels: [],
|
||||||
|
load: false,
|
||||||
|
network: '',
|
||||||
|
'no-cache': false,
|
||||||
|
'no-cache-filters': [],
|
||||||
|
outputs: [],
|
||||||
|
platforms: [],
|
||||||
|
provenance: '',
|
||||||
|
pull: false,
|
||||||
|
push: false,
|
||||||
|
sbom: '',
|
||||||
|
secrets: [],
|
||||||
|
'secret-envs': [],
|
||||||
|
'secret-files': [],
|
||||||
|
'shm-size': '',
|
||||||
|
ssh: [],
|
||||||
|
tags: ['user/app:latest'],
|
||||||
|
target: '',
|
||||||
|
ulimit: [],
|
||||||
|
'github-token': '',
|
||||||
|
estargz: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not add estargz parameters when estargz is false', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const inputs = {...baseInputs, push: true, estargz: false};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
expect(args.join(' ')).not.toContain('compression=estargz');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not add estargz parameters when push is false', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const inputs = {...baseInputs, push: false, estargz: true};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
expect(args.join(' ')).not.toContain('compression=estargz');
|
||||||
|
expect(core.warning).toHaveBeenCalledWith("eStargz compression requires push: true; the input 'estargz' is ignored.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not add estargz parameters when buildx version is < 0.10.0', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockImplementation(async (version: string) => {
|
||||||
|
return version === '>=0.6.0'; // Only 0.6.0 check passes, not 0.10.0.
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputs = {...baseInputs, push: true, estargz: true};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
expect(args.join(' ')).not.toContain('compression=estargz');
|
||||||
|
expect(core.warning).toHaveBeenCalledWith("eStargz compression requires buildx >= 0.10.0; the input 'estargz' is ignored.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add estargz output when estargz is true, push is true, and buildx >= 0.10.0', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const inputs = {...baseInputs, push: true, estargz: true};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
expect(args).toContain('--output');
|
||||||
|
const outputIndex = args.indexOf('--output');
|
||||||
|
expect(args[outputIndex + 1]).toBe('type=registry,compression=estargz,force-compression=true,oci-mediatypes=true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should modify existing registry output with estargz parameters', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const inputs = {
|
||||||
|
...baseInputs,
|
||||||
|
push: true,
|
||||||
|
estargz: true,
|
||||||
|
outputs: ['type=registry,dest=output.txt']
|
||||||
|
};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
expect(args).toContain('--output');
|
||||||
|
const outputIndex = args.indexOf('--output');
|
||||||
|
expect(args[outputIndex + 1]).toBe('type=registry,dest=output.txt,compression=estargz,force-compression=true,oci-mediatypes=true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not modify non-registry outputs with estargz parameters', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const inputs = {
|
||||||
|
...baseInputs,
|
||||||
|
push: true,
|
||||||
|
estargz: true,
|
||||||
|
outputs: ['type=docker']
|
||||||
|
};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
expect(args).toContain('--output');
|
||||||
|
const outputIndex = args.indexOf('--output');
|
||||||
|
expect(args[outputIndex + 1]).toBe('type=docker');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple outputs correctly', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const inputs = {
|
||||||
|
...baseInputs,
|
||||||
|
push: true,
|
||||||
|
estargz: true,
|
||||||
|
outputs: ['type=registry', 'type=docker']
|
||||||
|
};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
const argsStr = args.join(' ');
|
||||||
|
expect(argsStr).toContain('type=registry,compression=estargz,force-compression=true,oci-mediatypes=true');
|
||||||
|
expect(argsStr).toContain('type=docker');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work with existing registry output without additional params', async () => {
|
||||||
|
(mockToolkit.buildx.versionSatisfies as jest.Mock).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const inputs = {
|
||||||
|
...baseInputs,
|
||||||
|
push: true,
|
||||||
|
estargz: true,
|
||||||
|
outputs: ['type=registry']
|
||||||
|
};
|
||||||
|
const args = await getArgs(inputs, mockToolkit);
|
||||||
|
|
||||||
|
expect(args).toContain('--output');
|
||||||
|
const outputIndex = args.indexOf('--output');
|
||||||
|
expect(args[outputIndex + 1]).toBe('type=registry,compression=estargz,force-compression=true,oci-mediatypes=true');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -57,6 +57,7 @@ export interface Inputs {
|
|||||||
target: string;
|
target: string;
|
||||||
ulimit: string[];
|
ulimit: string[];
|
||||||
'github-token': string;
|
'github-token': string;
|
||||||
|
estargz: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInputs(): Promise<Inputs> {
|
export async function getInputs(): Promise<Inputs> {
|
||||||
@ -93,7 +94,8 @@ export async function getInputs(): Promise<Inputs> {
|
|||||||
tags: Util.getInputList('tags'),
|
tags: Util.getInputList('tags'),
|
||||||
target: core.getInput('target'),
|
target: core.getInput('target'),
|
||||||
ulimit: Util.getInputList('ulimit', {ignoreComma: true}),
|
ulimit: Util.getInputList('ulimit', {ignoreComma: true}),
|
||||||
'github-token': core.getInput('github-token')
|
'github-token': core.getInput('github-token'),
|
||||||
|
estargz: core.getBooleanInput('estargz')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,9 +209,30 @@ async function getBuildArgs(inputs: Inputs, context: string, toolkit: Toolkit):
|
|||||||
await Util.asyncForEach(inputs['no-cache-filters'], async noCacheFilter => {
|
await Util.asyncForEach(inputs['no-cache-filters'], async noCacheFilter => {
|
||||||
args.push('--no-cache-filter', noCacheFilter);
|
args.push('--no-cache-filter', noCacheFilter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check estargz requirements BEFORE modifying outputs.
|
||||||
|
const useEstargz = inputs.estargz && inputs.push && (await toolkit.buildx.versionSatisfies('>=0.10.0'));
|
||||||
|
|
||||||
|
if (inputs.estargz) {
|
||||||
|
if (!(await toolkit.buildx.versionSatisfies('>=0.10.0'))) {
|
||||||
|
core.warning("eStargz compression requires buildx >= 0.10.0; the input 'estargz' is ignored.");
|
||||||
|
} else if (!inputs.push) {
|
||||||
|
core.warning("eStargz compression requires push: true; the input 'estargz' is ignored.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Util.asyncForEach(inputs.outputs, async output => {
|
await Util.asyncForEach(inputs.outputs, async output => {
|
||||||
args.push('--output', output);
|
if (useEstargz && (output.startsWith('type=registry') || output === 'type=registry')) {
|
||||||
|
const estargzOutput = `${output},compression=estargz,force-compression=true,oci-mediatypes=true`;
|
||||||
|
args.push('--output', estargzOutput);
|
||||||
|
} else {
|
||||||
|
args.push('--output', output);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (useEstargz && inputs.outputs.length === 0) {
|
||||||
|
args.push('--output', 'type=registry,compression=estargz,force-compression=true,oci-mediatypes=true');
|
||||||
|
}
|
||||||
if (inputs.platforms.length > 0) {
|
if (inputs.platforms.length > 0) {
|
||||||
args.push('--platform', inputs.platforms.join(','));
|
args.push('--platform', inputs.platforms.join(','));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user