diff --git a/README.md b/README.md index f1d586b..e8c2be2 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ the following code should be used to pass it to the action: _Optional_ The name you want to use in the Docker registry. The name can only contain lowercase letters, numbers, hyphens ("-"), and underscores ("_"). +### `existing-image` + +_Optional_ Use an existing image instead of building an image. The image name needs to include a tag or digest. + ### `file` _Optional_ A path to an alternative Dockerfile. diff --git a/action.test.ts b/action.test.ts index 4bc3a77..e86f068 100644 --- a/action.test.ts +++ b/action.test.ts @@ -4,10 +4,20 @@ import {runAction} from './action'; import {randomBytes} from 'crypto'; import {mkdir} from 'node:fs/promises'; import {createApiClient} from './humanitec'; +import {exec as actionsExec} from '@actions/exec'; // Emulate https://github.com/actions/toolkit/blob/819157bf8/packages/core/src/core.ts#L128 -const setInput = (name: string, value: string): void => { - process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] = value; +const setInputWithState = (state: string[], name: string, value: string): void => { + const envName = `INPUT_${name.replace(/ /g, '_').toUpperCase()}`; + process.env[envName] = value; + + state.push(envName); +}; + +const clearInputs = (envNames: string[]) => { + for (const envName of envNames) { + process.env[envName] = ''; + } }; const fixtures = pathJoin(__dirname, './fixtures'); @@ -32,6 +42,10 @@ describe('action', () => { let repo: string; let commit: string; + const inputs: string[] = []; + const setInput = (name: string, value: string): void => { + return setInputWithState(inputs, name, value); + }; afterAll(async () => { const res = await humanitecClient.orgsOrgIdArtefactsGet(orgId, 'container'); @@ -73,6 +87,7 @@ describe('action', () => { afterEach(() => { process.exitCode = undefined; + clearInputs(inputs); }); test('succeeds', async () => { @@ -147,4 +162,36 @@ describe('action', () => { ), ); }); + + test('supports pushing an already existing image', async () => { + actionsExec('docker', ['pull', 'hello-world:latest']); + + setInput('existing-image', 'hello-world:latest'); + + await runAction(); + expect(process.exitCode).toBeFalsy(); + + const res = await humanitecClient.orgsOrgIdArtefactVersionsGet(orgId); + expect(res.status).toBe(200); + expect(res.data).toEqual( + expect.arrayContaining( + [ + expect.objectContaining({ + commit: commit, + name: `registry.humanitec.io/${orgId}/${repo}`, + }), + ], + ), + ); + }); + + test('fails when trying to specific an image on the same registry with a different tag', async () => { + actionsExec('docker', ['pull', 'hello-world:latest']); + actionsExec('docker', ['tag', 'hello-world:latest', `registry.humanitec.io/${orgId}/hello-world:latest`]); + + setInput('existing-image', `registry.humanitec.io/${orgId}/hello-world:latest`); + + await runAction(); + expect(process.exitCode).toBeTruthy(); + }); }); diff --git a/action.ts b/action.ts index 041f536..35d0fad 100644 --- a/action.ts +++ b/action.ts @@ -16,6 +16,7 @@ export async function runAction() { const token = core.getInput('humanitec-token', {required: true}); const orgId = core.getInput('organization', {required: true}); const imageName = core.getInput('image-name') || (process.env.GITHUB_REPOSITORY || '').replace(/.*\//, ''); + const existingImage = core.getInput('existing-image') || ''; const context = core.getInput('context') || core.getInput('dockerfile') || '.'; const file = core.getInput('file') || ''; let registryHost = core.getInput('humanitec-registry') || 'registry.humanitec.io'; @@ -76,18 +77,34 @@ export async function runAction() { version = commit; } const imageWithVersion = `${imageName}:${version}`; - const localTag = `${orgId}/${imageWithVersion}`; - const imageId = await docker.build(localTag, file, additionalDockerArguments, context); - if (!imageId) { - core.setFailed('Unable build image from Dockerfile.'); - return; + + let imageId; + if (existingImage) { + imageId = existingImage; + } else { + const localTag = `${orgId}/${imageWithVersion}`; + imageId = await docker.build(localTag, file, additionalDockerArguments, context); + if (!imageId) { + core.setFailed('Unable build image from Dockerfile.'); + return; + } } const remoteTag = `${registryHost}/${imageWithVersion}`; - const pushed = await docker.push(imageId, remoteTag); - if (!pushed) { - core.setFailed('Unable to push image to registry'); - return; + if (existingImage !== remoteTag) { + if (existingImage.startsWith(registryHost)) { + core.setFailed( + `The provided image seems to be already pushed, but the version tag is not matching.\n` + + `Expected: ${remoteTag}\n` + + `Provided: ${existingImage}`); + return; + } + + const pushed = await docker.push(imageId, remoteTag); + if (!pushed) { + core.setFailed('Unable to push image to registry'); + return; + } } const payload: AddArtefactVersionPayloadRequest = { diff --git a/action.yaml b/action.yaml index c46df83..cb93056 100644 --- a/action.yaml +++ b/action.yaml @@ -11,6 +11,10 @@ inputs: description: 'The name you want to refer to the image to in the Humanitec Platform.' required: false default: '' + existing-image: + description: 'Use an existing image instead of building an image. The image name needs to include a tag or digest.' + required: false + default: '' tag: description: 'Use tag when you want to bring your own tag.' required: false diff --git a/dist/index.js b/dist/index.js index 69626a5..cd239e9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41085,6 +41085,7 @@ async function runAction() { const token = core.getInput('humanitec-token', { required: true }); const orgId = core.getInput('organization', { required: true }); const imageName = core.getInput('image-name') || (process.env.GITHUB_REPOSITORY || '').replace(/.*\//, ''); + const existingImage = core.getInput('existing-image') || ''; const context = core.getInput('context') || core.getInput('dockerfile') || '.'; const file = core.getInput('file') || ''; let registryHost = core.getInput('humanitec-registry') || 'registry.humanitec.io'; @@ -41138,17 +41139,31 @@ async function runAction() { version = commit; } const imageWithVersion = `${imageName}:${version}`; - const localTag = `${orgId}/${imageWithVersion}`; - const imageId = await docker.build(localTag, file, additionalDockerArguments, context); - if (!imageId) { - core.setFailed('Unable build image from Dockerfile.'); - return; + let imageId; + if (existingImage) { + imageId = existingImage; + } + else { + const localTag = `${orgId}/${imageWithVersion}`; + imageId = await docker.build(localTag, file, additionalDockerArguments, context); + if (!imageId) { + core.setFailed('Unable build image from Dockerfile.'); + return; + } } const remoteTag = `${registryHost}/${imageWithVersion}`; - const pushed = await docker.push(imageId, remoteTag); - if (!pushed) { - core.setFailed('Unable to push image to registry'); - return; + if (existingImage !== remoteTag) { + if (existingImage.startsWith(registryHost)) { + core.setFailed(`The provided image seems to be already pushed, but the version tag is not matching.\n` + + `Expected: ${remoteTag}\n` + + `Provided: ${existingImage}`); + return; + } + const pushed = await docker.push(imageId, remoteTag); + if (!pushed) { + core.setFailed('Unable to push image to registry'); + return; + } } const payload = { name: `${registryHost}/${imageName}`,