Skip to content

Ship the LSP binaries, the Rider plugin, and the Homebrew/Scoop channels - #239

Merged
MelbourneDeveloper merged 15 commits into
mainfrom
fixes
Sep 3, 2026
Merged

Ship the LSP binaries, the Rider plugin, and the Homebrew/Scoop channels#239
MelbourneDeveloper merged 15 commits into
mainfrom
fixes

Conversation

@MelbourneDeveloper

Copy link
Copy Markdown
Collaborator

Releases published nothing but VSIXs. Anyone not using VS Code — Rider, Zed, Neovim, Helix, Emacs — had no way to install SharpLsp at all, and the docs described release jobs that had never existed.

Standalone server archives

Every release now publishes sharplsp-<platform>.tar.gz / .zip for all five built platforms, alongside the VSIXs and covered by SHA256SUMS.

The layout is dictated by the host's own sidecar resolution (installed_sidecar_exe layout 1), so unpacking anywhere and running sharplsp finds Roslyn and FCS with no env vars and no configuration. package-vsix-<platform> builds the Rust host and both sidecars once and emits both artifacts, so this costs no extra CI time.

tools/packaging/verify-archive.sh asserts the layout and unpacks and executes all three binaries — a .NET apphost separated from its managed assembly exists but cannot start, which no listing detects. It runs on every PR from ci-build.yml, not just at tag time.

Rider

The plugin was dead code. It sat in package com.forgelsp, hunted for a binary named forge-lsp, and sent forge/* requests to a host that only answers sharplsp/* — nothing it did could ever have worked, while CI compiled it and ran a coverage gate over it.

  • Renamed to com.sharplsp.rider / SharpLsp* before anything shipped, so no install is orphaned
  • All eight @JsonRequest names repointed at sharplsp/*, checked against the host's dispatch table
  • pluginVersion is now stamped by make _stamp-version; unstamped it would have shipped 0.1.0 from every tag
  • New build-rider release job with RIDER_REQUIRED=1, so a missing JDK fails loudly instead of publishing a release with no plugin

Homebrew and Scoop

publish-homebrew and publish-scoop push Formula/sharplsp.rb and bucket/sharplsp.json to the tap and bucket after the release succeeds. Both files are generated whole from the published archives — the Scoop manifest as an object serialized to JSON, never string-templated — so a checksum cannot survive a version bump. Prerelease tags are skipped; neither package manager has a prerelease channel.

Homebrew installs the sidecars to lib/sharplsp/ and Scoop's extract_dir leaves them beside sharplsp.exe; both are the layouts the host resolves. tools/packaging/verify-package-manifests.mjs asserts that on every PR.

Two root-cause fixes found while running CI locally

tools/dist/ was never committed. The bare dist/ pattern in .gitignore matches a directory of that name at any depth, so the workflows landed referencing scripts that were not in the repo. Renamed to tools/packaging/ — these are source, not build output.

A stray test-fixtures/workspace/.editorconfig promoted CS0219 to an error across the shared TestFixtures project. Refactor.cs carries an unused local on purpose — it is the fixture the CS0219 quick-fix test refactors — so the fixture build failed, taking down every VS Code chunk at pretest and five sidecar tests that build the same workspace. Nothing referenced the file.

Also on this branch

Test Explorer: multi-targeted projects collapse to one assembly root instead of rendering the whole tree twice, and the legacy VSTest adapter's decorated ids are parsed (test-names.ts).

Not automated

Publishing the two sidecar dotnet tool packages to NuGet — see #235. BREW_SCOOP_PAT must be granted to this repo; both publish jobs fail fast with an explicit message if it is absent.

The previous commit landed the release workflow and Makefile changes but not
the scripts they call: `tools/dist/` was swallowed whole by the bare `dist/`
pattern in .gitignore, which matches a directory of that name at ANY depth, not
just the build output at the repo root. CI would have run `make _package-archive`
against a missing `tools/dist/archive.sh`.

Renamed to `tools/packaging/` rather than negating the ignore rule. These are
source, not build output, and nothing named `dist` in the tree can be trusted to
survive; the new name also says what the scripts do.

Also removes src/editors/vscode/test-fixtures/workspace/.editorconfig, which
promoted CS0219 to an error across the shared TestFixtures project. Refactor.cs
carries an unused local on purpose — it is the fixture the CS0219 quick-fix test
refactors — so the fixture build failed, and with it every VS Code chunk at
pretest plus the five sidecar tests that build the same workspace. Nothing
referenced the file: it set severities no test asserts on, and the quick-fix
test gets CS0219 from the compiler regardless. Roslyn honours editorconfig
severity over the csproj's TreatWarningsAsErrors=false, so a fixture that needs
one must carry it in an isolated nested scope, never at the workspace root.
…ures

Test Explorer work plus the lint and format fixes CI enforces:

- `.split('\n')` in test-explorer-fixtures.ts was written with a literal newline
  instead of the escape, so the suite could not compile (TS1002) and every VS
  Code chunk died at pretest.
- Two `as vscode.TestItem` casts in test-explorer-adapter-ids.test.ts tripped
  @typescript-eslint/non-nullable-type-assertion-style; applied eslint --fix.
- Reformatted four files prettier reported as unformatted.

CLAUDE.md: no SharpLsp code is "legacy" — code that does not match the specs
gets deleted, not preserved.
Comment thread tools/packaging/render-package-manifests.mjs Fixed
Prettier gate in ci-build.yml rejected it.
Running a class group left every theory reporting "No result reported".

The two halves of a test id disagreed. `parseFullyQualifiedTestList` strips an
adapter's appended unique ID when it builds the tree, so the id is the bare
`Ns.Class.Method`. `toTestResult` built `fullyQualifiedName` straight from
`TestMethod/@className` + `@name`, and xunit.runner.visualstudio 2.2.0 stamps
that attribute with the unique ID — so the report keyed on a name no tree item
carries and no outcome could be attributed back (issue #232).

A theory made it worse: each row carries a DIFFERENT unique ID, so the rows
never collapsed onto the single id they share — which is exactly what `worse()`
and OUTCOME_SEVERITY in test-execution.ts already assume when they judge a
data-driven test by its worst row.

Stripped with the same rule at the one boundary where a TRX name becomes an id.
`displayName` keeps the decoration: it is a label, not a key. NUnit's
`Adds_Case(2,2,4)` still round-trips untouched — no space before the paren, and
its contents are not hex.
SHARPLSP_DAP_TRACE showed only what arrived FROM the adapter, so a response the
router synthesises or re-sequences itself — the attach retrier's, for one — was
invisible, and an unanswered client request could not be told from an answered
one. Same flag, same shape, on the way out.
Pressing Debug on a test reported the attach settled the moment
`startDebugging` resolved, which is only "the session exists" — several
DAP round trips before it can stop anywhere. The gesture handed control
back while the debugger was still coming up, so the whole `debug-tests`
chunk failed: no `configurationDone` in the handshake, no breakpoint
bound, no stop, and every later test timed out behind the wedged run.

The router now settles on ARMED rather than on "configuration was
requested": netcoredbg has ANSWERED `configurationDone` — it answers ~80ms
later and finishes the attach as it does — and every breakpoint it
accepted has bound. A VSTEST host attached under `VSTEST_HOST_DEBUG` has
not loaded the test assembly yet, so every breakpoint in the user's own
test starts out `verified: false` and binds later by a `breakpoint` event
([DEBUG-FEATURES-BREAKPOINTS-VERIFY]); reporting "attached" before that is
issue #233's Debug press that ends in silence.

Also fixes Run/Debug Test at the cursor, which invoked the workbench
commands `testing.runTests`/`testing.debugTests`. Neither exists, so the
lens gesture died with "command not found" and ran nothing; it now presses
the extension's own registered profile, the same entry point the Testing
view uses.

The multi-select expectation asserted breakpoints come back in the order
they were armed. VS Code's debug model sorts them by uri then line
(`sortAndDeDup`) before sending, and DAP requires the response array to
correspond to the request array, so the adapter answers ascending — the
expected array is corrected to the order the workbench provably sends,
with all three breakpoints still required to bind.

Local `make _run-vsix-suite CHUNK=debug-tests`: 22 passing, 0 failing
(was 0 passing, 9 failing).
`statSync(path).size === 0` followed by `readFileSync(path)` is a
check the read cannot rely on: the file may change between the two
(CodeQL js/file-system-race, high). Read once and judge the bytes in
hand — the emptiness check is then about the same bytes that get
hashed, and a release archive is no longer walked twice.
…test

`writeDebugTestFixture` only WROTE the project and solution, so the
restore and compile were paid by whichever test ran first, inside its
50s `DEBUG_TEST_MS` budget. C# fits in that; F# — FSharp.Core plus a
cold compiler start in a fresh scratch directory — does not. On Ubuntu
the first F# test timed out mid-build and every later test in the run,
F# and group alike, timed out queued behind the invocation still
building: 12 passing, 10 failing, all ten exactly 50.0s apart with no
work in between.

The fixture is now built where the cost belongs, in `suiteSetup`, which
already owns `FIXTURE_BUILD_MS`.
CI's extension log names the wedge exactly. The last C# debug test armed
its session at 08:28:58.075 and passed at 08:28:59.0; its `dotnet test`
was still running, because a Debug gesture resolves at the ATTACH and the
invocation continues until the debugged tests finish. `suiteTeardown`
then deleted the fixture directory at 08:29:00 — a fraction of a second
before that invocation would have written its TRX and exited. `dotnet
test` was left pointed at a directory that no longer existed and never
exited, and because every invocation the controller makes is serialised
behind one queue, it took the rest of the run with it: all four F# tests
and all six group tests timed out at exactly 50.0s intervals with no work
in between, 12 passing / 10 failing.

The teardown now waits for that queue to drain before removing the tree,
so it is ordered rather than lucky.
Prettier fits it on one line now that removeDirRecursive is gone.
@MelbourneDeveloper
MelbourneDeveloper merged commit 2ff8256 into main Sep 3, 2026
75 checks passed
@MelbourneDeveloper
MelbourneDeveloper deleted the fixes branch September 3, 2026 09:19
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.

2 participants