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.
quiet
quiet
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:
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:
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:
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.
username: Hearth CI
notify:
failure: true
recovery: true
pull-requests: false
pull-request-activity: true
| Key | Default | Meaning |
|---|---|---|
username | tonk | Bot name shown on posts |
avatar | the tonk icon | Bot avatar image URL |
notify.failure | true | Post when a watched workflow fails |
notify.recovery | true | Post the first green after a failure |
notify.pull-requests | false | Include PR-triggered CI runs in failure/recovery — off by default, because red PRs are part of iterating |
notify.pull-request-activity | true | Post 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:
- 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"
Action inputs
| Input | Required | Default | Notes |
|---|---|---|---|
webhook-url | yes | — | Empty value skips silently (fork-safe) |
title | yes | — | Truncated to 256 chars |
status | no | — | success / failure; adds ✅/❌ prefix and sets the default color |
description | no | repo · branch · sha + commit subject | Truncated to 4096 chars |
url | no | current run URL | Link target for the title |
color | no | from status | Decimal; failure 15548997, success 5763719, otherwise 3447003 |
username | no | tonk | Bot name for this post |
avatar-url | no | the tonk icon | Bot avatar image URL for this post |
footer | no | workflow 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.