site/blog/exploiting-github-actions/index.html
<!DOCTYPE html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Exploiting Github Actions</title>
<meta name="generator" content="Leter" />
<meta
name="description"
content="Exploring Github Actions as an alternative way to build/test and trigger email newsletters."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://cdn.jsdelivr.net/" crossorigin />
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/assets/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/icons/favicon-16x16.png"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"
/>
<style>
:root {
--background_color: #fff;
--page_align: 0 auto;
--text_font: "Rubik", sans-serif;
--text_color: #1b262c;
--text_size: 20px;
--heading_font: "Montserrat";
--heading_color: #1b262c;
--accent_color: #1b262c;
--link_color: #21bf73;
}
</style>
<link rel="stylesheet" href="/bullish.css" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Rubik|Montserrat:500,800&display=swap"
/>
<!-- MailerLite Universal -->
<script>
(function (m, a, i, l, e, r) {
m["MailerLiteObject"] = e;
function f() {
var c = { a: arguments, q: [] };
var r = this.push(c);
return "number" != typeof r ? r : f.bind(c.q);
}
f.q = f.q || [];
m[e] = m[e] || f.bind(f.q);
m[e].q = m[e].q || f.q;
r = a.createElement(i);
var _ = a.getElementsByTagName(i)[0];
r.async = 1;
r.src = l + "?v" + ~~(new Date().getTime() / 1000000);
_.parentNode.insertBefore(r, _);
})(
window,
document,
"script",
"https://static.mailerlite.com/js/universal.js",
"ml"
);
var ml_account = ml("accounts", "2024694", "v7f1i7g4p5", "load");
</script>
<!-- End MailerLite Universal -->
</head>
<body id="classic">
<section>
<div class="logo">
<a href="/"><img src="../logo.png" /></a>
</div>
<h1>Exploiting Github Actions</h1>
<hr />
<p>
At the beginning of this year, I launched a stock market newsletter
called <a href="https://bullish.email">Bullish</a>. I’ve been building
this project to be fully autonomous, trying to squeeze as much I can get
from free tiers available everywhere, keeping costs close to zero,
trying to build a completely automated micro SaaS product.
</p>
<p>
Bullish is a Ruby app that fetches data from multiple finance API’s and
synthesizes that into an email template generated twice a day that gets
triggered to MailerLite for delivery.
</p>
<p>
There are many ways to accomplish this task and the way I initially
solved it was to re-purpose a Raspberry Pi that was collecting dust to
run the project and set up CRON jobs to trigger emails and other tasks.
</p>
<p>
Although unusual, this configuration worked great, and the workflow was
seamless; all I had to do was ssh into Pi and pull from master to get
the latest updates.
</p>
<p>
Over time a couple of issues started to bother me. First, using
Raspberry Pi introduced a single point of failure in the process if
power was out, for example, or I was away from home, and something went
wrong, there was no way to fix it.
</p>
<p>
Another problem is that I would forget to run bundle to install or
update gems breaking the CRON jobs more often than not.
</p>
<p>
But the most concerning one was security-related. Bullish stores all of
its API keys in an env file, and although not in source control, these
keys had to be available in the Raspberry Pi for the service to run and
that alone was a big enough reason to look for a better, more scalable
and FREE solution.
</p>
<p>
There are many ways I could have solved this. The most elegant probably
being a Lambda function run on a schedule with API keys managed by a
service like AWS Secrets Manager, which I still might do at some point,
but this time around, I was looking for a quick win.
</p>
<p>
I’ve been using Github Actions to run tests on every push to master
branch, and one day it occurred to me, why not use this to have a
workflow to trigger emails and other tasks as well?
</p>
<p>
Doing some research, I found that Github Actions supports jobs triggered
by a scheduled event and has a generous free tier that would work
perfectly for my use case.
</p>
<p>
Github Actions is still pretty new and has some annoying limitations
like not sharing common data between jobs, leading to many duplicated
steps, but it does allow sharing environment variables across jobs.
</p>
<p>
Another neat feature is that Github offers managed secrets that get
injected in the container when the job executes, so no more API keys are
in the open.
</p>
<script src="https://gist.github.com/eduardosasso/f715bced11f2308f4670798b87b78131.js"></script>
<p>
Overall I am satisfied with this solution, maybe not definitive, but it
addresses most of my concerns like:
</p>
<ul>
<li>Single point of failure using Raspberry Pi</li>
<li>Forgetting to update Pi manually with the latest code after a git push</li>
<li>Secrets exposed in environment variables</li>
</ul>
<p>
And as a bonus, you get notified via email if your job ever fails,
giving free visibility that you would otherwise have to put together
yourself in services like CRON to figure out if a job ran successfully
or not.
</p>
<p>
Bullish is an open source project on
<a href="https://github.com/eduardosasso/bullish">GitHub</a>.
</p>
<p>Cheers.</p>
</section>
</body>
<script>
window.ga =
window.ga ||
function () {
(ga.q = ga.q || []).push(arguments);
};
ga.l = +new Date();
ga("create", "UA-148146327-2", "auto");
ga("send", "pageview");
</script>
<script async src="https://www.google-analytics.com/analytics.js"></script>
</html>