Merge main into feat/v0.8.3
Brings in PR #255 (npm wrapper Windows smoke). No file overlap with the 0.8.3 work on this branch.
This commit is contained in:
@@ -88,7 +88,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
@@ -101,35 +101,8 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build wrapper binaries
|
||||
run: cargo build --release --locked -p deepseek-tui-cli -p deepseek-tui
|
||||
- name: Prepare local release assets
|
||||
run: node scripts/release/prepare-local-release-assets.js "$RUNNER_TEMP/release-assets"
|
||||
- name: Start local release server
|
||||
run: |
|
||||
python3 -m http.server 8123 --directory "$RUNNER_TEMP/release-assets" >/tmp/deepseek-release-server.log 2>&1 &
|
||||
sleep 1
|
||||
- name: Pack npm wrapper
|
||||
working-directory: npm/deepseek-tui
|
||||
env:
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD: "1"
|
||||
DEEPSEEK_TUI_RELEASE_BASE_URL: http://127.0.0.1:8123/
|
||||
run: npm pack
|
||||
- name: Install packed wrapper
|
||||
env:
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD: "1"
|
||||
DEEPSEEK_TUI_RELEASE_BASE_URL: http://127.0.0.1:8123/
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/npm-smoke"
|
||||
cd "$RUNNER_TEMP/npm-smoke"
|
||||
npm init -y
|
||||
npm install "$GITHUB_WORKSPACE/npm/deepseek-tui"/deepseek-tui-*.tgz
|
||||
- name: Smoke wrapper entrypoints
|
||||
env:
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD: "1"
|
||||
DEEPSEEK_TUI_RELEASE_BASE_URL: http://127.0.0.1:8123/
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/npm-smoke"
|
||||
npx --no-install deepseek --help
|
||||
npx --no-install deepseek-tui --help
|
||||
- name: Smoke wrapper install and delegated entrypoints
|
||||
run: node scripts/release/npm-wrapper-smoke.js
|
||||
|
||||
# Check documentation builds without warnings
|
||||
docs:
|
||||
|
||||
+10
-16
@@ -62,26 +62,19 @@ without unpublished workspace dependencies and a packaging preflight for depende
|
||||
workspace crates. That avoids false negatives from crates.io not yet containing the
|
||||
new workspace version while still validating package contents before publish.
|
||||
|
||||
For npm wrapper verification:
|
||||
For npm wrapper verification, build the two shipped binaries and run the
|
||||
cross-platform smoke harness. This packs the npm wrapper, installs it into a
|
||||
clean temporary project, serves local release assets over HTTP, and checks both
|
||||
the dispatcher-to-TUI path (`deepseek doctor --help`) and the direct TUI
|
||||
entrypoint (`deepseek-tui --help`).
|
||||
|
||||
```bash
|
||||
cargo build --release --locked -p deepseek-tui-cli -p deepseek-tui
|
||||
node scripts/release/prepare-local-release-assets.js
|
||||
python3 -m http.server 8123 --directory target/npm-release-assets
|
||||
cd npm/deepseek-tui
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD=1 DEEPSEEK_TUI_RELEASE_BASE_URL=http://127.0.0.1:8123/ npm pack
|
||||
node scripts/release/npm-wrapper-smoke.js
|
||||
```
|
||||
|
||||
Then install the generated tarball in a clean temp directory and smoke the entrypoints:
|
||||
|
||||
```bash
|
||||
tmpdir="$(mktemp -d)"
|
||||
cd "${tmpdir}"
|
||||
npm init -y
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD=1 DEEPSEEK_TUI_RELEASE_BASE_URL=http://127.0.0.1:8123/ npm install /path/to/deepseek-tui-*.tgz
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD=1 DEEPSEEK_TUI_RELEASE_BASE_URL=http://127.0.0.1:8123/ npx --no-install deepseek --help
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD=1 DEEPSEEK_TUI_RELEASE_BASE_URL=http://127.0.0.1:8123/ npx --no-install deepseek-tui --help
|
||||
```
|
||||
Set `DEEPSEEK_TUI_KEEP_SMOKE_DIR=1` to keep the temporary pack/install
|
||||
directory for inspection.
|
||||
|
||||
To exercise `npm run release:check` locally as well, regenerate the local asset
|
||||
directory with a full asset matrix fixture before starting the server:
|
||||
@@ -94,7 +87,8 @@ DEEPSEEK_TUI_VERSION=X.Y.Z DEEPSEEK_TUI_RELEASE_BASE_URL=http://127.0.0.1:8123/
|
||||
|
||||
Set `DEEPSEEK_TUI_VERSION` to the npm package version you are verifying for that local run.
|
||||
|
||||
The CI workflow runs the same tarball install + smoke test on Linux and macOS.
|
||||
The CI workflow runs the same tarball install + delegated-entrypoint smoke test
|
||||
on Linux, macOS, and Windows.
|
||||
|
||||
## Rust Crates Release
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const fsp = require("fs/promises");
|
||||
const http = require("http");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||
const packageDir = path.join(repoRoot, "npm", "deepseek-tui");
|
||||
const prepareAssetsScript = path.join(
|
||||
repoRoot,
|
||||
"scripts",
|
||||
"release",
|
||||
"prepare-local-release-assets.js",
|
||||
);
|
||||
|
||||
function shellQuote(value) {
|
||||
return /\s/.test(value) ? JSON.stringify(value) : value;
|
||||
}
|
||||
|
||||
function usesWindowsCommandShim(command) {
|
||||
return process.platform === "win32" && (command === "npm" || command === "npx");
|
||||
}
|
||||
|
||||
function runCommand(command, args, options = {}) {
|
||||
const cwd = options.cwd || repoRoot;
|
||||
console.log(`$ ${[command, ...args].map(shellQuote).join(" ")}`);
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...(options.env || {}),
|
||||
},
|
||||
encoding: "utf8",
|
||||
shell: usesWindowsCommandShim(command),
|
||||
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
if (!options.capture) {
|
||||
return new Promise((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("close", (status) => {
|
||||
if (status === 0) {
|
||||
resolve({ stdout: "", stderr: "" });
|
||||
} else {
|
||||
reject(new Error(`${command} exited with status ${status}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("close", (status) => {
|
||||
if (status === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
return;
|
||||
}
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
reject(new Error(`${command} exited with status ${status}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function serveDirectory(root) {
|
||||
const server = http.createServer(async (request, response) => {
|
||||
try {
|
||||
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
|
||||
const decodedPath = decodeURIComponent(requestUrl.pathname);
|
||||
const filePath = path.resolve(root, `.${decodedPath}`);
|
||||
const relative = path.relative(root, filePath);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
response.writeHead(403);
|
||||
response.end("forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileStat = await fsp.stat(filePath);
|
||||
if (!fileStat.isFile()) {
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
"Content-Length": fileStat.size,
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
fs.createReadStream(filePath).pipe(response);
|
||||
} catch (error) {
|
||||
response.writeHead(error && error.code === "ENOENT" ? 404 : 500);
|
||||
response.end(error && error.message ? error.message : "not found");
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
resolve({
|
||||
baseUrl: `http://127.0.0.1:${address.port}/`,
|
||||
server,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parsePackJson(stdout) {
|
||||
const trimmed = stdout.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("npm pack did not return package metadata");
|
||||
}
|
||||
const parsed = JSON.parse(trimmed);
|
||||
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
||||
if (!first || !first.filename) {
|
||||
throw new Error(`npm pack metadata did not include a filename: ${trimmed}`);
|
||||
}
|
||||
return first.filename;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "deepseek-npm-smoke-"));
|
||||
const releaseAssetsDir = path.join(tempRoot, "release-assets");
|
||||
const packDir = path.join(tempRoot, "pack");
|
||||
const installDir = path.join(tempRoot, "install");
|
||||
let keepTemp = process.env.DEEPSEEK_TUI_KEEP_SMOKE_DIR === "1";
|
||||
let server;
|
||||
|
||||
try {
|
||||
await fsp.mkdir(packDir, { recursive: true });
|
||||
await fsp.mkdir(installDir, { recursive: true });
|
||||
|
||||
await runCommand(process.execPath, [prepareAssetsScript, releaseAssetsDir]);
|
||||
const served = await serveDirectory(releaseAssetsDir);
|
||||
server = served.server;
|
||||
|
||||
const env = {
|
||||
DEEPSEEK_TUI_FORCE_DOWNLOAD: "1",
|
||||
DEEPSEEK_TUI_RELEASE_BASE_URL: served.baseUrl,
|
||||
};
|
||||
const pack = await runCommand(
|
||||
"npm",
|
||||
["pack", "--json", "--pack-destination", packDir],
|
||||
{
|
||||
capture: true,
|
||||
cwd: packageDir,
|
||||
env,
|
||||
},
|
||||
);
|
||||
const tarball = path.join(packDir, parsePackJson(pack.stdout));
|
||||
|
||||
await runCommand("npm", ["init", "-y"], { cwd: installDir });
|
||||
await runCommand("npm", ["install", tarball], { cwd: installDir, env });
|
||||
await runCommand("npx", ["--no-install", "deepseek", "doctor", "--help"], {
|
||||
cwd: installDir,
|
||||
env,
|
||||
});
|
||||
await runCommand("npx", ["--no-install", "deepseek-tui", "--help"], {
|
||||
cwd: installDir,
|
||||
env,
|
||||
});
|
||||
|
||||
console.log(`npm wrapper smoke passed with local assets from ${served.baseUrl}`);
|
||||
} catch (error) {
|
||||
keepTemp = true;
|
||||
console.error(`npm wrapper smoke failed: ${error.message}`);
|
||||
console.error(`Smoke workspace retained at ${tempRoot}`);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (server) {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
if (!keepTemp) {
|
||||
await fsp.rm(tempRoot, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user