Linting Markdown files in GitLab CIΒΆ
A while ago I was looking for a way to lint Markdown files and came across markdownlint in my search.
There are two CLI tools for it, markdownlint-cli and markdownlint-cli2.
Based on the comparison and rationale by one of the authors I gave markdownlint-cli2 a try first.
It is node-based and has lots of supported ways of invocation, such as a pre-commit hook, works well with the markdownlint vscode extension, and can be run as a container (including an image containing custom rules).
We are using GitLab which has code quality scanning support. You can import code quality results from a CI/CD job. Depending on the tier you have, you can see code quality findings in various places in the merge request UI helping the merge request author and reviewer(s). The code quality findings are provided as a JSON file during a CI/CD job. The report format is based on the CodeClimate report specification.
There was unfortunately no support for this yet.
At first, I wrote a custom formatter that lived in our private repo.
But instead of leaving it buried in a private repo, I wanted to make it available to all our own repositories and the markdownlint-cli2 community.
I checked if there was appetite for integrating this into markdownlint-cli2 and ended up contributing a code quality formatter.
It is published as an npm package: https://www.npmjs.com/package/markdownlint-cli2-formatter-codequality.
Mentioned in the GitLab documentation
As I am getting the latest URLs to the GitLab documentation I noticed that the code quality formatter is now also mentioned in it
One thing that is a bit tricky is to have a consistent developer experience.
markdownlint-cli2 is invoked locally in the IDE via the extension, at commit-time as a pre-commit hook, and in CI.
In our case, we run this in non-JavaScript-based projects meaning that we can't just add the formatter as a development dependency.
In addition, locally, we want to use the pretty formatter, whereas in CI we want to use the code quality formatter (or both for job logs).
The way this can work is by having a dedicated config for CI.
This can be used on its own just to define the additional formatter, or extend the markdownlint-cli2 root config.
In the latter case, this is how it would look:
# https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2yaml
ignores:
- .gitlab/merge_request_templates/Default.md
gitignore: true
# https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2yaml
# additional configuration to the config in the root of the project
# this additional config is used during the markdownlint CI job
outputFormatters:
- - markdownlint-cli2-formatter-default
- - markdownlint-cli2-formatter-codequality
Now, you can run markdownlint-cli2 in GitLab CI/CD in a job as follows:
markdownlint:
stage: lint
image:
name: davidanson/markdownlint-cli2:<version> # (1)
# overwrite default entrypoint (which is a call to markdownlint-cli2)
entrypoint: ['']
script:
# use the config file that is stored outside the root
- markdownlint-cli2 --config .gitlab/.markdownlint-cli2.yaml "**/*.md"
artifacts:
when: always
reports:
codequality: markdownlint-cli2-codequality.json
- Avoid surprises and pin the version. Then use Renovate to get automated dependency updates.
One tricky thing in terms of consistent developer experience is using custom rules.
There are some helpful rules provided in the davidanson/markdownlint-cli2-rules image.
While it is easy to switch to that image in the CI job, it is tricky to get the same experience locally (again, if you are dealing with a non-JavaScript project).
The vscode extension does not support custom rules.
Due to that, I ended up specifying the custom rule in the GitLab-specific configuration file.
It's a bit annoying to have your pre-commit checks pass locally only to find out that the linting job in the pipeline fails.
As I was writing the post about updating additional_dependencies in pre-commit hooks using Renovate I realized that pre-commit also supports node dependencies (markdownlint-cli2 is a node tool).
So we can apply the same concept as outlined above to our pre-commit configuration as well, leaving only the editor without custom rules support.
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: <version>
hooks:
- id: markdownlint-cli2
language: node
args: [--config, pre-commit.markdownlint-cli2.yaml]
additional_dependencies:
- markdownlint-rule-max-one-sentence-per-line@<version>
- mkdocs-material-linter@<version>
# https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2yaml
# additional configuration to the config in the root of the project
# that is used when running markdownlint-cli2 via the pre-commit hook
customRules:
# Enforce one sentence per line
# semantic line breaks (https://sembr.org/)
# See: https://github.com/DavidAnson/markdownlint/pull/719
- markdownlint-rule-max-one-sentence-per-line
What if I am using GitHub Actions?
You can apply the same concept as shown in this post.
See this reusable markdownlint workflow that creates the dedicated config file on the fly.
You might want to use the markdownlint-cli2-action in your CI job which already contains a formatter that contains annotations.
!!! note "Updates to this blog post
**27.02.2026:** Added details about using additional dependencies in `pre-commit` hooks.