Skip to content

feat(tokens): add colors for pull requests' states and checks - #16

Merged
kalaspuff merged 1 commit into
mainfrom
fctl/108-i-want-to-be-able-to-listen-to-webhooks
Oct 21, 2026
Merged

kalaspuff merged 1 commit into
mainfrom
fctl/108-i-want-to-be-able-to-listen-to-webhooks

Conversation

@fctl-sh

@fctl-sh fctl-sh Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

fctl's web app is starting to show each pull request of a session with its state and its checks, as GitHub reports them (hyperstructure/fctl#176). These tokens give it GitHub's own colors for that, so that people recognize the states at a glance:

Token Light Dark For
pr-open #3fb950 #3fb950 an open pull request
pr-draft #9198a1 #9198a1 a draft
pr-queued #d29922 #d29922 one in a merge queue
pr-merged #ab7df8 #ab7df8 a merged one
pr-closed #f85149 #f85149 a closed one
pr-unknown #59636e #9198a1 one nobody has looked up yet
checks-passed #3fb950 #3fb950 its checks all passed
checks-pending #d29922 #d29922 some are still to finish
checks-failed #f85149 #f85149 one failed

They are not brand colors: tokens.css says they are only for pull requests and their checks. Only pr-unknown changes with the theme (muted text); the others are the same in both, as the person who asked for them specified.

How I looked at it

npm run check, npm test and npm run build pass. The playground lists the new tokens with the others (playground/main.ts):

Light:

The playground's tokens in the light theme, with the pr-* and checks-* swatches in the last rows

Dark:

The playground's tokens in the dark theme

Merging releases it (a feat: v0.6.0); hyperstructure/fctl#176 then pins that tag in place of this branch's commit.

Made in the fctl session https://app.fctl.sh/s/ses_01M5FGTZXMEJVX5CD3MRXQ6XPB

GitHub's own colors for a pull request's state (open, draft, queued,
merged, closed) and its checks (passed, pending, failed), the same in both
themes, so that fctl shows them as people know them from GitHub. Unknown,
for a pull request nobody looked up yet, is muted text: #9198a1 on dark,
#59636e on light. The playground shows them with the other tokens.
@kalaspuff
kalaspuff merged commit e93a28a into main Oct 21, 2026
3 checks passed
@kalaspuff
kalaspuff deleted the fctl/108-i-want-to-be-able-to-listen-to-webhooks branch October 21, 2026 17:03
fctl-sh Bot added a commit to hyperstructure/fctl that referenced this pull request Oct 21, 2026
The release with the pull request and checks colors (hyperstructure/design#16),
in place of its branch's commit.
kalaspuff pushed a commit to hyperstructure/fctl that referenced this pull request Oct 21, 2026
…176)

Sessions learned what became of their pull requests only from their
agents, which look each one up when a turn ends, until it merges. A pull
request merged or closed after the session's last turn, as most are,
stayed "open" in the lists and on the session page for good, and nothing
said whether its checks passed. Now GitHub tells the control plane,
through the GitHub App's webhook, and session lists, the session page
and `fctl show` show each pull request's current state and whether its
checks are green ([ADR
0032](docs/adr/0032-githubs-webhook-keeps-sessions-pull-requests-current.md)).

## How it works

- **The webhook**, `POST /api/v1/github/webhook`, takes deliveries
signed with `GITHUB_WEBHOOK_SECRET` (HMAC-SHA256, constant time; 401
otherwise, 501 without the secret) for the events Pull request, Check
run, Check suite and Status (`fctl_domain::webhooks`).
- **A delivery is a cue, a lookup the answer.** D1's new
`pull_request_sessions` (migration 0019, backfilled from every session's
list) says which sessions list which pull request, with its head commit,
which commit statuses name. For each listed pull request a delivery
concerns, the control plane asks GitHub with one GraphQL query (title,
state, draft, merge queue entry, times, and the head commit's checks
counted by state), with a token of the installation that sent it, cached
per isolate while valid. When the lookup fails, a `pull_request`
delivery's payload gives the state.
- **The sessions** get it at SessionDO's new `/pull-requests`, which
keeps what is newer (`fctl_domain::pulls::update`): GitHub's `updatedAt`
orders what GitHub and agents say, so an agent's stale state file can't
reopen a merged pull request; an agent's `open` leaves a queued one
queued (`gh` can't see merge queues); checks go by when the control
plane asked. Viewers and dashboards see the change at once, D1 within
seconds.
- **Queued** is the webhook's action `enqueued` (or `queued`), a merge
queue entry, or `mergeStateStatus` (else `mergeable_state`) of `queued`
or `in_merge_queue`.
- **When the state changed** (`state_changed_at`): when GitHub says it
merged, closed or joined the queue, or else when fctl saw it change.
Agents now ask `gh pr view` for `updatedAt`, `createdAt`, `mergedAt` and
`closedAt` too (older `gh` falls back as before).
- **Lookups without a delivery:** pull requests are looked up as
sessions find them, and the hourly job looks up to 30 that aren't merged
or closed and that nobody asked about for six hours. That catches missed
deliveries and brings sessions from before the webhook up to date.
- Instances without `GITHUB_WEBHOOK_SECRET` look nothing up and behave
as before.

`PullRequestRef` gains optional `state_changed_at`, `updated_at` and
`checks` (`PullRequestChecks`: passed, failed, pending, sha, at), and
`state` may be `queued`: older hosts, agents and CLIs ignore them.

## What people see

Each pull request gets GitHub's octicon for its state in GitHub's color
(open, draft, queued, merged, closed, or unknown until someone looked it
up), and while it is open a mark for its checks: all passed ✓, some
pending ●, one failed ✕. Tooltips and screen readers say it in words
("fctl#171, open, 1 of 4 checks failed"). The Pull requests section says
when it merged, closed or joined the queue, and how many checks passed.
`fctl show` prints `app#12 open, 1 of 4 checks failed`. The colors are
the new `pr-*` and `checks-*` tokens of @hyperstructure/design v0.6.0
(hyperstructure/design#16); the table's last row, unknown, was cut off
in the request, so its colors (`#59636e` light, `#9198a1` dark) and icon
(git-pull-request) are my pick.

## Before merging

- [x] Merge hyperstructure/design#16, which released v0.6.0, then pin
`github:hyperstructure/design#v0.6.0` in `web/package.json` in place of
the branch commit `c2c2094`: daa2e12.
- [ ] On app.fctl.sh's GitHub App (fctl-sh): under General, make the
webhook Active with the URL `https://app.fctl.sh/api/v1/github/webhook`
and a new secret (`openssl rand -hex 32`); under Permissions & events,
subscribe to Pull request, Check run, Check suite and Status (no new
permissions). Then `echo "$SECRET" | cargo xtask secrets fctl-sh --set
GITHUB_WEBHOOK_SECRET`. Until then, nothing changes.
- [ ] Merge hyperstructure/fctl-site#36, the privacy policy, once this
is deployed with the webhook on.

## How I verified it

- Unit tests: the merge rules, merge queue signs, state times, check
counting and GitHub's time format (`fctl_domain::pulls`); signatures
against GitHub's published example, event parsing and the lookup's
answer (`fctl_domain::webhooks`); the agent's `gh pr view` times; `fctl
show`; the web app's names, labels and colors (`lib/pulls.test.ts`,
`row.test.tsx`). `cargo xtask check` passes.
- The GraphQL query and the exact request the Worker sends (installation
token, headers, body), replayed with curl against
#170 while its checks ran: GitHub answers it, and
counts a cancelled run as a failure, as the parser does.
- The migration's backfill on SQLite with all earlier migrations, both
list formats, run twice.
- End to end on `cargo xtask dev`, with dev sessions whose pull requests
I seeded in their storage (dev projects have no repository), a throwaway
app config so the endpoint is on, and deliveries signed as GitHub signs
them: unsigned and forged ones get 401, `ping` 200, one about an
unlisted pull request reaches no session. A `closed` (merged) delivery
turned the open session page and the project's list to "merged just now"
without a reload, and D1 followed; `ready_for_review` and `enqueued`
moved a draft to open and queued. With the app's credentials refused by
GitHub, lookups failed and payloads stood in, as intended; check
deliveries changed nothing then. The hourly job looked up only the pull
request nobody had asked about.

## Screenshots

On the dev stack with seeded pull requests, at e1c7383 (v0.6.0, pinned
since, has the same colors). The project's sessions, light, 1440 px:

| Before | After |
|---|---|
| ![Before: the project's session list names pull requests as plain
links, "PRs app#15,
docs#4"](https://app.fctl.sh/api/v1/published/djAccGd19metn04iOC8GKmqI7zXa4h3500B6jikmyIB/before-list-light.png)
| ![After: each pull request has its state's icon in GitHub's color and
a checks
mark](https://app.fctl.sh/api/v1/published/2f3LO2JoqAfzDbBFquo561RUVvkpg21YqWzVOGMwNNV/after-list-light.png)
|

The same, dark:

![After, dark: open, unknown, merged, closed, draft and queued pull
requests with their
checks](https://app.fctl.sh/api/v1/published/76bsPAHiMTx1hrrfxoXOU0ZMEDTLwzQ1VUzmfLh6hWG/after-list-dark.png)

A session's header and Pull requests section, light:

| Before | After |
|---|---|
| ![Before: pull requests with a generic icon, title and
reference](https://app.fctl.sh/api/v1/published/ezCkxHh67Vk6enctbtU153kIbRDSz7ZepkcNYbqZ8pK/before-session-light.png)
| ![After: a draft with 2 of 5 checks pending, and a pull request queued
23 minutes ago whose 6 checks
passed](https://app.fctl.sh/api/v1/published/v0our6bwrgU82yK5h28lmsDylTMcsfwdxSRKF1XJfCa/after-session-light.png)
|

Dark, a merged and a closed pull request, and the details sheet on a
phone (390 px):

![After, dark: Merged 2 hours ago, Closed
yesterday](https://app.fctl.sh/api/v1/published/ogjFFc2KHNgOELKZTEkL8o9d0FwBMv3k2XCYZN07Gvt/after-session-dark.png)

<img alt="After, dark, phone: the details sheet lists the pull requests
with their state and checks first"
src="https://app.fctl.sh/api/v1/published/ejBGb92yb2ZDCB0igk5MoSigifNUcrcfm3rXYTjtgzB/after-phone-dark.png"
width="390">

Refs #8: the same deliveries can wake agents when CI fails or a review
comes in.

Made in the fctl session
https://app.fctl.sh/s/ses_01M5FGTZXMEJVX5CD3MRXQ6XPB

---------

Co-authored-by: fctl-sh[bot] <343971779+fctl-sh[bot]@users.noreply.github.com>
Co-authored-by: fctl-sh[bot] <fctl-sh[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant