Automating with Clojure + Github Actions

tags: clojure

I have used Clojure + Github Actions to automate two things so far: a script that periodically calls the Twitter API and does some analytics and one that does synchronization between two APIs. This combination does two things very well:

  1. You can run a job periodically, for free, without having your own server.
  2. Interacting with APIs is simple and easy.

Being dynamically-typed and data-oriented, Clojure is great for writing quick automation scripts. In particular, interacting with APIs is easy once you are comfortable working with maps, vectors and sequences. Additionally, Clojure’s built-in support for .edn makes it easy to store and read small amounts of data.

In this case, we want to run the job in a serverless manner, so where would we store the data? While it is possible to add a dependency on an external service to store small blobs of data, a far faster (and free) way is to leverage git and GitHub itself.

Normally, building this interface with git and GitHub would be tedious, but that’s where Github Actions I love it for hobby projects because I get 2000 free minutes of runtime a month, a growing collection of supported actions and secrets management. comes in.

After banging out a quick prototype of your script, the next thing you would want to do is to think about how you manage environment variables and secrets. This happens often when working with APIs since you often need to manage keys and secrets that should not be included as part of the source code. Fortunately for us, Clojure’s weavejester/enviorn allows us to easily work with environment-dependent data. When doing local testing, secrets can be stowed in a gitignored profiles.clj file. When running in production, your Clojure binary can consume environment variables which are managed by GitHub.

Another thing you might want to do is think about how to persist data. The easy way in Clojure is to write to the filesystem and let git + actions handle the rest. e.g.

;; dump data
(spit "data.clj" (pr-str [:a :b :c]))`
;; read data
(read-string (slurp "data.clj"))`

Then, in your GitHub Actions config you can use an action to commit state to the git repo.

The last thing to productionize is to figure out how to run Clojure code on GitHub Actions. actions/setup-clojure gets that ready for you, and since I use Leiningen for my projects, I can do a lein run after that.

What my GitHub Actions config looks like:

name: "Tracker"

on:
  push:
    branches:
      - master
  schedule:
    -   cron: "0 16,18,20,22,0,2,4,6 * * *"

jobs:
  backup:
    runs-on: ubuntu-latest
    name: Backup
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v2
      - uses: DeLaGuardo/setup-graalvm@2.0
        with:
          graalvm-version: '19.3.1.java11'
      - uses: DeLaGuardo/setup-clojure@2.0
        with:
          tools-deps: '1.10.1.469'
      - run: CONSUMER_KEY="${{ secrets.ConsumerKey }}" CONSUMER_SECRET="${{ secrets.ConsumerSecret }}" lein run
      - name: Commit changes
        uses: elstudio/actions-js-build/commit@v3
        with:
          commitMessage: Automated snapshot

Let me know on Twitter if this was useful!