- The problem
- Setup
- The general solution
- Husky + Lint Staged ("do-it-yourself" method)
- Lefthook ("all-in-one" method) 🥊
The problem
When developing in a dockerized environment (for example with docker compose) you often find yourself in the following situtation:
- Several containers are running at once.
- Each container has it's own linter (eslint, tslint, pylint, …) with it's own settings.
- However..you
git commit
changes from your host/dev machine which doesn't have any of the linters installed.
In this article I'll show a quick solution so you can run every linter in the right container and always keep your code clean and pretty.
We'll do that with two methods:
- 🐶Husky +🚫💩 Lint Staged (the "do-it-yourself" method)
- 🥊Lefthook (the "all-in-one" method) (my favorite ⭐)
The final project can be found here: docker-linters-example
Setup
To demonstrate the problem I've created two sample containers:
- container1 (containers/container1 ) - NodeJS project using ESlint linter.
- container2 (containers/container2) - Python project with Pylint linter.
The docker-compose.yml
file looks something like this:
version: "3.4"
services:
container1:
image: container1
container_name: container1
build: containers/container1
volumes:
- ./containers/container1/src:/app/src
container2:
image: container2
container_name: container2
build: containers/container2
volumes:
- ./containers/container2/src:/app/src
The general solution
In order to run each linter in it's container will do the following:
- Install a git hooks framework on the host (your dev machine) so it will install a "pre-commit" hook that runs whenever
git commit
is called. - Configure this hook to check if the staged files match any of the containers source files (for example
container/container/src/**/*.js
for all the javascript files in container1 ) - Run the linter command with
docker run
. - Additionally: mount the source directories as volumes to automatically fix files (for example with
eslint --fix
).
For example for "container 1" (NodeJS) the command will be:
docker run --rm -v container1/src:/app/src container1 sh -c "yarn lint --fix <GIT_STAGED_FILES>"
And for container 2 (Python) it will be:
docker run --rm -v container2/src:/app/src container2 sh -c "pylint <GIT_STAGED_FILES>"
Husky + Lint Staged ("do-it-yourself" method)
We'll Start by installing the required libraries:
npm install --no-save husky lint-staged
This will install two libraries:
- Husky - Will add a pre-commit hook to our .git/hooks folder.
- Lint staged - will run a linter according to matched patterns on the staged files.
(Why --no-save
? Because we want the git hooks on the host and not include it as part of the dev/production code)
Then we'll create a .huskyrc
configuration file:
{
"hooks": {
"pre-commit": "lint-staged -r -p false"
}
}
This will run "lint-staged" whenever we git commit files with the following options:
-r
- pass the file as relative to the root of the project instead of absolute ones. This means that instead of/home/user/docker-linters-example/containers/container1/src/file1.js
the file path passed to the command will be:containers/containers1/src/file1.js
(you'll see why soon).-p false
- run commands sequentially (optional: to prevent some issues)
Now for the second part: matching files to linters.
For that we'll create a .lintstagedrc.js
configuration file:
const path = require("path");
module.exports = {
"containers/container1/src/**/*.js": (absolutePaths) => {
const cwd = process.cwd();
const relativePaths = absolutePaths
.map((file) =>
path.relative(cwd, file).replace("containers/container1/", "")
)
.join(" ");
return `docker run --rm -v ${cwd}/containers/container1/src:/app/src container1 sh -c \"yarn lint --fix ${relativePaths}\"`;
},
"containers/container2/src/**/*.py": (absolutePaths) => {
const cwd = process.cwd();
const relativePaths = absolutePaths
.map((file) =>
path.relative(cwd, file).replace("containers/container2/", "")
)
.join(" ");
return `docker run --rm -v ${cwd}/containers/container2/src:/app/src container2 sh -c \"pylint ${relativePaths}\"`;
},
};
This configuration will match the staged files by using patterns to find to which container they belong. For pattern match will do the following (for example lines 4–11 for container1 files) :
- Tranform the path of every file to match the path of the container, for example
containers/container1/src/file1.js
will becomesrc/file1.js
. that is because container1's working directory is already incontainers/container1
so we need to trim every path. - Execute a docker run command with all the files in it, for example:
docker run - rm -v ${cwd}/containers/container1/src:/app/src container1 sh -c \"yarn lint - fix ${relativePaths}\"
Let's break the parts:
--rm
- terminate the container after running.-v
- mount the source volumes from host to container to reflect changes.sh -c yarn lint --fix ${relativePaths}
(the command) run the linter (in this case: ESlint)
So for container1 the final command will look like:
docker run --rm -v /home/user/docker-linters-example/containers/container1/src:/app/src container 1 sh -c "yarn lint --fix src/file1.js src/file2.js
Voila, we now get our files linted and fixed in every commit:
husky > pre-commit (node v12.18.1)
[ 'containers/container2/src/file1.py' ]
✔ Preparing...
⚠ Running tasks...
❯ Running tasks for containers/container1/src/**/*.js
✖ docker run --rm -v /home/user/docker-linters-example/containers/container1/src:/app/src container1 sh -c "yarn lint --fix src/file1.js src/file2.js" [FAILED]
❯ Running tasks for containers/container2/src/**/*.py
✖ docker run --rm -v /home/user/docker-linters-example/containers/container2/src:/app/src container2 sh -c "pylint src/file1.py" [FAILED]
↓ Skipped because of errors from tasks. [SKIPPED]
✔ Reverting to original state because of errors...
✔ Cleaning up...
✖ docker run --rm -v /home/user/docker-linters-example/containers/container1/src:/app/src container1 sh -c "yarn lint --fix src/file1.js src/file2.js":
error Command failed with exit code 1.
yarn run v1.22.4
$ eslint --fix src/file1.js src/file2.js
/app/src/file1.js
1:1 warning Unexpected console statement no-console
3:7 error 'x' is assigned a value but never used no-unused-vars
/app/src/file2.js
1:1 warning Unexpected console statement no-console
3:7 error 'x' is assigned a value but never used no-unused-vars
✖ 4 problems (2 errors, 2 warnings)
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
✖ docker run --rm -v /home/user/docker-linters-example/containers/container2/src:/app/src container2 sh -c "pylint src/file1.py":
************* Module file1
src/file1.py:1:0: C0114: Missing module docstring (missing-module-docstring)
-----------------------------------
Your code has been rated at 0.00/10
husky > pre-commit hook failed (add --no-verify to bypass)
Lefthook ("all-in-one" method) 🥊
This method is my favorite because Lefthook includes the features of both Husky and Lint Staged. In other words it has both a git hook manager and can track the staged files from git which saves a lot of time and configuration.
We'll start again by installing the required libraries:
npm install --no-save @arkweid/lefthook
Then we'll configure lefthook by adding a lefthook.yml
file:
pre-commit:
parallel: true
commands:
lint_container1:
root: "containers/container1/"
glob: "*.js"
run: docker run --rm -v ${PWD}/src:/app/src container1 sh -c "yarn lint --fix {staged_files}" && git add {staged_files}
lint_container2:
root: "containers/container2/"
glob: "*.py"
run: docker run --rm -v ${PWD}/src:/app/src container2 sh -c "pylint {staged_files}" && git add {staged_files}
This does the same as the Husky+Lint Staged method above:
- Matches each container files to their correct linter configuration.
- Automatically transforms the paths from the host ones to the container ones (by using the
root
property) - runs
docker run
with the correct linter.
Result:
Lefthook v0.7.2
RUNNING HOOKS GROUP: pre-commit
EXECUTE > lint_container2
************* Module file1
src/file1.py:1:0: C0114: Missing module docstring (missing-module-docstring)
-----------------------------------
Your code has been rated at 0.00/10
EXECUTE > lint_container1
yarn run v1.22.4
$ eslint --fix ./src/file1.js ./src/file2.js
/app/src/file1.js
1:1 warning Unexpected console statement no-console
3:7 error 'x' is assigned a value but never used no-unused-vars
/app/src/file2.js
1:1 warning Unexpected console statement no-console
3:7 error 'x' is assigned a value but never used no-unused-vars
✖ 4 problems (2 errors, 2 warnings)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
SUMMARY: (done in 3.31 seconds)
🥊 lint_container2
🥊 lint_container1