Skip to content

Commit

Permalink
Merge pull request #7 from mr-smithers-excellent/feature/add-build-args
Browse files Browse the repository at this point in the history
Add support for Docker build args
  • Loading branch information
mr-smithers-excellent authored Jan 17, 2020
2 parents a9e30d9 + 3d24566 commit 74e4dc0
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 23 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@

Builds a Docker image and pushes it to the private registry of your choosing.

## Requirements
## Basic usage

* [GitHub Actions Beta](https://github.com/features/actions) program participation
* Run [checkout action](https://github.com/actions/checkout) before using this action
* Ensure you run the [checkout action](https://github.com/actions/checkout) before using this action
* Add the following to a workflow `.yml` file in the `/.github` directory of your repo
```yaml
steps:
- uses: actions/[email protected]

- uses: mr-smithers-excellent/docker-build-push@v1.0
- uses: mr-smithers-excellent/docker-build-push@v2
with:
image: repo/image
tag: latest
registry: registry-url.io
dockerfile: Dockerfile.ci
username: username
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
```
Expand All @@ -29,6 +29,7 @@ steps:
| tag | Docker image tag (see [Tagging the image with GitOps](#tagging-the-image-using-gitops)) | No |
| registry | Docker registry host | Yes |
| dockerfile | Location of Dockerfile (defaults to `Dockerfile`) | No |
| buildArgs | Docker build arguments in format `KEY=VALUE,KEY=VALUE` | No |
| username | Docker registry username | No |
| password | Docker registry password or token | No |

Expand All @@ -40,7 +41,7 @@ steps:
* Modify sample below and include in your workflow `.github/workflows/*.yml` file

```yaml
uses: mr-smithers-excellent/docker-build-push@v1.0
uses: mr-smithers-excellent/docker-build-push@v2
with:
image: docker-hub-repo/image-name
registry: docker.io
Expand All @@ -57,7 +58,7 @@ with:
* Ensure you set the username to `_json_key`

```yaml
uses: mr-smithers-excellent/docker-build-push@v1.0
uses: mr-smithers-excellent/docker-build-push@v2
with:
image: gcp-project/image-name
registry: gcr.io
Expand All @@ -73,7 +74,7 @@ with:
* Modify sample below and include in your workflow `.github/workflows/*.yml` file

```yaml
uses: mr-smithers-excellent/docker-build-push@v1.0
uses: mr-smithers-excellent/docker-build-push@v2
with:
image: image-name
registry: [aws-account-number].dkr.ecr.[region].amazonaws.com
Expand Down
6 changes: 5 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: 'Docker Build & Push'
description: 'Builds a Docker image and pushes to a private registry'
author: 'Sean Smith'
inputs:
image:
description: 'Name of the Docker image'
Expand All @@ -14,6 +15,9 @@ inputs:
description: 'Location of Dockerfile, if not Dockerfile in root directory'
required: false
default: 'Dockerfile'
buildArgs:
description: 'Docker build arguments in format KEY=VALUE,KEY=VALUE'
required: false
username:
description: 'Docker registry username'
required: false
Expand All @@ -28,4 +32,4 @@ runs:
main: 'dist/index.js'
branding:
icon: 'anchor'
color: 'black'
color: 'blue'
38 changes: 35 additions & 3 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,24 @@ const cp = __webpack_require__(129);
const core = __webpack_require__(470);
const fs = __webpack_require__(747);
const { context } = __webpack_require__(469);
const maxBufferSize = __webpack_require__(535);

const isGitHubTag = ref => ref && ref.includes('refs/tags/');

const isMasterBranch = ref => ref && ref === 'refs/heads/master';

const isNotMasterBranch = ref => ref && ref.includes('refs/heads/') && ref !== 'refs/heads/master';

const createBuildCommand = (dockerfile, imageName, buildArgs) => {
let buildCommandPrefix = `docker build -f ${dockerfile} -t ${imageName}`;
if (buildArgs) {
const argsSuffix = buildArgs.map(arg => `--build-arg ${arg}`).join(' ');
buildCommandPrefix = `${buildCommandPrefix} ${argsSuffix}`;
}

return `${buildCommandPrefix} .`;
};

const createTag = () => {
core.info('Creating Docker image tag...');
const { sha } = context;
Expand Down Expand Up @@ -566,15 +577,15 @@ const createTag = () => {
return dockerTag;
};

const build = imageName => {
const build = (imageName, buildArgs) => {
const dockerfile = core.getInput('dockerfile');

if (!fs.existsSync(dockerfile)) {
core.setFailed(`Dockerfile does not exist in location ${dockerfile}`);
}

core.info(`Building Docker image: ${imageName}`);
cp.execSync(`docker build -f ${dockerfile} -t ${imageName} .`);
cp.execSync(createBuildCommand(dockerfile, imageName, buildArgs), { maxBuffer: maxBufferSize });
};

const isEcr = registry => registry && registry.includes('amazonaws');
Expand Down Expand Up @@ -1570,17 +1581,28 @@ module.exports = require("child_process");
const core = __webpack_require__(470);
const docker = __webpack_require__(95);

// Convert buildArgs from String to Array, as GH Actions currently does not support Arrays
const processBuildArgsInput = buildArgsInput => {
let buildArgs = null;
if (buildArgsInput) {
buildArgs = buildArgsInput.split(',');
}

return buildArgs;
};

const run = () => {
try {
// Get GitHub Action inputs
const image = core.getInput('image', { required: true });
const registry = core.getInput('registry', { required: true });
const tag = core.getInput('tag') || docker.createTag();
const buildArgs = processBuildArgsInput(core.getInput('buildArgs'));

const imageName = `${registry}/${image}:${tag}`;

docker.login();
docker.build(imageName);
docker.build(imageName, buildArgs);
docker.push(imageName);

core.setOutput('imageFullName', imageName);
Expand Down Expand Up @@ -7347,6 +7369,16 @@ const factory = __webpack_require__(47);
module.exports = factory();


/***/ }),

/***/ 535:
/***/ (function(module) {

const maxBufferSize = 50 * 1024 * 1024;

module.exports = maxBufferSize;


/***/ }),

/***/ 536:
Expand Down
13 changes: 12 additions & 1 deletion src/docker-build-push.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
const core = require('@actions/core');
const docker = require('./docker');

// Convert buildArgs from String to Array, as GH Actions currently does not support Arrays
const processBuildArgsInput = buildArgsInput => {
let buildArgs = null;
if (buildArgsInput) {
buildArgs = buildArgsInput.split(',');
}

return buildArgs;
};

const run = () => {
try {
// Get GitHub Action inputs
const image = core.getInput('image', { required: true });
const registry = core.getInput('registry', { required: true });
const tag = core.getInput('tag') || docker.createTag();
const buildArgs = processBuildArgsInput(core.getInput('buildArgs'));

const imageName = `${registry}/${image}:${tag}`;

docker.login();
docker.build(imageName);
docker.build(imageName, buildArgs);
docker.push(imageName);

core.setOutput('imageFullName', imageName);
Expand Down
15 changes: 13 additions & 2 deletions src/docker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ const cp = require('child_process');
const core = require('@actions/core');
const fs = require('fs');
const { context } = require('@actions/github');
const maxBufferSize = require('../src/settings');

const isGitHubTag = ref => ref && ref.includes('refs/tags/');

const isMasterBranch = ref => ref && ref === 'refs/heads/master';

const isNotMasterBranch = ref => ref && ref.includes('refs/heads/') && ref !== 'refs/heads/master';

const createBuildCommand = (dockerfile, imageName, buildArgs) => {
let buildCommandPrefix = `docker build -f ${dockerfile} -t ${imageName}`;
if (buildArgs) {
const argsSuffix = buildArgs.map(arg => `--build-arg ${arg}`).join(' ');
buildCommandPrefix = `${buildCommandPrefix} ${argsSuffix}`;
}

return `${buildCommandPrefix} .`;
};

const createTag = () => {
core.info('Creating Docker image tag...');
const { sha } = context;
Expand Down Expand Up @@ -38,15 +49,15 @@ const createTag = () => {
return dockerTag;
};

const build = imageName => {
const build = (imageName, buildArgs) => {
const dockerfile = core.getInput('dockerfile');

if (!fs.existsSync(dockerfile)) {
core.setFailed(`Dockerfile does not exist in location ${dockerfile}`);
}

core.info(`Building Docker image: ${imageName}`);
cp.execSync(`docker build -f ${dockerfile} -t ${imageName} .`);
cp.execSync(createBuildCommand(dockerfile, imageName, buildArgs), { maxBuffer: maxBufferSize });
};

const isEcr = registry => registry && registry.includes('amazonaws');
Expand Down
3 changes: 3 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const maxBufferSize = 50 * 1024 * 1024;

module.exports = maxBufferSize;
57 changes: 50 additions & 7 deletions tests/docker-build-push.test.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,83 @@
jest.mock('@actions/core');
jest.mock('child_process');

const core = require('@actions/core');
const cp = require('child_process');
const docker = require('../src/docker');
const run = require('../src/docker-build-push');
const maxBufferSize = require('../src/settings');

beforeAll(() => {
docker.build = jest.fn();
docker.push = jest.fn();
});

const mockInputs = (image, registry, tag, buildArgs, dockerfile) => {
core.getInput = jest
.fn()
.mockReturnValueOnce(image)
.mockReturnValueOnce(registry)
.mockReturnValueOnce(tag)
.mockReturnValueOnce(buildArgs)
.mockReturnValueOnce(dockerfile);
};

describe('Create & push Docker image', () => {
test('Valid Docker inputs', () => {
const image = 'gcp-project/image';
const registry = 'gcr.io';
const tag = 'dev-1234567';
const buildArgs = '';
const dockerfile = 'Dockerfile';

docker.login = jest.fn();
docker.createTag = jest.fn().mockReturnValueOnce(tag);
mockInputs(image, registry, null, buildArgs, dockerfile);
core.setOutput = jest.fn().mockReturnValueOnce('imageFullName', `${registry}/${image}:${tag}`);
cp.execSync = jest.fn();

run();

expect(docker.createTag).toHaveBeenCalledTimes(1);
expect(core.getInput).toHaveBeenCalledTimes(5);
expect(core.setOutput).toHaveBeenCalledWith('imageFullName', `${registry}/${image}:${tag}`);
expect(cp.execSync).toHaveBeenCalledWith(`docker build -f ${dockerfile} -t ${registry}/${image}:${tag} .`, {
maxBuffer: maxBufferSize
});
});
});

describe('Create & push Docker image with build args', () => {
test('Valid Docker inputs with build args', () => {
const image = 'gcp-project/image';
const registry = 'gcr.io';
const tag = 'latest';
const buildArgs = 'VERSION=1.1.1,BUILD_DATE=2020-01-14';
const dockerfile = 'Dockerfile.custom';

docker.login = jest.fn();
docker.createTag = jest.fn().mockReturnValueOnce(tag);
core.getInput = jest
.fn()
.mockReturnValueOnce(image)
.mockReturnValueOnce(registry)
.mockReturnValueOnce(null);
mockInputs(image, registry, null, buildArgs, dockerfile);
core.setOutput = jest.fn().mockReturnValueOnce('imageFullName', `${registry}/${image}:${tag}`);
cp.execSync = jest.fn();

run();

expect(docker.createTag).toHaveBeenCalledTimes(1);
expect(core.getInput).toHaveBeenCalledTimes(3);
expect(core.getInput).toHaveBeenCalledTimes(5);
expect(core.setOutput).toHaveBeenCalledWith('imageFullName', `${registry}/${image}:${tag}`);
expect(cp.execSync).toHaveBeenCalledWith(
`docker build -f ${dockerfile} -t ${registry}/${image}:${tag} --build-arg VERSION=1.1.1 --build-arg BUILD_DATE=2020-01-14 .`,
{
maxBuffer: maxBufferSize
}
);
});
});

describe('Create Docker image causing an error', () => {
test('Docker login error', () => {
docker.createTag = jest.fn().mockRejectedValue('some-tag');
docker.build = jest.fn();
const error = 'Error: Cannot perform an interactive login from a non TTY device';
docker.login = jest.fn().mockImplementation(() => {
throw new Error(error);
Expand Down
21 changes: 20 additions & 1 deletion tests/docker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const core = require('@actions/core');
const cp = require('child_process');
const fs = require('fs');
const docker = require('../src/docker.js');
const maxBufferSize = require('../src/settings');

describe('Create Docker image tag from git ref', () => {
test('Create from tag push', () => {
Expand Down Expand Up @@ -101,7 +102,25 @@ describe('core and cp methods', () => {

docker.build(image);
expect(fs.existsSync).toHaveBeenCalledWith('Dockerfile');
expect(cp.execSync).toHaveBeenCalledWith(`docker build -f Dockerfile -t ${image} .`);
expect(cp.execSync).toHaveBeenCalledWith(`docker build -f Dockerfile -t ${image} .`, {
maxBuffer: maxBufferSize
});
});

test('Build with build args', () => {
core.getInput.mockReturnValue('Dockerfile');
fs.existsSync.mockReturnValueOnce(true);
const image = 'docker.io/this-project/that-image:latest';
const buildArgs = ['VERSION=latest', 'BUILD_DATE=2020-01-14'];

docker.build(image, buildArgs);
expect(fs.existsSync).toHaveBeenCalledWith('Dockerfile');
expect(cp.execSync).toHaveBeenCalledWith(
`docker build -f Dockerfile -t ${image} --build-arg VERSION=latest --build-arg BUILD_DATE=2020-01-14 .`,
{
maxBuffer: maxBufferSize
}
);
});
});

Expand Down

0 comments on commit 74e4dc0

Please sign in to comment.