Skip to content

feat: better merge flow and polling#232

Merged
Flo0806 merged 3 commits intomainfrom
feat/merge-flow-polling
Mar 15, 2026
Merged

feat: better merge flow and polling#232
Flo0806 merged 3 commits intomainfrom
feat/merge-flow-polling

Conversation

@Flo0806
Copy link
Contributor

@Flo0806 Flo0806 commented Mar 15, 2026

Summary

  • Merging as admin - better flow
  • Polling for better UX

Related issue(s)

Closes #231

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • CI

Checklist

  • Tests added/updated
  • i18n keys added/updated (if needed)
  • No breaking changes

Summary by CodeRabbit

  • New Features

    • Merge bypass for admins with visual indicator, hint, and adaptive merge action behavior
    • Merge UI now supports both direct PRs and contributions and adjusts review/merge controls accordingly
  • Performance

    • More efficient change detection and background updates using lightweight checks to avoid unnecessary refreshes
    • New endpoint for quick work-item change detection with caching-aware responses
  • Localization

    • Added English and German texts for the merge bypass flow

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Adds admin "merge without waiting" bypass support and ETag-based real-time polling for work items; updates WorkItemHeader PR/merge UI and state handling; exposes new merge metadata from GraphQL; adds a lightweight /check endpoint; fixes i18n; and introduces a broken import in image upload that needs attention.

Changes

Cohort / File(s) Summary
Work item header / PR & merge UI
app/components/work-item/WorkItemHeader.vue
Derive PR presence via hasPr/activePr; lazy-fetch merge status; add canBypassRules handling, bypass hint UI, adaptive merge button labels/colors, and safer handling of null activePr.
Polling composable & page provide
app/composables/useWorkItemPolling.ts, app/pages/repos/[owner]/[repo]/work-items/[id].vue
Replace fingerprint polling with ETag-based lightweight check; composable now exposes only trigger; simplified start/stop timer; page now provide('hasPr', hasPr).
Server: check endpoint
server/api/repository/[owner]/[repo]/work-items/[id]/check.get.ts
New endpoint that performs GitHub REST conditional GET (supports 304), invalidates work-item cache on change, and returns { changed: boolean }.
Merge status backend & types
server/api/repository/[owner]/[repo]/pulls/[number]/merge-status.get.ts, shared/types/merge.ts
GraphQL query now requests viewerCanMergeAsAdmin; response exposes canBypassRules in merge status result and updated local types.
i18n / schema
i18n/locales/en.json, i18n/locales/de.json, i18n/schema.json
Add workItems.merge.mergeBypass and workItems.merge.bypassHint keys and update schema entries.
⚠️ Broken import (needs immediate fix)
server/api/upload/image.post.ts
Removed imports validateImageFile and imageExtFromType while their usages remain — will cause runtime errors on image upload.

Sequence Diagram(s)

sequenceDiagram
    participant User as Admin User
    participant UI as WorkItemHeader
    participant API as Merge-status API
    participant GitHub as GitHub API

    User->>UI: Click merge (bypass option shown)
    UI->>API: GET merge-status (reads canBypassRules)
    API->>GitHub: GraphQL query (includes viewerCanMergeAsAdmin)
    GitHub-->>API: returns mergeability + viewerCanMergeAsAdmin
    API-->>UI: merge-status with canBypassRules
    alt Admin bypass allowed
        User->>UI: Confirm merge (bypass)
        UI->>API: POST merge (REST)
        API->>GitHub: execute merge (may bypass checks)
        GitHub-->>API: merge result
        API-->>UI: success/failure
    end
Loading
sequenceDiagram
    participant Page as Work Item Page
    participant Poll as useWorkItemPolling
    participant Check as /check endpoint
    participant GitHub as GitHub REST API
    participant Cache as Server Cache

    Page->>Poll: onMounted → start interval
    loop every interval
        Poll->>Check: GET /work-items/{id}/check (with ETag)
        Check->>GitHub: conditional REST GET (ETag)
        alt 304 Not Modified
            GitHub-->>Check: 304
            Check-->>Poll: {changed:false}
        else 200 Modified
            GitHub-->>Check: 200 + body
            Check->>Cache: invalidate work-item cache
            Check-->>Poll: {changed:true}
            Poll-->>Page: merge fresh data into workItem
        end
    end
    Page->>Poll: stop on navigation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nudge the branch with gentle paws,

When checks are red, I still applause.
ETags listen, polling through the night,
Admins may merge by shielded light.
A hop, a merge — the code takes flight.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning One critical out-of-scope issue: server/api/upload/image.post.ts removes imports for validateImageFile and imageExtFromType without removing their usage, creating a runtime error unrelated to the merge/polling feature. Either restore the imports in image.post.ts or remove the unused validateImageFile/imageExtFromType code to prevent runtime errors.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: better merge flow and polling' directly relates to the main changes: implementing an admin merge bypass flow and implementing real-time polling for work items.
Description check ✅ Passed The description covers the summary adequately and includes the related issue, type of change, and checklist completion. However, it is quite brief and lacks specific details about the implementation approach.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #231: implements viewerCanMergeAsAdmin GraphQL query, adds bypass merge UI with warning styling, introduces lightweight check endpoint with ETag support, and implements polling behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/merge-flow-polling
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/composables/useWorkItemPolling.ts (1)

15-35: Guard against overlapping poll ticks.

setInterval with an async tick can start a new round before the previous one completes, causing duplicate concurrent requests under slow responses.

Suggested refactor
+  let inFlight = false
+
   async function tick() {
+    if (inFlight) return
+    inFlight = true
     try {
       // Lightweight check with ETag — 304 = free, no API cost
       const { changed } = await requestFetch<{ changed: boolean }>(checkUrl.value)
       if (!changed) return
@@
     }
     catch {
       // Network error — skip this round
     }
+    finally {
+      inFlight = false
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useWorkItemPolling.ts` around lines 15 - 35, The tick
function can overlap when previous async work hasn't finished; add a reentrancy
guard (e.g., a boolean like isTickRunning) referenced by tick and start to
prevent concurrent runs: at the top of tick return early if isTickRunning is
true, set it true before starting async operations and reset it in a finally
block so it always clears after success or error; update references to
timer/POLL_INTERVAL only if you switch scheduling strategy, but the minimal fix
is to add and use isTickRunning inside tick (affecting tick, start, requestFetch
and workItem usage remains unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/composables/useWorkItemPolling.ts`:
- Around line 44-49: The empty catch in the onMounted block is causing a lint
failure; replace the empty catch with proper error handling by capturing the
error from requestFetch(checkUrl.value) and logging or handling it (e.g., catch
(err) { console.error('requestFetch failed for', checkUrl.value, err) } or call
an existing error handler), then continue to call start(); update the onMounted
async function around requestFetch/checkUrl and ensure requestFetch, checkUrl,
and start references remain unchanged.

---

Nitpick comments:
In `@app/composables/useWorkItemPolling.ts`:
- Around line 15-35: The tick function can overlap when previous async work
hasn't finished; add a reentrancy guard (e.g., a boolean like isTickRunning)
referenced by tick and start to prevent concurrent runs: at the top of tick
return early if isTickRunning is true, set it true before starting async
operations and reset it in a finally block so it always clears after success or
error; update references to timer/POLL_INTERVAL only if you switch scheduling
strategy, but the minimal fix is to add and use isTickRunning inside tick
(affecting tick, start, requestFetch and workItem usage remains unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 481fa364-b31b-4534-8afc-2d643b2d12af

📥 Commits

Reviewing files that changed from the base of the PR and between 363d1b7 and f7b0a71.

📒 Files selected for processing (10)
  • app/components/work-item/WorkItemHeader.vue
  • app/composables/useWorkItemPolling.ts
  • app/pages/repos/[owner]/[repo]/work-items/[id].vue
  • i18n/locales/de.json
  • i18n/locales/en.json
  • i18n/schema.json
  • server/api/repository/[owner]/[repo]/pulls/[number]/merge-status.get.ts
  • server/api/repository/[owner]/[repo]/work-items/[id]/check.get.ts
  • server/api/upload/image.post.ts
  • shared/types/merge.ts
💤 Files with no reviewable changes (1)
  • server/api/upload/image.post.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/composables/useWorkItemPolling.ts`:
- Around line 23-25: The current guard skips updating workItem.value when it is
initially null/undefined, losing valid refresh payloads; in the
useWorkItemPolling composable change the conditional logic around workItem.value
so that when fresh is present you always assign/merge it into workItem.value
(e.g. merge with an empty object or assign fresh directly) instead of requiring
an existing workItem.value; apply this fix to both occurrences that reference
workItem.value and fresh (the block around the first if (fresh &&
workItem.value) and the similar block at lines 57-59) so fresh updates are not
dropped when workItem.value was previously null.
- Around line 18-19: The polling code in useWorkItemPolling is destructuring
const { changed } = await requestFetch(checkUrl.value) which throws a TypeError
when requestFetch returns undefined for HTTP 304; change the call so you
explicitly inspect the raw response/status before destructuring (e.g., await
requestFetch(...) -> resp; if !resp || resp.status === 304 return early) and
only then access resp.data or destructure changed; update any dependent logic in
useWorkItemPolling and keep checkUrl.value as the request target so we avoid
using exceptions to handle the 304 case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 858323cd-ebed-4f99-9a31-0bdd39fadfdc

📥 Commits

Reviewing files that changed from the base of the PR and between f7b0a71 and 2173a32.

📒 Files selected for processing (1)
  • app/composables/useWorkItemPolling.ts

Comment on lines +18 to +19
const { changed } = await requestFetch<{ changed: boolean }>(checkUrl.value)
if (!changed) return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Nuxt 4.3.1 (useRequestFetch/ofetch), what is the behavior of $fetch<{ changed: boolean }>(url) when the server responds with HTTP 304 and an empty body—does it throw, return undefined, or parse to an object?

💡 Result:

In Nuxt 4.3.1, $fetch is Nuxt’s wrapper around ofetch. (nuxt.com)

In ofetch, HTTP 304 is treated as a “null body” response (nullBodyResponses includes 304), so ofetch skips reading/parsing the body entirely. (github.com)

As a result:

  • $fetch<{ changed: boolean }>(url) does not throw just because the status is 304 (errors are only thrown for status >= 400 unless you opt out). (github.com)
  • It returns undefined (because _data is never set when there’s no body, and $fetch returns r._data). (github.com)
  • It does not parse to an object like { changed: false } automatically. (github.com)

Citations:


🏁 Script executed:

cat -n app/composables/useWorkItemPolling.ts | head -80

Repository: flumen-dev/flumen.dev

Length of output: 2421


Handle HTTP 304 explicitly instead of relying on exception handling.

Line 18 destructures from a response that becomes undefined when the server returns a 304 (no body). This throws a TypeError that falls into the catch block—functionally correct for this polling logic, but swallows the expected response together with real network errors and obfuscates intent.

A defensive check avoids unnecessary exception overhead:

Proposed fix
       // Lightweight check with ETag — 304 = free, no API cost
-      const { changed } = await requestFetch<{ changed: boolean }>(checkUrl.value)
-      if (!changed) return
+      const response = await requestFetch<{ changed: boolean }>(checkUrl.value)
+      if (!response?.changed) return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { changed } = await requestFetch<{ changed: boolean }>(checkUrl.value)
if (!changed) return
const response = await requestFetch<{ changed: boolean }>(checkUrl.value)
if (!response?.changed) return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useWorkItemPolling.ts` around lines 18 - 19, The polling code
in useWorkItemPolling is destructuring const { changed } = await
requestFetch(checkUrl.value) which throws a TypeError when requestFetch returns
undefined for HTTP 304; change the call so you explicitly inspect the raw
response/status before destructuring (e.g., await requestFetch(...) -> resp; if
!resp || resp.status === 304 return early) and only then access resp.data or
destructure changed; update any dependent logic in useWorkItemPolling and keep
checkUrl.value as the request target so we avoid using exceptions to handle the
304 case.

Comment on lines +23 to +25
if (fresh && workItem.value) {
workItem.value = { ...workItem.value, ...fresh }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t drop updates when workItem.value is initially null/undefined.

Line 23 and Line 57 currently skip assignment unless workItem.value already exists. That can lose legitimate refresh results.

Proposed fix
-      if (fresh && workItem.value) {
-        workItem.value = { ...workItem.value, ...fresh }
-      }
+      if (fresh) {
+        workItem.value = workItem.value
+          ? { ...workItem.value, ...fresh }
+          : fresh
+      }
...
-      if (fresh && workItem.value) {
-        workItem.value = { ...workItem.value, ...fresh }
-      }
+      if (fresh) {
+        workItem.value = workItem.value
+          ? { ...workItem.value, ...fresh }
+          : fresh
+      }

Also applies to: 57-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useWorkItemPolling.ts` around lines 23 - 25, The current
guard skips updating workItem.value when it is initially null/undefined, losing
valid refresh payloads; in the useWorkItemPolling composable change the
conditional logic around workItem.value so that when fresh is present you always
assign/merge it into workItem.value (e.g. merge with an empty object or assign
fresh directly) instead of requiring an existing workItem.value; apply this fix
to both occurrences that reference workItem.value and fresh (the block around
the first if (fresh && workItem.value) and the similar block at lines 57-59) so
fresh updates are not dropped when workItem.value was previously null.

@Flo0806 Flo0806 merged commit 996ed21 into main Mar 15, 2026
9 of 10 checks passed
@Flo0806 Flo0806 deleted the feat/merge-flow-polling branch March 15, 2026 08:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: bypass branch protection rules when merging

1 participant