@storyshelf/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 and @storyshelf/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
Section titled “Install”npm i @storyshelf/notify-gitpnpm add @storyshelf/notify-gityarn add @storyshelf/notify-git- npm package — install tarballs and version history.
- Source on GitHub — package directory on
main.
Configure
Section titled “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.
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
Section titled “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:
POST /api/v1/projects/{slug}/notification-channelsContent-Type: application/json
{ "provider": "github", "config": { "owner": "acme", "repo": "widgets", "prefix": "storyshelf-web" }, "secret": "ghp_..."}config—{ owner, repo, prefix?, mode?, appId?, installationId? }for GitHub (modeisstatusesby default; see GitHub check runs). GitLab also takeshost(defaulthttps://gitlab.com; set it to your instance URL when self-hosted).prefixnames the statuses (<prefix>/testing,<prefix>/review,<prefix>/storybook) and defaults tostoryshelf.secret— the token. GitHub:repo:statusscope. GitLab:apiscope. In GitHubchecksmode it is the GitHub App’s private key (PEM) instead. It is stored AES-256-GCM encrypted with the serverSECRETand 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
Section titled “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) <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. 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) post nothing. Branch protection setup is in Merge gate setup.
GitHub check runs
Section titled “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.configtakesappIdandinstallationId(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 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 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 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
Section titled “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
Section titled “Delivery”State delivery needs a confirmed 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, 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-Afterhandling. - After the last attempt the failure is logged, the channel row keeps the last error and its time, and a
sys:notify-failedevent 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 aslastErrorandlastErrorAt. - 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
Section titled “Direct use”Each provider is a StateNotifierProvider from @storyshelf/core/adapter/notifier. Bind one to a channel and call setState:
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 for channels, subscriptions and events, and ADR 0014 for the decision record.