18 KiB
status, system, requirements, created, owners
| status | system | requirements | created | owners | |||
|---|---|---|---|---|---|---|---|
| current | integrations |
|
2026-08-24 |
|
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, Data, Control flow, Failure, Presentation |
REQ-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-002 |
Data, Automatic merge, Explicit retry, Task-row projection, Failure, Persistence, Presentation |
Components and responsibilities
GitHub provider boundary
apps/backend/internal/github/client.godefines the typed merge outcome.gh_client.goandpat_client.gocall GitHub's asynchronous merge endpoint.- The clients bind an automatic request to an expected head and pace pending status reads within the request context.
graphql.goreads and normalizesmergeQueueEntrystate and metadata.
Integration service and storage
service_pr.goroutes the merge request through workspace-scoped write credentials and invalidates stale pull-request data after success.controller.goexposes the existing merge endpoint and returns the typedmergedorqueuedresult.service_pr_watch.goapplies authoritative queue observations and publishes the normal pull-request update.models.goowns the normalizedPRStatusand persistedTaskPRfields.store.goowns thegithub_task_prsschema and all queue-field write paths.- The CI automation store owns a durable per-pull-request merge-attempt journal.
- The automation service exposes an explicit retry command for one linked pull request. The controller does not call the merge provider directly.
Task projection
apps/backend/internal/task/statussummaryreduces full pull-request state to a bounded task status, including aggregate active auto-fix and auto-merge flags.apps/backend/internal/backendapp/status_summary_adapter.gosupplies the stored queue and per-pull-request automation state during live updates and restart hydration.
Web application
apps/web/lib/api/domains/github-pr-api.tstypes the merge result.apps/web/lib/types/github.tsdefines the queue-state andTaskPRfields.pr-merge-button.tsxcontrols eligibility, submission, feedback, and refresh.pr-merge-queue-status.tsxnormalizes queue presentation and duration copy.pr-ci-automation-controls.tsxmaps a typed automatic-merge error to the explicit retry command. Loading errors keep their separate refresh action.pr-task-icon.tsxoverlays bounded automation indicators on the task-row pull-request icon and lazily hydrates per-pull-request details for its pointer, keyboard, or touch disclosure.- The task icon, status chip, summary, and detail components reuse the same automation labels and per-pull-request option matching.
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 arequeued,awaiting_checks,mergeable,unmergeable, andlocked.merge_queue_position INTEGER NULLstores GitHub's one-based position.merge_queue_estimated_time_to_merge_seconds INTEGER NULLstores 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.
TaskStatusSummary.pull_request also carries two optional aggregate booleans:
auto_fix_enabledis true when at least one active linked pull request has auto-fix enabled.auto_merge_enabledis true when at least one active linked pull request has auto-merge enabled.
These fields are task-row decorations. They do not replace the per-pull-request automation options, authorize automation, or carry an unbounded option list.
The existing merge endpoint remains:
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:
{"status":"merged"}
{"status":"queued"}
The provider request uses merge_action=default. GitHub therefore selects the
allowed direct or queued path.
Client.MergePR accepts a request value with the merge method and an optional
expected head SHA. Automatic merge requests require a non-empty expected head.
The clients send it as GitHub's sha field. A different provider head fails the
request instead of merging a pull-request revision that Kandev did not review.
The manual merge action keeps its existing behavior until its separate contract
requires head binding.
GitHub returns the merge status at the response root. It returns the polling
UUID and failure message inside details. The provider clients use
details.uuid to poll a pending request and details.message to report a
failed request. A pending response without details.uuid is invalid.
The clients also decode details.expected_head_sha for error diagnostics.
github_task_ci_pr_state keeps the automatic attempt journal for each linked
pull request:
last_merge_signaturestores the readiness state that authorized an attempt.last_merge_attempt_atstores when Kandev reserved the attempt.last_queue_attempt_head_shastores the head accepted by GitHub or observed in the active queue.last_merge_resultstoresin_flight,failed, oraccepted.last_error_kindidentifiesauto_mergeerrors separately from loading, auto-fix, and other automation errors.
The readiness signature includes the task, repository, pull request, pull-request lifecycle state, head SHA, check conclusion, mergeability, review decision, review count, required-review presence and value, pending-check count, and unresolved-thread count. Including lifecycle state prevents a closed pull request from reusing an attempt reserved while it was open, including after a later reopen.
The explicit retry endpoint is:
POST /api/v1/github/tasks/:taskId/ci-automation/retry-merge
Content-Type: application/json
{"repository_id":"repo-id","pr_number":3117}
The endpoint returns 202 Accepted after it durably authorizes a new evaluation.
This response does not claim that GitHub accepted or completed the merge.
Control flow
Merge request
- The frontend derives eligibility from the current pull-request state.
- The user activates the existing merge action.
- The service selects workspace-scoped personal-write credentials.
- The provider starts or resumes GitHub's asynchronous merge request.
- The provider polls a pending request until GitHub returns a terminal result.
- The controller returns
mergedorqueuedto the frontend. - 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.
Automatic merge
- The evaluator refreshes the exact linked pull request.
- It requires fresh state, a non-empty head SHA, and all readiness gates.
- It computes the complete readiness signature.
- Before the unchanged-signature guard, it consumes a pending one-shot retry
authorization for this exact repository and pull request, if one exists.
That authorization bypasses only the unchanged-signature guard. The
evaluator still requires fresh state, the current head, and every readiness
gate. Without an authorization, an unchanged
in_flight,failed, oracceptedsignature stops the automatic path. - It reserves an
in_flightattempt in storage before any provider side effect. - It calls the provider with the exact observed head SHA.
- It records
failedoracceptedfrom the provider result. An accepted result clears a priorauto_mergeerror for that pull request.
If the reservation cannot be stored, the evaluator does not call GitHub. A changed readiness signature can authorize one later automatic attempt after all gates pass.
Pending polling
The provider waits at least one second between pending-status reads. The wait observes cancellation and the existing two-minute request budget. A conflict response resumes the request with its returned UUID. A missing, expired, or unknown request fails with a diagnostic error.
Explicit retry
The retry command authorizes one new evaluation for the exact repository and
pull-request number. It applies only to a failed attempt or to an expired
in_flight attempt. Expiration is an explicit transition:
expired in_flight -> failed -> authorized retry. A retry cannot be authorized
while the original attempt is still in_flight, and it does not bypass checks,
reviews, mergeability, thread, or head requirements.
The service persists the retry authorization and publishes the normal automation evaluation event. The evaluator then refreshes the pull request and runs the full gate sequence. The HTTP handler never calls GitHub directly.
Task-row automation projection
The authoritative pull-request loader reads linked pull requests and their per-pull-request automation options in bounded batches. It supplies the two switches with each internal pull-request observation. The summary derives each aggregate flag from open pull requests only.
The status-summary projector subscribes to github.task_ci_options.updated.
It refreshes the task's authoritative pull-request observations before it
publishes a complete replacement summary. Existing pull-request update events
use the same observations, so a merge, close, unlink, or option change removes
obsolete indicators without browser-side polling.
The task row reads only status_summary. It does not mount
useTaskCIAutomationOptions for every visible task and does not issue an HTTP
request per row. When the user opens the icon disclosure, one deduplicated lazy
request hydrates the full pull-request records and per-pull-request automation
options needed for detailed text.
Queue observation
The batched GraphQL selection reads this field:
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.
An active queue entry reconciles the attempt journal to accepted. A merged
state does the same. Either observation clears only an error whose kind is
auto_merge. It preserves unrelated automation errors.
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.
- An active queue or merged observation marks the matching attempt accepted.
- 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.
An automatic provider failure records failed, stores an auto_merge error,
and blocks automatic repetition for the unchanged readiness signature. A changed
signature or an explicit retry can rearm the attempt.
After a process interruption, Kandev first reconciles a stored in_flight
attempt with authoritative queue and merged state. If neither state appears
before the attempt deadline, Kandev marks the attempt failed and exposes the
explicit retry. It does not resubmit the unchanged attempt automatically.
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 attempt journal uses additive columns in github_task_ci_pr_state. A
startup migration classifies only recognized stable legacy merge-error prefixes
as auto_merge. An unknown or empty error kind is never cleared as an automatic
merge error.
Reservation, result, error-kind, and retry-authorization changes use storage transactions. The existing per-PR singleflight remains an optimization. Durable reservation is the correctness boundary across concurrent events and restarts.
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.
The retry endpoint uses the same task and workspace authorization. It accepts only the exact pull request that is linked to the task.
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.
The task-row pull-request icon is the nearest shipped status-summary exemplar. It overlays a small yellow dot at the top-left for active auto-fix and a small purple dot at the top-right for active auto-merge. A contrasting surface ring keeps both dots distinct from every pull-request status color. The dots are supplementary; accessible text names each enabled setting.
Fine-pointer hover and keyboard focus extend the existing pull-request tooltip with an Automation section. The section identifies enabled settings by active pull request when per-pull-request options differ. A coarse-pointer activation uses the existing compact task status drawer pattern and a touch-sized icon target. Both presentations reuse the same lazy data and view model. The task row remains the primary navigation target, and the automation disclosure does not change settings.
An auto_merge error shows Retry on desktop and mobile for the selected pull
request. Other stored automation errors show Refresh because repeating a merge
would not address them. Loading errors keep their existing retry of the state
request.
Mobile keeps the existing inset status drawer and its single scroll owner. The shared error row has a target of at least 44 by 44 CSS pixels. This change does not add a drawer, dialog, or navigation surface.
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.tsapps/web/e2e/tests/pr/mobile-pr-merge-queue.spec.tsapps/web/e2e/tests/pr/pr-status-badge.spec.tsapps/web/e2e/tests/pr/mobile-pr-sidebar-automation-indicators.spec.ts