Skip to content

ci: unregress tart image gen for macos - #25134

Closed
nektro wants to merge 10 commits into
mainfrom
nektro-patch-24649
Closed

ci: unregress tart image gen for macos#25134
nektro wants to merge 10 commits into
mainfrom
nektro-patch-24649

Conversation

@nektro

@nektro nektro commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

as is this gets bun run machine:macos:14 working when you have tart and sshpass installed.
given the links in the comment for tart.Machine.snapshot it looks like this would be integratabtle into CI proper without too much followup work, at least for arm64.

testing with [build images] but no need to [publish images] on merge yet

13 screenshot image
15 screenshot image
26 screenshot image

tart can only be installed on macOS so darwin image builds will need to run on a macos queue
that should be easy to setup (can be another hosted queue) but feels outside the scope of this pr

@robobun

robobun commented Nov 27, 2025

Copy link
Copy Markdown
Collaborator
Updated 11:02 PM PT - Nov 26th, 2025

@nektro, your commit 85c93db has 3 failures in Build #32499 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 25134

That installs a local version of the PR into your bun-25134 executable, so you can run:

bun-25134 --bun

@nektro
nektro marked this pull request as ready for review November 27, 2025 06:20
@coderabbitai

This comment was marked as duplicate.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
scripts/bootstrap.sh (1)

315-318: Invalid shell operator ~ - this will cause a syntax error.

The ~ operator is not valid POSIX shell syntax. The [ (test) command doesn't support regex matching. This will fail on POSIX-compliant shells.

Use case for pattern matching, which is POSIX-compliant:

-		if [ "$alpine" ~ "_" ]; then
-			release="$(print "$alpine" | cut -d_ -f1)-edge"
+		case "$alpine" in
+		*_*)
+			release="$(print "$alpine" | cut -d_ -f1)-edge"
+			;;
+		*)
+			release="$alpine"
+			;;
+		esac
+		if false; then
+			: # placeholder to maintain structure

Or more cleanly:

-		if [ "$alpine" ~ "_" ]; then
-			release="$(print "$alpine" | cut -d_ -f1)-edge"
-		else
-			release="$alpine"
-		fi
+		case "$alpine" in
+		*_*)
+			release="$(print "$alpine" | cut -d_ -f1)-edge"
+			;;
+		*)
+			release="$alpine"
+			;;
+		esac
scripts/utils.mjs (1)

2240-2272: Timeout handler is missing — connection can hang indefinitely

The review comment is accurate. Node.js's timeout option to net.connect() calls socket.setTimeout(timeout), which only emits a 'timeout' event on socket inactivity; it does not automatically close the socket or emit 'error'.

Since the code only listens for "connect" and "error" events, a timeout will leave the promise unresolved, causing waitForPort to hang indefinitely when connecting to a black-hole address.

The suggested fix to add explicit timeout handling is necessary:

-    const connected = new Promise((resolve, reject) => {
-      const socket = connect({ host: hostname, port, timeout: 10_000 });
+    const connected = new Promise((resolve, reject) => {
+      const socket = connect({ host: hostname, port, timeout: 10_000 });
       socket.on("connect", () => {
         socket.destroy();
         console.log("Connected:", `${hostname}:${port}`);
         resolve();
       });
+      socket.on("timeout", () => {
+        socket.destroy(new Error("Connection timed out"));
+      });
       socket.on("error", error => {
         socket.destroy();
         reject(error);
       });
     });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 908ab9c and 85c93db.

📒 Files selected for processing (6)
  • .buildkite/ci.mjs (5 hunks)
  • package.json (1 hunks)
  • scripts/bootstrap.sh (24 hunks)
  • scripts/machine.mjs (1 hunks)
  • scripts/tart.mjs (5 hunks)
  • scripts/utils.mjs (6 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Applies to src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js} : Use `process.platform` and `process.arch` for platform detection; these values are inlined and dead-code eliminated at build time

Applied to files:

  • package.json
  • scripts/tart.mjs
  • .buildkite/ci.mjs
📚 Learning: 2025-11-24T18:37:30.259Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:30.259Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Never use hardcoded port numbers in tests. Always use `port: 0` to get a random port

Applied to files:

  • scripts/utils.mjs
📚 Learning: 2025-11-24T18:35:50.422Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: .cursor/rules/writing-tests.mdc:0-0
Timestamp: 2025-11-24T18:35:50.422Z
Learning: Applies to test/cli/**/*.{js,ts,jsx,tsx} : When testing Bun as a CLI, use the `spawn` API from `bun` with the `bunExe()` and `bunEnv` from `harness` to execute Bun commands and validate exit codes, stdout, and stderr

Applied to files:

  • scripts/tart.mjs
  • scripts/machine.mjs
📚 Learning: 2025-11-24T18:37:30.259Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:30.259Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : When spawning Bun processes in tests, use `bunExe` and `bunEnv` from `harness` to ensure the same build of Bun is used and debug logging is silenced

Applied to files:

  • .buildkite/ci.mjs
🧬 Code graph analysis (2)
scripts/tart.mjs (2)
scripts/machine.mjs (6)
  • result (126-136)
  • result (138-138)
  • result (1231-1231)
  • result (1232-1232)
  • result (1388-1388)
  • snapshot (591-600)
scripts/utils.mjs (14)
  • result (145-145)
  • result (257-283)
  • result (370-370)
  • result (924-924)
  • result (926-926)
  • result (2725-2725)
  • result (2791-2791)
  • result (3061-3061)
  • result (3065-3065)
  • result (3079-3079)
  • result (3161-3161)
  • error (255-255)
  • error (368-368)
  • error (858-858)
.buildkite/ci.mjs (4)
scripts/machine.mjs (1)
  • cloud (1145-1145)
scripts/utils.mjs (1)
  • os (1680-1680)
scripts/runner.node.mjs (2)
  • os (353-353)
  • platform (2062-2062)
scripts/tart.mjs (1)
  • platform (46-46)
🪛 Biome (2.1.2)
scripts/tart.mjs

[error] 87-94: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

🔇 Additional comments (17)
package.json (1)

95-98: LGTM!

The new macOS machine configurations are well-structured and consistent with existing configurations. The cloud=tart and arch=arm64 choices are appropriate since Tart only supports darwin/aarch64 platforms.

.buildkite/ci.mjs (3)

106-109: LGTM!

The expanded macOS platform support (releases 13, 15, 26) aligns with the new package.json configurations and tart.mjs distro mappings.


667-669: LGTM!

Correctly excludes both freebsd and darwin from the release step dependencies, as darwin builds are handled separately via tart and aren't part of the main CI release flow yet.


1082-1082: LGTM!

Appropriately excludes darwin platforms from automated image creation since tart-based image builds require a macOS host, which is outside the scope of the current CI infrastructure.

scripts/bootstrap.sh (6)

25-27: LGTM!

The change from exit 1 to kill $$ correctly handles the case where error() is called inside a subshell. With exit 1, only the subshell would die and the script would continue. Using kill $$ ensures the entire script terminates.


458-460: Consider if brew upgrade is the intended behavior here.

Running brew upgrade during package manager initialization will upgrade all installed packages, which can be time-consuming and may cause unexpected changes. Other package managers only run update (refresh package lists) at this stage.

Was upgrade intentional, or should this be brew update to match the behavior of other package managers?

 	brew)
-		package_manager upgrade
+		package_manager update
 		;;

682-682: LGTM!

The Homebrew installation uses the canonical install script URL from GitHub.


1320-1322: Verify the package name rust for Homebrew.

Homebrew uses the package name rust, but this installs Rust via Homebrew's formula. This is correct, but note that Homebrew's Rust may lag behind the official releases.


1729-1739: LGTM!

Proper darwin/aarch64 support added for the age encryption tool with the correct SHA256 hash for verification.


1169-1170: The original review comment's concern is not valid. Verification confirms that lld@19 (and other versioned variants) exist as separate Homebrew formulas. Modern Homebrew does package lld as a separate versioned formula, not only as part of llvm. The code at lines 1169-1170 correctly installs and links both packages separately, which is consistent with how other package managers handle these packages in the same script.

scripts/machine.mjs (1)

34-34: LGTM!

Clean refactor moving spawnScp to utils.mjs for centralized reuse across modules. The import aligns with the tart.mjs changes that also now import spawnScp from utils.

scripts/tart.mjs (3)

2-2: LGTM!

Correct imports added from utils.mjs to support SCP and SSH functionality.


51-51: LGTM!

Correctly adds macOS 26 (Tahoe) to the distro mapping, aligning with the new package.json configurations.


269-276: Placeholder implementation - confirm this is intentional.

The snapshot() method currently just returns the tag without actually creating or pushing a snapshot. The TODO comments indicate this needs further implementation to push to ghcr.io.

Is this placeholder acceptable for this PR, with the actual implementation to follow? If so, consider adding a more explicit warning or throwing an error for now to prevent accidental misuse:

const snapshot = async tag => {
  console.warn("snapshot() is not yet implemented - returning tag as placeholder");
  // TODO: push to ghcr and return that full remote_image_name:tag
  return tag;
};
scripts/utils.mjs (3)

118-128: SpawnOptions: detached JSDoc addition is consistent and non-breaking

This just documents an option that spawn already forwards through to nodeSpawn via ...options, so it’s backward‑compatible and clarifies usage for new call sites.


2169-2196: getCloudMetadata: reduced retries to 3 keeps failures bounded but still safe

Dropping curl retries from 10 to 3 significantly shortens worst‑case wait time while still logging a warning and returning undefined on persistent failure. Given all callers already handle an undefined result, this change looks reasonable.


3028-3085: spawnSsh: lower default retries to 3 — behavior change is localized and clear

The new retries = 3 default bounds the SSH retry loop more tightly while preserving the existing behavior of only retrying on exit code 255 without “Authenticated” in the verbose log. This looks fine, but it does reduce the grace period for slow‑to‑come‑up machines; if there are known slow environments, you may want to override retries at those call sites rather than relying on the default.

Comment thread .buildkite/ci.mjs
const { os, arch, distro, release, features } = platform;
const { publishImages } = options;
const action = publishImages ? "publish-image" : "create-image";
const cloud = os == "darwin" ? "tart" : "aws";

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.

🧹 Nitpick | 🔵 Trivial

Prefer strict equality (===) for consistency.

The comparison uses == while the rest of the codebase uses === for string comparisons.

-  const cloud = os == "darwin" ? "tart" : "aws";
+  const cloud = os === "darwin" ? "tart" : "aws";
📝 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 cloud = os == "darwin" ? "tart" : "aws";
const cloud = os === "darwin" ? "tart" : "aws";
🤖 Prompt for AI Agents
In .buildkite/ci.mjs around line 618, the conditional uses loose equality (==)
to compare os to "darwin"; change it to strict equality (===) to match codebase
conventions and avoid type-coercion issues—update the expression so it uses ===
for the comparison while keeping the existing ternary result.

Comment thread scripts/tart.mjs
Comment on lines +87 to 94
return new Promise(async resolve => {
const result = await this.spawn(["get", name], {
json: true,
throwOnError: error => !/does not exist/i.test(inspect(error)),
});
if (!result) resolve(undefined);
else resolve({ Name: name, ...result });
});

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.

⚠️ Potential issue | 🟠 Major

Remove unnecessary Promise wrapper with async executor.

The static analysis correctly flags this: async Promise executors are problematic because:

  1. Errors thrown in async executors won't reject the Promise—they'll be unhandled
  2. The return statements inside resolve() calls don't exit the async function

The existing code also has a control flow issue: after resolve(undefined) on line 92, execution continues to line 93.

Simplify by removing the Promise wrapper entirely:

   async getVm(name) {
-    return new Promise(async resolve => {
-      const result = await this.spawn(["get", name], {
-        json: true,
-        throwOnError: error => !/does not exist/i.test(inspect(error)),
-      });
-      if (!result) resolve(undefined);
-      else resolve({ Name: name, ...result });
+    const result = await this.spawn(["get", name], {
+      json: true,
+      throwOnError: error => !/does not exist/i.test(inspect(error)),
     });
+    if (!result) return undefined;
+    return { Name: name, ...result };
   },

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (2.1.2)

[error] 87-94: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

🤖 Prompt for AI Agents
In scripts/tart.mjs around lines 87 to 94, remove the unnecessary new
Promise(async resolve => { ... }) wrapper and instead make the function body
directly async: await this.spawn([...], ...); if the spawn returns a falsy
result return undefined immediately, otherwise return { Name: name, ...result };
do not call resolve or use an async executor, and let thrown errors propagate
(or wrap in a normal try/catch if you need to convert errors) so control flow is
correct and no code runs after the early return.

Comment thread scripts/utils.mjs
alii added a commit that referenced this pull request Apr 15, 2026
scripts/tart.mjs has been broken since it was added — it called
spawnSsh/spawnSshSafe/spawnScp/spawnRdp without importing or defining
them, so any `machine.mjs --cloud=tart` invocation would crash on first
SSH/SCP. This unblocks the macOS image-build path.

- tart.mjs: import spawnSsh/spawnSshSafe/spawnScp from utils.mjs
- tart.mjs: drop dead rdp() (called non-existent spawnRdp; macOS uses
  VNC, not RDP)
- tart.mjs: fix getVm() always-truthy bug — when `tart get` returns
  nothing it now returns undefined instead of `{Name: name}`, so
  cloneVm() correctly falls through to pull when the local image is
  missing
- tart.mjs: switch base image suffix -xcode → -base (cirruslabs base
  images are ~25GB vs ~50GB+ with full Xcode; bootstrap.sh installs
  our own toolchain) and add macOS 26 Tahoe to the distros map
- tart.mjs: implement snapshot(label) — stop VM then `tart push` to
  ghcr.io/oven-sh/<label>. machine.mjs:1559 already calls
  machine.snapshot() on the publish-image path; without this the tart
  cloud crashes there.
- tart.mjs: fix error message printing `undefined` instead of the
  release number
- machine.mjs / utils.mjs: move spawnScp from machine.mjs to utils.mjs
  (next to spawnSsh) so tart.mjs can import it without a cycle

Supersedes the tart.mjs portion of #25134 (drops the unrelated FreeBSD
and azure-removal changes from that branch).
@alii

alii commented Apr 15, 2026

Copy link
Copy Markdown
Member

#29315

@alii alii closed this Apr 15, 2026
alii added a commit that referenced this pull request Apr 15, 2026
scripts/tart.mjs has been broken since it was added — it called
spawnSsh/spawnSshSafe/spawnScp/spawnRdp without importing or defining
them, so any `machine.mjs --cloud=tart` invocation would crash on first
SSH/SCP. This unblocks the macOS image-build path.

- tart.mjs: import spawnSsh/spawnSshSafe/spawnScp from utils.mjs
- tart.mjs: drop dead rdp() (called non-existent spawnRdp; macOS uses
  VNC, not RDP)
- tart.mjs: fix getVm() always-truthy bug — when `tart get` returns
  nothing it now returns undefined instead of `{Name: name}`, so
  cloneVm() correctly falls through to pull when the local image is
  missing
- tart.mjs: switch base image suffix -xcode → -base (cirruslabs base
  images are ~25GB vs ~50GB+ with full Xcode; bootstrap.sh installs
  our own toolchain) and add macOS 26 Tahoe to the distros map
- tart.mjs: implement snapshot(label) — stop VM then `tart push` to
  ghcr.io/oven-sh/<label>. machine.mjs:1559 already calls
  machine.snapshot() on the publish-image path; without this the tart
  cloud crashes there.
- tart.mjs: fix error message printing `undefined` instead of the
  release number
- machine.mjs / utils.mjs: move spawnScp from machine.mjs to utils.mjs
  (next to spawnSsh) so tart.mjs can import it without a cycle

Supersedes the tart.mjs portion of #25134 (drops the unrelated FreeBSD
and azure-removal changes from that branch).
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.

3 participants