mirror of
https://github.com/docker/build-push-action.git
synced 2026-03-27 08:27:01 +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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
* [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 |
|
||||
| `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 }}`) |
|
||||
| `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
|
||||
|
||||
|
||||
@ -108,6 +108,10 @@ inputs:
|
||||
description: "GitHub Token used to authenticate against a repository for Git context"
|
||||
default: ${{ github.token }}
|
||||
required: false
|
||||
estargz:
|
||||
description: "Enable eStargz compression for faster image pulls (requires push: true)"
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
outputs:
|
||||
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",
|
||||
"dependencies": {
|
||||
"@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-node": "^1.6.1",
|
||||
"@docker/actions-toolkit": "0.37.1",
|
||||
@ -2379,38 +2379,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@buf/blacksmith_vm-agent.bufbuild_es": {
|
||||
"version": "1.10.0-20250304023716-e8d233d92eac.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",
|
||||
"dependencies": {
|
||||
"@buf/googleapis_googleapis.bufbuild_es": "1.10.0-20250203201857-83c0f6c19b2f.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-20251002224722-c44b45f26c5e.1.tgz",
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@buf/blacksmith_vm-agent.connectrpc_es": {
|
||||
"version": "1.6.1-20250304023716-e8d233d92eac.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",
|
||||
"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-20251002224722-c44b45f26c5e.2.tgz",
|
||||
"dependencies": {
|
||||
"@buf/blacksmith_vm-agent.bufbuild_es": "1.10.0-20250304023716-e8d233d92eac.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"
|
||||
"@buf/blacksmith_vm-agent.bufbuild_es": "1.10.0-20251002224722-c44b45f26c5e.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@connectrpc/connect": "^1.6.1"
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"packageManager": "yarn@3.6.3",
|
||||
"dependencies": {
|
||||
"@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-node": "^1.6.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;
|
||||
ulimit: string[];
|
||||
'github-token': string;
|
||||
estargz: boolean;
|
||||
}
|
||||
|
||||
export async function getInputs(): Promise<Inputs> {
|
||||
@ -93,7 +94,8 @@ export async function getInputs(): Promise<Inputs> {
|
||||
tags: Util.getInputList('tags'),
|
||||
target: core.getInput('target'),
|
||||
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 => {
|
||||
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 => {
|
||||
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) {
|
||||
args.push('--platform', inputs.platforms.join(','));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user