Files
kandev/docs/specs/integrations/system-design/github-pr-merge-queue.md
T
2026-08-24 21:30:53 +00:00

198 lines
7.9 KiB
Markdown

---
status: current
system: integrations
requirements:
- REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001
created: 2026-08-24
owners:
- kandev
---
# GitHub PR Merge Queue System Design
## Purpose and boundaries
The integration system owns the GitHub merge operation and the normalized
merge-queue state. The task system consumes a bounded pull-request summary. The
UI renders the integration state but does not own a second queue contract.
This design covers the complete vertical path. It includes the GitHub clients,
storage, task projection, HTTP response, frontend types, React surfaces, and
desktop and mobile behavior.
## Requirement mapping
| Requirement | Design sections |
| --- | --- |
| `REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001` | [Components](#components-and-responsibilities), [Data](#data-and-contracts), [Control flow](#control-flow), [Failure](#failure-and-recovery), [Presentation](#presentation-and-responsive-behavior) |
## Components and responsibilities
### GitHub provider boundary
- `apps/backend/internal/github/client.go` defines the typed merge outcome.
- `gh_client.go` and `pat_client.go` call GitHub's asynchronous merge endpoint.
- `graphql.go` reads and normalizes `mergeQueueEntry` state and metadata.
### Integration service and storage
- `service_pr.go` routes the merge request through workspace-scoped write
credentials and invalidates stale pull-request data after success.
- `controller.go` exposes the existing merge endpoint and returns the typed
`merged` or `queued` result.
- `service_pr_watch.go` applies authoritative queue observations and publishes
the normal pull-request update.
- `models.go` owns the normalized `PRStatus` and persisted `TaskPR` fields.
- `store.go` owns the `github_task_prs` schema and all queue-field write paths.
### Task projection
- `apps/backend/internal/task/statussummary` reduces full pull-request state to
a bounded task status.
- `apps/backend/internal/backendapp/status_summary_adapter.go` supplies the
stored queue state during live updates and restart hydration.
### Web application
- `apps/web/lib/api/domains/github-pr-api.ts` types the merge result.
- `apps/web/lib/types/github.ts` defines the queue-state and `TaskPR` fields.
- `pr-merge-button.tsx` controls eligibility, submission, feedback, and refresh.
- `pr-merge-queue-status.tsx` normalizes queue presentation and duration copy.
- The task icon, status chip, summary, and detail components reuse that
presentation model.
## Data and contracts
`github_task_prs` stores the last authoritative queue observation:
- `merge_queue_state TEXT NOT NULL DEFAULT ''` stores the normalized GitHub
state. Supported values are `queued`, `awaiting_checks`, `mergeable`,
`unmergeable`, and `locked`.
- `merge_queue_position INTEGER NULL` stores GitHub's one-based position.
- `merge_queue_estimated_time_to_merge_seconds INTEGER NULL` stores an optional
non-negative duration.
An empty state with null metadata means that no active queue entry is stored.
The same three fields appear in the `TaskPR` HTTP, boot, and WebSocket payloads.
The bounded task projection reduces active membership to `queued`.
The existing merge endpoint remains:
```http
PUT /api/v1/github/prs/:owner/:repo/:number/merge?workspace_id=:workspaceId
Content-Type: application/json
{"merge_method":"squash"}
```
`merge_method` is optional. It accepts `merge`, `squash`, or `rebase`. A
successful response is one of these objects:
```json
{"status":"merged"}
{"status":"queued"}
```
The provider request uses `merge_action=default`. GitHub therefore selects the
allowed direct or queued path.
## Control flow
### Merge request
1. The frontend derives eligibility from the current pull-request state.
2. The user activates the existing merge action.
3. The service selects workspace-scoped personal-write credentials.
4. The provider starts or resumes GitHub's asynchronous merge request.
5. The provider polls a pending request until GitHub returns a terminal result.
6. The controller returns `merged` or `queued` to the frontend.
7. The frontend shows outcome-specific feedback and requests a state refresh.
Only GitHub's `enqueued` result maps to `queued`. An already merged pull request
maps to `merged`. A provider failure remains retryable.
### Queue observation
The batched GraphQL selection reads this field:
```graphql
mergeQueueEntry { state position estimatedTimeToMerge }
```
`graphql.go` records whether the response observed queue membership. This guard
distinguishes an authoritative null from REST and `gh pr view` responses that
cannot read queue data.
`SyncTaskPR` atomically replaces the queue state, position, and estimate after
an authoritative GraphQL observation. It preserves all three fields after a
queue-unaware read. It clears them after an authoritative null, merge, or close.
The update then publishes `github.task_pr.updated` through the existing path.
## State transitions
- An open pull request with no observed queue entry is not queued.
- A non-null authoritative entry creates or updates active queue membership.
- A later authoritative null removes active queue membership.
- A merged or closed state clears queue metadata and wins presentation priority.
- A queue-unaware or failed read preserves the last complete observation.
- An unknown future non-empty state remains active and uses generic UI copy.
## Failure and recovery
Missing credentials, permission errors, provider validation errors, rate limits,
and transport errors return a non-success response. These errors do not change
the stored pull-request state or show a successful outcome.
An unknown successful provider result fails closed. Kandev does not claim that
the pull request merged or entered the queue.
A failed GraphQL queue read preserves the last complete observation. The next
successful authoritative sync updates or clears it. This behavior also protects
queue state during restart recovery and mixed GraphQL, REST, and CLI refreshes.
## Persistence
The queue fields use additive SQLite columns in `github_task_prs`. Fresh schema,
migration, create, replace, restore, and update paths round-trip the complete
entry. Existing rows start with no queue entry and converge after a successful
GraphQL status sync.
The estimate remains a duration. Kandev does not convert it to a durable target
timestamp because GitHub can change the estimate on each observation.
## Security
The merge action uses the active workspace's personal-write GitHub route. It
requires the same pull-request permissions as GitHub's merge API. Kandev does
not bypass repository rules or elevate the user.
## Presentation and responsive behavior
An active queue entry uses `#966600`. Terminal states take precedence. Queue
membership takes precedence over other non-terminal icon states.
The task hover summary, desktop status popover, phone drawer, and pull-request
detail panel show localized state and position. Estimates below one minute use a
localized sub-minute label. Estimates of one minute or more round up to a
localized whole-minute duration. Missing estimates omit estimate text instead
of inventing a value. Unknown provider states use generic queue copy.
Desktop and mobile share state derivation and actions. Mobile uses the existing
Review surface and status drawer. The action has a touch-sized target, does not
depend on hover, and does not create document-level horizontal overflow.
## Verification evidence
Focused backend coverage exists in the GitHub client, GraphQL conversion,
storage, synchronization, and task-status packages. Frontend unit coverage
exists for merge eligibility, merge outcomes, queue formatting, and status
priority. Playwright coverage exists in:
- `apps/web/e2e/tests/pr/pr-merge-queue.spec.ts`
- `apps/web/e2e/tests/pr/mobile-pr-merge-queue.spec.ts`
## Related delivery records
- [Queue-status visibility plan](../../../plans/github-pr-merge-queue-status/plan.md)
- [Original queue-aware merge action plan](../../../plans/github-pr-merge-queue/plan.md)