Notification channels

Discord is the default. Slack and ntfy work the same way. You can run more than one at a time.

Turning one on

Two things are needed for each channel: its name in CHANNELS, and its credential as a GitHub Actions secret.

ChannelSecretValue
discordDISCORD_WEBHOOKA webhook URL from a channel in your server.
slackSLACK_WEBHOOKAn incoming webhook URL from your workspace.
ntfyNTFY_URLA full topic URL, for example https://ntfy.sh/your-topic.

Then edit CHANNELS in qoffee/settings.py, comma separated, in priority order.

CHANNELS = "discord,slack,ntfy"

If you name a channel and its secret is missing or does not look like a URL, the run fails immediately with exit code 1 and says which secret is empty. It does not start work and discover three hours later that it cannot report the result.

Which channel has to succeed

By default, the first channel in the list must confirm delivery before any tag is updated. The others are allowed to fail.

This matters because of the ordering: nothing is untagged until the message is known to have arrived. If every configured channel were optional, a single broken webhook could let a job drop silently out of tracking without you ever being told it failed.

You can name the required channels explicitly.

CHANNELS = "discord,ntfy"
REQUIRED_CHANNELS = "discord,ntfy"

With that, both have to confirm. If either fails, no tags change and the same news is sent again next run. Naming a channel in REQUIRED_CHANNELS that is not in CHANNELS is a configuration error and fails at startup.

What each channel does with the message

Qoffee builds a message as structure, not as text: a title, some sections, and items with a status and an optional error detail. Each channel turns that into whatever its service expects. Discord wants **bold**, Slack wants *bold*, ntfy wants neither.

Every service also caps message length. Long batches are split on line boundaries into as many posts as needed, so a job is never truncated halfway through and a post is never rejected for being oversized.

Adding another one

A channel knows two things: how to render a message, and how to post it. It knows nothing about jobs, tags or tracking, which is what makes adding one a single self contained file plus one line in the registry.

Telegram, email, Teams and generic webhooks would all fit.

The channels live in qoffee/channels/, one file each, and registry.py maps a name to its secret and constructor.