GitHub Actions integration

Monitor scheduled workflows, including the cancelled runs and schedule drift that look like your fault.

.github/workflows/nightly-sync.yml
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:

.github/workflows/with-drumbeats.yml
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"
.github/workflows/nightly-sync.yml
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.sh
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.sh

Give each matrix cell its own run ID#

yaml
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#

SituationWhat Drumbeats seesWhat to do
A step failsfailure from the if: failure() stepNothing. This is the designed path
The run is cancelledA start with no finishSet max_duration_seconds on the monitor
The schedule fires latePossibly a MISSED for a job that then succeedsWiden grace_period_seconds
The schedule is disabled for inactivityMISSED, repeatedlyReal signal. Re-enable it in the Actions tab
The runner itself diesA start with no finishServer-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.