@rvaiglobal/github-issue-creator
v0.1.1
Published
Tiny zero-dependency backend library for GitHub issue operations (create, get, list, update) with a PAT read from the environment.
Readme
github-issue-creator
A tiny, zero-dependency backend library for GitHub issue operations. It reads a Personal Access Token and a default repository from the environment and exposes five functions — create, get, list, list-all, and update issues.
- Zero runtime dependencies — uses the native
fetchin Node>=20.19. - Backend only — no CLI, no server, no browser, no UI. Your app owns the route and auth.
- Token stays server-side — never returned in a result, never logged.
- Raw GitHub objects — returns exactly what the GitHub REST API returns; no custom schema to learn.
Table of contents
- Install
- Quick start
- Configuration
- Creating a token (PAT)
- API reference
- Error handling
- Using it from a frontend
- Notes & caveats
- License
Install
npm install github-issue-creator
# or
pnpm add github-issue-creator
# or
yarn add github-issue-creatorRequires Node >= 20.19 (uses the built-in global fetch). ESM only —
import, not require.
Quick start
import { createIssue } from "github-issue-creator";
// With GITHUB_TOKEN and GITHUB_REPOSITORY set in the environment:
const issue = await createIssue({ title: "Login broken", body: "Steps to reproduce..." });
console.log(issue.number, issue.html_url);Configuration
The token and the target repository are read from two environment variables:
| Variable | Example | Meaning |
|---|---|---|
| GITHUB_TOKEN | ghp_xxx / github_pat_xxx | Personal Access Token used for every request. |
| GITHUB_REPOSITORY | octocat/hello-world | Default repo in owner/repo form (the GitHub Actions convention). |
Set them on your backend (e.g. a .env file loaded with node --env-file=.env,
your process manager, or your host's secret store):
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxx
GITHUB_REPOSITORY=octocat/hello-worldPer-call overrides. Every function accepts optional connection fields, so the env vars are not mandatory — you can pass them explicitly instead:
await createIssue({ title: "x", token: process.env.OTHER_TOKEN, repo: "acme/other" });
await listIssues({ owner: "acme", repo: "web" });If a token cannot be resolved (no env var and no override) the function throws a configuration error before making any network call. The same applies to a missing or malformed repository.
Creating a token (PAT)
The minimum permission for all five functions is Issues: Read and write.
Fine-grained token (recommended)
- https://github.com/settings/personal-access-tokens/new
- Repository access → Only select repositories → choose your repo(s) (or All repositories).
- Permissions → Repository → Issues → Read and write (Metadata: Read-only is added automatically).
- Generate token and copy it.
Classic token
- https://github.com/settings/tokens → Tokens (classic) → Generate new token.
- Select the
reposcope (orpublic_repofor public repos only). - Generate and copy it.
Treat the token as a secret. Keep it server-side, never commit it, never expose it to a browser. Add
.envto your.gitignore.
API reference
All functions are async, repo-scoped, and return GitHub's raw response
objects (so issue.number, issue.html_url, issue.state, issue.labels,
etc. are GitHub's own fields). Any non-2xx response throws a
GitHubApiError.
Every function takes a single options object. Beyond the fields listed per
function, all of them also accept the shared
connection options (token, owner, repo, baseUrl).
createIssue(options)
Creates an issue. Optional fields are omitted from the request when not provided.
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | ✅ | Issue title. |
| body | string | — | Markdown body. |
| labels | string[] | — | Label names. |
| assignees | string[] | — | GitHub usernames to assign. |
| milestone | number | — | Milestone number (not title). |
Returns: the created issue object. Throws on validation errors (e.g. a
nonexistent assignee → 422) or missing permission (403).
const issue = await createIssue({
title: "Checkout fails on Safari",
body: "Tested on Safari 17. Stack trace attached.",
labels: ["bug", "checkout"],
assignees: ["octocat"],
milestone: 3,
});
console.log(issue.number); // e.g. 128
console.log(issue.html_url); // https://github.com/owner/repo/issues/128getIssue(options)
Fetches a single issue by number.
| Field | Type | Required | Description |
|---|---|---|---|
| number | number | ✅ | Issue number (positive integer). |
Returns: the issue object. Throws 404 if the issue does not exist.
const issue = await getIssue({ number: 42 });
console.log(issue.state); // "open" | "closed"listIssues(options)
Returns one page of issues for the repository.
| Field | Type | Default | Description |
|---|---|---|---|
| page | number | 1 | Page number. |
| perPage | number | 30 | Items per page (max 100). |
| state | string | open | open | closed | all. |
| labels | string | — | Comma-separated label names, e.g. "bug,ui". |
| assignee | string | — | Username, none, or *. |
| creator | string | — | Username of the issue author. |
| mentioned | string | — | Username mentioned in the issue. |
| milestone | string \| number | — | Milestone number, none, or *. |
| since | string | — | ISO 8601 timestamp; only issues updated at/after. |
| sort | string | created | created | updated | comments. |
| direction | string | desc | asc | desc. |
Returns: { issues, hasNextPage, nextPage }
| Property | Type | Description |
|---|---|---|
| issues | object[] | The page of issue objects. |
| hasNextPage | boolean | Whether another page exists. |
| nextPage | number \| null | The next page number, or null. |
let page = 1;
do {
const { issues, hasNextPage, nextPage } = await listIssues({ state: "all", page, perPage: 100 });
for (const issue of issues) console.log(issue.number, issue.title);
page = nextPage;
if (!hasNextPage) break;
} while (page);Pull requests are included. GitHub's list-issues endpoint returns PRs as issues, and this library does not filter them out. Skip them with
issues.filter((i) => !i.pull_request)if you only want true issues.
listAllIssues(options)
Auto-paginates and returns every matching issue as a single array.
Accepts the same filters as listIssues, plus:
| Field | Type | Default | Description |
|---|---|---|---|
| maxItems | number | Infinity | Stop after collecting this many items. |
| perPage | number | 100 | Page size used while paginating. |
Returns: object[] — all matching issues (capped at maxItems).
const open = await listAllIssues({ state: "open" });
console.log(`${open.length} open issues`);
// Cap the work on large repos:
const recent = await listAllIssues({ state: "all", sort: "updated", maxItems: 200 });updateIssue(options)
Updates an existing issue. Only the fields you pass are sent; everything else is left unchanged.
| Field | Type | Required | Description |
|---|---|---|---|
| number | number | ✅ | Issue number. |
| title | string | — | New title. |
| body | string | — | New body. |
| state | string | — | open or closed (close/reopen). |
| labels | string[] | — | Replaces the label set. |
| assignees | string[] | — | Replaces the assignee set. |
| milestone | number \| null | — | Milestone number, or null to clear. |
Returns: the updated issue object.
// Close an issue
await updateIssue({ number: 42, state: "closed" });
// Reopen and relabel
await updateIssue({ number: 42, state: "open", labels: ["bug", "regression"] });Connection options
Every function accepts these optional fields to override the environment:
| Field | Type | Default | Description |
|---|---|---|---|
| token | string | process.env.GITHUB_TOKEN | PAT for this call. |
| repo | string | from GITHUB_REPOSITORY | "owner/repo". |
| owner | string | — | Owner; use together with a bare repo name. |
| baseUrl | string | https://api.github.com | API base URL — set for GitHub Enterprise. |
Repository resolution order: explicit { owner, repo } → explicit
{ repo: "owner/repo" } → GITHUB_REPOSITORY.
GitHubApiError
Thrown on any non-2xx response.
| Property | Type | Description |
|---|---|---|
| message | string | GitHub's error message, verbatim. |
| status | number | HTTP status (e.g. 403, 404, 422). |
| errors | object[] | GitHub's field-level errors, when present. |
| documentationUrl | string | GitHub's docs link, when present. |
Error handling
import { createIssue, GitHubApiError } from "github-issue-creator";
try {
await createIssue({ title: "New bug" });
} catch (err) {
if (err instanceof GitHubApiError) {
// GitHub's exact message and status — surface it to your user or logs
console.error(`GitHub ${err.status}: ${err.message}`);
if (err.errors) console.error(err.errors);
} else {
// Configuration error (missing token/repo) or a network failure
console.error(err.message);
}
}Common statuses: 401 bad credentials · 403 token lacks permission · 404
repo/issue not visible to the token · 410 issues disabled on the repo · 422
validation failed (see errors).
Using it from a frontend
This package exposes functions, not HTTP routes — that is deliberate, it keeps the token on your server. If a browser needs to trigger an operation, call the library from your own backend route and let your app handle auth:
Express
import { createIssue, GitHubApiError } from "github-issue-creator";
app.post("/api/issues", async (req, res) => {
try {
// ...your authentication + input validation here...
const issue = await createIssue({ title: req.body.title, body: req.body.body });
res.status(201).json({ number: issue.number, url: issue.html_url });
} catch (err) {
const status = err instanceof GitHubApiError ? err.status : 500;
res.status(status).json({ error: err.message });
}
});Next.js (app router) — app/api/issues/route.js
import { createIssue } from "github-issue-creator";
export async function POST(req) {
const { title, body } = await req.json();
const issue = await createIssue({ title, body });
return Response.json({ number: issue.number, url: issue.html_url }, { status: 201 });
}Notes & caveats
- Raw objects. Returns are GitHub's response objects unchanged — refer to the GitHub Issues REST docs for the full field list.
- PRs in lists.
listIssues/listAllIssuesinclude pull requests (GitHub treats them as issues). Filter oni.pull_requestif you don't want them. - List vs. read consistency. A just-created issue can take a moment to appear
in list results (GitHub's list index is eventually consistent), while
getIssuereflects it immediately. - No retries/throttling. Calls are made once. If you need rate-limit backoff or retries, wrap the functions in your app.
- No deletes. GitHub has no issue-delete API; close via
updateIssueinstead.
License
MIT
