diff --git a/.gitignore b/.gitignore index 4814714..d04cb5e 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ jspm_packages/ .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# IDE +.idea/ \ No newline at end of file diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index 68da145..0d03f4b 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -23,6 +23,7 @@ describe('getInputs', () => { image: 'docker.io/tonistiigi/binfmt:latest', platforms: 'all', cacheImage: true, + localCachePath: '' } as context.Inputs ], [ @@ -36,6 +37,7 @@ describe('getInputs', () => { image: 'docker/binfmt:latest', platforms: 'arm64,riscv64,arm', cacheImage: false, + localCachePath: '' } as context.Inputs ], [ @@ -48,6 +50,21 @@ describe('getInputs', () => { image: 'docker.io/tonistiigi/binfmt:latest', platforms: 'arm64,riscv64,arm', cacheImage: true, + localCachePath: '' + } as context.Inputs + ], + [ + 3, + new Map([ + ['platforms', 'arm64'], + ['cache-image', 'false'], + ['local-cache-path', '/tmp/cache'], + ]), + { + image: 'docker.io/tonistiigi/binfmt:latest', + platforms: 'arm64', + cacheImage: false, + localCachePath: '/tmp/cache' } as context.Inputs ] ])( diff --git a/action.yml b/action.yml index 6135f30..738d8a0 100644 --- a/action.yml +++ b/action.yml @@ -19,6 +19,12 @@ inputs: description: 'Cache binfmt image to GitHub Actions cache backend' default: 'true' required: false + local-cache-path: + description: > + Local path to store the binfmt image. Using this enables local caching instead of GitHub Actions cache. + Note: The "latest" tag won't auto-update - delete the cached file to fetch updates. + default: '' + required: false outputs: platforms: diff --git a/src/context.ts b/src/context.ts index 46b4d14..3562098 100644 --- a/src/context.ts +++ b/src/context.ts @@ -5,12 +5,14 @@ export interface Inputs { image: string; platforms: string; cacheImage: boolean; + localCachePath: string; } export function getInputs(): Inputs { return { image: core.getInput('image') || 'docker.io/tonistiigi/binfmt:latest', platforms: Util.getInputList('platforms').join(',') || 'all', - cacheImage: core.getBooleanInput('cache-image') + cacheImage: core.getBooleanInput('cache-image'), + localCachePath: core.getInput('local-cache-path') || '' }; } diff --git a/src/local-cache.ts b/src/local-cache.ts new file mode 100644 index 0000000..bf0e78e --- /dev/null +++ b/src/local-cache.ts @@ -0,0 +1,57 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as core from '@actions/core'; +import {Docker} from '@docker/actions-toolkit/lib/docker/docker'; + +function getImageCacheFileName(imageName: string): string { + return `${imageName.replace(/[\/\:]/g, '-')}.tar`; +} + +export async function loadDockerImageFromCache(localCachePath: string, imageName: string): Promise { + const cacheFilePath = path.join(localCachePath, getImageCacheFileName(imageName)); + + try { + if (fs.existsSync(cacheFilePath)) { + await Docker.getExecOutput(['load', '-i', cacheFilePath], { + ignoreReturnCode: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr.match(/(.*)\s*$/)?.[0]?.trim() ?? 'unknown error'); + } + core.info(`Loaded image from ${cacheFilePath}`); + }); + } else { + core.info(`Cache file not found at ${cacheFilePath}, pulling image instead`); + await Docker.pull(imageName); + } + } catch (error) { + core.warning(`Failed to check/load cache file: ${error}`); + await Docker.pull(imageName); + } +} + +export async function saveDockerImageToCache(localCachePath: string, imageName: string): Promise { + const cacheFilePath = path.join(localCachePath, getImageCacheFileName(imageName)); + + try { + if (!fs.existsSync(localCachePath)) { + fs.mkdirSync(localCachePath, {recursive: true}); + } + + if (fs.existsSync(cacheFilePath)) { + core.info(`Cache file already exists at ${cacheFilePath}, skipping save`); + return; + } + + await Docker.getExecOutput(['save', '-o', cacheFilePath, imageName], { + ignoreReturnCode: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr.match(/(.*)\s*$/)?.[0]?.trim() ?? 'unknown error'); + } + core.info(`Saved image to ${cacheFilePath}`); + }); + } catch (error) { + core.warning(`Failed to save image to cache file: ${error}`); + } +} diff --git a/src/main.ts b/src/main.ts index 9d1386c..ce46a74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import * as core from '@actions/core'; import * as actionsToolkit from '@docker/actions-toolkit'; import {Docker} from '@docker/actions-toolkit/lib/docker/docker'; +import {loadDockerImageFromCache, saveDockerImageToCache} from './local-cache'; interface Platforms { supported: string[]; @@ -19,9 +20,15 @@ actionsToolkit.run( await Docker.printInfo(); }); - await core.group(`Pulling binfmt Docker image`, async () => { - await Docker.pull(input.image, input.cacheImage); - }); + if (input.localCachePath !== '') { + await core.group(`Pulling binfmt Docker image`, async () => { + await loadDockerImageFromCache(input.localCachePath, input.image); + }); + } else { + await core.group(`Pulling binfmt Docker image`, async () => { + await Docker.pull(input.image, input.cacheImage); + }); + } await core.group(`Image info`, async () => { await Docker.getExecOutput(['image', 'inspect', input.image], { @@ -56,5 +63,15 @@ actionsToolkit.run( core.setOutput('platforms', platforms.supported.join(',')); }); }); + }, + + // post + async () => { + const input: context.Inputs = context.getInputs(); + if (input.localCachePath !== '') { + await core.group(`Saving binfmt Docker image`, async () => { + await saveDockerImageToCache(input.localCachePath, input.image); + }); + } } );