GitHub
Create issues, post comments, leave inline diff comments, and submit PR reviews via the GitHub REST API
The GitHub transport is included in @betternotify/github. It delivers notifications through the GitHub REST API as issues, issue comments, PR thread comments, inline diff comments, or pull request reviews. It uses plain fetch() with zero external dependencies, so it works in Node.js, Bun, Cloudflare Workers, and any runtime with a global fetch.
The package ships a single polymorphic channel — githubChannel() — with five actions: issues, issue comments, PR comments, PR line comments, and PR reviews.
Getting your token
- Go to github.com/settings/tokens and create a Fine-grained personal access token (or a classic PAT).
- Grant Read and Write access to Issues and Pull requests for the repositories you need.
- Copy the token.
Store it as an environment variable:
GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxInstall
npm install @betternotify/github @betternotify/coreUsage
Unlike email or SMS, the GitHub channel exposes five distinct actions through a single channel. Pick the action that matches what you want to do:
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! }),
},
});Creating issues
Use .issue() to create GitHub issues. The builder requires title and body slots:
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 send args
Prop
Type
Commenting on issues
Use .issueComment() to post a comment on an existing issue or pull request. The builder requires a body slot:
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
Issue comment send args
Prop
Type
Commenting on pull requests
Use .prComment() to post a simple thread comment on a pull request. The builder requires a body slot:
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 comment send args
Prop
Type
Inline PR line comments
Use .prLineComment() to leave an inline comment on a pull request diff with optional line range support. The builder requires a body slot:
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 line comment send args
Prop
Type
Reviewing pull requests
Use .prReview() to submit a pull request review. The builder requires a body slot:
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
PR review send args
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.
const github = githubChannel({ defaults: { repo: 'acme/api' } });
await notify.bugReport.send({
input: { summary: 'Typo', severity: 'low' },
repo: 'acme/docs',
});Channel options
Prop
Type
Transport options
Prop
Type
Batch sending
All five actions support .batch() for sending multiple notifications in a single call:
await notify.bugReport.batch([
{
input: { summary: 'Typo in footer', severity: 'low' },
labels: ['bug', 'low-priority'],
},
{
input: { summary: 'Broken docs link', severity: 'medium' },
labels: ['bug', 'docs'],
},
]);Verifying the token
Call verify() at startup to confirm the token is valid and see which GitHub user it belongs to:
const transport = githubTransport({ token: process.env.GITHUB_TOKEN! });
const { ok, details } = await transport.verify!();
if (!ok) {
throw new Error(`Invalid GitHub token: ${details}`);
}
console.log('Authenticated as:', details); // { login: 'octocat' }Endpoint: GET /user
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)),
});Mock transport
Use mockGithubTransport() for testing. It records every sent message and returns synthetic URLs:
import { mockGithubTransport } from '@betternotify/github';
const transport = mockGithubTransport();
const notify = createClient({
catalog,
transportsByChannel: { github: transport },
});
await notify.bugReport.send({
input: { summary: 'Test issue', severity: 'low' },
});
console.log(transport.messages);
// [{ action: 'issue', title: '...', body: '...', id: 'github-mock-1' }]Error handling
The transport maps HTTP status codes to Better-Notify error codes using the shared mapHttpStatus utility:
| HTTP status | Better-Notify code | Retriable |
|---|---|---|
| 401 | CONFIG | No |
| 403 | CONFIG | No |
| 404 | CONFIG | No |
| 422 | VALIDATION | No |
| 429 | RATE_LIMITED | Yes |
| 5xx | PROVIDER | Yes |
Network failures are wrapped as PROVIDER errors and timeouts as TIMEOUT errors.
Retriability helper
Use isGithubRetriable() to check whether an error from the GitHub transport is worth retrying:
import { isGithubRetriable } from '@betternotify/github';
if (!result.ok && isGithubRetriable(result.error)) {
// safe to retry
}GitHub Enterprise
Point the transport at your GitHub Enterprise Server instance by setting baseUrl:
const transport = githubTransport({
token: process.env.GHE_TOKEN!,
baseUrl: 'https://github.example.com/api/v3',
});