zandoh/tonk · GitHub Actions → Discord · MIT

One ping when it breaks. One when it’s fixed.

tonk watches your GitHub Actions workflows and posts to Discord at exactly two moments: when a workflow fails, and when it goes back to green after failing. Not every red run. Not every routine success. No server, no database, nothing to host.

eight runs of CI on main — what tonk posts, and what it doesn’t
#212quiet
#213quiet
#214posts ❌
#215still red
quiet
#216still red
quiet
#217posts ✅
#218quiet
#219quiet
# build-status
tonkAPPToday at 09:41
❌ CI failed on main
fix: handle empty webhook payload (3f2c1a8)
tonkAPPToday at 10:17
✅ CI back to green on main
fix: quote the payload before posting (8b41d2e)

How it works

Posting an embed is easy.
Knowing when not to is the product.

The hard part of CI notifications is state: knowing that this red build is worth a ping, and that this green build is the first green after a red streak — not the hundredth routine success. Most tools solve that with a service and a database.

tonk keeps no state at all. At notification time it asks the GitHub API for the previous completed run of the same workflow on the same branch. If that run was red and this one is green, that’s a recovery — one back to green per incident, never one per green push.

Reusable workflow

notify.yml — call it from a thin workflow_run caller and get failure alerts plus recovery detection for every workflow you name. Runs outside your workflows, so a run that dies before its own notify step would have executed still reports.

Composite action

action.yml at the repo root — posts one embed from any step, for anything that isn’t failure or recovery: release announcements, digests, deploy summaries. The same guard rails apply.

Pull-request activity

pr.yml — a second reusable workflow that posts opened, reopened, merged, and closed embeds under the tonk name and icon. Everything Discord’s first-party GitHub app sends, minus the “GitHub” branding.

Quick start

Three steps, one file, done.

Store a Discord webhook as a secret

Create the webhook in Discord (channel → Integrations → Webhooks), then:

shell
gh secret set TONK_DISCORD_WEBHOOK --repo you/your-repo

Add a thin caller workflow

Name the workflows to watch — workflow_run requires explicit names, no wildcards. This file is the whole integration:

.github/workflows/tonk.yml
name: tonk
on:
  workflow_run:
    workflows: [CI, Release]
    types: [completed]
permissions:
  actions: read # recovery detection reads run history
  contents: read # reads .github/discord.yml — required on private repos
jobs:
  notify:
    uses: zandoh/tonk/.github/workflows/notify.yml@v2
    secrets:
      webhook: ${{ secrets.TONK_DISCORD_WEBHOOK }}

Optionally, give the repo a personality

Add .github/discord.yml to set the bot name and toggle which events post — see Configuration. Skip it and sensible defaults apply.

That’s it. Failures in the watched workflows post a red embed; the first green run after a failure posts a back to green — and only the first.

Want pull-request activity too? Add a pull_request trigger and a second job to the same tonk.yml — opened, reopened, merged, and closed each post a tonk-branded embed, so you can retire Discord’s first-party GitHub app. Each event runs only its matching job:

.github/workflows/tonk.yml
name: tonk
on:
  workflow_run:
    workflows: [CI, Release]
    types: [completed]
  pull_request:
    types: [opened, reopened, closed]
permissions:
  actions: read
  contents: read
jobs:
  ci:
    if: github.event_name == 'workflow_run'
    uses: zandoh/tonk/.github/workflows/notify.yml@v2
    secrets:
      webhook: ${{ secrets.TONK_DISCORD_WEBHOOK }}
  pr:
    if: github.event_name == 'pull_request'
    uses: zandoh/tonk/.github/workflows/pr.yml@v2
    secrets:
      webhook: ${{ secrets.TONK_DISCORD_WEBHOOK }}

Configuration

One small YAML file, every key optional.

tonk reads .github/discord.yml from the default branch of the calling repo (the base ref for pr.yml). No file means all defaults.

.github/discord.yml
username: Hearth CI
notify:
  failure: true
  recovery: true
  pull-requests: false
  pull-request-activity: true
KeyDefaultMeaning
usernametonkBot name shown on posts
avatarthe tonk iconBot avatar image URL
notify.failuretruePost when a watched workflow fails
notify.recoverytruePost the first green after a failure
notify.pull-requestsfalseInclude PR-triggered CI runs in failure/recovery — off by default, because red PRs are part of iterating
notify.pull-request-activitytruePost PR opened / reopened / merged / closed embeds (pr.yml)

One-off embeds

Anything else worth announcing.

For notifications that aren’t failure or recovery — releases, digests, deploy summaries — call the composite action directly from any step:

any workflow step
- name: Announce release on Discord
  uses: zandoh/tonk@v2
  with:
    webhook-url: ${{ secrets.TONK_DISCORD_WEBHOOK }}
    title: 🚀 my-tool v1.2.3 released
    description: ${{ steps.notes.outputs.body }}
    url: https://github.com/you/my-tool/releases/tag/v1.2.3
    color: "5793266"
tonkAPPToday at 16:20
🚀 my-tool v1.2.3 released
Adds retry with backoff, fixes the flaky websocket reconnect, and drops Node 18 support.

Action inputs

InputRequiredDefaultNotes
webhook-urlyesEmpty value skips silently (fork-safe)
titleyesTruncated to 256 chars
statusnosuccess / failure; adds ✅/❌ prefix and sets the default color
descriptionnorepo · branch · sha + commit subjectTruncated to 4096 chars
urlnocurrent run URLLink target for the title
colornofrom statusDecimal; failure 15548997, success 5763719, otherwise 3447003
usernamenotonkBot name for this post
avatar-urlnothe tonk iconBot avatar image URL for this post
footernoworkflow name

Guarantees

A notification must never break the thing it reports on.

Unset webhook → silent skip

Fork PRs don’t receive secrets; their builds must not fail because of a missing notification credential. An empty webhook-url logs a line and exits 0.

Discord outage → warning, not failure

Delivery errors emit a ::warning:: annotation and exit 0. A Discord outage shouldn’t flip a green run red.

Untrusted text can’t inject

Titles, commit messages, and bodies flow through env vars into jq — never through shell interpolation.

Versioning: v1 is a moving major tag; the reusable workflow and the composite action are tagged together and reference each other at the same major. Pin @v1, or a commit SHA if your repo hash-pins actions.