Daily sms with github actions

Introduction

I had a daemon a while back on my raspberry pi that would send me and my girlfriend a sms in the morning. The text of the sms usually was a quote or a joke or some positive inspiration.

It was fine but recently I have been really enjoying setting up workflows using Github actions and wanted to move this to actions.

Github actions runner

This is the process that listens for and processes jobs from github.

Github actions billable minutes are favourably priced in general but what is even better is that jobs running on self hosted hardware are free.

I can run the GitHub actions runner, which is open source, and run it on my raspberry pi and all jobs on it will not count against billable minutes.

Configure a self hosted runner using the amazingly simple documentation for your repo: https://github.com/:user/:repo/settings/actions/runners/new

Each runner process can only take jobs for a single repo but you can have multiple of these processes running on the same machine.

Codebase lol: https://github.com/actions/runner/blob/be9632302ceef50bfb36ea998cea9c94c75e5d4d/src/Sdk/WebApi/WebApi/WrappedException.cs#L153

Workflow yaml

This the file that defines what the job is to do.

I want to build and run my code every morning at 8am or when I push to main and the job should run on my pi. To do that my workflow file looks something like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: Send reminder

on:
schedule:
- cron: "0 8 * * *" # 8 am every day
push:
branches:
- main # commits to main
workflow_dispatch: # run manually

jobs:
build:
runs-on: [self-hosted] # tag that tells GH actions to use the self hosted raspberry pi runner - can be changed to use GH's servers
steps:
- uses: actions/checkout@v2 # checkout the code
# use this if not on a self hosted runner that already has java installed
# - uses: actions/setup-java@v2
# with:
# distribution: 'temurin'
# java-version: '16'
# architecture: 'arm'
# cache: 'maven'
- name: Build with Maven
run: mvn compile package # compile the code - 11 MB jar
- name: Run jar
env:
TWILIO_USERNAME: ${{ secrets.TWILIO_USERNAME }} # set twilio keys as env vars using github secrets
TWILIO_PWD: ${{ secrets.TWILIO_PWD }}
run: java -jar target/sms.jar # run the code that sends the text

Project pom

This is the file that defines how to build the app and what dependencies should be brought in.

The relevant sections of the pom are the ones that allows building the fat jar and the dependencies that were used for the project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>${main.class}</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>

The only dependencies we will be bringing in are Unirest for Http requests and Json and Twilio SDK for sending messages.

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>3.13.0</version>
</dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio</artifactId>
<version>8.20.0</version>
</dependency>

Data sources

Where do the quotes and jokes come from? This is the snippet that grabs the data that will be texted.

There are 7 data sources. 4 make network calls to get the data and 3 read a local json file [part of the repo] and pick a random one.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  // list of function references List<()->String>
val servicesApi = listOf(
::getAdvice, // https://api.adviceslip.com/advice
::favQuote, // https://favqs.com/api/qotd
::getAffirmation, // https://www.affirmations.dev/
::getDadJoke, // https://icanhazdadjoke.com/
::getLocalQuote, // https://github.com/JamesFT/Database-Quotes-JSON/blob/master/quotes.json
::getLocalJoke, // https://github.com/taivop/joke-dataset/blob/master/reddit_jokes.json
::getLocalMotivation // https://gist.github.com/b1nary/ea8fff806095bcedacce
)

// sample request using unirest
Unirest.get("https://favqs.com/api/qotd")
.asJson()
.body
.`object`
.getJSONObject("quote")

Picking a service

Pick at random works for a lot of use cases. Pick a random number up to 7 and use that as the index - call invoke() to actually trigger the function.

Run catching to capture exceptions and retry. It also returns a Result<T> which has a convenient api

1
2
3
4
5
6
7
fun getMessage(retry: Int = 0): String {
if (retry > 3) return ""
return runCatching { servicesApi[Random.nextInt(0, servicesApi.size)].invoke() }
.onFailure { return getMessage(retry + 1) }
.onSuccess { return it }
.getOrDefault("")
}

Twilio send sms

1
Message.creator(PhoneNumber("phone-num"), "msg-service-id", "text msg body").create()

Performance on pi

It takes roughly 3 minutes to do the maven compile step and 12 seconds to do the run jar step. The pi spins up to 70-80% Cpu and uses 1.1GB of the 4GB of memory available.

Parting thoughts

GH actions allows for all kinds of automation. It can be used for many other workflows other than pure CI/CD pipelines. Keep it in mind when trying to deploy code. It fills a really nice niche.