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:
@@ -19,8 +19,10 @@ const signingKeyPath = resolve(String(values["signing-key"]));
|
||||
const publicKeyPath = resolve("distribution/release-signing-public.pem");
|
||||
const packageMetadata = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
|
||||
const packageLock = JSON.parse(readFileSync(resolve("package-lock.json"), "utf8"));
|
||||
const nodeRuntime = JSON.parse(readFileSync(resolve("distribution/node-runtime.json"), "utf8"));
|
||||
const version = String(packageMetadata.version ?? "").trim();
|
||||
if (!version) throw new Error("package.json 缺少版本号");
|
||||
validateNodeRuntime(nodeRuntime);
|
||||
if (out === projectRoot || !out.startsWith(`${projectRoot}${sep}`)) throw new Error("--out 必须是项目目录下的专用子目录");
|
||||
assertPrivateKey(signingKeyPath);
|
||||
const privateKey = readFileSync(signingKeyPath);
|
||||
@@ -75,19 +77,44 @@ cpSync(publicKeyPath, join(out, publicKeyName));
|
||||
cpSync(resolve("skills/dingtalk-approval/SKILL.md"), join(out, skillName));
|
||||
rmSync(clientStaging, { recursive: true, force: true });
|
||||
|
||||
const artifacts = {
|
||||
const baseArtifacts = {
|
||||
installer: artifact(installerName),
|
||||
publicKey: { ...artifact(publicKeyName), keyId },
|
||||
client: artifact(clientName),
|
||||
skill: artifact(skillName),
|
||||
};
|
||||
const commonBootstrapReplacements = new Map([
|
||||
["{{NODE_VERSION}}", nodeRuntime.version], ["{{NODE_MINIMUM_VERSION}}", nodeRuntime.minimumVersion],
|
||||
["{{MANIFEST_URL}}", `${downloadBaseUrl}/manifest.json`],
|
||||
["{{INSTALLER_URL}}", baseArtifacts.installer.url], ["{{INSTALLER_SHA256}}", baseArtifacts.installer.sha256],
|
||||
["{{PUBLIC_KEY_URL}}", baseArtifacts.publicKey.url], ["{{PUBLIC_KEY_SHA256}}", baseArtifacts.publicKey.sha256],
|
||||
["{{NODE_SOURCE_BASES_POWERSHELL}}", nodeRuntime.sourceBases.map((value) => JSON.stringify(value)).join(", ")],
|
||||
["{{NODE_SOURCE_BASES_POSIX}}", nodeRuntime.sourceBases.join(" ")],
|
||||
...Object.entries(nodeRuntime.platforms).flatMap(([platform, runtime]) => {
|
||||
const prefix = `{{NODE_${platform.replace(/-/g, "_").toUpperCase()}_`;
|
||||
return [[`${prefix}FILE}}`, runtime.file], [`${prefix}SHA256}}`, runtime.sha256]];
|
||||
}),
|
||||
]);
|
||||
const bootstrapWindowsName = "dingtalk-approval-bootstrap.ps1";
|
||||
const bootstrapPosixName = "dingtalk-approval-bootstrap.sh";
|
||||
renderTemplate("distribution/bootstrap-windows.template.ps1", join(out, bootstrapWindowsName), commonBootstrapReplacements);
|
||||
renderTemplate("distribution/bootstrap-posix.template.sh", join(out, bootstrapPosixName), commonBootstrapReplacements);
|
||||
execFileSync("sh", ["-n", join(out, bootstrapPosixName)]);
|
||||
const artifacts = {
|
||||
...baseArtifacts,
|
||||
bootstrapWindows: artifact(bootstrapWindowsName),
|
||||
bootstrapPosix: artifact(bootstrapPosixName),
|
||||
};
|
||||
const manifest = {
|
||||
schemaVersion: 3,
|
||||
version,
|
||||
createdAt: new Date().toISOString(),
|
||||
gatewayUrl,
|
||||
signing: { algorithm: "Ed25519", keyId },
|
||||
requirements: { node: ">=22.13.0" },
|
||||
requirements: {
|
||||
node: `>=${nodeRuntime.minimumVersion}`,
|
||||
managedNode: { version: nodeRuntime.version, platforms: Object.keys(nodeRuntime.platforms).sort() },
|
||||
},
|
||||
artifacts,
|
||||
};
|
||||
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
||||
@@ -103,6 +130,9 @@ const replacements = new Map([
|
||||
["{{MANIFEST_SIGNATURE_URL}}", `${downloadBaseUrl}/manifest.sig`], ["{{GATEWAY_URL}}", gatewayUrl],
|
||||
["{{INSTALLER_URL}}", artifacts.installer.url], ["{{INSTALLER_SHA256}}", artifacts.installer.sha256],
|
||||
["{{PUBLIC_KEY_URL}}", artifacts.publicKey.url], ["{{PUBLIC_KEY_SHA256}}", artifacts.publicKey.sha256],
|
||||
["{{BOOTSTRAP_WINDOWS_URL}}", artifacts.bootstrapWindows.url], ["{{BOOTSTRAP_WINDOWS_SHA256}}", artifacts.bootstrapWindows.sha256],
|
||||
["{{BOOTSTRAP_POSIX_URL}}", artifacts.bootstrapPosix.url], ["{{BOOTSTRAP_POSIX_SHA256}}", artifacts.bootstrapPosix.sha256],
|
||||
["{{NODE_VERSION}}", nodeRuntime.version], ["{{NODE_MINIMUM_VERSION}}", nodeRuntime.minimumVersion],
|
||||
["{{RELEASE_KEY_ID}}", keyId],
|
||||
]);
|
||||
let prompt = template;
|
||||
@@ -133,6 +163,38 @@ function lockedVersion(name) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderTemplate(source, target, replacements) {
|
||||
let body = readFileSync(resolve(source), "utf8");
|
||||
for (const [from, to] of replacements) body = body.replaceAll(from, String(to));
|
||||
if (/\{\{[^}]+\}\}/.test(body)) throw new Error(`${source} 仍包含未替换占位符`);
|
||||
writeFileSync(target, body, "utf8");
|
||||
}
|
||||
|
||||
function validateNodeRuntime(value) {
|
||||
if (value?.schemaVersion !== 1 || !/^\d+\.\d+\.\d+$/.test(value.version)
|
||||
|| !/^\d+\.\d+\.\d+$/.test(value.minimumVersion)) {
|
||||
throw new Error("node-runtime.json 版本信息无效");
|
||||
}
|
||||
if (!Array.isArray(value.sourceBases) || value.sourceBases.length < 2) throw new Error("Node 运行时至少需要两个下载源");
|
||||
for (const source of value.sourceBases) {
|
||||
const url = parseHttps(source, "Node source base");
|
||||
if (!url.pathname.endsWith(`/v${value.version}/`) || url.search || url.hash) {
|
||||
throw new Error("Node 下载源必须固定精确版本目录");
|
||||
}
|
||||
}
|
||||
const expected = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
||||
if (JSON.stringify(Object.keys(value.platforms ?? {}).sort()) !== JSON.stringify(expected)) {
|
||||
throw new Error("Node 运行时必须完整覆盖 Windows/Linux/macOS 的 x64 与 ARM64");
|
||||
}
|
||||
for (const [platform, runtime] of Object.entries(value.platforms)) {
|
||||
if (typeof runtime.file !== "string" || !runtime.file.includes(`v${value.version}`)
|
||||
|| !/^[a-f0-9]{64}$/.test(runtime.sha256)) throw new Error(`Node 运行时 ${platform} 无效`);
|
||||
if (platform.startsWith("win32-") ? !runtime.file.endsWith(".zip") : !runtime.file.endsWith(".tar.gz")) {
|
||||
throw new Error(`Node 运行时 ${platform} 使用了不支持的归档格式`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertPrivateKey(path) {
|
||||
const stat = statSync(path);
|
||||
if (!stat.isFile()) throw new Error("发布签名私钥不是普通文件");
|
||||
|
||||
Reference in New Issue
Block a user