IJAI-382 declare the AIR docs gate's bun to Bazel; an unrunnable gate now fails

The architecture and agent-catalog gates ran docs/scripts/*.ts through
community/tools/bun.cmd, which downloads bun into a per-user cache. ADR 0008 accepted
the consequence - "the gate on TC is best-effort by design: where bun cannot run, model
validation coverage degrades to skipped tests" - and a gate that may skip eventually
does, indistinguishably from a green one.

MODULE.bazel now fetches the six bun release archives with http_archive, pinned to the
same version and checksums as bun.cmd, and @community//build:bun selects the host's.
air-plugin-tests_test declares it in data and passes its $(rlocationpath) as
-Dair.docs.bun.path; AirDocsRuntime resolves that through runfiles and runs the binary
directly, so neither the wrapper, its cache, nor the network is consulted. Outside Bazel
the wrapper is still the entry point, since there is no runfiles tree to resolve against.

GateResult.Unavailable and the catalog test's infrastructure assumptions are now hard
failures. Verified both directions: with ~/Library/Caches/.../monorepo-tools/bun moved
aside the target still passes, and pointing the property at a missing file reports FAILED
naming the data dependency instead of SKIPPED.

The archives are mirrored on packages.jetbrains.team because bazel_downloader.cfg routes
github.com through cache-redirector.jetbrains.com, which does not serve the oven-sh/bun
repository. BunToolConsistencyTest fails if the MODULE.bazel and bun.cmd pins ever drift.

bun.cmd itself is unchanged and remains the entry point for BT, the ij-proxy MCP server,
render-guides and pnpm run check. air-plugin-tests_test stays external for the other
reason - AirRepoScan walks the working tree and plugins/air is 129 Bazel packages - which
NON_SANDBOXED_BASELINE now records. No behavior change outside tests, so no spec update
was needed.

(cherry picked from commit 3c1ba7e7cca24aaeb68e290cdadfaba4b07a4f0d)

GitOrigin-RevId: 380c87ec3940392eb9b24b53bfa479277b815d70
This commit is contained in:
Vladimir Krivosheev
2026-08-01 01:02:22 +00:00
committed by intellij-monorepo-bot
parent ac26662086
commit 29dc56c1a3
4 changed files with 173 additions and 0 deletions
+39
View File
@@ -124,6 +124,45 @@ override_repo(
remote_java_tools_windows = "remote_java_tools_windows_with_vc_redist",
)
# bun as a declared Bazel input, so a test that needs the JS runtime does not depend on
# `tools/bun.cmd` having populated its per-user cache — the wrapper downloads on demand, which a
# sandboxed test cannot do (`sandbox = True` implies `block-network`). See //build:bun for the
# host-selected alias, and plugins/air/docs/decisions/0016-bazel-provided-bun.md for why the AIR
# architecture gate needs it.
#
# The version and checksums must stay in sync with `tools/bun.cmd`; `BunToolConsistencyTest` enforces
# that. The checksums are the upstream release archives', which is what `sha256` here wants too.
BUN_VERSION = "1.3.14"
# A mirror of the GitHub release, byte-identical: `bazel_downloader.cfg` routes github.com through
# cache-redirector.jetbrains.com, and the redirector does not serve the `oven-sh/bun` repo.
BUN_BASE_URL = "https://packages.jetbrains.team/files/p/ij/intellij-build-dependencies/bun"
BUN_PLATFORMS = {
"linux_x64": "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f",
"linux_aarch64": "a27ffb63a8310375836e0d6f668ae17fa8d8d18b88c37c821c65331973a19a3b",
"windows_x64": "0a0620930b6675d7ba440e81f4e0e00d3cfbe096c4b140d3fff02205e9e18922",
"windows_aarch64": "89841f5a57f2348b67ec0839b718f4bf4ea7d07c371c9ba4b77b6c790f918953",
"darwin_x64": "4183df3374623e5bab315c547cfa0974533cd457d86b73b639f7a87974cd6633",
"darwin_aarch64": "d8b96221828ad6f97ac7ac0ab7e95872341af763001e8803e8267652c2652620",
}
[
http_archive(
name = "bun_%s" % platform,
build_file = "//build:BUILD.bun.bazel",
sha256 = sha256,
# the release zip nests everything under a directory named after the archive
strip_prefix = "bun-%s" % platform.replace("_", "-"),
url = "{base}/{version}/bun-{platform}.zip".format(
base = BUN_BASE_URL,
platform = platform.replace("_", "-"),
version = BUN_VERSION,
),
)
for platform, sha256 in BUN_PLATFORMS.items()
]
# Register the extension that runs jps-to-bazel converter and provides target lists for dev-build.
# This creates the @jps_dynamic_deps_community repository.
use_repo(
+49
View File
@@ -27,6 +27,55 @@ jps_to_bazel_targets_json(
visibility = ["//visibility:public"],
)
[
config_setting(
name = "host_%s" % name,
constraint_values = constraints,
)
for name, constraints in {
"darwin_aarch64": [
"@platforms//os:macos",
"@platforms//cpu:arm64",
],
"darwin_x64": [
"@platforms//os:macos",
"@platforms//cpu:x86_64",
],
"linux_aarch64": [
"@platforms//os:linux",
"@platforms//cpu:arm64",
],
"linux_x64": [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
"windows_aarch64": [
"@platforms//os:windows",
"@platforms//cpu:arm64",
],
"windows_x64": [
"@platforms//os:windows",
"@platforms//cpu:x86_64",
],
}.items()
]
# The bun runtime for the current host, declared in //:MODULE.bazel. Depend on this from `data` and
# resolve it with BazelTestUtil.getFileFromBazelRuntime instead of running `tools/bun.cmd`, which
# needs its per-user cache or the network — neither of which a sandboxed test has.
alias(
name = "bun",
actual = select({
":host_darwin_aarch64": "@bun_darwin_aarch64//:binary",
":host_darwin_x64": "@bun_darwin_x64//:binary",
":host_linux_aarch64": "@bun_linux_aarch64//:binary",
":host_linux_x64": "@bun_linux_x64//:binary",
":host_windows_aarch64": "@bun_windows_aarch64//:binary",
":host_windows_x64": "@bun_windows_x64//:binary",
}),
visibility = ["//visibility:public"],
)
# Can't name the target 'installers' since it triggers elevation in Windows
java_binary(
name = "i_build_target",
+17
View File
@@ -0,0 +1,17 @@
# Build file for the `bun_*` archives declared in //:MODULE.bazel.
# The archives are the same GitHub release zips `tools/bun.cmd` pins, with the top-level
# `bun-<platform>` directory stripped, so the runtime is a single binary at the root.
#
# Not named `bun`: the extracted binary is itself `bun`, and a rule sharing a source file's name
# makes `:bun` self-referential — Bazel reports it as a dependency cycle.
filegroup(
name = "binary",
srcs = glob(
[
"bun",
"bun.exe",
],
allow_empty = True,
),
visibility = ["//visibility:public"],
)
@@ -0,0 +1,68 @@
package com.intellij.tools.cmd
import com.intellij.openapi.application.PathManager
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import kotlin.io.path.readText
/**
* `tools/bun.cmd` and the `bun_*` archives in `MODULE.bazel` must pin the same bun.
*
* They are two independent entry points to the same runtime: the wrapper serves everything outside
* Bazel (`BT`, the ij-proxy MCP server, `pnpm run check`), while the archives serve Bazel tests that
* cannot download anything. If the two pins drift, the same script runs on two different bun versions
* depending on who invoked it, which is the kind of difference that only shows up as an
* unreproducible test failure.
*/
@Timeout(1, unit = TimeUnit.MINUTES)
class BunToolConsistencyTest {
@Test
fun moduleBazelPinsTheSameVersionAsTheWrapper() {
val declared = Regex("""^BUN_VERSION = "([^"]+)"""", RegexOption.MULTILINE).find(moduleBazel.readText())
assertThat(requireNotNull(declared) { "$moduleBazel declares no BUN_VERSION" }.groupValues[1])
.describedAs(
"MODULE.bazel BUN_VERSION must match TOOL_VERSION in tools/bun.cmd; update both, and refresh " +
"BUN_PLATFORMS with the new archive checksums"
)
.isEqualTo(CmdToolTestUtil.parseToolVersion("bun.cmd"))
}
@Test
fun moduleBazelPinsTheSameChecksumsAsTheWrapper() {
// `sha256` in an http_archive and TOOL_CHECKSUM_* in the wrapper both hash the release archive,
// so they are comparable directly.
val wrapper = CmdToolTestUtil.resolveToolsDir().resolve("bun.cmd").readText()
val expected = WRAPPER_CHECKSUM_VARIABLES.mapValues { (_, variable) ->
val match = Regex("""^export $variable="([0-9a-f]{64})"""", RegexOption.MULTILINE).find(wrapper)
requireNotNull(match) { "tools/bun.cmd declares no $variable" }.groupValues[1]
}
assertThat(starlarkPlatformChecksums())
.describedAs("MODULE.bazel BUN_PLATFORMS must match the TOOL_CHECKSUM_* values in tools/bun.cmd")
.isEqualTo(expected)
}
/** `BUN_PLATFORMS` as declared in `MODULE.bazel`, keyed by the platform used in the archive name. */
private fun starlarkPlatformChecksums(): Map<String, String> {
val block = Regex("""BUN_PLATFORMS = \{(.*?)}""", RegexOption.DOT_MATCHES_ALL).find(moduleBazel.readText())
requireNotNull(block) { "$moduleBazel declares no BUN_PLATFORMS" }
return Regex(""""([a-z0-9_]+)":\s*"([0-9a-f]{64})"""").findAll(block.groupValues[1])
.associate { it.groupValues[1] to it.groupValues[2] }
}
private val moduleBazel: Path
get() = Path.of(PathManager.getCommunityHomePath()).resolve("MODULE.bazel")
}
/** MODULE.bazel platform key to the wrapper variable holding that platform's archive checksum. */
private val WRAPPER_CHECKSUM_VARIABLES = mapOf(
"linux_x64" to "TOOL_CHECKSUM_LINUX_X64",
"linux_aarch64" to "TOOL_CHECKSUM_LINUX_ARM64",
"windows_x64" to "TOOL_CHECKSUM_WINDOWS_X64",
"windows_aarch64" to "TOOL_CHECKSUM_WINDOWS_ARM64",
"darwin_x64" to "TOOL_CHECKSUM_MACOS_X64",
"darwin_aarch64" to "TOOL_CHECKSUM_MACOS_ARM64",
)