Skip to content

docs(legal): say what GitHub tells app.fctl.sh about pull requests - #36

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

hyperstructure/fctl#176 turns on what GitHub's webhook tells app.fctl.sh: when pull requests and their checks change in the repositories its GitHub App is installed on, the control plane asks GitHub about the pull requests sessions list, and session lists and pages show their state and checks. The privacy policy says so:

  • What we collect → Projects and sessions: a session's pull requests come with their state, when it changed and how many of their checks passed.
  • What we collect → GitHub: GitHub tells us when pull requests and their checks change in the repositories the app is installed on; for the pull requests sessions list we keep their title and state, when it changed, their latest commit and how many checks passed, failed or haven't finished, and nothing else of what GitHub sends.
  • Who else receives it → GitHub: we ask GitHub what became of those pull requests.

How long they are kept doesn't change: with their session, until its project is deleted. updated moves to 2026-10-21.

Merge this when hyperstructure/fctl#176 is deployed and the app's webhook is on.

How I looked at it

npm run check (the two warnings are on main too) and FCTL_DOCS_DIR=../fctl npm run build with the fctl branch's docs, which renders /privacy and the docs' new section and links.

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

app.fctl.sh's GitHub App takes GitHub's webhook: GitHub tells it when pull
requests and their checks change in the repositories the app is installed
on. For the pull requests sessions list, app.fctl.sh asks GitHub about them
and keeps their state, when it changed, their latest commit and how many
of their checks passed; it keeps nothing else of what GitHub sends.
@kalaspuff
kalaspuff merged commit 1c2f535 into main Oct 21, 2026
4 checks passed
@kalaspuff
kalaspuff deleted the fctl/108-i-want-to-be-able-to-listen-to-webhooks branch October 21, 2026 17:23
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