GitHub Actions integration
Monitor scheduled workflows, including the cancelled runs and schedule drift that look like your fault.
name: Nightly sync
on:
schedule:
- cron: "0 2 * * *"
jobs:
sync:
runs-on: ubuntu-latest
env:
DRUMBEATS_MONITOR: 11111111-2222-3333-4444-555555555555
RUN_ID: "gh-${{ github.run_id }}-${{ github.run_attempt }}"
steps:
- uses: actions/checkout@v4
- name: Drumbeats start
run: curl -sf --max-time 3 "https://api.drumbeats.io/v1/ping/$DRUMBEATS_MONITOR/start?run_id=$RUN_ID"
- name: Run sync
run: ./scripts/sync.sh
- name: Drumbeats success
if: success()
run: curl -sf --max-time 3 "https://api.drumbeats.io/v1/ping/$DRUMBEATS_MONITOR/success?run_id=$RUN_ID"
- name: Drumbeats failure
if: failure()
run: |
curl -sf --max-time 3 -X POST \
"https://api.drumbeats.io/v1/ping/$DRUMBEATS_MONITOR/failure?run_id=$RUN_ID" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg p "Workflow ${{ github.workflow }} failed. ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" '{payload: $p}')"name: Nightly sync
on:
schedule:
- cron: "0 2 * * *"
jobs:
sync:
runs-on: ubuntu-latest
env:
DRUMBEATS_MONITOR: 11111111-2222-3333-4444-555555555555
RUN_ID: "gh-${{ github.run_id }}-${{ github.run_attempt }}"
steps:
- uses: actions/checkout@v4
- name: Drumbeats start
run: curl -sf --max-time 3 "https://api.drumbeats.io/v1/ping/$DRUMBEATS_MONITOR/start?run_id=$RUN_ID"
- name: Run sync
run: ./scripts/sync.sh
- name: Drumbeats success
if: success()
run: curl -sf --max-time 3 "https://api.drumbeats.io/v1/ping/$DRUMBEATS_MONITOR/success?run_id=$RUN_ID"
- name: Drumbeats failure
if: failure()
run: |
curl -sf --max-time 3 -X POST \
"https://api.drumbeats.io/v1/ping/$DRUMBEATS_MONITOR/failure?run_id=$RUN_ID" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg p "Workflow ${{ github.workflow }} failed. ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" '{payload: $p}')"if: success() and if: failure() make exactly one of the two final steps run. Including github.run_attempt in the run ID keeps re-runs from colliding with the original.
Put the run's own URL in the failure payload. The alert then links straight to the failing workflow, which saves a hop during triage.
Use a reusable workflow#
Past two monitored workflows, hoist the pattern so each consumer is a few lines:
name: With Drumbeats
on:
workflow_call:
inputs:
monitor-id:
required: true
type: string
command:
required: true
type: string
jobs:
run:
runs-on: ubuntu-latest
env:
RUN_ID: "gh-${{ github.run_id }}-${{ github.run_attempt }}"
API: "https://api.drumbeats.io/v1/ping/${{ inputs.monitor-id }}"
steps:
- uses: actions/checkout@v4
- name: Drumbeats start
run: curl -sf --max-time 3 "$API/start?run_id=$RUN_ID"
- name: Run command
run: ${{ inputs.command }}
- name: Drumbeats success
if: success()
run: curl -sf --max-time 3 "$API/success?run_id=$RUN_ID"
- name: Drumbeats failure
if: failure()
run: curl -sf --max-time 3 "$API/failure?run_id=$RUN_ID"name: With Drumbeats
on:
workflow_call:
inputs:
monitor-id:
required: true
type: string
command:
required: true
type: string
jobs:
run:
runs-on: ubuntu-latest
env:
RUN_ID: "gh-${{ github.run_id }}-${{ github.run_attempt }}"
API: "https://api.drumbeats.io/v1/ping/${{ inputs.monitor-id }}"
steps:
- uses: actions/checkout@v4
- name: Drumbeats start
run: curl -sf --max-time 3 "$API/start?run_id=$RUN_ID"
- name: Run command
run: ${{ inputs.command }}
- name: Drumbeats success
if: success()
run: curl -sf --max-time 3 "$API/success?run_id=$RUN_ID"
- name: Drumbeats failure
if: failure()
run: curl -sf --max-time 3 "$API/failure?run_id=$RUN_ID"name: Nightly sync
on:
schedule:
- cron: "0 2 * * *"
jobs:
sync:
uses: ./.github/workflows/with-drumbeats.yml
with:
monitor-id: 11111111-2222-3333-4444-555555555555
command: ./scripts/sync.shname: Nightly sync
on:
schedule:
- cron: "0 2 * * *"
jobs:
sync:
uses: ./.github/workflows/with-drumbeats.yml
with:
monitor-id: 11111111-2222-3333-4444-555555555555
command: ./scripts/sync.shGive each matrix cell its own run ID#
env:
RUN_ID: "gh-${{ github.run_id }}-${{ matrix.target }}"env:
RUN_ID: "gh-${{ github.run_id }}-${{ matrix.target }}"Without the matrix key, every cell sends overlapping start and finish pings to the same monitor and Drumbeats cannot pair them. Durations become meaningless and hung cells go undetected.
Know that scheduled workflows drift#
GitHub does not guarantee schedule: triggers fire on time. Under load they can be delayed by many minutes, and GitHub disables schedules entirely on repositories with no activity for 60 days.
Set grace_period_seconds generously, 900 seconds or more, on any monitor watching a scheduled workflow. A tight grace period on a GitHub-scheduled job produces false MISSED alerts that are nothing to do with your code.
What happens when it breaks#
| Situation | What Drumbeats sees | What to do |
|---|---|---|
| A step fails | failure from the if: failure() step | Nothing. This is the designed path |
| The run is cancelled | A start with no finish | Set max_duration_seconds on the monitor |
| The schedule fires late | Possibly a MISSED for a job that then succeeds | Widen grace_period_seconds |
| The schedule is disabled for inactivity | MISSED, repeatedly | Real signal. Re-enable it in the Actions tab |
| The runner itself dies | A start with no finish | Server-side hang detection |
How you get alerted#
A failure opens a FAILED incident once failure_tolerance is reached and pages every notification group on the monitor. If you put the run URL in the payload, the alert links back to the workflow.
Pick the right monitor type#
Use a Cron monitor for schedule: workflows, matching the same expression and timezone. Use an Event-driven monitor for workflow_dispatch and repository_dispatch, since those have no schedule to be late against.
Next#
Monitor types for that choice in more depth. Ping API for the endpoints these steps call. Production hardening for the language-agnostic rules. Alternatives if you are still choosing a vendor.