GitHub Actions
Scheduled workflows fail quietly. A run errors and the notification email gets lost, GitHub starts it late or skips it under load, or the schedule is disabled after 60 days without activity in a public repository. A heartbeat monitor catches each of these, because a missing ping is an alert.
You create monitors in the dashboard under Monitors → New monitor. The workflows below only send pings, so they need no API key.
Setup
- Create a heartbeat monitor with the workflow's cron expression and time zone
UTC. GitHub schedules always run in UTC. - Set the grace period to the workflow's normal run time plus at least 30 minutes, because GitHub may start scheduled runs late.
- Copy the ping URL (
https://app.asniher.com/api/heartbeat/<token>) and add it as a repository secret namedASNIHER_HEARTBEAT_URL(Settings → Secrets and variables → Actions).
Report the whole workflow
Ping /start in the first job and report the result in a final job that runs whatever happened. The body links the ping to the run, so the ping log points straight at the logs.
name: nightly-report
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
env:
ASNIHER_HEARTBEAT_URL: ${{ secrets.ASNIHER_HEARTBEAT_URL }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
jobs:
start:
runs-on: ubuntu-latest
steps:
- run: curl -fsS -m 10 --retry 3 -o /dev/null -X POST "$ASNIHER_HEARTBEAT_URL/start" || true
report:
needs: start
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- run: ./scripts/generate-report.sh
heartbeat:
needs: [start, report]
if: always()
runs-on: ubuntu-latest
steps:
- name: Report success
if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
run: curl -fsS -m 10 --retry 3 -o /dev/null --data-raw "$RUN_URL" "$ASNIHER_HEARTBEAT_URL"
- name: Report failure
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: curl -fsS -m 10 --retry 3 -o /dev/null --data-raw "$RUN_URL" "$ASNIHER_HEARTBEAT_URL/fail"
- A failed or cancelled job, including one stopped by
timeout-minutes, sends/failand alerts right away. - A run that never starts sends nothing and alerts when it is overdue.
|| trueon the start ping keeps a network problem from failing the workflow.
Add every job of the workflow to the needs list of the heartbeat job.
Report a single step
For a workflow with one important step, run it through asniher-run. It pings /start, runs the command, and reports the exit status with the last 10 KB of output. The step fails when the command fails, as before.
- name: Generate report
run: |
curl -fsSL https://app.asniher.com/asniher-run.sh -o "$RUNNER_TEMP/asniher-run"
sh "$RUNNER_TEMP/asniher-run" -- ./scripts/generate-report.sh
env:
ASNIHER_HEARTBEAT_URL: ${{ secrets.ASNIHER_HEARTBEAT_URL }}
The output appears in the ping log and in failure alerts. Do not print secrets in a step you report this way.
Self-hosted and Windows runners
asniher-run and the curl commands above work on Linux and macOS runners. On Windows runners, use PowerShell:
- name: Report success
shell: pwsh
run: Invoke-RestMethod -Method Post -Uri $env:ASNIHER_HEARTBEAT_URL -Body $env:RUN_URL -TimeoutSec 10
Monitor what you deploy
A deploy workflow does not need a heartbeat. Create an HTTP monitor for the deployed site's health endpoint once, and it checks the site around the clock, not only when a workflow runs. Add a status badge to the README to show the result.
Check that it works
Run the workflow by hand from the Actions tab (the workflow_dispatch trigger above allows it). The monitor's ping log shows a start ping, then a success or failure ping with the run's URL as output.