Chored
Chores, sorted.
chored lets you share chore definitions and code between many repositories. Built on deno with Typescript, it requires zero-installation and has lightweight, on-demand dependencies.
You can use it for anything, but itâs built primarily to solve the problem of CI/CD reuse between repositories.
Chored is still in early development, it may change significantly in the future
Chored aims to solve problems withâŚ
Repetitive CI definitions: Want to run the same (or similar) Github Workflows in many different repositories? Write a function once to generate a whole workflow (or maybe just a few steps), and call it from each repository.
Repetitive boilerplate between repositories: CI definitions are just a special case of files which might need some tweaks for a given project, but are pretty similar and could easily be shared (with some parameterisation). Configuration for compilers, build tools, publication scripts, etc.
Reusable development chores: many languages have a kind of task runner (rake, npm scripts, etc). They usually have ways to share common tasks, but theyâre usually heavyweight (publish a package). With chored, sharing a chore is as simple as writing its URL in an import statement.
Ad-hoc shell scripts: Iâve written plenty of project-specific shell scripts because bash is ubiquitous. But with chored, youâve got a modern typescript runtime available and ready.
CI lock-in: most forms of re-use (e.g. Githb Actions) are tied to a vendor. If you have a better form of abstraction (like a real programming language), you donât need to invest in vendor-specific form of reuse (defining and referencing Github Actions).
CI spaghetti-code: we used to struggle with âworks on my machineâ, now itâs often âonly works in CIâ. When CI steps are a thin wrapper around chores, we have the ability to run (and test!) that same code on developer machines. It wonât magically make your environment consistent between dev and CI, but it can give you the tooling to manage that better.
Getting started
curl -sSL https://deno.land/x/chored/install.sh | bashThis will create a skeleton choredefs/render.ts and then run it to generate the bare minimum files:
choredwrapper scrpt.gitattributesmetadata
If you donât already have deno available, it will be downloaded into ~/.cache/chored
Running a third-party chore
./chored https://deno.land/x/chored/choredefs/greet.ts --string name anonymousCreating your own chores
Chores are simply functions that accept a single options argument. Export a main function in a file under choredefs/ and boom - youâve made a chore with that filename:
// choredefs/greet.ts
export default function(opts: { name: string, home: string }) {
console.log(`Hello, ${opts.name}. I see your home directory is ${opts.home}`)
}$ ./chored greet --name=Tim --env home=HOMEYou can put multiple functions in the one file, by passing the function name after the chore name (e.g. ./chored greet dev would run the dev function in choredefs/greet.ts).
--string foo bar and --bool someFeature true also have shorthands, e.g. --foo=bar and --someFeature
Chore aliases
Instead of running a third-party chore directly, itâs usually more convenient to add an alias. e.g. create choredefs/greet.ts containing:
export * from 'https://raw.githubusercontent.com/timbertson/chored/main/choredefs/greet.ts'or for deno.land modules:
export * from 'https://deno.land/x/chored/choredefs/greet.ts'You can also import the third party chore but expose a wrapper function, which passes options specific to your project, for example.
import { default as greet } from 'https://raw.githubusercontent.com/timbertson/chored/main/choredefs/greet.ts'
// I don't know why you want to hardcode the user's name, but that's examples for you...
export default function(opts: {}) {
return greet({ name: "tim" })
}Dependency managament
Since itâs built on deno, remote dependencies are simply typescript imports, and theyâre fetched only on first use - if you donât run a module, its dependencies donât need to be downloaded.
In almost all cases, you should lock a dependency to a concrete version - a release tag or commit, rather than a branch name. To help with this, the builtin deps bump action automatically pins github imports to a commit, and deno.land imports to a version.
To control the specifics, you can place a branch (or wildcard tag) in the URL anchor for github, and wildcard version for deno.land.
So you can write:
import { greet } from 'https://raw.githubusercontent.com/timbertson/chored/main/choredefs/greet.ts#main'or:
import { greet } from 'https://deno.land/x/chored/choredefs/greet.ts'..and then run ./chored deps bump. Thatâll place the latest git revision (for github) or release tag (for deno.land) main into the URL. The #main will remain, so that it knows which branch to track on future runs.
You can also use a wildcard for tags, e.g. #v1.2.* for a crude emulation of semver.
Using development dependencies:
Firstly, for chored itself you can replace any deno.land imports with raw.githubusercontent. This lets you pin to a specific commit, and you can easily use your own fork. If you update the import in your render chore and then run ./chored/render, the ./chored script itself will now main.ts from the new location.
Secondly, you can use local (on-disk) versions of deno.land an raw.githubusercontent.com dependencies. Just run deno --local; it will print a setup message if you havenât defined the appropriate chore locally.
Design tradeoffs in chored:
Pros:
- Mainstream language: Typescript, maybe youâve hard of it?
- lightweight code reuse: It doesnât get easier than âimport code from the internetâ
- Lightweight chore definitions: A chore is just a function, so theyâre easy to write and compose
- Static typing: Missed out some options? Importing an incompatible version of a library? Get a type error upfront instead of an obscure runtime bug after youâve published half a relese.
Cons:
- Size:
denoisnât tiny; itâs an 80mb binary (when decompressed). Itâs reused across invocations of course, but if you run it on ephemeral VMs that wonât help you. - Niche: Typescript is incredibly mainstream, but most people use it with
node.denoâs flavour has some pecliarities that may make tooling, code reuse and IDE integration harder. - Startup time: By design, every chore invocation is typechecked. This isnât instantaneous, and means that things like tab completion may require tradeoffs to keep them snappy.
Comparisons:
Chored isnât the first project of itâs kind, heck itâs the second project that Iâve personally authored to try and tame the problems with repetitive CI/CD.
Projen
Thereâs a lot of similarities with projen, and chored takes quite a few cues from projen. The main differences:
- projen focuses heavly on pre-canned project types. Chored is (currently) less opinionated and requires some more curation
- projenâs task system requires a bit more effort and configuration, but suppots arbitrary programs instead of just typesceipt
- typescript is spported in projen, but it requires nonstandard setup
Github actions
github actions are quite nice, but they have some serious usability issues.
- you canât run actions locally
- youâre locked into githubâs ecosystem
- reuse is low: actions are reusable, but if they donât do exactly what you need youâre out of luck, youâll need to create a new one
Dagger
A recent addition to this space, uses the cue language for configuration and the docker BuildKit backend for portability. The differences mostly stem from the choice of building blocks. Choredâs unit of abstraction is a typescript function, while Daggerâs is a command running in a docker container.
- both cue and deno support remote imports (yay!)
- dagger has a more consistent runtime environment (itâs docker)
- dagger is coarse and heavyweight in terms of making docker images and defining tasks, itâs much more work than writing a Typescript function.
- dagger is intended for defining CI tasks, which you can also run locally. Itâs unclear how convenient it is for running ad-hoc tasks on a developer machine
dhall-ci (with dall-render)
Dhall ci is the precursor to chored - it was built with similar goals, on the same ideals.
The main difference is that dhall-render only really handles file generation. Whereas with chored thatâs just one builtin chore, there are countless others that can be executed on your development machine, in CI, or both.
- both dhall and deno support remote imports
- dhall is an intentionally limited language, typescript is very liberal. Dhall forces good hygeine, but this can make some things unpleasant to represent ergonomically
- dhall-ci only handles the generation of files using reusable components, it doesnât have anything to say about actually runing reusable code within CI or on your machine
- dhall-render ends up generating a number of its own internal scripts in each repo, which is distasteful. chored has a much lower footprint in terms of generated files, because of its ability to remotely import code (not just expressions)
- dhall-ci assumes you already have dhall and ruby installed, chored is fully self-bootstrapping
npx
npm is really a compettor to deno, not chored, since itâs basically a way to ârun node packages without installing them firstâ. chored could have been built on npx, but it would have been worse:
- dependencies are coarse-grained and per-project, a package is the smallest unit of re-use and canât be required only for certain tasks
- less self-contained (it would need
node,npxand a typescript compiler)
A love letter to zero-installation
I have a long history of appreciating systems which donât require stateful management. That is, running (or building) a program shouldnât care whatâs on your system, it should just do the right thing. Systems supporting this mode of operation feel conceptually like you can just ârun code from the internetâ, although obviously they have caching to ensure you only pay the cost of downloading a program once.
- zero install
- nix
- dhall
Nix is the most mainstream of these, but itâs also a big investment - you have to go all in. Deno is likely to be somewhat mainstream, and is very low investment, because you can (largely) just Run Some Typescript.