Design and Architecture

GitHub Comment Rendering

How Zeeq renders and updates owned GitHub comments.

Summary

Zeeq renders GitHub comments from lightweight queue signals. A producer does not send Markdown and does not store a rendered document in Zeeq. Instead, it publishes a GitHubCommentWriteRequested message with a logical target, a render kind, optional sections to clear, and an optional reference to an authoritative code-review record.

The writer then reads the current GitHub comment body, parses Zeeq markers into a small document object model, applies the requested section updates, and writes the full comment body back to GitHub.

The important rule is:

The live GitHub comment body is the rendered document state.

Zeeq stores only the durable pointers and coordination rows needed to find and safely update that comment.

Why the comment body is the document state

Earlier designs stored a desired-state row in Postgres and used queued messages as wake-up signals for that row. The current design removes that table. This avoids keeping a second rendered-document copy in sync with GitHub and lets the GitHub comment itself remain the source of the published artifact.

This matters because Zeeq comments are updated over time. A pull request can receive an immediate queued acknowledgement first, then later receive completed review sections, evidence, links, and follow-up actions. Each pass should preserve content it does not own.

The render loop is therefore:

Queue signal
  -> acquire target lease
  -> resolve existing GitHub comment
  -> parse Zeeq marker DOM from the current body
  -> clear explicitly named sections
  -> render sections for the signal kind
  -> preserve untouched sections
  -> write the full marked body back to GitHub
  -> persist or repair the target anchor

Key pieces

PiecePurpose
GitHubCommentWriteRequestedLightweight queue signal for one render/write pass.
GitHubCommentTargetSelectorStable logical identity for one owned GitHub comment target.
GitHubCommentAnchorDurable pointer from a target selector to a GitHub comment ID.
GitHubCommentLeaseShort-lived write lease for a target.
GitHubCommentDomParserParses the live GitHub comment body into one-level Zeeq sections.
GitHubCommentDomRendererApplies clears and section patches, then renders the full body.
IGitHubCommentSectionRendererFeature-specific section renderer for one or more render kinds.
GitHubCommentResolverFinds the existing GitHub comment by anchor or marker scan.
GitHubCommentWriterUpdates an existing GitHub comment or creates the first one.
CodeReviewRuntimeDigestProcess-local T-Digest that tracks completed review runtimes for footer percentiles.

Targets and anchors

A target selector names the logical comment Zeeq owns. The selector is also the basis for the lease key and anchor key.

Examples:

Target kindScope keyMeaning
PullRequestSummaryrootThe main top-level Zeeq PR issue comment.
ReviewThreadStable review-thread keyA Zeeq-owned comment in a code-review thread.
StandaloneIssueCommentMessage family keyA separate Zeeq issue comment outside the PR summary.

GitHubCommentAnchor stores the resolved GitHub comment ID for the target. This keeps the common path cheap: if the anchor is present and valid, Zeeq can fetch or update the known GitHub comment directly.

The anchor is a repairable lookup cache, not the document state. If the anchor is missing or stale, the resolver scans the pull request comments for the matching Zeeq root marker. When the scan finds the comment, the handler repairs the anchor before continuing.

Marker DOM

Zeeq wraps owned comments in HTML comments. These comments are invisible in GitHub's rendered UI but stable in the Markdown source.

The root marker identifies the logical target:

<!-- (000000):zeeq:pr-comment-root:start -->
...
<!-- zeeq:pr-comment-root:end -->

Child sections identify replaceable regions:

<!-- (200000):zeeq:pr-comment-header:start -->
Review queued
<!-- zeeq:pr-comment-header:end -->

<!-- (500000):zeeq:pr-findings:start -->
No findings yet.
<!-- zeeq:pr-findings:end -->

The value in parentheses is the order key. Sections are rendered in lexical order by that key, not by renderer registration order. Reserved anchors keep the root/header near the top and the footer near the bottom, while leaving space to insert new sections later.

One-level rule

The DOM supports one root and one level of direct child sections.

This is intentional. GitHub comments are Markdown strings, not a full structured document store. A one-level DOM keeps the parser predictable and keeps ownership simple: a renderer may replace one named section, and every other section remains untouched.

Different Zeeq-looking marker text inside a section is treated as raw Markdown owned by that section. A nested start marker for the same section is considered malformed and is ignored for that section, because there is no safe way to infer which end marker the writer intended to own.

Preserving existing state

Renderers do not rebuild the whole comment from scratch. The handler parses the live comment body first, then the renderer applies explicit changes:

  1. Remove sections named by GitHubCommentWriteRequested.Clear.
  2. Run registered IGitHubCommentSectionRenderer instances for the message kind.
  3. Apply returned patches.
  4. Keep all unpatched sections as they appeared in the parsed DOM.
  5. Sort by order key and write the full body back.

This is why later events can safely accumulate state. For example, an immediate queued message can write the header, status, and footer. A completed review message can later replace findings and evidence while preserving unrelated sections already present in the comment.

Closed or merged pull request webhooks do not publish a replacement status comment. They update the durable pull request state and gate new review creation, but leave the last rendered review findings in place.

The pull request footer includes process-local runtime percentiles for completed code-review executions:

50th percentile runtime: 01:23; 95th: 04:56

If the application process has not recorded any review runtimes yet, the footer renders:

50th percentile runtime: (no data); 95th: (no data)

CodeReviewRuntimeDigest is registered as a singleton. CodeReviewRunRequestedHandler records one runtime observation after review execution starts and finishes, including failed executions that reach the runner. Capacity deferrals and terminal no-ops are not runtime samples.

T-Digest updates take a short write lock because TDigest.Add is not thread-safe. Each write publishes an immutable percentile snapshot, and footer rendering reads that snapshot with volatile semantics instead of locking on the digest. This keeps comment rendering cheap and non-blocking at the cost of reading the last published snapshot rather than the mutable digest directly. This data is intentionally not durable: it resets when the web or worker process restarts and should be treated as live operational context, not a billing or audit record.

Leases

GitHub writes for the same target must be serialized. Without serialization, two queue consumers could read the same old comment body, render different changes, and have the later write overwrite the earlier one.

Zeeq uses GitHubCommentLease rows to serialize a target. The table is unlogged because lease rows are transient coordination state. Losing all leases after an unclean Postgres shutdown is acceptable: the next worker can reacquire the target and re-read the current GitHub comment body.

Lease acquire is protected by a transaction-scoped Postgres advisory lock. The advisory lock exists only for the short acquire transaction:

begin transaction
  -> take transaction advisory lock for target
  -> check existing lease row
  -> insert or take over expired lease
commit transaction

The handler does not hold a database transaction, database connection, or advisory lock while calling GitHub. This keeps the design compatible with PgBouncer transaction pooling and avoids tying up database connections during external network calls.

Renewal and cancellation

The comment write handler runs two tasks inside the lease window:

TaskRole
Work taskResolves, parses, renders, writes to GitHub, and updates the anchor.
Renewal taskSleeps for half the lease duration, then extends the lease expiry.

Renewal uses a separate DbContext created by IDbContextFactory<PostgresDbContext>. That is required because the work task uses scoped stores backed by the message scope's PostgresDbContext, and EF Core contexts are not thread-safe.

If renewal fails, the renewal task cancels the work token and the handler fails. This prevents a worker from continuing toward a GitHub write after it has lost the target lease. The queue can retry the message later.

The lease is released in finally with owner checking. Release ignores caller cancellation so cleanup can still run after a timeout or shutdown signal. A stale worker cannot delete a newer worker's lease because release requires the same worker ID that acquired the row.

Crash recovery

There are three important crash cases:

CaseRecovery behavior
Worker crashes before writing GitHubLease renewal stops; the lease expires; a retry can acquire and render from the current GitHub body.
Worker writes GitHub but crashes before anchor updateLater work scans for the target root marker, finds the comment, and repairs the anchor.
Postgres restarts uncleanlyThe unlogged lease table is cleared; the next worker can acquire and re-read GitHub.

The durable pieces are the GitHub comment body and the anchor row. The lease row is deliberately disposable.

Message shape

GitHubCommentWriteRequested is intentionally small:

FieldPurpose
OrganizationId and TeamIdTenant routing and authorization context.
OwnerQualifiedRepoNameGitHub repository name used by the resolver and writer.
TargetLogical comment identity.
KindTells section renderers which update is being requested.
ClearNamed sections to remove before rendering new patches.
CodeReviewRecordId and CodeReviewCreatedAtUtcOptional reference to the authoritative review row.

The message does not carry rendered Markdown, JSON payload sections, or desired-state versions. If a renderer needs completed review content, the handler loads the referenced CodeReviewRecord.

Adding a new section

To add a new section:

  1. Add a stable marker name to GitHubCommentMarkers.
  2. Add a default order key, or ensure the patch provides one.
  3. Implement IGitHubCommentSectionRenderer.
  4. Add tests that prove the new renderer replaces only its own section and preserves unrelated sections.

Do not renumber existing sections unless there is a clear reason. The order-key space should leave room for new sections between existing anchors.

Testing strategy

The design is intentionally testable without excessive mocking:

LayerTest style
Parser and rendererPure unit tests over strings.
Lease and anchor storesPostgres integration tests with Testcontainers.
Resolver and writerFake GitHub client tests.
Queue handlerFocused tests using fake stores, fake writer, and controlled lease timing.

The core rendering behavior should be proven without GitHub or Postgres. The storage and integration seams then get narrower tests against the real systems they wrap.