Have a look at our new Handbook: "Transitioning from Monolith to Microservices"!  Discover →

    6 Dec 2022 · Software Engineering

    3 CI/CD Jobs for Any Repository

    10 min read

    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.


    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

    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

    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
    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
            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 }' \
    • .DEFAULT_GOAL means that running the singular make command will run the help 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 that make 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 and go provide an fmt subcommand that unifies code formatting. Whenever possible, enforcing formatting standards with jobs that call commands like go 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.


    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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Writen by:
    Tyler is a freelance consultant and technical writer with experience ranging from racking servers in cold aisles to frontend development. When he's not building infrastructure or writing functional code full-time, he can usually be found doing the same thing in his homelab.
    Avatar for Tyler Langlois
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.