Mario Finelli Blog

GitHub Actions: automating invoice creation

23 May 2021

I recently tried my hand at automating my invoice creation workflow. For the last few years I have just kept a LibreOffice document around and made a new copy and changed all of the details and tried not to miss anything when I needed to issue a new invoice. More recently, I have found the excellent LaTeX invoice class from Trey Hunner. But my process was still mostly manual with a lot of potential for error when copy-pasting for new invoices. We can do better! So I set out to automate the most error-prone parts of my invoice workflow.

My basic directory structure for invoices looks like so:

.
  + client1
    + 001_YYYYMMDD
    + 002_YYYYMMDD
  + client2

Each client has their own directory and inside of each directory is a numbered and dated directory with all of the invoice files for that invoice. More specifically, this is a symbolic link to the invoice class and the Makefile (which we'll get into more detail about shortly). It then also includes the actual invoice content as its own file.

So the first step is to script up the creation of these directories, ensure that we create the correct symlinks, and fill in some details about the invoice ahead of time (namely the statement number and the date).

#!/bin/bash -e
# new.bash

# create a new invoice repository for a project
# usage: new.bash project

if [[ $# -ne 1 ]]; then
  
echo >&2 "usage: $(basename "$0") project"
  exit 1
fi

# this should work on macos and linux
today="$(date '+%Y%m%d')"

if [[ ! -d $1 ]]; then
  
mkdir "$1"
  next=001
else
  
last="$(find "$1" -type d | sort | tail -n1 |
    sed 's|^.*/\([0-9]\{3\}\)_.*$|\1|')"
  next="$(printf '%03d' "$((10#"$last" + 1))")"
fi

mkdir "${1}/${next}_${today}"
cd "${1}/${next}_${today}"

ln -snf ../../Makefile Makefile
ln -snf ../../invoice.cls invoice.cls

sed -"s|\today|$(date '+%B %-d, %Y')|" \
  -"s|Statement \#1:|Statement \#$((10#"$next")):|" \
  ../../invoice.tex > invoice.tex

exit 0

Let's walk through some of the more interesting bits of this script. After we get today's date in YYYYMMDD format for use later we check if we already have a directory for the specified client. If we don't then it means we have a new client (Hooray!) and so we create a new directory and initialize the counter at one. If we do have a directory then we instead need to find the next invoice number.

We do this by first grabbing the last invoice: find "$1" -type d | sort | tail -n1. This pipeline finds all of the directories (find -type d) in the client's directory and then sorts them and then grabs the last one. This works because the invoice directories are ordered with zero-padded numbers to start. Then we continue the pipeline with sed 's|^.*/\([0-9]\{3\}\)_.*$|\1|' which leaves us with just the latest zero-padded invoice number.

Then to calculate the next invoice number we just add one. Note the 10#$last syntax here which converts to base 10 first, otherwise the number gets treated as an octal and we end up with an error value too great for base (error token is "008").

The next few steps are pretty straightforward, we just create a new directory for the new invoice and symlink in our static files. Then we take our invoice template and substitute in the current date and the invoice number for before and then write out a fresh copy into the new invoice directory.

Now in order to generate a new invoice for a client we simply run the script: ./new.bash clientcode, edit the resulting invoice.tex, and then git commit the entire directory.

It's safe to git add the entire directory since we ignore the artifact outputs:

*.aux
*.log
*.out
*.pdf

Let's quickly look at the static makefile that we use, but there's nothing special going on here:

invoice.pdf: invoice.cls invoice.tex
        pdflatex invoice.tex

clean:
        rm -rf invoice.pdf invoice.aux invoice.log invoice.out

.PHONY: clean

So now the last step is to automate the creation of the invoice pdf that we can then send to clients. This is where GitHub Actions comes in. We can define the workflow to create and upload the PDF with a simple YAML file that runs when we push a new git tag. I have settled on a tagging convention that matches the directory structure and so when I want to issue a new invoice I tag the resulting commit 001_clientcode the same as the directory in which the invoice files are contained.

When the automation runs it will create a new GitHub release (alongside which we can upload release artifacts, namely invoice.pdf), create the PDF, attach it to the release and then also store a copy in a bucket on Google Cloud which we use as a backup. I like storing the PDFs attached to releases on GitHub because I spend more time logged into GitHub than I do to GCP and so I can quickly download any invoice that I need from the releases page instead of the Google Cloud console.

Here's the workflow that facilitates this:

---
name: Create Invoice
on:
  push:
    tags:
      - '*'

jobs:
  invoice:
    name: Generate PDF
    runs-on: ubuntu-latest
    steps:
      - run: sudo apt-get install -y texlive texlive-latex-extra
      - uses: actions/checkout@v2
      - id: cwd
        run: |
          g="$(sed -e 's|^refs/tags/||' -e 's|_|/|' <<< "${{ github.ref }}")*"
          n="$(sed -e 's|^refs/tags/||' -e 's|_.*||' <<< "${{ github.ref }}")"
          v="$(sed -e 's|^refs/tags/||' -e 's|.*_||' <<< "${{ github.ref }}")"
          t="$(echo $g)"
          echo "::set-output name=tag::$t"
          echo "::set-output name=name::$n"
          echo "::set-output name=number::$v"
      - uses: google-github-actions/setup-gcloud@master
        with:
          service_account_key: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }}
          export_default_credentials: true
      - run: |
          cd ${{ steps.cwd.outputs.tag }}
          make
      - uses: google-github-actions/upload-cloud-storage@main
        with:
          path: ${{ steps.cwd.outputs.tag }}/invoice.pdf
          destination: invoice-bucket/${{ steps.cwd.outputs.tag }}
      - uses: ncipollo/release-action@v1
        with:
          artifacts: ${{ steps.cwd.outputs.tag }}/invoice.pdf
          artifactContentType: application/pdf
          name: ${{ steps.cwd.outputs.name }} ${{ steps.cwd.outputs.number }}
          token: ${{ secrets.GITHUB_TOKEN }}

This should be mostly straightforward, but a quick recap. First we need to install the texlive package since it's not installed on the GitHub Ubuntu runner image by default. Then we pull the entire git tag, the client code, and the invoice number from the git tag (${{ github.ref }}). We setup the gcloud utility with our GCP service account credentials. Then we actually create the invoice PDF (cd 001_clientcode; make). Next, we upload the PDF to our Google Cloud Storage bucket. Then, finally, we create a new GitHub release for the tag and attach the invoice PDF there too.

Overall, this workflow has drastically simplified how I create and issue invoices. One improvement that I will be making in the future will be to keep the client name and address in a template file in each client directory so that I don't need to edit it into each invoice.tex manually and can just stick to the actual billing details.