# @storyshelf/notify-git

> GitHub and GitLab notification providers for StoryShelf — commit statuses for the merge gate and one pull-request comment per build.

Source: https://storyshelf.js.org/packages/notify-git/

`@storyshelf/notify-git` makes GitHub or GitLab show the current state of a build: a **commit status** you can require in branch protection, and a single **pull-request or merge-request comment** that is rewritten as the build changes. It is a `state` notifier family member, the sibling of [`@storyshelf/notify-chat`](https://storyshelf.js.org/packages/notify-chat/) and [`@storyshelf/notify-email`](https://storyshelf.js.org/packages/notify-email/), and it has no runtime dependencies: every call goes through `@storyshelf/core/utils` (`httpJson`), which adds a timeout and retries with `Retry-After`.

## Install

```sh
npm i @storyshelf/notify-git
```

- [npm package](https://www.npmjs.com/package/@storyshelf/notify-git) — install tarballs and version history.
- [Source on GitHub](https://github.com/GuptaSiddhant/StoryShelf/tree/main/packages/notify-git) — package directory on `main`.

## Configure

Pass the providers to `createShelfApp`. There is no separate git option: git providers sit in the same `notifications.notifiers` list as chat and email.

```ts
import { createShelfApp } from "@storyshelf/app";
import { chatNotifiers } from "@storyshelf/notify-chat";
import { gitNotifiers } from "@storyshelf/notify-git";

const app = createShelfApp({
  database,
  storage,
  notifications: { notifiers: [...chatNotifiers, ...gitNotifiers] },
});
```

`gitNotifiers` aggregates all three providers:

| Provider kind | Writes to                                        | Use for                     |
| ------------- | ------------------------------------------------ | --------------------------- |
| `github`      | GitHub commit statuses and pull-request comments | GitHub.com repositories     |
| `gitlab`      | GitLab commit statuses and merge-request notes   | GitLab.com or self-managed  |
| `git-log`     | The server log                                   | Tests and local development |

The log provider's kind is `git-log` because `notify-chat` already owns `log`; provider kinds are unique across packages.

## Channels

Providers only make a kind *available*. Project admins then add a **git channel** of that kind in **Project → Settings → Notifications** (the “Add a git channel” card) or through the API:

```http
POST /api/v1/projects/{slug}/notification-channels
Content-Type: application/json

{
  "provider": "github",
  "config": { "owner": "acme", "repo": "widgets", "prefix": "storyshelf-web" },
  "secret": "ghp_..."
}
```

- **`config`** — `{ owner, repo, prefix?, mode?, appId?, installationId? }` for GitHub (`mode` is `statuses` by default; see [GitHub check runs](#github-check-runs)). GitLab also takes `host` (default `https://gitlab.com`; set it to your instance URL when self-hosted). `prefix` names the statuses (`<prefix>/testing`, `<prefix>/review`, `<prefix>/storybook`) and defaults to `storyshelf`.
- **`secret`** — the token. GitHub: `repo:status` scope. GitLab: `api` scope. In GitHub `checks` mode it is the GitHub App’s private key (PEM) instead. It is stored AES-256-GCM encrypted with the server `SECRET` and decrypted in memory only when a status is written. It is separate from the CLI project token: the CLI token identifies a build, this token writes to your repository.

A git channel always belongs to a project (a site-wide channel cannot be git), needs a token (or, in GitHub `checks` mode, the App key), and has a fixed event filter: it follows the build’s state rather than a topic list, so the event picker does not apply to it.

## What gets posted

For every build with a commit SHA, each enabled git channel of the build’s project shows the build’s **current state**. One function derives it from the build, so a build that was never captured still gets its statuses, and showing the same state twice changes nothing:

| Status               | Posted when                                    | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<prefix>/testing`   | `testing` is on                                | `pending` while the build is captured and while review is open (its description says how many changes are left), `success` when every new and changed snapshot is approved, `failure` when changes are rejected or capture fails, `cancelled` when a user cancels the build (GitLab `canceled`, GitHub `error`; the description is `Cancelled`).                                                                                                                                                                                                                                                                                                                                                          |
| `<prefix>/review`    | `testing` is off and the project has review on | `pending` while the build waits for a verdict (with several required approvals the description shows the progress, `Waiting for approvals: 1 of 2`; default-branch builds are accepted when publishing ends and go straight to `success`), `success` once the build is approved (the project’s required approvals are present), `failure` when a reviewer rejects it or the build failed, `cancelled` when a user cancelled it. The merge gate of a hosting-only project. With review off an accepted build shows a final `success` (`Review is off`), which clears a `pending` status left from when review was on. With `testing` on this status is not posted: `<prefix>/testing` stays the only gate. |
| `<prefix>/storybook` | every build with a commit SHA                  | `pending` until publishing finishes, then `success` linking to the published Storybook, or `failure` when the build failed without a published Storybook. Informational: do not require it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

Without `testing` (a hosting-only instance, see [Hosting only](https://storyshelf.js.org/concepts/hosting-only/)) `<prefix>/review` and `<prefix>/storybook` are posted, and `<prefix>/storybook` with a closing `<prefix>/review` `success` when the project has review off. Whether review is on is read from the project when the status is written, so switching it off changes the next status shown for a build. Statuses link to the build page with an absolute URL from the [public origin](https://storyshelf.js.org/guides/config/#public-origin). While none is known the statuses carry no link (`target_url` is omitted) and no pull-request comment is written, since its job is to link.

On top of the statuses, one **comment** on the pull request (GitHub) or merge request (GitLab) of the commit links the build page and the published Storybook and summarizes the visual tests and snapshot counts. It is keyed by a hidden marker built from the build URL, so it is updated in place, never duplicated. A commit with no open pull request gets no comment.

Builds with a synthetic local identity (uploads without git, see [Without git](https://storyshelf.js.org/guides/without-git/)) post nothing. Branch protection setup is in [Merge gate setup](https://storyshelf.js.org/guides/merge-gate/).

### GitHub check runs

A GitHub channel shows a build in one of two ways, set by `mode`:

- **`statuses`** (default): commit statuses, written with a personal or fine-grained token. This is what the table above describes.
- **`checks`**: **check runs**, written by a **GitHub App** installed on the repository, with no personal access token. `config` takes `appId` and `installationId` (numbers from the App’s settings and the installation URL) and the channel’s secret is the App’s private key. The key is stored encrypted like any channel secret, is never returned by the API or shown again in the UI (the channel list says only that a secret is set), and is scrubbed from logs and error text. [Merge gate setup](https://storyshelf.js.org/guides/merge-gate/#github-app-checks) walks through creating the App.

Check runs sit beside statuses, not on top of them: a channel uses one mode. They are driven by the same function as the statuses, so each status above becomes a run with the same name (`<prefix>/testing`, `<prefix>/review`, `<prefix>/storybook`), which is also the name you require in branch protection. GitLab has no check runs; a GitLab channel always posts commit statuses.

| Build state                                                    | Check run                |
| -------------------------------------------------------------- | ------------------------ |
| Not captured yet                                               | `queued`                 |
| Capturing, comparing, review open, waiting for a verdict       | `in_progress`            |
| Approved                                                       | `completed`, `success`   |
| Rejected, or capture failed                                    | `completed`, `failure`   |
| Cancelled by a user                                            | `completed`, `cancelled` |
| Storybook publish failed (`<prefix>/storybook`, informational) | `completed`, `neutral`   |

A run is found by the commit SHA and a stable external id (`storyshelf:<name>`) and **updated** as the build moves, so a commit shows one run per name however many times the state is written. Its details link is the absolute build URL (nothing is posted while no [public origin](https://storyshelf.js.org/guides/config/#public-origin) is confirmed).

**Annotations.** `<prefix>/testing` lists every `new` or `changed` snapshot as an annotation titled with the story, viewport and browser, anchored at the story file, whose details carry the **review URL of that snapshot** (`…/builds/<id>/diff?snapshot=<id>`). GitHub accepts 50 annotations per request, so they are posted in batches of 50 through check-run updates; at most 200 are posted, and the summary then says `and N more changed snapshots` with a link to the build. A repeated delivery skips the annotations the run already shows. GitHub never removes annotations: after you approve a change its annotation stays on the run, while the run itself completes as `success`.

The App needs these repository permissions: **Checks: read and write** (the check runs), **Pull requests: read and write** (the pull-request comment) and **Contents: read** (to resolve the commit). Commit-status write access is optional and not used in `checks` mode. The Administration permission is only for the [`storyshelf protect`](https://storyshelf.js.org/guides/cli/client/#storyshelf-protect) helper, which runs with your own admin token, not the App.

Authentication uses no library: the provider signs an RS256 JWT with the key, exchanges it for an installation token through the shared `httpJson` helper, and caches that token per channel until about five minutes before it expires.

### The prefix

The status name is `<prefix>/<status>`; the channel’s `prefix` defaults to `storyshelf` and carries no project slug. A repository that holds several projects gives each project’s channel its own prefix (`storyshelf-web`, `storyshelf-docs`), so their statuses do not collide and each can be required on its own. The prefix may contain letters, digits, dots, dashes and underscores.

## Delivery

State delivery needs a confirmed [public origin](https://storyshelf.js.org/guides/config/#public-origin) (`PUBLIC_BASE_URL`, the platform’s address or one an admin confirmed; the address of a browser counts only once confirmed): every status and the comment link to the build, so the package works with no other base URL. A hosting-only instance needs it too, and then still posts `<prefix>/storybook` with the published Storybook’s URL. While none resolves nothing is queued or posted, and no channel shows an error. When an admin confirms an origin on **System**, the builds still pending or in review are shown again; with `PUBLIC_BASE_URL` set at start-up, delivery resumes with the next state change.

State delivery is **durable and latest-wins**. Every build transition and the end of publishing queue one `notify` job per git channel on the [database queue](https://storyshelf.js.org/concepts/hosting-only/), next to `capture` and `publish` jobs, with a consumer of its own so a status never waits behind a running capture.

- A job names a channel and a build, not a status. When it runs it reads the build as it is *then* and writes that, so a retried, delayed or reordered job can never post a stale status. A request for a channel and build that already has a job waiting is folded into it.
- A refused write (a revoked token, a missing scope, an unreachable host) is retried up to five attempts in total, waiting 10 s, 20 s, 40 s and 80 s between them. Outbound calls also have the shared HTTP helper’s timeout and `Retry-After` handling.
- After the last attempt the failure is logged, the channel row keeps the **last error and its time**, and a [`sys:notify-failed`](https://storyshelf.js.org/guides/notifications/#site-wide-admin-alerts) event goes to the site-wide channels. The Notifications settings page shows the channel as **failing** with that error until the next successful delivery clears it; the notification-channel API returns the same fields as `lastError` and `lastErrorAt`.
- Writes are idempotent and nothing is read from the git host during capture; a build of a commit that is already merged is captured like any other.

A status can lag the build change by a queue polling interval. On a host with **no queue consumer** (a hosting-only instance on a serverless platform) there is nothing to retry from, so the state is delivered **inline** in the request or publish path, without durability: a failure is recorded on the channel and the next transition writes the current state again. Chat and email channels are not part of this: messages are sent inline and best-effort, and a failure never fails the triggering action.

## Direct use

Each provider is a `StateNotifierProvider` from `@storyshelf/core/adapter/notifier`. Bind one to a channel and call `setState`:

```ts
import { gitHubNotifier } from "@storyshelf/notify-git";

const adapter = gitHubNotifier.create({
  config: { owner: "acme", repo: "widgets" },
  secret: process.env.GITHUB_TOKEN,
});

await adapter.setState({
  channel,
  target: { kind: "status", sha, context: "storyshelf/testing", description: "Review closed" },
  state: "success",
  url: "https://shelf.example.com/projects/web/builds/01J...",
});
```

The status `context` is used verbatim: the caller owns the prefix, the provider adds nothing. StoryShelf derives the contexts, descriptions and links itself; direct use is for custom hosts. A `{ kind: "comment", sha, markdown }` target upserts the review comment instead.

See [Notifications](https://storyshelf.js.org/guides/notifications/) for channels, subscriptions and events, and [ADR 0014](https://github.com/GuptaSiddhant/StoryShelf/blob/main/docs/adr/0014-git-hosts-and-notifications.md) for the decision record.
