Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci-macos-compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
submodules: recursive

- name: Select Xcode
Expand Down
15 changes: 9 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Validate Depot runner guards
run: ./tests/test_ci_self_hosted_guard.sh
Expand All @@ -35,10 +35,13 @@ jobs:
working-directory: web
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
- name: Install Bun
run: |
set -euo pipefail
curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.10
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
Comment on lines +40 to +44

@cubic-dev-ai cubic-dev-ai Bot Mar 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Avoid curl | bash for tool installation in CI. This runs an unverified remote script and weakens supply-chain safety compared with a pinned setup action.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 40:

<comment>Avoid `curl | bash` for tool installation in CI. This runs an unverified remote script and weakens supply-chain safety compared with a pinned setup action.</comment>

<file context>
@@ -35,10 +35,13 @@ jobs:
 
-      - name: Setup Bun
-        uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
+      - name: Install Bun
+        run: |
+          set -euo pipefail
</file context>
Suggested change
- name: Install Bun
run: |
set -euo pipefail
curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.10
echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
- name: Setup Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2
with:
bun-version: 1.3.10
Fix with Cubic

Comment on lines +40 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the workflow file to see current implementation and context
cat -n .github/workflows/ci.yml | head -60

Repository: manaflow-ai/cmux

Length of output: 2011


🏁 Script executed:

# Check if there's git history showing what was previously used for Bun setup
git log --oneline -20 .github/workflows/ci.yml

Repository: manaflow-ai/cmux

Length of output: 109


🏁 Script executed:

# Search for any Bun-related setup actions or references in the repo
rg -i "bun" --type yaml

Repository: manaflow-ai/cmux

Length of output: 122


🌐 Web query:

official Bun GitHub Action setup-bun version pinning

💡 Result:

To pin Bun’s version with the official GitHub Action (oven-sh/setup-bun), set bun-version (or point to a version file):

- uses: oven-sh/setup-bun@v2
  with:
    bun-version: "1.3.3"   # also supports "latest", "canary", "1.3.x"

[2]

Or:

- uses: oven-sh/setup-bun@v2
  with:
    bun-version-file: ".bun-version"  # also supports package.json / .tool-versions

[1]

If you don’t specify a version, the action will auto-detect from package.json (packageManager first, then engines.bun), otherwise it installs latest. [1]

To pin the action itself (recommended for reproducibility), use a specific tag (example latest shown in the repo UI as v2.1.3, released Mar 4, 2026):

- uses: oven-sh/setup-bun@v2.1.3
  with:
    bun-version: "1.3.3"

[1][3]

Sources: official action README [1], Bun docs CI/CD guide [2], action releases/tags [3].


Switch to the official pinned Bun action instead of piping an installer into bash.

The current implementation executes a mutable remote script at job time (https://bun.sh/install), which loses the immutability and auditability of a pinned action. Use the official oven-sh/setup-bun@v2.1.3 action with an explicit version instead:

- uses: oven-sh/setup-bun@v2.1.3
  with:
    bun-version: "1.3.10"

This provides commit-level immutability, reviewed/auditable code, and proper version pinning for both the action and the Bun runtime.

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

In @.github/workflows/ci.yml around lines 40 - 44, Replace the current "Install
Bun" step that pipes https://bun.sh/install into bash with the official pinned
GitHub Action; stop using the curl | bash approach and instead use the
oven-sh/setup-bun action (e.g., oven-sh/setup-bun@v2.1.3) and pass the
bun-version input set to "1.3.10" so the workflow is immutable and auditable;
update the step name if needed and remove the echo to GITHUB_PATH since the
action manages installation paths.


- name: Install dependencies
run: bun install --frozen-lockfile
Expand All @@ -50,7 +53,7 @@ jobs:
runs-on: macos-15
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive

Expand Down Expand Up @@ -157,7 +160,7 @@ jobs:
runs-on: depot-macos-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive

Expand Down
107 changes: 92 additions & 15 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2101,7 +2101,7 @@ final class WindowBrowserPortal: NSObject {
let containerView = entry.containerView,
!containerView.isHidden else { continue }
guard webView.superview === containerView else { continue }
refreshHostedWebViewPresentation(
invalidateHostedWebViewGeometry(
webView,
in: containerView,
reason: "externalGeometry"
Expand Down Expand Up @@ -2378,14 +2378,16 @@ final class WindowBrowserPortal: NSObject {
_ webView: WKWebView,
in containerView: WindowBrowserSlotView,
reason: String,
phase: String
phase: String,
reattachRenderingState: Bool
) {
guard !containerView.isHidden else { return }
guard !containerView.isHostedInspectorDividerDragActive else {
#if DEBUG
dlog(
"browser.portal.refresh.skip web=\(browserPortalDebugToken(webView)) " +
"container=\(browserPortalDebugToken(containerView)) reason=\(reason) phase=\(phase) drag=1"
"container=\(browserPortalDebugToken(containerView)) reason=\(reason) phase=\(phase) " +
"drag=1 reattach=\(reattachRenderingState ? 1 : 0)"
)
#endif
return
Expand Down Expand Up @@ -2414,34 +2416,58 @@ final class WindowBrowserPortal: NSObject {
scrollView.displayIfNeeded()
}
webView.layoutSubtreeIfNeeded()
webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)")
if reattachRenderingState {
webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)")
}
Comment on lines +2419 to +2421

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant else branch, divergence risk

The else block contains the exact same three displayIfNeeded() calls as the end of the if block. The only meaningful difference between the two branches is the browserPortalReattachRenderingState call, so the display work can be lifted out of the conditional entirely. As written, any future addition to one branch (e.g. an extra flush or guard) must be duplicated in the other or a silent regression is introduced.

Suggested change
if reattachRenderingState {
webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)")
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()
} else {
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()
}
if reattachRenderingState {
webView.browserPortalReattachRenderingState(reason: "\(reason):\(phase)")
}
containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()

containerView.displayIfNeeded()
webView.displayIfNeeded()
(webView.window ?? hostView.window)?.displayIfNeeded()
#if DEBUG
dlog(
"browser.portal.refresh web=\(browserPortalDebugToken(webView)) " +
"\(reattachRenderingState ? "browser.portal.refresh" : "browser.portal.invalidate") " +
"web=\(browserPortalDebugToken(webView)) " +
"container=\(browserPortalDebugToken(containerView)) reason=\(reason) " +
"phase=\(phase) frame=\(browserPortalDebugFrame(containerView.frame))"
)
#endif
}

private func invalidateHostedWebViewGeometry(
_ webView: WKWebView,
in containerView: WindowBrowserSlotView,
reason: String
) {
runHostedWebViewRefreshPass(
webView,
in: containerView,
reason: reason,
phase: "geometry",
reattachRenderingState: false
)
}

private func refreshHostedWebViewPresentation(
_ webView: WKWebView,
in containerView: WindowBrowserSlotView,
reason: String
) {
guard !containerView.isHidden else { return }

runHostedWebViewRefreshPass(webView, in: containerView, reason: reason, phase: "immediate")
runHostedWebViewRefreshPass(
webView,
in: containerView,
reason: reason,
phase: "immediate",
reattachRenderingState: true
)
DispatchQueue.main.async { [weak self, weak webView, weak containerView] in
guard let self, let webView, let containerView else { return }
self.runHostedWebViewRefreshPass(
webView,
in: containerView,
reason: reason,
phase: "async"
phase: "async",
reattachRenderingState: true
)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { [weak self, weak webView, weak containerView] in
Expand All @@ -2450,11 +2476,45 @@ final class WindowBrowserPortal: NSObject {
webView,
in: containerView,
reason: reason,
phase: "delayed"
phase: "delayed",
reattachRenderingState: true
)
}
}

private enum HostedWebViewPresentationUpdateKind {
case none
case geometryOnly
case refresh

private static let geometryOnlyReasons: Set<String> = [
"frame",
"bounds",
"webFrame",
"webFrameBottomDock",
]

private static let refreshReasons: Set<String> = [
"syncAttachContainer",
"syncAttachWebView",
"reveal",
"transientRecovery",
"anchor",
]

static func resolve(reasons: [String]) -> Self {
guard !reasons.isEmpty else { return .none }
let reasonSet = Set(reasons)
if !reasonSet.isDisjoint(with: Self.refreshReasons) {
return .refresh
}
if reasonSet.isSubset(of: Self.geometryOnlyReasons) {
return .geometryOnly
}
return .refresh
}
}

private func moveWebKitRelatedSubviewsIfNeeded(
from sourceSuperview: NSView,
to containerView: WindowBrowserSlotView,
Expand Down Expand Up @@ -3272,8 +3332,13 @@ final class WindowBrowserPortal: NSObject {
let hostedInspectorAdjustedDuringSync =
containerOwnsWebView &&
hostView.reapplyHostedInspectorDividerIfNeeded(in: containerView, reason: "portal.sync")
if !shouldHide, containerOwnsWebView, !refreshReasons.isEmpty {
if hostedInspectorAdjustedDuringSync && !recoveredFromTransientGeometry {
let presentationUpdateKind = HostedWebViewPresentationUpdateKind.resolve(
reasons: refreshReasons
)
if !shouldHide, containerOwnsWebView, presentationUpdateKind != .none {
if presentationUpdateKind == .refresh &&
hostedInspectorAdjustedDuringSync &&
!recoveredFromTransientGeometry {
#if DEBUG
dlog(
"browser.portal.refresh.skip web=\(browserPortalDebugToken(webView)) " +
Expand All @@ -3282,11 +3347,23 @@ final class WindowBrowserPortal: NSObject {
)
#endif
} else {
refreshHostedWebViewPresentation(
webView,
in: containerView,
reason: "\(source):" + refreshReasons.joined(separator: ",")
)
let refreshReason = "\(source):" + refreshReasons.joined(separator: ",")
switch presentationUpdateKind {
case .none:
break
case .geometryOnly:
invalidateHostedWebViewGeometry(
webView,
in: containerView,
reason: refreshReason
)
case .refresh:
refreshHostedWebViewPresentation(
webView,
in: containerView,
reason: refreshReason
)
}
}
}
if containerOwnsWebView, !hostedInspectorAdjustedDuringSync {
Expand Down
Loading
Loading