GitHub
Create issues, post comments, leave inline diff comments, and submit PR reviews on GitHub
The GitHub channel is provided by @betternotify/github. It creates issues, posts issue comments, leaves simple PR thread comments, posts inline diff comments with line range support, and submits pull request reviews through the GitHub REST API.
When to use GitHub
GitHub is the right channel when your notifications are GitHub actions — filing bug reports, commenting on issues, leaving inline code review notes, or approving pull requests. Use it for automated triage, CI/CD feedback, release approvals, or any workflow that should produce a GitHub artifact rather than a message in a chat or inbox.
npm install @betternotify/githubSetup
import { createNotify } from '@betternotify/core';
import { githubChannel } from '@betternotify/github';
const github = githubChannel({ defaults: { repo: 'acme/api' } });
const rpc = createNotify({ channels: { github } });Actions
Unlike other channels that expose a flat builder (rpc.email(), rpc.slack()), the GitHub channel exposes five distinct actions through a picker:
rpc.github().issue() // create an issue
rpc.github().issueComment() // comment on an issue or PR
rpc.github().prComment() // simple PR thread comment
rpc.github().prLineComment() // inline comment on a PR diff with line range support
rpc.github().prReview() // submit a PR reviewEach action has its own slots and send arguments. You can mix all five in a single catalog.
Issue
Creates a new GitHub issue. Requires title and body slots.
Slots
Prop
Type
Send arguments
Prop
Type
Example
const catalog = rpc.catalog({
bugReport: rpc
.github()
.issue()
.input(z.object({ summary: z.string(), severity: z.string() }))
.title(({ input }) => `[${input.severity}] ${input.summary}`)
.body(({ input }) => `**Severity:** ${input.severity}\n\n${input.summary}`),
});
await notify.bugReport.send({
input: { summary: 'Login 500 error', severity: 'critical' },
labels: ['bug', 'critical'],
assignees: ['octocat'],
});Endpoint: POST /repos/{owner}/{repo}/issues
Issue Comment
Posts a comment on an existing issue or pull request. Requires a body slot.
Slots
Prop
Type
Send arguments
Prop
Type
Example
const catalog = rpc.catalog({
reviewAck: rpc
.github()
.issueComment()
.input(z.object({ reviewer: z.string(), message: z.string() }))
.body(({ input }) => `**${input.reviewer}:** ${input.message}`),
});
await notify.reviewAck.send({
input: { reviewer: 'octocat', message: 'Looking into this now.' },
issueNumber: 42,
});Endpoint: POST /repos/{owner}/{repo}/issues/{issue_number}/comments
PR Comment
Posts a simple thread comment on a pull request. Requires a body slot.
Slots
Prop
Type
Send arguments
Prop
Type
Example
const catalog = rpc.catalog({
ciFeedback: rpc
.github()
.prComment()
.input(z.object({ message: z.string() }))
.body(({ input }) => input.message),
});
await notify.ciFeedback.send({
input: { message: 'All checks passed. Ready to merge.' },
prNumber: 42,
});Endpoint: POST /repos/{owner}/{repo}/issues/{issue_number}/comments
PR Line Comment
Leaves an inline comment on a pull request diff with optional line range support. Requires a body slot.
Slots
Prop
Type
Send arguments
Prop
Type
Example
const catalog = rpc.catalog({
codeNote: rpc
.github()
.prLineComment()
.input(z.object({ note: z.string() }))
.body(({ input }) => input.note),
});
await notify.codeNote.send({
input: { note: 'This needs a null check.' },
prNumber: 42,
commitId: 'abc123def456',
path: 'src/index.ts',
line: 15,
});Endpoint: POST /repos/{owner}/{repo}/pulls/{pull_number}/comments
PR Review
Submits a pull request review. Requires a body slot.
Slots
Prop
Type
Send arguments
Prop
Type
Example
const catalog = rpc.catalog({
releaseApproval: rpc
.github()
.prReview()
.input(z.object({ version: z.string(), notes: z.string() }))
.body(({ input }) => `Approved for **${input.version}**.\n\n${input.notes}`),
});
await notify.releaseApproval.send({
input: { version: 'v2.0.0', notes: 'New auth flow, SSO fix.' },
prNumber: 123,
event: 'APPROVE',
});Endpoint: POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews
Full example
All five actions in a single catalog:
import { createNotify, createClient } from '@betternotify/core';
import { githubChannel, githubTransport } from '@betternotify/github';
import { z } from 'zod';
const github = githubChannel({ defaults: { repo: 'acme/api' } });
const rpc = createNotify({ channels: { github } });
const catalog = rpc.catalog({
bugReport: rpc
.github()
.issue()
.input(z.object({ summary: z.string(), details: z.string() }))
.title(({ input }) => `[Bug] ${input.summary}`)
.body(({ input }) => input.details),
reviewAck: rpc
.github()
.issueComment()
.input(z.object({ message: z.string() }))
.body(({ input }) => input.message),
ciFeedback: rpc
.github()
.prComment()
.input(z.object({ message: z.string() }))
.body(({ input }) => input.message),
codeNote: rpc
.github()
.prLineComment()
.input(z.object({ note: z.string() }))
.body(({ input }) => input.note),
releaseApproval: rpc
.github()
.prReview()
.input(z.object({ version: z.string() }))
.body(({ input }) => `Approved for release **${input.version}**.`),
});
const notify = createClient({
catalog,
transportsByChannel: {
github: githubTransport({ token: process.env.GITHUB_TOKEN! }),
},
});
await notify.bugReport.send({
input: { summary: 'Login 500 error', details: 'Users see 500 on SSO login.' },
labels: ['bug'],
});
await notify.reviewAck.send({
input: { message: 'On it.' },
issueNumber: 42,
});
await notify.ciFeedback.send({
input: { message: 'All checks passed.' },
prNumber: 100,
});
await notify.codeNote.send({
input: { note: 'Missing null check here.' },
prNumber: 100,
commitId: 'abc123',
path: 'src/auth.ts',
line: 42,
});
await notify.releaseApproval.send({
input: { version: 'v2.0.0' },
prNumber: 100,
event: 'APPROVE',
});Channel options
Prop
Type
Repository resolution
The repo field follows a fallback chain: per-send repo arg → defaults.repo from githubChannel(). If neither is set, the transport returns a VALIDATION error.
The format must be owner/repo (e.g. acme/api). Invalid formats also return a VALIDATION error.
Using with Handlebars
The title and body slots accept resolver functions, so you can plug in any template engine:
import Handlebars from 'handlebars';
const titleTpl = Handlebars.compile('[{{severity}}] {{summary}}');
const bodyTpl = Handlebars.compile('**Severity:** {{severity}}\n\n{{details}}');
const catalog = rpc.catalog({
bugReport: rpc
.github()
.issue()
.input(z.object({ summary: z.string(), details: z.string(), severity: z.string() }))
.title(({ input }) => titleTpl(input))
.body(({ input }) => bodyTpl(input)),
});Transport
Built-in: githubTransport
The package includes a ready-to-use transport that calls the GitHub REST API via fetch:
import { githubTransport } from '@betternotify/github';
const transport = githubTransport({
token: process.env.GITHUB_TOKEN!,
});For GitHub Enterprise, set baseUrl:
const transport = githubTransport({
token: process.env.GHE_TOKEN!,
baseUrl: 'https://github.example.com/api/v3',
});Call transport.verify() to validate your token at startup via GET /user.
See GitHub transport for full transport options, error handling, and per-send overrides.
Mock transport
Use mockGithubTransport() in tests:
import { mockGithubTransport } from '@betternotify/github';
const transport = mockGithubTransport();
// after sending...
console.log(transport.messages); // [{ action: 'issue', title: '...', body: '...', id: 'github-mock-1' }]Multi-transport
Use multiTransport from core for failover across multiple GitHub tokens or instances:
import { githubTransport } from '@betternotify/github';
import { multiTransport } from '@betternotify/core/transports';
const transport = multiTransport({
strategy: 'failover',
transports: [
{ transport: githubTransport({ token: process.env.GITHUB_TOKEN_PRIMARY! }) },
{ transport: githubTransport({ token: process.env.GITHUB_TOKEN_FALLBACK! }) },
],
});