Recently, GitHub Actions added another tool to the workflows we’ve all centralized around GitHub, making it even easier to have bots do the process-heavy things that we humans don’t want to do.

My initial thought when I heard the news was how this plays with Travis CI, which we also use, and then I led to thinking automating our linting via Actions is a logical first step (and we now are using that, although it’s still a work-in-progress!)

Through that project, I learned a few things about GitHub Actions, and perhaps the ideology behind what GitHub is trying to do here.

Swank!

So, let’s explore a few of my learnings and throughout this article, we’ll build out a simple GitHub Action. We’ll make an action which is triggered on a any commit, and reviews the commit message

How Actions Run — Scaffolding our project

The first step — and the easiest one — is the setup we need to do on our repository.

It’s pretty neat. Essentially, each Action runs within a Docker container, which is given a variety of context from Github (environment variables, the repository at hand), which then can execute well … anything you could within a Docker container!

By and large, the primary thing I learned from this process was how lacking in knowledge I am with my bash scripting, but I’ll share some neat tricks and best practices I found shortly!

An action is broken down like so:

+-- Dockerfile

+-- README.md

+-- your_entry_script.sh

+-- ... any additional folders for organization

The Dockerfile can be as simple or complex as you wish, but ultimately it’s a place to setup a bit of Action metadata (LABELs, etc.), install dependencies, and initialize the entry command for the container. Here’s what our git commit message project uses, which you can copy down as a starting point:

FROM alpine:latest LABEL "com.github.actions.name"="git-commit-message-check"

LABEL "com.github.actions.description"="Check the quality of the git commit message"

LABEL "com.github.actions.icon"="check-square"

LABEL "com.github.actions.color"="yellow" RUN apk add --no-cache \

jq \

curl \

git COPY "run-git-message-check.sh" /usr/bin/run-git-message-check CMD ["sh", "/usr/bin/run-git-message-check"]

I think the beauty of GitHub Actions is that they, implicitly through the nature of the design, encourage light-weight actions that do one thing, and do it well.

This mentality begins at the root, with the minimal alpine Docker container. This container comes with the bare essentials, so any dependency, even as common as curl must be installed.

We install a few more dependencies our script will need (you’ll see soon!), and then COPY our primary script to the container. Note that you’ll want to chmod +x your script locally to ensure when it’s copied over, it’s executable!

Building out the shell script (& What I learned about bash)

Now, you can write in any language that’ll run in a Docker container, but it was fun to try and flex my (non-existent) bash muscles!

The first thing to know about Actions is that upon running any script, you have some neat additional context to work with. Actions provide a robust runtime environment, giving you a lot of information about your repository, the triggered event, and the actors (systems or people) behind it.

A fun way to dig into this is to make an Action that well, does nothing but outputs the runtime environment. Take a look:



#!/bin/bash set -e

set -o pipefail main() {

jq --raw-output . "$GITHUB_EVENT_PATH"

} main

Add this to your project as run-git-message-check.sh. At this point, with just this shell code and the Docker container, you are very close to adding an action. If you want to pause now and try, I recommend reading the GitHub documentation on Creating a new GitHub Action, as it goes into the details of rolling it out which we won’t in this article. (Mainly because we’re focusing on the Action’s functionality here, and the rest is connecting pieces!)

I love how simple this shell script is, and I learned a few useful bits:

set -e stops the execution of the shell script whenever there’s any error from a command or pipeline. This keeps our action fairly sane, and is something new to me in the bash world!

stops the execution of the shell script whenever there’s any error from a command or pipeline. This keeps our action fairly sane, and is something new to me in the bash world! Similar to that, I learned a lot about pipefail! There’s been great writing on this already, so I recommend that as a side read!

I learned about shellcheck, which lints your bash! In my example repo linked later, I provide a means of using this against the code we write.

That aside, I punched that shell code and Docker container into a workflow and you can see a snippet of the output within the Actions tab of GitHub (snipped for brevity):

### STARTED post git quality control 19:04:00Z



Pulling image: gcr.io/github-actions-images/action-runner:latest

latest: Pulling from github-actions-images/action-runner

cd784148e348: Already exists

1a34a342bc8b: Pulling fs layer

571706cf6933: Pulling fs layer

....

....

(5/10) Installing curl (7.61.1-r1)

(6/10) Installing expat (2.2.5-r0)

(7/10) Installing pcre2 (10.31-r0)

(8/10) Installing git (2.18.1-r0)

(9/10) Installing oniguruma (6.8.2-r0)

(10/10) Installing jq (1.6_rc1-r1)

Executing busybox-1.28.4-r2.trigger

Executing ca-certificates-20171114-r3.trigger

OK: 20 MiB in 23 packages

---> b3a1810ba635

Step 7/8 : COPY "run-git-mesage-check.sh" /usr/bin/run-git-message-check

Beautiful! Essentially it’s building and running the Docker container, and the entire log is contained within this output.

The runtime environment

As we did above, we used jq to inspect GITHUB_EVENT_PATH, which is a useful JSON file full of metadata like so:

{

"after": "1870aa8285a40ac84debc34885314b0553c1de55",

"base_ref": null,

"before": "b0dba7f5a0b290bfabcf4498ff19980f3c0b7fd6",

"commits": [

{

"added": [],

"author": {

"email": "bart.ciszk@gmail.com",

"name": "Bartek Ciszkowski",

"username": "bartek"

},

"committer": {

"email": "bart.ciszk@gmail.com",

"name": "Bartek Ciszkowski",

"username": "bartek"

},

"distinct": true,

"id": "1870aa8285a40ac84debc34885314b0553c1de55",

"message": "Fix COPY command in Docker",

"modified": [

"Dockerfile"

],

"removed": [],

"timestamp": "2019-01-15T21:14:45-05:00",

"tree_id": "a791cf15588173a748df25d3b92208f31d384025",

"url": "https://github.com/bartek/action-git-message/commit/1870aa8285a40ac84debc34885314b0553c1de55"

}

],

...

}

This data was what came out when our script ran that jq command against the repository I was building for this project. You can see the latest commit (in which I made a typo on the COPY command). The above is just a tiny snippet of the metadata available to you in the GitHub event JSON.

Glorious! So much information. For our project, we’ll leverage the commits array, looking at each message, and validating it’s meeting our best practices. Yeah, this bot is a bit passive aggressive, but remember, this is just a way to learn! Your real live bot could be nicer.

Alright, let’s build out the core of this script. I loved this article on git message best practices, and it highlights these rules:

My bash-foo was in for a serious test here, so I focused on ensuring rule 2 has been met, while adding the functionality to post a comment on GitHub for all relevant commit sha’s. I’ll try to hide my lack of bash skills by saying I’m keeping the example simple to be concise, but I bet someone will come here, read this, and provide a one-liner to reduce my script to nothing (Actually — I’d appreciate that). Without further ado:

#!/bin/bash set -e

set -o pipefail

API_VERSION=v3

API_HEADER="Accept: application/vnd.github.${API_VERSION}+json; application/vnd.github.antiope-preview+json"

AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" URI= https://api.github.com API_VERSION=v3API_HEADER="Accept: application/vnd.github.${API_VERSION}+json; application/vnd.github.antiope-preview+json"AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}"

for i in ${!COMMIT_IDS[@]};

do

id=${COMMIT_IDS[$i]}

curl -sSL -H "${AUTH_HEADER}" -H "${API_HEADER}" -d

done post_quality_control_message() {for i in ${!COMMIT_IDS[@]};doid=${COMMIT_IDS[$i]}curl -sSL -H "${AUTH_HEADER}" -H "${API_HEADER}" -d @quality_message .json -H "Content-Type: application/json" -X POST "${URI}/repos/${GITHUB_REPOSITORY}/commits/${id}/comments"done } main() {

commits=$(jq --raw-output .commits "$GITHUB_EVENT_PATH")

for row in $(echo "${commits}" | jq -r '.[] |

message="$(echo "$row" | base64 --decode | jq -r '.message')" COMMIT_IDS=()for row in $(echo "${commits}" | jq -r '.[] | @base64 '); domessage="$(echo "$row" | base64 --decode | jq -r '.message')" if [ ${#message} -ge 50 ]; then

COMMIT_IDS+="$(echo "$row" | base64 --decode | jq -r '.id')"

continue

fi done post_quality_control_message;

} main

For the record, you can find all this code on my repository.

There’s a few things going on here but, in summary:

As a first step, I use jq to get the commits from the Github Event data. This again is a JSON file provided by the GitHub Actions runtime environment

to get the commits from the Github Event data. This again is a JSON file provided by the GitHub Actions runtime environment Our action is fired whenever there’s any push event in the repository (this is defined when you make the actual workflow), so we have the context of the git commit, among other details within our environment.

I then flex my non-existent bash muscles and iterate over each commit I’ve found in the JSON, capturing the length of the message, checking if it’s greater than 50 characters or more — and if so — storing that commit id for reporting later.

Then, iterating over all collected commit ids, I use the GitHub API to send a message to the commit comments. Here’s what that looks like:

So friendly! A bit of work necessary on formatting!

And with that, we have a functional GitHub Action.

Now, what we’ve created today isn’t useful in isolation, but it’s given us some idea of what GitHub Actions can do! We’ve seen that we get a wonderful and slim runtime environment, we can run a variety of arbitrary commands, and we can interact with the GitHub API.

All these pieced together and you get quite the powerful ecosystem!

I really enjoyed spending some time with GitHub Actions and I have a general excitement about their capabilities. As someone who spends a significant part of his brain power thinking about how to improve process and reduce barriers for developers, this addition provides more enlightenment.

There’s a lot more for me to learn, and I hope you found this as a good start. I highly recommend the introductory talk for GitHub Actions, when they were introduced at AWS re:invent earlier in 2018.

If you had issues running what I’ve provided in my repository, or want to provide additional input, please, reach out! I’d love to learn more.