Documentation menu

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

  1. Create a heartbeat monitor with the workflow's cron expression and time zone UTC. GitHub schedules always run in UTC.
  2. Set the grace period to the workflow's normal run time plus at least 30 minutes, because GitHub may start scheduled runs late.
  3. Copy the ping URL (https://app.asniher.com/api/heartbeat/<token>) and add it as a repository secret named ASNIHER_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 /fail and alerts right away.
  • A run that never starts sends nothing and alerts when it is overdue.
  • || true on 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.

Questions about this page? Email support@asniher.com.