Setting up a generative art project using #rstats and GitHub Actions

generative art github actions

For the last wee while, I’d been wanting to explore the fascinating field of Generative Art and add a continuous deployment project to my portfolio. Enter the @aRtfulBot, a Twitter bot I created to explore both of these at once!

Cara Thompson
09-10-2021

There are three components to the @aRtfulBot: an R script that generates a piece of generative art, a twitter bot, and a series of Github Actions. The focus of this post is on the technical side of the latter two, with a few whys alongside the hows.

An R script that creates a piece of generative art

I won’t go into detail about the Generative Art side of the bot, because there are plenty of great resources out there already. Neither will I share the code behind my pieces, because writing your own code and being pleasantly surprised by its output is a huge part of the creative fun. All I’ll say is that the resource I found most helpful for setting up my Monochrome Trigonometry series was this post, in which Devin Hunt provides a few tools to explore the maths behind wave patterns. That, plus the title of the series and the fact that I did it all within the tidyverse, give you a few clues as to its building blocks.

There are a heap of great generative artists out there, using a lot of different tools to generate truly mindblowing stuff. I’ve enjoyed watching the #Rtistry community grow and look forward to seeing how this field evolves!

The aRtfulBot

The main idea of this project was to set up a twitter bot that would tweet a new piece of generative art every day. My biggest source of help in setting up this bit of the project was this post by Matt Dray.

Me or the bot?

The first step was to decide which twitter account to use for this. I decided to set up a dedicated account for the bot, to allow me to still be “me” in my personal account and share my favourite images there. I then applied for developer access from within that dedicated account, so that I could use the Twitter API.

Keys and Tokens

Once developer access had been granted, I created a bot within the @aRtfulBot’s profile via the developer portal and generated the necessary Access and API Keys and Tokens. With that, I had all I needed to create the rest of the magic1 via Github Actions.

A series of Github Actions

Github Actions are run via a YAML file that sits within the main directory of a Github repo. Create an empty text file inside a folder called .github/workflows/ and call it main.yml (so that it carries out actions on the main branch) and you can start automating the running of code chunks within the repo. Here are the questions the file needs to answer.

When should the action run?

This is determined using a Cron. Crontab.guru makes it really easy to check your Cron is set up as you intend it to be. The five pieces of the Cron statement are Minute, Hour, Day (of the month), Month, and Day (of the week). By leaving the last three as *, it runs every day at the time specified by the first two (in my case, at 12.00).

on:
  schedule:
    - cron: '0 12 * * *'

In addition, I wanted to be able to trigger the workflow manually to test things as I was setting up the project, so I added this line:

  workflow_dispatch:

What is the job called?

That one’s easy. The key here as always is to be kind to your future self and give the job a name you’ll instantly understand when you revisit it at a later date!

jobs:
  aRtful_Bot-post:

What should it run on?

A few of the projects I looked at in setting this up ran on windows-latest or macOS-latest, presumably to replicate the user’s familiar environment. I also started out with windows-latest, but that then required a lot of package installations every time, which wasn’t as straightforward as I’d hoped and didn’t seem very efficient. And then I spotted this approach in David Keyes’ setup:

runs-on: ubuntu-latest
container: rocker/tidyverse

Using Ubuntu allows you to make use of containers, and of course there is one that has all the tidyverse packages installed and ready to roll! Since those were all the packages I needed, this was a great solution. If I need other packages for a future project, I’ll likely create and upload my own container and call it from within Github Actions, rather that installing all the necessary packages into an empty workspace every time. But that’s for another day!

What should it do?

Now comes the fun part of specifying the various steps that need to be taken. The first one is to create some space in which to run the code:

steps:
- uses: actions/checkout@v2

Create and save an image

This bit is taken care of in the R script. It is run in the following step:

- name: Create image and save it
  run: Rscript monochrome-trigonometry/monochrome-trigonometry.R

I highly recommend adding print statements inside the R script to help with debugging. I spent ages trying to figure out why I couldn’t tweet the image (more on that below), and having a few print statements like the following definitely helped keep me sane.

#  [Code to create plot]
print("Plot created!")

# [Code to save the image to image_file]
print("Image saved!")

# Checking it can be found
if(file.exists(image_file)) {
  print("*** Found file!")
} else {print("*** Can't find file!")}

Commit the image in the workspace and pull it back to the repo

I wanted to keep a copy of all the images I tweet as files within the repo rather than just entrusting them to Twitter, so I added this step:

- name: Commit image and push to origin
  run: |
    git config --local user.email "actions@github.com"
    git config --local user.name "GitHub Actions"
    git add monochrome-trigonometry/images
    git commit -m 'New image' || echo "No changes to commit"
    git push origin || echo "No changes to commit"

The echo || "No changes to commit" statement prevents it from failing during testing. In my R script, the randomisation that makes the image different every day is tied to the date. All the images produced on the same day are identical, so without this statement, the commit stage failed during testing because there were no changes to commit.

Tweet it!

This, to me, was the steepest bit of the learning curve. Try as I may, I couldn’t get the tweet to work using R’s {rtweet} from within the R script when it was running within Github Actions. I could get it to work locally, and I even downloaded Docker to replicate the container environment and made it work that way, but once inside the Github Actions structure, it would not tweet the image alongside the tweet. So I decided to take a different approach. There’s definitely something to be said for using tools designed for specific purposes, so going with a Github Action that was built to send tweets with media attached seemed like a good bet.

In the R script, I assign a three-digit number to each image, based on its order in the Monochrome Trigonometry series. The number is assigned to the file name within the R script, so my initial plan was to just use some regex on that file name, but I now needed a different approach from outside the R script. Since I’m committing each image as I go, I initially thought of retrieving the image that was changed in the latest commit. The trouble there is that during testing, the code was changing but the image wasn’t, so there sometimes wasn’t an image in the latest commit. Instead, I needed to retrieve the name of the image file which was added most recently to the image folder. For that, I needed a bit of Unix shell command and grepping!

Here’s where I ended up: List (ls) all the relevant files in the relevant directory (-A), reverse their order (-r) so that when they’re sorted by time (-t) the newest one is at the end. Next, take the last bit of the file name (tail) of the last file in the list (-n 1). From that file name, grep three digits in a row ([0-9][0-9][0-9]) and print only (-o) the bits of the string that match that pattern (i.e. those three digits - the neater ways of saying “three digits” didn’t do the trick here). Finally, save those three digits to a variable called image_number in the Github Environment that we’ll be able to retrieve later.

And here’s what that looks like:

- name: Get image number for tweet
  run: echo "image_number=$(ls -Art monochrome-trigonometry/images/ | tail -n 1 | grep -o [0-9][0-9][0-9])" >> $GITHUB_ENV

Finally, we’re ready to tweet the image and a message which includes the number of the image! Note that the media_paths variable is just temp.png. In the R script, I save the image within the images/ folder with the series title and its three-digit number, but I also save it as temp.png so that I can easily point to it here without needing yet another fiddly step to retrieve a different file name every time. Sneaky, but it works!

This final step also makes use of the Secrets. Those Key and Tokens we talked about earlier should be stored as secrets within your repo. Make sure they never make it into a piece of code that others could access (including any early commits while you’re testing things!). Here goes the tweet!

- name: Tweet
  uses: snow-actions/tweet@v1.1.0
  env:
    CONSUMER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
    CONSUMER_API_SECRET_KEY: ${{ secrets.TWITTER_API_SECRET_KEY }}
    ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
    ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
  with:
    status: "Monochrome Trigonometry ${{ env.image_number }}\n\nDone in #rstats with #ggplot2\n#Rtistry #generativeart #creativecoding"
    media_paths: |
      temp.png

The use of #s required a bit of experimenting with how escape characters work in YAML. In my case, the "s around the entire string took care of the problem, but this exchange was handy for figuring out the details.

Pulling it all together

That was all pretty bitty, so here’s the full YAML file for easier reading:

name: aRtful_Bot

# Controls when the action will run. 
on:
  # Triggers the workflow once a day at 12
  schedule:
    - cron: '0 12 * * *'
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:
jobs:
  aRtful_Bot-post:
    runs-on: ubuntu-latest
    container: rocker/tidyverse
    steps:
      # Checks-out the repository under $GITHUB_WORKSPACE, so the job can access it
      - uses: actions/checkout@v2
      - name: Create image and save it
        run: Rscript monochrome-trigonometry/monochrome-trigonometry.R
      - name: Commit image and push to origin
        run: |
          git config --local user.email "actions@github.com"
          git config --local user.name "GitHub Actions"
          git add monochrome-trigonometry/images
          git commit -m 'New image' || echo "No changes to commit"
          git push origin || echo "No changes to commit"
      - name: Get image number for tweet
        # Looks up latest image file rather than the file changed in latest commit 
        # in case there were no changes to latest image to commit (e.g. during testing)
        run: echo "image_number=$(ls -Art monochrome-trigonometry/images/ | tail -n 1 | grep -o [0-9][0-9][0-9])" >> $GITHUB_ENV
      - name: Tweet
        uses: snow-actions/tweet@v1.1.0
        env:
          CONSUMER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
          CONSUMER_API_SECRET_KEY: ${{ secrets.TWITTER_API_SECRET_KEY }}
          ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
          ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
        with:
          status: "Monochrome Trigonometry ${{ env.image_number }}\n\nDone in #rstats with #ggplot2\n#Rtistry #generativeart #creativecoding"
          media_paths: |
            temp.png

Getting the tweet to work involved a lot of trial and error, but sitting back and watching it tweet a new piece of generative art every day makes all the steepest bits of the learning curve worth it!

Gif of the “It’s alive!” moment from the 1931 Frankenstein movie

  1. It’s not magic, it’s code; but it’s still pretty fun!↩︎

Citation

For attribution, please cite this work as

Thompson (2021, Sept. 10). Cara R Thompson | Building stories with data: Setting up a generative art project using #rstats and GitHub Actions. Retrieved from https://www.cararthompson.com/posts/2021-09-10-setting-up-the-artfulbot/

BibTeX citation

@misc{thompson2021setting,
  author = {Thompson, Cara},
  title = {Cara R Thompson | Building stories with data: Setting up a generative art project using #rstats and GitHub Actions},
  url = {https://www.cararthompson.com/posts/2021-09-10-setting-up-the-artfulbot/},
  year = {2021}
}