chored is a utility for handling repetitive chores and files.

There are many repositories. Lots of them have similar (tedious!) things they need to do, which aren’t particular to that repository:

  • building
  • linting
  • testing
  • generation of configuration for tools:
    • CI configuration
    • build settings
    • all sorts if common or shared files
  • release management
  • documentation generation
  • pull request automation
  • updating dependencies

chored allows you to reuse solutions to these problems, and any others you can think of. It’s minimally intrusive and lightweight (just add one small wrapper script to your repo).

And even when tasks aren’t exactly the same, chored facilitates code reuse as easily as importing a URL, but as powerful as real libraries in a real programming language (because it is both of those things).

“Like Github actions?”

It allows much better sharing of functionality (without the vendor lock-in and awkward abstractions), but it just executes code - you can run it on your machine or within CI, ideally both! To help with this is also has builtin functionality for generating github workflow files to run chores in CI.

“Oh, like a rake task. Or an NPM task. Or [plenty more task runners]”

Kind of! I consider those heavyweight though, because they bring in a dependency manager with its own configuration and stateful lifecycle. They’re also only about tasks, while chored expects you’ll also use it to generate repetitive files.

The closest project I know of is probably projen, except projen’s focus is on extensive file generation for specific project types, while chored has less extensive project types and more focus on the task system. chored is also simpler because it relies on deno and Typescript, while projen supports multiple languages.

Lightweight, stateless dependency management:

Basically, I want to remove this:

$ yarn run mytask
# (it doesn't work)
$ yarn install
# (it still doesn't work)
$ rm -rf node_modules && yarn insall
$ yarn run mytask
# (it works :facepalm:)

And this:

$ rake mytask
rbenv: version `2.6.5' is not installed

# (take a deep breath)
$ rbenv install `cat .ruby-version`
$ rbenv exec gem install bundler
$ rbenv exec bundle install
$ rbenv exec bundle exec rake mytask

(this can be written more tersely, but that often makes it more confusing, not less)

With most package managers, your system (the state of files on your machine) is typically out of sync with the desired state. You need to yarn install etc to bring them in sync, after every change.

With deno, your system can’t be inconsistent, it may just have some uncached imports.

  • anyone running a given module will use the exact same dependencies, regardless of the state of your machine (it’ll just be slower from an empty cache)

Lightweight abstraction:

At its heart, a chore definition (choredef) is simply a function, accepting a single arguments object. This can be whatever shape you need, and you can make fields optional or mandatory in the usual typescript way.

./chored collects your commandline arguments into an object, and passes this to your chosen chore. Here’s what a trivial greet chore looks like, it accepts no options and prints a simple message:

// choredefs/greet.ts
export default function(opts: {}) {
   console.log("Hello, world!")
}

Types!

Chored uses typescript throughout. When invoking a chore, it typechecks the arguments you provided on the commandline with the arguments accepted by that chore. If you’re missing something, or pass in the wrong type, or pass in an option that isn’t recognised, you’ll get an error. Nice.

Of course, the benefit of types extends to writing your own chores, reusing third party modules, etc.

Files!

Sometimes you want to run a thing right now. Other times, you want to generate a file to tell your CI system to run a thing at some other time. Or maybe you just want to generate some super standard boilerplate files across many repos, like compiler / linter config, LICENSE files, release scripts, etc.

You could even build a kubernetes abstraction within chored - you design the input types and then implement conversions to YAML files that you either keep on-disk or send directly into kubectl apply.

Chored has first-class support for generating files - in fact, the chored script in your repository is managed via this file rendering chore, so updating that chore will also bring in any changes to the chored script. How meta!

Background - how I got here:

Chored is not my first attempt to solve this problem. Previously, I built dhall-render and dhall-ci, which were based on an experiment I pursued in my day job. These together aimed to solve the problem of generating files, with many of the same goals.

However even though I’m quite pleased with the results, there were some downsides that come from only generating files, and not being able to execute arbitrary logic.

  • you end up generating a lot of scripts, which clutters a repository (ususally written in bash or dependency-free ruby) To avoid the clutter I experimented with generating Makefiles, which is clearly a red flag considering how much I hate automake!
  • you end up pushing logic through awkwardly-shaped holes
    • where in a real language you might do some logic based on $GITHB_HEAD_REF or $GITHUB_REF to detect the branch name across both push and pull_request github events, when generating files you have to serialize that as inline bash expressions, and those are not pretty, and definitely not readable.
  • generating files can help with declarative systems like github actions, but there’s tremendous value in deemphasizing this kind of vendor-specific solution. chored encourages me to write code that also works outside of github actions, because it’s so much more convenient to use and test.

More recently, I also encountered projen. While I find it too cumbersome to adopt wholesale due to its support for multiple languages, I realised that I really wanted some of the benefits afforded by its implementation - notably the task system and the excellent support for Typescript in most editors.