Trunk Check’s superpower is that it seamlessly leverages dozens of existing tools. Virtually every tool inside of Check is actually an external tool wrapped in a plugin. A plugin is a snippet of configuration code that lets Check automatically download, configure, and run other tools. In this tutorial I’ll walk you through creating a simple linter in Javascript, then move the tool and its config to a full GitHub project that can be versioned and auto installed. Let’s get started!
Note: though this tutorial uses Javascript for the tool, it is applicable to any language.
In Trunk Check’s vocabulary, a plugin is just a snippet of configuration code. A plugin can include definitions of how to download a tool, how to run it with Trunk Check, and how to interpret the tool’s output into something that Trunk Check can understand.
Trunk Check has several types of plugins, named based on what they do. A linter is a plugin which checks some attribute of files and reports the problem. Linters (generally) do not modify files. A formatter is a linter which does modify the files to adhere to some rule (ex: no semicolons in JS code, tabs vs. spaces, etc.).
For this tutorial we will make a tiny NodeJS script that checks if a filename contains the word ‘foo’. Since it only checks but does not modify the code, this script will be a linter.
Create a simple linter tool
First let’s create a simple script that checks if the filename passed in contains the word foo
. Open a text editor and paste in this script. You’ll note that the script is getting the filename from process.argv
. Inputs to the script are regular standard command line arguments.
1import process from "process"23// throw errors on missing info4if(process.argv.length < 3) throw new Error("missing filename")56const filename = process.argv[2]7if(filename.match(/foo/)) {8 // on error, print to std and *also* exit with 19 console.log(`Warning. file has 'foo' in the10 name '${filename}'`)11 process.exit(1)12}13// on pass, print nothing and exit with 014process.exit(0)
Now let’s test this script on a file.
1node foofinder.js my_file.txt2node foofinder.js my_foo_file.txt
We don’t need to use real filenames to test because this particular script only checks the passed in name, not the actual file on disk. The first line prints out nothing because there is no foo
in the filename. The second line above does print output indicating it found a foo
.
Using a Script as a Check Linter
All linter plugins must produce output in one of a few formats. The simplest is called pass_fail
. A pass fail plugin either returns exit code 0 for success or 1 for failure and also prints some output about the problem to STDOUT. The input for our script above is a single filename to check. If no filename is passed in then the script throws an error. Since this is a NodeJS script, any uncaught error will automatically exit with 1 and an error message. Throwing errors on input and config failures is very important for debugging. Whenever a tool fails Trunk Check will automatically save the error output to a log file so that you can investigate why it failed.
Note: We used the output format pass_fail
for this script, but for more complex tools other formats are supported.
Now that we have our tool as a script, we can integrate it into a project as a Check plugin. Let’s go to another repo where we have already set up Trunk Check. This means we already ran trunk init
on it and it has a .trunk/
directory.
Open the trunk.yaml
file for editing and put this definition of foofinder
in the lint definitions section.
1lint:2 ...3 definitions:4 ...5 - name: foofinder6 files: [ALL]7 commands:8 - name: lint9 output: pass_fail10 success_codes: [0, 1]11 runtime: node12 run: node ${workspace}/../trunk_plugin_tutorial/findfoo.js ${target}13 read_output_from: stdout
A plugin config defines both where the tool is and how to run it. In the definition above the config tells Trunk Check the name of the tool (foofinder
), which file types it can be run on (ALL
), what output format it uses (pass_fail
), the exit codes it uses (which are always [0, 1]
) . Finally it specifies which runtime to use (node
), and the command to run it. The variable workspace
represents the current workspace where we are running trunk check. The variable target
represents the filename that will be passed to findfoo.js. The read_output_from
section indicates that Check will get the tool's output from stdout (standard output)
Now enable the foofinder
tool by typing trunk check enable foofinder
on the command line in your project repo. This will modify the trunk.yaml
to formally enable the plugin.
Now create an empty file in this repo that has the word foo in it. From most command lines you can run touch src/foo.ts
to create an empty file.
Now run trunk check
and you should see something like this:
$ trunk check
1$ trunk check2Checking 100% [=============================>] 31/31 2.3s3 ISSUES4src/foo.ts:0:05 0:0 high Warning. file has 'foo' in the name 'src/foo.ts'6foofinder/error
Success! We have a working linter! If you had any trouble try running trunk check –verbose
to get more detail about what is happening under the hood.
Move the tool to a local repo
Now that we have the above tool working we want to put the tool under version control and shart it with our team.
Note: Trunk Check has an existing repo of plugins for users to share tools and integrations for one another. If you think your tool plugin would be good for the wider world, please feel free to contribute it there as well.
Let’s start by putting it in a local git repo on your machine and add this plugin.yaml to the root of the repo for the tool.
1version: 0.12lint:3 definitions:4 - name: foofinder5 files: [ALL]6 runtime: node7 commands:8 - name: lint9 output: pass_fail10 success_codes: [0,1]11 run: node ${plugin}/findfoo.js ${target}12 read_output_from: stdout
This is the same tool definition as before, but it now uses the plugin
variable to show where the script file is.
Back in the project repo we can add a reference to the plugin (after removing the other changes we made if you still have them) using the command below. This will connect your local plugin repo with your local project repo.
1trunk plugins add ../trunk_plugin_tutorial --id=foofinder
Now the trunk.yaml
in your main project will have a reference to foofinder
like this:
1plugins:2 sources:3 - id: foofinder4 local: ../trunk_plugin_tutorial
You will also need to enable the plugin in the enabled section of the yaml or with the command line:
1trunk check enable foofinder
If all goes well, you can add a file with foo in the name and see it printed out, just like before.
Move the tool to a github repo
Now that we know the plugin is working, let’s push the plugin repo to GitHub and reference it using a URL so everyone can use it, like this:
1cd trunk_plugin_tutorial2git push3cd ../my_project4trunk plugins add5https://github.com/joshmarinacci/trunk_plugin_tutorial.git --id=foofinder
This will look for the latest release of your code. If you haven’t done a release, then you can create a release with the GitHub interface, or else reference a full commit sha or a git tag. Tags are generally the easiest. To create a tag, in your tool repo run:
1git tag v0.1.02git push origin v0.1.0
Back in your project repo run:
1trunk plugins add https://github.com/joshmarinacci/trunk_plugin_tutorial v0.1.0 --id=foofinder
Your project’s plugins section of the .trunk/trunk.yaml
will now look like this:
1plugins:2 sources:3 - id: foofinder4 ref: v0.1.05 uri: https://github.com/joshmarinacci/trunk_plugin_tutorial6 ...
Again, make sure foofinder
is enabled with:
1trunk check enable foofinder
Now when you run trunk check it will automatically download and cache foofinder
from GitHub. Huzzah!
Conclusion
In this post we’ve seen how to integrate a custom script as a Linter in Trunk Check. Then how to publish the linter from a local directory or a GitHub repository. Depending on your needs you might want to keep it local or make it completely public. If you think your plugin would benefit the wider open source community please consider submitting it to Trunk’s public plugins repository.
Now that your linter script is working, you can further customize it, such as:
Only running the linter on certain file types.
Use one of the outer output types besides pass/fail, such as regex.
Use other exit codes and environment variables.
Make your plugin be a formatter instead of a linter.