Grammar mistakes. Spelling errors. Tabs versus spaces. Searching through shell history to find the right command to run your tests.
If you’ve spent time doing any of these things, then you’ve probably felt like your time could have been better spent fixing bugs and writing documentation. After all, we can write programs and scripts to check spelling errors and enforce code style for us. The key piece is automating it, and CI/CD is a great place to run that automation. By dedicating a small amount of time now to define some common code review drudgery as CI/CD jobs, you’ll lift the burden of those tasks off yourself and fellow team members every time a new change arrives in your repository — no matter the programming language.
In this post, we’ll explore CI/CD jobs that are both widely applicable and generally useful that they can fit into any repository.
Prose
We often think that contributing to a repository solely takes the form of programming, but commits are often both code and written language. Prose can be crucial to help collaborators understand difficult concepts and crystallize tribal knowledge in well-documented places.
Written content can appear in a variety of forms, such as:
- The well-known
README.md
file at the root of most repositories. - In a dedicated
docs/
or similar directory with multiple files. - Scattered throughout code in the form of inline comments.
With so many files to check and varying levels of writing ability among team members, automated checks to validate spelling and other simple rules are prime candidates for CI jobs. That automation is useful for nearly any repository. A wide selection of tools means that jobs can range from simple spell-checking to high-level style guidelines to maintain documentation consistency.
One tool that addresses spelling and style is Vale: a system for validating that text conforms to a configurable set of rules. Integrating Vale into a CI/CD pipeline to enforce writing guidelines for documentation is an effective way to offload otherwise-tedious editorial work to a consistent system.
The first step to enabling Vale for a repository is to generate a .vale.ini
file that defines the desired rules for Vale to use when it performs style checking. For example, place the following .vale.ini
at the root of a repository to leverage proselint as well as write-good (two projects that offer pre-defined style rules).
StylesPath = styles
MinAlertLevel = suggestion
Vocab = Base
Packages = proselint, write-good
[*]
BasedOnStyles = Vale, proselint, write-good
Next, Install the vale
executable. Vale is available for a variety of platforms, so choose the method native to your operating system. For example, use the command below to install Vale on OS X using homebrew:
brew install vale
Or, use this command to install Vale on Windows:
choco install vale
On Linux, you can install Vale with most package managers under the name vale.
Next, use the sync
subcommand, which reads the .vale.ini
file and populates the directory indicated in the StylesPath
setting (in our case, styles/
) with the desired styles noted under Packages
.
vale sync
Vale will download the necessary files to satisfy the configuration in .vale.ini
. Once complete, you should commit .vale.ini
as well as the styles
directory so that they’re available later for CI/CD jobs to use.
git add .vale.ini styles
git commit -m 'Add vale configuration'
To perform checks locally, run vale against any text file, such as a README
:
vale README.md
Vale will exit non-zero upon finding errors, while warnings and suggestions are often stylistic. Here’s a snippet of an example warning:
5:157 warning 'be automated' may be passive write-good.Passive
voice. Use active voice if you
can.
Programming-oriented syntax checkers seldom require additional configuration, but prose can often be subjective and may require more fine-tuning. For example, some technical terms may fail spell-checking:
51:22 error Did you really mean Vale.Spelling
'subcommand'?
If you need to accept particular words as part of your repository’s vocabulary, add one word per line to the file styles/Vocab/Base/accept.txt
and Vale will accept the indicated words as correctly spelled.
Integrating Vale into a CI/CD environment becomes an exercise in installing the vale executable and expressing a simple task that runs:
vale **/*.md
. . . or a similar wildcard glob to enumerate the desired set of targeted files. We’ll explore simple methods to load vale
into a testing environment near the end of this guide using a generalized pattern for executing scripted tasks.
Common code formatting
One of the most time-honored debates among programmers is tabs versus spaces. While the battle may rage between strangers, you and your team should have established formatting practices not only for indentation but for other syntax rules as well. For example, enforcing consistent line endings can alleviate noisy diffs between Linux and Windows editors.
Fortunately, the EditorConfig project establishes a single formatting method that most editors and IDEs honor. These settings can apply to nearly all source code regardless of language, making the .editorconfig
file an excellent choice to reduce noisy commits arising from style disagreements or misconfigured editors.
EditorConfig supports a wide range of options, including:
- Indentation style — whether with tabs or spaces — and indentation width for soft (space-based) indentation.
- charset to enforce valid (or invalid) characters in matched files.
- Options to catch unwanted trailing whitespace.
- Flexible matching to selectively apply rules to specific files.
The .editorconfig
file is typically used during writing inside an editor. Providing an .editorconfig
as part of a code repository can ensure that all authors use the same rules for code and prose (as long as contributors enable EditorConfig features in their chosen editor).
To use EditorConfig, create an .editorconfig
file at the root of your repository. Here’s an example configuration file:
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{json,yml}]
indent_style = space
indent_size = 2
This .editorconfig defines the following settings:
root = true
instructs any editors and checkers to stop searching upward in the filesystem hierarchy for any parent.editorconfig
files. Without this line, any.editorconfig
files in parent directories would apply. You should include a line with the root setting to apply settings consistently on all developer systems.- For all file types (
[*]
), use line feed characters to terminate lines and ensure that a new line exists at the end of all files. - For all JSON and YAML files (
[*.{json,yml}]
), enforce the use of spaces for indentation, and use two space characters for each indent level.
After committing the .editorconfig
file, all contributors should enable EditorConfig in their respective editors (instructions for a wide variety of programs is available on the EditorConfig home page). However, you may also define a CI job to ensure that its rules are consistently applied.
editorconfig-checker is a stand-alone application to check for consistency between an .editorconfig
and its repository’s files. To install it, download a compiled release artifact from the project’s GitHub releases page or use the go
command natively to fetch and compile the latest version.
go install github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@latest
Once installed, use the editorconfig-checker
command to check all files in the repository against your .editorconfig
file:
$ editorconfig-checker
README.md:
17: Wrong indentation type(spaces instead of tabs)
You and your team can refine formatting rules as you find a style that fits the desired tone for your documentation. Consult the EditorConfig home page for additional information about available settings.
EditorConfig and Vale are powerful, but we can take them further by providing a streamlined entry point for both CI jobs and regular users.
Helper scripts
Easily accessible scripts can help reduce friction for new users and regular contributors. New users can rely on a reliable interface for shared tasks, and returning committers can save effort by codifying frequently-used commands. In either case, providing a consistent mechanism is a great boon to any code repository.
The most important decision is choosing tools and commands that are accessible. There are numerous options to choose from: make
is a nearly ubiquitous tool, whereas utilities like just
offer more modern features at the cost of ensuring its presence on all systems. This tradeoff can vary between teams, so for these examples, we’ll use make
because it’s nearly always already installed.
One of the most useful first-run experiences with a repository is to glean a list of possible commands at a glance. A default Makefile
target for help
makes this easily discoverable:
.DEFAULT_GOAL := help
.PHONY: help
help: ## Display this help text
@awk 'BEGIN { FS = ":.*?## "; printf "Available targets:\n"; }; \
/^[a-zA-Z\-_0-9]+:/ { printf "\t\033[36m%-20s\033[0m %s\n", $$1, $$2 }' \
$(MAKEFILE_LIST)
- .DEFAULT_GOAL means that running the singular
make
command will run thehelp
target by default. - .PHONY indicates that “help” is the name of our target, but not the name of a file.
- The awk command will parse the
Makefile
with logic to present its contents in a clear way. - $(MAKEFILE_LIST) is a variable that contains all
Makefiles
thatmake
is aware of.
Running a simple make
at the root of an example repository returns output similar to the following:
Available targets:
help Display this help text.
test Run the test suite
Any new arrivals to the codebase can proceed to get started quickly with pre-written scripts or make
targets that they learn from make help
.
Another helpful approach is to wrap other tasks within sandboxed environments so that running them becomes portable and alleviates the need to set up local installations for command-line tools. As an added benefit, it offers a similar execution environment inside CI/CD.
For example, consider the previous section’s use of the editorconfig-checker
command. If you’d like to perform this check within CI/CD, installation is a necessary prerequisite to running the command. However, you may alternatively wrap the check within a docker
command which provides a common installation path and consistent behavior. This Makefile
target runs a docker
command that mounts the repository into the container and performs EditorConfig checks.
.PHONY: editorconfig
editorconfig: ## Perform editorconfig checks
@docker run --rm --volume $$PWD:/check mstruebing/editorconfig-checker
Performing EditorConfig tests is a matter of running:
make editorconfig
Similarly, you can wrap the call to vale
within a container as well. This Makefile
target will perform style checks for files in the docs/
directory:
.PHONY: vale
vale: ## Run vale style checks
@docker run --rm -v $$PWD/styles:/styles \
--rm -v $$PWD/:/docs \
-w /docs jdkato/vale docs
Wrapping helper scripts inside of docker
means that, instead of a long list of repository prerequisites such as editorconfig-checker
, vale
, etc., users just need a functional installation of Docker to run an assortment of different commands. Additionally, the double-commented documentation string beginning with ##
will display all shortcuts when using make help
to make them easy to find.
General guidance for specific languages
We’ve taken a look at three different approaches that are useful for nearly any type of language repository, but some languages offer common strategies that may be useful if you and your team happen to be using them:
- Commands like
terraform
andgo
provide anfmt
subcommand that unifies code formatting. Whenever possible, enforcing formatting standards with jobs that call commands likego
fmt
can greatly reduce churn in codebases due to changes in style or other inconsistencies. - Some languages like Python natively support sandbox mechanisms like venv which you can use in place of Docker.
- If your team uses one editor consistently, consider committing to a common repository editor configuration. For example, the
.vscode
directory can store common settings for a specific codebase. Even venerable editors like Emacs support this practice with the.dir-locals.el
file.
Conclusion
We hope that this article has been a helpful exploration of some of the more generally applicable types of CI/CD jobs that you might use in your projects. When choosing which shortcuts and scripts to integrate into your repository, remember the compounding effects of automation: when you share ways to save engineering time, everybody benefits — often repeatedly, particularly when automation gets used in CI/CD environments.