GitHub
Channels

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/github

Setup

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 review

Each 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! }) },
  ],
});

On this page