eduardosasso/bullish

View on GitHub
site/blog/exploiting-github-actions/index.html

Summary

Maintainability
Test Coverage
<!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>