GitHub
Transports

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

  1. Go to github.com/settings/tokens and create a Fine-grained personal access token (or a classic PAT).
  2. Grant Read and Write access to Issues and Pull requests for the repositories you need.
  3. Copy the token.

Store it as an environment variable:

GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Install

npm install @betternotify/github @betternotify/core

Usage

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 statusBetter-Notify codeRetriable
401CONFIGNo
403CONFIGNo
404CONFIGNo
422VALIDATIONNo
429RATE_LIMITEDYes
5xxPROVIDERYes

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',
});

On this page