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
49 changes: 49 additions & 0 deletions .github/workflows/update-lsp-pins.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Update pinned LSP server versions

on:
schedule:
# monthly - 00:00 UTC on the 1st
- cron: '0 0 1 * *'
workflow_dispatch:

jobs:
update-pins:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
ref: main
- name: Update built-in LSP server versions
run: |
npm run updateBuiltInLSPS
git status
shell: bash

- name: Create update-lsp-pins Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
commit-message: 'chore: update built-in LSP server versions'
committer: GitHub <noreply@github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
title: '[Release BOT] Update built-in LSP server versions'
branch: bot/update-lsp-pins
add-paths: |
src/config.json
src-node/package.json
src-node/package-lock.json
body: |
Updates every built-in language server to its latest stable release:
- runtime-installed pins in `src/config.json` (`lsp_server_pins`): intelephense (npm),
pyrefly and ruff (PyPI)
- bundled servers in `src-node/package.json` (+ lockfile): @vtsls/language-server (TS/JS),
vscode-langservers-extracted (JSON)

The TS/JSON/PHP/Python LSP integration suites exercise these exact versions for real,
so a green CI on this PR means the new versions actually work. Merge only when green.
- Auto-generated by `update-lsp-pins.yml` action
- name: Check outputs
if: ${{ steps.cpr.outputs.pull-request-number }}
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
150 changes: 150 additions & 0 deletions build/update-lsp-pins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/*
* Updates every built-in language server to its latest STABLE release:
* - runtime-installed pins in src/config.json (config.lsp_server_pins): intelephense (npm),
* pyrefly and ruff (PyPI), and
* - bundled servers in src-node/package.json: @vtsls/language-server (TS/JS) and
* vscode-langservers-extracted (JSON) - written as EXACT versions, with the src-node
* lockfile regenerated so `npm ci` stays green.
*
* Prereleases are never picked up: npm's `latest` dist-tag is stable by convention and any
* version carrying a prerelease suffix is skipped with a warning.
*
* Run by the scheduled update-lsp-pins GitHub workflow (npm run updateBuiltInLSPS), which opens
* a PR with the diff - the TS/JSON/PHP/Python LSP integration suites exercise these exact
* versions for real, so CI green on that PR means the new versions actually work before they
* ship. Never run as part of normal builds: pins must not drift under a developer mid-work.
*
* Usage: node build/update-lsp-pins.js [--dry-run]
*/

/* eslint-env node */

const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");

const CONFIG_PATH = path.join(__dirname, "..", "src", "config.json");
const SRC_NODE_DIR = path.join(__dirname, "..", "src-node");
const SRC_NODE_PKG_PATH = path.join(SRC_NODE_DIR, "package.json");
const DRY_RUN = process.argv.includes("--dry-run");

// bundled servers shipped inside src-node - bumped in src-node/package.json as exact versions
const BUNDLED_LSP_PACKAGES = ["@vtsls/language-server", "vscode-langservers-extracted"];

async function _fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP " + response.status + " for " + url);
}
return response.json();
}

// npm's `latest` dist-tag - the stable channel by convention (prereleases live on other tags)
async function latestNpmVersion(pkg) {
const meta = await _fetchJson("https://registry.npmjs.org/" + encodeURIComponent(pkg) + "/latest");
return meta.version;
}

async function latestPyPIVersion(pkg) {
const meta = await _fetchJson("https://pypi.org/pypi/" + pkg + "/json");
return meta.info.version;
}

function _isPrerelease(version) {
return version.indexOf("-") !== -1;
}

async function updateRuntimePins() {
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
const pins = config.config.lsp_server_pins;
if (!pins) {
throw new Error("config.lsp_server_pins missing in " + CONFIG_PATH);
}
const latest = {
intelephense: await latestNpmVersion("intelephense"),
pyrefly: await latestPyPIVersion("pyrefly"),
ruff: await latestPyPIVersion("ruff")
};
let changed = false;
for (const key of Object.keys(latest)) {
if (_isPrerelease(latest[key])) {
console.warn(key + ": skipping prerelease " + latest[key] + ", keeping " + pins[key]);
} else if (pins[key] !== latest[key]) {
console.log(key + ": " + pins[key] + " -> " + latest[key]);
pins[key] = latest[key];
changed = true;
} else {
console.log(key + ": " + pins[key] + " (up to date)");
}
}
if (!changed || DRY_RUN) {
if (changed) {
console.log("--dry-run: not writing " + CONFIG_PATH);
}
return;
}
// no trailing newline - matches the existing file and the gulp config writer exactly
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 4), "utf8");
console.log("Updated " + CONFIG_PATH);
}

async function updateBundledServers() {
const pkgJson = JSON.parse(fs.readFileSync(SRC_NODE_PKG_PATH, "utf8"));
let changed = false;
for (const pkg of BUNDLED_LSP_PACKAGES) {
const current = pkgJson.dependencies[pkg];
if (!current) {
throw new Error(pkg + " missing from src-node/package.json dependencies");
}
const latest = await latestNpmVersion(pkg);
if (_isPrerelease(latest)) {
console.warn(pkg + ": skipping prerelease " + latest + ", keeping " + current);
} else if (current !== latest) {
console.log(pkg + ": " + current + " -> " + latest);
pkgJson.dependencies[pkg] = latest;
changed = true;
} else {
console.log(pkg + ": " + current + " (up to date)");
}
}
if (!changed || DRY_RUN) {
if (changed) {
console.log("--dry-run: not writing " + SRC_NODE_PKG_PATH);
}
return;
}
fs.writeFileSync(SRC_NODE_PKG_PATH, JSON.stringify(pkgJson, null, 4) + "\n", "utf8");
console.log("Updated " + SRC_NODE_PKG_PATH);
// keep the committed lockfile in sync or `npm ci` fails on manifest mismatch;
// --package-lock-only avoids touching node_modules
console.log("Regenerating src-node/package-lock.json ...");
execFileSync("npm", ["install", "--package-lock-only"], { cwd: SRC_NODE_DIR, stdio: "inherit" });
}

(async function main() {
await updateRuntimePins();
await updateBundledServers();
}()).catch(function (err) {
console.error(err);
process.exit(1);
});
34 changes: 33 additions & 1 deletion docs/API-Reference/utils/NodeUtils.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,34 @@ This is only available in the native app.
| url | <code>string</code> |
| browserName | <code>string</code> |

<a name="_npmInstallInFolder"></a>

## \_npmInstallInFolder(moduleNativeDir) ⇒ [<code>CancellablePromise</code>](#CancellablePromise)
Runs the bundled npm install in the given folder. Rejects if an install is already in
progress there (two npm processes on one node_modules corrupt each other).

The returned promise carries a `cancel()` method - cancellation is only meaningful for
the specific install you started, so it lives on the handle rather than as a module API.
Cancelling kills the npm process; the promise then rejects with a "cancelled" error.

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| moduleNativeDir | <code>string</code> | platform path of the folder holding package.json |

<a name="downloadFile"></a>

## downloadFile(url, destFile, [options]) ⇒ <code>Promise.&lt;void&gt;</code>
## downloadFile(url, destFile, [options]) ⇒ [<code>CancellablePromise</code>](#CancellablePromise)
Downloads a URL to a file on disk, fully node-side (native fetch, streamed to disk).
When an expected sha256 is given, a mismatch deletes the file and rejects - a resolved
promise means the file holds exactly the pinned bytes.
This is only available in the native app.

**Kind**: global function
**Returns**: [<code>CancellablePromise</code>](#CancellablePromise) - the returned promise carries a `cancel()` method that aborts
the download mid-stream - the promise then rejects with a "cancelled" error and the
partial file is deleted

| Param | Type | Description |
| --- | --- | --- |
Expand Down Expand Up @@ -239,3 +258,16 @@ Retrieves the directory path for system settings. This method is applicable to n

- <code>Error</code> If the method is called in browser app.

<a name="CancellablePromise"></a>

## CancellablePromise : <code>Promise</code>
A Promise that additionally carries a `cancel()` method aborting the underlying
operation - the promise then rejects with a "cancelled" error.

**Kind**: global typedef
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| cancel | <code>function</code> | aborts the in-flight operation |

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"prepare": "husky install",
"_serveTest": "http-server . -p 5000 -c-1",
"zipTestFiles": "gulp zipTestFiles",
"updateBuiltInLSPS": "node build/update-lsp-pins.js",
"test": "echo please see `Running and debugging tests` section in readme.md",
"testIntegHelp": "echo By default this command only runs unit tests.To Run integration tests, please see `Running and debugging tests` section in readme.md",
"testChromium": "npm run testIntegHelp && npx playwright test --project=chromium",
Expand Down
Loading
Loading