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
5 changes: 3 additions & 2 deletions docs/engineering/developing-on-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ Date: 2026-10-16

Operational guidance for a Windows workstation. Everything here is about the developer's machine;
the test suite's own isolation rules live in `testing-style.md` and the gates in `quality-gates.md`.
A fresh worktree is prepared as `quality-gates.md` describes before its first gate; a workstation's
own Bun is often not the pinned one, so the pinned Bun goes first on `PATH`.
A fresh worktree is prepared as `quality-gates.md` describes before its first gate; a workstation
keeps one Bun, and when that is not the pinned one the gate runs with the Bun mismatch switch that
`quality-gates.md` describes.

## Native AOT publishing

Expand Down
9 changes: 6 additions & 3 deletions docs/engineering/quality-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,11 @@ It refuses a run filtered to tests that start no server too: every gate and CI l
checkout, so an unprepared one is named before it costs a run.

One exception is explicit: `OPENCODE_SDK_TESTS_BUN_MISMATCH=allow` lets a gate host whose only Bun
is another version run on it. It is for a host the maintainer has named, never a default, and CI
never sets it; `setup-pinned-server` installs and verifies the exact pin. Such a host departs from
is another version run on it. The maintainer's rule is one Bun per machine, so a local gate host runs
its one Bun with this switch until the pin's `packageManager` names that version, and its result is
local evidence only. The switch is set per command, never as a default, and CI never sets it
(`CiPinnedBunTests`): `setup-pinned-server` installs and verifies the exact pin, so CI is the
exact-pin authority. Such a host departs from
the pin twice: its install and its gate both run on its own Bun. The frozen lockfile keeps the
installed dependency tree identical to the pin's; the Bun that runs the server is what differs. The
variable allows only a different version: a missing Bun, checkout or install is still refused, and
Expand Down Expand Up @@ -235,7 +238,7 @@ target framework updates it in the same change.
The detector ignores using directives; a baseline entry that matches no clone fails the gate like a new clone does.

The lint job runs `dotnet run --file tools/opencode-tool.cs -- verify-clones`: the pinned jscpd in
`tools/jscpd/`, installed with the pinned Bun from its own lockfile, at a 50-token minimum over four
`tools/jscpd/`, installed with the machine's Bun from its own lockfile, at a 50-token minimum over four
scopes. Hand-written `src/`, `tools/` and `tests/` are gated against the committed baselines in
`tools/jscpd/`: a clone a baseline does not hold fails the job, `tools/` also fails above 3%
duplicated lines, and `tests/` above 6%. The tests ceiling is the measurement plus 0.25 points,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ internal static async Task<Readiness> FindAsync(
{
problems.Add(
$"The bun on PATH is {bunVersion}, but the pin's packageManager is bun@{pinned.Version} "
+ $"('{pinned.PackageJson}'). Put bun {pinned.Version} first on PATH.");
+ $"('{pinned.PackageJson}'). A machine keeps one Bun: set {BunMismatchVariable}={AllowBunMismatch} "
+ "to run on it as local evidence only.");
}
}

Expand All @@ -135,7 +136,7 @@ internal static async Task<Readiness> FindAsync(
internal static string? MissingInstall(IFileSystem fileSystem, string submodule) =>
fileSystem.Directory.Exists(fileSystem.Path.Combine(submodule, "node_modules"))
? null
: $"The pinned server dependencies are not installed under '{submodule}'. Run there, with the pinned bun: bun install --frozen-lockfile --ignore-scripts";
: $"The pinned server dependencies are not installed under '{submodule}'. Run there: bun install --frozen-lockfile --ignore-scripts";

internal static string Submodule(IFileSystem fileSystem, string repositoryRoot) =>
fileSystem.Path.Combine(repositoryRoot, "external", "opencode");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public async Task FindAsync_Should_Name_Both_Versions_When_The_Bun_On_Path_Is_No
await Assert.That(readiness.Problems).HasSingleItem();
await Assert.That(readiness.Problems[0]).Contains("The bun on PATH is " + OtherBun);
await Assert.That(readiness.Problems[0]).Contains("bun@" + PinnedBun);
await Assert.That(readiness.Problems[0]).Contains("set OPENCODE_SDK_TESTS_BUN_MISMATCH=allow");
await Assert.That(readiness.AllowedMismatch).IsNull();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using OpenCode.Sdk.Tools.Tests.Support;
using Testably.Abstractions;

namespace OpenCode.Sdk.Tools.Tests.RepositoryGates;

/// <summary>
/// CI is the exact-pin authority for the Bun that runs the pinned server: it installs the version
/// the upstream checkout's <c>packageManager</c> names, and it never sets the switch that lets a
/// local gate host run on another Bun (<c>PinnedServerPrerequisites.BunMismatchVariable</c> in
/// <c>OpenCode.Sdk.Tests</c>, whose name these tests repeat because Tools.Tests cannot reference it).
/// </summary>
public sealed class CiPinnedBunTests
{
private const string BunMismatchVariable = "OPENCODE_SDK_TESTS_BUN_MISMATCH";
private const string PinnedBunInstall = "bun-version-file: external/opencode/package.json";

private readonly RealFileSystem _fileSystem = new();

[Test]
public async Task Ci_Should_Never_Set_The_Bun_Mismatch_Switch()
{
var root = new RepositoryRoot(_fileSystem).Locate();
var files = _fileSystem.Directory
.EnumerateFiles(_fileSystem.Path.Combine(root, ".github"), "*", SearchOption.AllDirectories)
.ToList();
var contents = await Task.WhenAll(files.Select(file => _fileSystem.File.ReadAllTextAsync(file)));
var mentioning = files
.Where((_, index) => contents[index].Contains(BunMismatchVariable, StringComparison.Ordinal))
.Select(file => _fileSystem.Path.GetRelativePath(root, file))
.ToList();

await Assert.That(files).IsNotEmpty();
await Assert.That(mentioning).IsEmpty();
}

[Test]
public async Task Ci_Should_Install_The_Bun_The_Pinned_Checkout_Names()
{
var root = new RepositoryRoot(_fileSystem).Locate();
var action = await _fileSystem.File.ReadAllTextAsync(
_fileSystem.Path.Combine(root, ".github", "actions", "setup-pinned-bun", "action.yml"));

await Assert.That(action).Contains(PinnedBunInstall);
}
}
4 changes: 2 additions & 2 deletions tools/OpenCode.Sdk.Tools/Clones/CloneDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace OpenCode.Sdk.Tools.Clones;

/// <summary>
/// Runs the pinned jscpd over one clone scope: it writes the scope's jscpd config into a work
/// folder, runs the detector through the pinned Bun from the repository root, and reads the totals of
/// folder, runs the detector through Bun from the repository root, and reads the totals of
/// the JSON report it writes. A gated scope runs against its baseline and fails on a new clone or
/// stale baseline entry or above its threshold; with an update it rewrites the baseline instead. Every scope fails when it
/// analyses no file. jscpd's own defaults stay: it honours .gitignore, follows no symbolic link, and
Expand All @@ -34,7 +34,7 @@ public void EnsureInstalled(string root)
if (!_fileSystem.File.Exists(_fileSystem.Path.Combine(root, RunnerPath)))
{
throw new CloneDetectorException(
"The clone detector is not installed. Run once, with the pinned Bun first on PATH: "
"The clone detector is not installed. Run once, with the machine's Bun: "
+ "cd " + CloneScopes.DetectorFolder + " && bun install --frozen-lockfile --ignore-scripts");
}
}
Expand Down
2 changes: 1 addition & 1 deletion tools/jscpd/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode-sdk-dotnet-clone-detector",
"private": true,
"description": "The pinned jscpd that `opencode-tool verify-clones` runs; installed with the pinned Bun from bun.lock.",
"description": "The pinned jscpd that `opencode-tool verify-clones` runs; installed with Bun from bun.lock.",
"dependencies": {
"jscpd": "5.4.1"
}
Expand Down
Loading