Resume as Code

I avoid brand new tech until I have an itch to scratch: a small, self-contained problem that seems it might lend itself nicely to $someTool. I'm usually unsure I'll be able to achieve what I have in mind. But maybe...

This time, the problem was something that had annoyed me for as long as I'd been employed: resumes. Not the fact of them, or even writing them, necessarily. Just managing different versions.

I was swimming in Dropbox folders with dozens of files saved in various formats; I needed to maintain multiple documents to preserve different iterations, and export PDFs for distribution. Version control was a nightmare.

LaTeX and git to the Rescue

One day I happened upon Daniel Cousineau's resume. Daniel was using a tool I'd heard of but never worked with called LaTeX (a system for generating documents, and not a particularly new one), along with GitHub releases to track his resume's evolution. It seemed promising.

I set up alessbell/resume, rewrote my resume as resume.tex and, back in June, released v1.0 🎉

Wherefore Art GitHub Actions

Having a resume managed in GitHub was a very welcome change, but there were still a few manual steps every time I wanted to cut a release:

  1. Draft a new release via GitHub UI. Tag my commit and begin manually creating the release.
  2. Manually compile resume.tex and upload the PDF as a release asset. GitHub automatically includes the source code in both zip and tarball formats, but I wanted to include a compiled resume.pdf, too. I'd run pdflatex locally and drag and drop the file, again via GitHub UI.
  3. Update the copy of resume.pdf in Gatsby's /static folder. I'd take my new PDF and drag and drop it into my local copy of the codebase for this website, since I want aless.co/resume.pdf to always display the latest version. Then I'd manually commit it and open a PR.

Once I'd isolated the steps that were candidates for automation, I sketched out the ideal workflow: first, automating releases in alessbell/resume, then somehow pinging another repository when a new release was published (?), and finally, the other repository (this blog) would download resume.pdf from the latest release, commit it and open a PR... maybe?

I had no idea how feasible this all was, still knowing little to nothing about the GitHub Actions API. But automating even one step would be a win!

Spoiler Alert

tl;dr my ideal workflow was possible, so I built it 🐙💜

If you'd like to browse the code, steps 1 and 2 are achieved by the main workflow in alessbell/resume. Step 3 is handled by actions in this blog's repository, namely /commit-resume. For a walk-through of the code, keep reading 😎

A screenshot of the GitHub Pull Requests UI showing the first PR created by my GitHub action: entitled 'Update resume to v1.1', this PR automatically updates my aless.co resume PDF with the one it downloaded from the latest automated release in another repository

1. Compile the PDF and Automate Releases

I figured automating the release part would be easy—surely there's already an action for that—but I wasn't so sure about compiling the LaTeX to PDF for inclusion in the release.

Delightfully, both steps turned out to be trivial to implement. I found off-the-shelf actions for both: xu-cheng/latex-action for compiling my LaTeX to PDF and softprops/action-gh-release for creating the GitHub release with the compiled PDF from the previous step attached as an asset.

My first workflow looked like this:

name: Publish new release of resume
on:
  push:
    tags:
      - 'v*.*'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@master
      - name: Compile LaTeX document
        uses: xu-cheng/latex-action@master
        with:
          root_file: resume.tex
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          files: resume.pdf
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This workflow runs on any push event targeting tags matching the glob pattern v*.* (docs). The first step, actions/checkout, checks out the repository to the current GitHub workspace so that the workflow can access the contents of your repository.

After that, I can pass the next step the name of my file, resume.tex, and the subsequent step can directly access the compiled resume.pdf from the previous one. In order to author a release, I need to pass in a GITHUB_TOKEN; luckily GitHub already makes certain environment variables available.

2. Ping Another Repository

In order to keep the version of my resume displayed on this website up-to-date, I'd need a way to kick off a new workflow each time I published alessbell/resume.

Luckily, such an event exists: repository_dispatch triggers a webhook event "when you want to trigger a GitHub Actions workflow for activity that happens outside of GitHub," or, in this case, in a different repository's action. A simple curl request with the correct auth token, headers and body does the trick.

This is the whole bash script from alessbell/resume/ping-repo:

#!/bin/bash
main() {
  curl -XPOST -H "Accept: application/vnd.github.everest-preview+json" \
  -H "Content-Type: application/json" \
  -H "Authorization: token ${GITHUB_TOKEN}" \
  "https://api.github.com/repos/${REPO}/dispatches" \
  --data '{"event_type": "update_resume"}'
}
 
main

There were two small caveats here. First, because this POST request is being dispatched for a repository other than the one from the action's execution context, I needed a personal access token with repository scope set as a secret on the repository. I stored my secret as PA_TOKEN and passed it in the way I had the others:

- name: Pings repo
  uses: ./ping-repo
  env:
    GITHUB_TOKEN: ${{ secrets.PA_TOKEN }}
    REPO: alessbell/aless.co

Second, certain events, e.g. push will run on any branch unless the scope is narrowed by specifying a certain branch or tag. When I was testing this repository_dispatch event, however, nothing was happening despite having pushed a workflow to a branch in my blog's repository listening for this exact dispatch event. It wasn't until I pushed the blog's workflow config to master that I saw it spring to life, activated by my Postman request to the /dispatches endpoint.

3. Download New PDF, Open PR

This final step felt like a stretch goal, but it proved to be just enough work for a train ride from Rhode Island to NYC. The first part involves fetching the latest version of alessbell/resume, and GitHub's API has a dedicated endpoint for retrieving information about a repository's latest release:

RELEASES_URL=https://api.github.com/repos/alessbell/resume/releases/latest
 
RES=$(curl -sSL -H "${AUTH_HEADER}" -H "${HEADER}" --user "${GITHUB_ACTOR}" -X GET ${RELEASES_URL})
VERSION=$(echo "${RES}" | jq --raw-output '.tag_name')
PDF_URL="https://github.com/alessbell/resume/releases/download/${VERSION}/resume.pdf"
 
# download resume.pdf and save in /static/resume.pdf
curl -L0 "${PDF_URL}" --output ./static/resume.pdf

Once I had the file downloaded, I'd just need to commit it and open a PR. This time, I'd try to use a pre-existing action with a bit less luck: I wasn't able to integrate vsoch/pull-request-action directly (I'll be the first to say it could have been user error -- when I'm out of my comfort zone, I need to be able to tinker with the code), but reading its source taught me a lot about how to write a similar action that would work for my case.

I wound up with ~90 lines of bash and successfully used jq to process JSON for the first time. There was plenty of trial and error along the way, but once I plugged it all together, it Just Worked.


Writing bash and yaml isn't part of my day job, but it was a lot of fun once I got started; tinkering with GitHub Actions was the perfect excuse to learn something new while scratching an itch.

Is there a workflow you're thinking about automating? I'd be curious to hear about it -- you can ping me on twitter at @alessbell or email me at web[at]bellisar.io.