<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aaron</title>
    <description>The latest articles on DEV Community by Aaron (@aaronphilip2003).</description>
    <link>https://dev.to/aaronphilip2003</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F947209%2F1cd1b1bf-39ed-4254-9f03-6839baf51318.png</url>
      <title>DEV Community: Aaron</title>
      <link>https://dev.to/aaronphilip2003</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aaronphilip2003"/>
    <language>en</language>
    <item>
      <title>The Free Cron Scheduler Hiding in Every GitHub Repo You Own</title>
      <dc:creator>Aaron</dc:creator>
      <pubDate>Thu, 18 Jun 2026 05:08:44 +0000</pubDate>
      <link>https://dev.to/aaronphilip2003/the-free-cron-scheduler-hiding-in-every-github-repo-you-own-2h0c</link>
      <guid>https://dev.to/aaronphilip2003/the-free-cron-scheduler-hiding-in-every-github-repo-you-own-2h0c</guid>
      <description>&lt;p&gt;I used it to build a zero-cost "dead man's switch" that catches silent failures before I do.&lt;/p&gt;

&lt;p&gt;Quick context before anything else: I packaged this exact recipe, plus six more like it, into a small cookbook — &lt;a href="https://aaronverse22672.gumroad.com/l/wyaqnl" rel="noopener noreferrer"&gt;The GitHub Actions Cookbook&lt;/a&gt; ($10, instant download, PDF + ready-to-fork workflow files). Everything below is the full, real recipe from it, not a watered-down preview. If it's useful to you, the other six are built the same way.&lt;/p&gt;

&lt;p&gt;Now, here's the thing almost nobody talks about.&lt;/p&gt;

&lt;h2&gt;
  
  
  You already have a free cron scheduler. You're just not using it.
&lt;/h2&gt;

&lt;p&gt;If you've ever set up GitHub Actions, it was probably for one job: run the tests, build the thing, ship it on push. That's the entire relationship most developers have with it.&lt;/p&gt;

&lt;p&gt;Buried in the same feature, though, is a &lt;code&gt;schedule&lt;/code&gt; trigger. It runs on standard cron syntax. It costs nothing on a public repo, and a generous free allowance on a private one. It runs on infrastructure you never provision, patch, or get paged about at 3am. You already pay for none of it, and you already have it, in every repo you own.&lt;/p&gt;

&lt;p&gt;I started actually using it after a small, annoying failure taught me a lesson the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: things fail silently
&lt;/h2&gt;

&lt;p&gt;I had a script — doesn't matter exactly what it did, the shape of the problem is the point — that was supposed to run on a schedule and quietly do its job. One day I needed it, and it hadn't run in over two weeks. No error. No email. No red X anywhere. It just stopped, and nothing in my world noticed.&lt;/p&gt;

&lt;p&gt;That's the specific kind of failure that's worse than a crash: a crash is loud. Silence is invisible until the exact moment you need the thing that silently stopped happening.&lt;/p&gt;

&lt;p&gt;The fix isn't complicated once you frame it correctly: you don't need to monitor the process itself, you need something else watching for its &lt;em&gt;absence&lt;/em&gt;. That's the entire idea behind a "dead man's switch," and it turns out GitHub Actions is a genuinely good place to build one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recipe: a heartbeat, and something watching it
&lt;/h2&gt;

&lt;p&gt;The shape is simple. Something — your script, your other system, even you manually — touches a file in a repo on a schedule, proving it's alive. A separate, scheduled GitHub Actions job checks how long it's been since that file last changed. If it's been too long, it sends you an alert. If not, it does nothing, which is exactly what you want 99% of the time.&lt;/p&gt;

&lt;p&gt;Here's the complete workflow file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Heartbeat Monitor&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;   &lt;span class="c1"&gt;# daily at 9am UTC&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check-heartbeat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;   &lt;span class="c1"&gt;# full history — required, see below&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check last heartbeat&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;LAST=$(git log -1 --format=%ct -- heartbeat.txt)&lt;/span&gt;
          &lt;span class="s"&gt;NOW=$(date +%s)&lt;/span&gt;
          &lt;span class="s"&gt;HOURS=$(( (NOW - LAST) / 3600 ))&lt;/span&gt;
          &lt;span class="s"&gt;echo "Hours since last heartbeat: $HOURS"&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$HOURS" -gt 24 ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "STALE=true" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Alert if stale&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env.STALE == 'true'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -X POST -H 'Content-Type: application/json' \&lt;/span&gt;
            &lt;span class="s"&gt;-d '{"content": "No heartbeat in 24+ hours."}' \&lt;/span&gt;
            &lt;span class="s"&gt;"${{ secrets.DISCORD_WEBHOOK_URL }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking through what's actually happening: the job runs once a day and pulls the repo's full git history (that &lt;code&gt;fetch-depth: 0&lt;/code&gt; matters, more on that in a second). It looks at when &lt;code&gt;heartbeat.txt&lt;/code&gt; was last touched, using &lt;code&gt;git log&lt;/code&gt; rather than the filesystem's modified time, because git history survives a fresh checkout and a file's mtime doesn't. It does the arithmetic in plain bash, sets an environment flag if too much time has passed, and only fires the alert step if that flag is set. No alert fires on a normal day — the workflow just runs, finds nothing wrong, and exits quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up, start to finish
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an empty &lt;code&gt;heartbeat.txt&lt;/code&gt; at the root of your repo and commit it once.&lt;/li&gt;
&lt;li&gt;Whatever process you're actually monitoring needs to update and commit that file on its own schedule — a single line with a timestamp is enough.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;DISCORD_WEBHOOK_URL&lt;/code&gt; repository secret (&lt;strong&gt;Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt;), pointed at a Discord channel webhook. Slack's incoming-webhook format works too, with a small tweak to the payload shape.&lt;/li&gt;
&lt;li&gt;Adjust the &lt;code&gt;24&lt;/code&gt; in the threshold check to match how often your process is actually supposed to run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole setup. Ten minutes, generously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing that will bite you
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fetch-depth: 0&lt;/code&gt; is not a throwaway line — it's the part of this recipe that breaks silently if you skip it. GitHub's default checkout only pulls the most recent commit, not the full history. Without the full history, &lt;code&gt;git log -1 --format=%ct -- heartbeat.txt&lt;/code&gt; either returns nothing or returns the wrong timestamp, and your dead man's switch quietly stops doing the one thing it exists to do — catching silence. It fails exactly the way the thing it's supposed to monitor failed, which is a little too on-the-nose for my taste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes from here
&lt;/h2&gt;

&lt;p&gt;Once this pattern clicks, it generalizes fast: schedule trigger, a step that checks or fetches something, a conditional step that reacts. Swap the middle step and you get a different tool entirely. I've used the same shape to build a free uptime watchdog for a side project's API, an automatic cleaner for issues nobody ever closes, and a script that quietly turns a changing metric into a personal dataset by committing one line to a CSV every day — no database, no server, just git.&lt;/p&gt;

&lt;p&gt;Those three, plus three more, are written up the same way — full code, real setup steps, the specific gotcha that catches people — in &lt;a href="https://aaronverse22672.gumroad.com/l/wyaqnl" rel="noopener noreferrer"&gt;The GitHub Actions Cookbook&lt;/a&gt;. Seven recipes, a PDF walkthrough, and all seven workflows as standalone files you can drop straight into &lt;code&gt;.github/workflows/&lt;/code&gt;. Ten dollars, and if you've got a GitHub account, you already have everything else you need.&lt;/p&gt;




&lt;p&gt;What's the most useful thing you've automated with GitHub Actions that has nothing to do with CI? Curious what other "free cron job" tricks people are running.&lt;/p&gt;

</description>
      <category>github</category>
      <category>automation</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
