feat: bootstrap portable Node runtimes
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash, createPublicKey, generateKeyPairSync, sign } from "node:crypto";
|
||||
import {
|
||||
chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, symlinkSync, writeFileSync,
|
||||
chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, symlinkSync, writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, delimiter, dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
cleanupLegacyInstallations, clientPaths, releaseKeyId, runClientInstaller, verifySignedManifest,
|
||||
cleanupLegacyInstallations, clientLauncherBodies, clientPaths, releaseKeyId, runClientInstaller, verifySignedManifest,
|
||||
type ClientReleaseManifest,
|
||||
} from "../src/client-installer.js";
|
||||
|
||||
@@ -19,6 +19,18 @@ test("client paths stay in stable per-user locations on all supported platforms"
|
||||
assert.equal(clientPaths({}, "linux", "/users/alice").bridgeCommand, "/users/alice/.local/share/dingtalk-approval/bin/dingtalk-approval-bridge");
|
||||
});
|
||||
|
||||
test("client launchers pin the selected Node executable on Windows and POSIX", () => {
|
||||
const windows = clientLauncherBodies("win32", "C:\\Users\\100% ready\\node.exe");
|
||||
assert.match(windows.cli, /^@echo off\r\n"C:\\Users\\100%% ready\\node\.exe"/);
|
||||
assert.match(windows.bridge, /launcher\.mjs" bridge %\*/);
|
||||
assert.doesNotMatch(windows.bridge, /\r\nnode /);
|
||||
|
||||
const posix = clientLauncherBodies("linux", "/Users/alice's runtime/node");
|
||||
assert.match(posix.cli, /exec '\/Users\/alice'"'"'s runtime\/node'/);
|
||||
assert.match(posix.bridge, /launcher\.mjs" bridge "\$@"/);
|
||||
assert.throws(() => clientLauncherBodies("win32", "C:\\bad\nnode.exe"), /不支持的字符/);
|
||||
});
|
||||
|
||||
test("signed manifest verification rejects tampering and a foreign key", () => {
|
||||
const trusted = generateKeyPairSync("ed25519");
|
||||
const foreign = generateKeyPairSync("ed25519");
|
||||
@@ -107,7 +119,13 @@ test("generic installer preserves identity across safe updates, rollback, downgr
|
||||
};
|
||||
const publicPath = join(root, "release-public.pem");
|
||||
writeFileSync(publicPath, publicPem, { mode: 0o600 });
|
||||
const env: NodeJS.ProcessEnv = { PATH: [fakeBin, process.env.PATH ?? ""].filter(Boolean).join(delimiter), DINGTALK_APPROVAL_CLIENT_HOME: appRoot };
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
PATH: [fakeBin, process.env.PATH ?? ""].filter(Boolean).join(delimiter),
|
||||
DINGTALK_APPROVAL_CLIENT_HOME: appRoot,
|
||||
npm_execpath: process.platform === "win32"
|
||||
? join(fakeBin, "node_modules", "npm", "bin", "npm-cli.js")
|
||||
: join(fakeBin, "fake-npm.mjs"),
|
||||
};
|
||||
const output: string[] = [];
|
||||
const commandContext = {
|
||||
env, platform: process.platform, home: join(root, "home"), fetchFn: fetchFn as typeof fetch,
|
||||
@@ -143,7 +161,14 @@ test("generic installer preserves identity across safe updates, rollback, downgr
|
||||
const state = JSON.parse(readFileSync(paths.stateFile, "utf8"));
|
||||
assert.equal(state.version, "1.1.0-rc.5");
|
||||
assert.equal(state.host, undefined);
|
||||
assert.equal(state.nodeExecutable, realpathSync(process.execPath));
|
||||
assert.equal(state.managedNode, false);
|
||||
assert.equal(state.nodeVersion, process.versions.node);
|
||||
assert.deepEqual(installOutput.nodeRuntime, {
|
||||
version: process.versions.node, managed: false, executable: realpathSync(process.execPath),
|
||||
});
|
||||
assert.doesNotMatch(readFileSync(paths.bridgeCommand, "utf8"), /--url/);
|
||||
assert.match(readFileSync(paths.bridgeCommand, "utf8"), new RegExp(realpathSync(process.execPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
const stableBridgePath = paths.bridgeCommand;
|
||||
const initialRuntime = state.runtimeDir;
|
||||
|
||||
@@ -189,6 +214,7 @@ test("generic installer preserves identity across safe updates, rollback, downgr
|
||||
const health = JSON.parse(output.pop()!);
|
||||
assert.equal(health.ok, true);
|
||||
assert.equal(health.checks.find((check: { name: string }) => check.name === "npm").ok, true);
|
||||
assert.equal(health.checks.find((check: { name: string }) => check.name === "nodeRuntime").ok, true);
|
||||
|
||||
output.length = 0;
|
||||
assert.equal(await runClientInstaller([
|
||||
@@ -371,6 +397,9 @@ if (process.argv.includes("--login") || process.argv.includes("--post-install-lo
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
if (process.argv[2] === "--version") { console.log("11.0.0"); process.exit(0); }
|
||||
if (!process.env.npm_config_cache || !process.env.npm_config_cache.includes("dingtalk-approval-install-")) {
|
||||
console.error("installer did not isolate the npm cache"); process.exit(3);
|
||||
}
|
||||
const index = process.argv.indexOf("--prefix");
|
||||
if (index < 0 || !process.argv[index + 1]) { console.error("missing prefix"); process.exit(2); }
|
||||
const prefix = process.argv[index + 1];
|
||||
|
||||
Reference in New Issue
Block a user