Table of Contents
- The problem
- How it works
- Design decisions and the alternatives
- What the run history shows
- A real outage, and a monitor that switched itself off
- Limits
The problem
The repository's README states the reason it exists in two sentences. On 23 July 2026
muhittincamdali.com went down and stayed down for about three and a half days, because all of its
monitoring ran on the same server as the site. When the server could not be reached, neither could
anything that would have reported it.
The fix is a check that lives somewhere else. site-uptime runs entirely on GitHub Actions: no
server, no secret beyond the token every workflow gets, and no account with a monitoring service.
The first commit is from 3 August 2026; the repository has three commits and two workflows, about
110 lines of YAML in total.
How it works
uptime.yml is scheduled with cron: "*/5 * * * *" and can also be started by hand. Each run
requests two URLs, the home page and /api/health/, and treats the site as up only if both return
HTTP 200. A failure is retried once after 30 seconds before it counts:
1probe() {2 HOME_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 20 "$SITE/" || echo 000)3 HEALTH_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 20 "$SITE/api/health/" || echo 000)4 echo "home=$HOME_CODE health=$HEALTH_CODE"5 [ "$HOME_CODE" = "200" ] && [ "$HEALTH_CODE" = "200" ]6}Checking both URLs separates two failures. The home page can be served while the application
behind it is broken; the health endpoint covers that. -m 20 caps each request at 20 seconds, and
|| echo 000 turns a connection failure into a status code the comparison can handle.
Design decisions and the alternatives
An issue as the alert and as the state. When both attempts fail, the workflow looks for an
open issue with the label outage. If there is none, it opens one with the time, the URLs it
checked and a short runbook; if there is one, it adds a "still down" comment. When the site answers
again, it comments and closes the issue:
1EXISTING=$(gh issue list --repo "$REPO" --state open --label outage --json number --jq '.[0].number // empty')2if [ -n "$EXISTING" ]; then3 NOW=$(date -u +"%Y-%m-%d %H:%M UTC")4 gh issue comment "$EXISTING" --repo "$REPO" --body "✅ RECOVERED at $NOW — site and health endpoint both 200."5 gh issue close "$EXISTING" --repo "$REPO"6fiThe issue title carries the time of the first failure, and the runbook in the body lists where to
look first: the hosting account, the server's network status and the application service, plus a
pointer to the July incident. A run can also be started by hand from the Actions tab
(*Site Uptime Check* → *Run workflow*), which is the quickest way to confirm a recovery.
This gives three things without any extra storage. Only one alert is opened per outage, because
the open issue is the record that an outage is in progress. Delivery is GitHub's: the README notes
that a new issue sends the owner an email. And the issue's comments become the outage log, with a
timestamp for every failed run.
The alternatives were a hosted monitoring service or a small check on a second machine. Both work,
but both add something to maintain: an account, a host, credentials. The whole point here was a
check that does not share anything with the server it watches; GitHub is already where the code
lives.
The smallest permissions that work. The workflow declares issues: write and
contents: read and uses only github.token. A failed run also ends with exit 1, so the Actions
tab shows red runs during an outage, not only the issue.
No overlapping runs. concurrency: group: uptime with cancel-in-progress: false queues a run
behind one that is still retrying instead of cancelling it. Two runs racing could otherwise both see
"no open issue" and open two.
What the run history shows
The schedule says every five minutes. That would be 288 runs a day. GitHub's documentation is
careful about this: scheduled runs can be delayed when Actions is under high load, and some queued
runs may be dropped. The repository's own run history shows how much that matters.
As of 15 October 2026 the uptime workflow had 1,082 runs in total since 3 August. For the last
200 runs, the gaps between scheduled runs were:
Period | Scheduled runs | Median gap | Shortest | Longest |
|---|---|---|---|---|
8–19 September 2026 | 83 | 184 min | 100 min | 355 min |
20 September–2 October | 73 | 248 min | 104 min | 498 min |
5–15 October 2026 | 43 | 319 min | 151 min | 585 min |
So the check runs every few hours, not every five minutes. For this site that is still the
difference that matters: the July outage went unnoticed for days, while the next one was reported by
the first run that saw it, even if that run came hours after the site went down.
A real outage, and a monitor that switched itself off
28–30 September 2026. The first failed run came at 07:34 UTC on 28 September, and issue #1 was
opened a minute later. Over the next two and a half days ten more runs failed, each adding a
"still down" comment. The run at 18:36 UTC on 30 September saw both URLs return 200, commented and
closed the issue. The issue and its comments are the whole record; the repository does not say what
caused the outage, so neither does this article.
2 October 2026. Two days later GitHub disabled the "Site Uptime Check" workflow. GitHub turns
off scheduled workflows in public repositories after 60 days without repository activity, and the
only commit was from 3 August. The scheduled runs themselves, including the ones that commented on
issue #1, did not keep it alive. A
monitor that silently stops is worse than none, because it still looks set up.
The fix, committed on 5 October, is a second workflow that makes a commit once a month. It writes a
timestamp to .github/keepalive through the contents API:
1FILE=.github/keepalive2STAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")3SHA=$(gh api "repos/$GITHUB_REPOSITORY/contents/$FILE" --jq .sha 2>/dev/null || true)4gh api -X PUT "repos/$GITHUB_REPOSITORY/contents/$FILE" \5 -f message="chore: keepalive $STAMP" \6 -f content="$(printf '%s\n' "$STAMP" | base64)" \7 ${SHA:+-f sha="$SHA"} > /dev/null${SHA:+-f sha="$SHA"} passes the existing file's SHA only when there is one, so the first run
creates the file and later runs update it. The workflow needs contents: write and uses no
third-party action. Its comment notes one more detail: a commit made with GITHUB_TOKEN does not
start other workflows, so the keepalive cannot trigger a cascade of runs. The first keepalive commit
was made by a manual run the same evening; the schedule (17 6 1 * *) takes over from 1 November.
Limits
- Resolution is hours, not minutes. The measured gaps above are what to expect. A short outage
between two runs is not seen at all.
- It depends on GitHub. If GitHub Actions or issue notifications are down, so is the alert.
That is acceptable here because the point is to be independent of the site's server, not of
everything.
- One location, two status codes. The check runs from GitHub's runners and only compares status
codes. It does not check page content, response time or certificate expiry.
- The keepalive is not yet proven by a full cycle. It was added after the shutdown; whether it
keeps the schedule alive across the next 60-day window will show in the run history.
- Single purpose. The site URL is written into the workflow, and the repository has no license
file. It is a working setup for one site, not a reusable tool.
Source code: github.com/muhittincamdali/site-uptime
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

