mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: release v1.21.0
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const item = argv[index];
|
||||
if (!item.startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
const key = item.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
args[key] = "true";
|
||||
continue;
|
||||
}
|
||||
args[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function normalizeVersion(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.replace(/^v/, "");
|
||||
}
|
||||
|
||||
function compareStableVersionsDesc(left, right) {
|
||||
const leftParts = normalizeVersion(left)
|
||||
.split(".")
|
||||
.map((part) => Number(part));
|
||||
const rightParts = normalizeVersion(right)
|
||||
.split(".")
|
||||
.map((part) => Number(part));
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
const diff = (rightParts[index] || 0) - (leftParts[index] || 0);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
return normalizeVersion(right).localeCompare(normalizeVersion(left));
|
||||
}
|
||||
|
||||
function extractKeys(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap(extractKeys);
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return [];
|
||||
}
|
||||
if (typeof value.key === "string") {
|
||||
return [value.key];
|
||||
}
|
||||
if (Array.isArray(value.objects)) {
|
||||
return value.objects.flatMap(extractKeys);
|
||||
}
|
||||
if (Array.isArray(value.result)) {
|
||||
return value.result.flatMap(extractKeys);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function releaseVersionFromKey(key, channel = "stable") {
|
||||
const escapedChannel = channel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = key.match(new RegExp(`^lime/${escapedChannel}/v([^/]+)/`));
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function planR2ReleaseCleanup(options) {
|
||||
const channel = options.channel || "stable";
|
||||
const keep = Math.max(1, Number(options.keep || 3));
|
||||
const keys = Array.from(new Set(options.keys || [])).sort();
|
||||
const current = normalizeVersion(options.currentVersion);
|
||||
const minimum = normalizeVersion(options.minimumSupportedVersion);
|
||||
const versions = Array.from(
|
||||
new Set(
|
||||
keys.map((key) => releaseVersionFromKey(key, channel)).filter(Boolean),
|
||||
),
|
||||
).sort(compareStableVersionsDesc);
|
||||
const protectedVersions = new Set(
|
||||
versions.slice(0, keep).map(normalizeVersion),
|
||||
);
|
||||
|
||||
if (current) {
|
||||
protectedVersions.add(current);
|
||||
}
|
||||
if (minimum) {
|
||||
protectedVersions.add(minimum);
|
||||
}
|
||||
|
||||
const deleteKeys = keys.filter((key) => {
|
||||
const version = releaseVersionFromKey(key, channel);
|
||||
return version && !protectedVersions.has(normalizeVersion(version));
|
||||
});
|
||||
|
||||
return {
|
||||
deleteKeys,
|
||||
protectedVersions: Array.from(protectedVersions).sort(
|
||||
compareStableVersionsDesc,
|
||||
),
|
||||
versions,
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const inputPath = args.input;
|
||||
if (!inputPath) {
|
||||
throw new Error("--input is required");
|
||||
}
|
||||
const raw = JSON.parse(fs.readFileSync(path.resolve(inputPath), "utf8"));
|
||||
const plan = planR2ReleaseCleanup({
|
||||
channel: args.channel || process.env.LIME_RELEASE_CHANNEL || "stable",
|
||||
currentVersion:
|
||||
args.current || process.env.RELEASE_TAG || process.env.GITHUB_REF_NAME,
|
||||
keep: args.keep || process.env.LIME_R2_KEEP_RELEASES || "3",
|
||||
keys: extractKeys(raw),
|
||||
minimumSupportedVersion:
|
||||
args.minimum || process.env.LIME_MINIMUM_SUPPORTED_VERSION || "",
|
||||
});
|
||||
|
||||
const outputPath = args.output ? path.resolve(args.output) : "";
|
||||
if (outputPath) {
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
outputPath,
|
||||
`${plan.deleteKeys.join("\n")}${plan.deleteKeys.length ? "\n" : ""}`,
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`${plan.deleteKeys.join("\n")}${plan.deleteKeys.length ? "\n" : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isCli =
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
|
||||
if (isCli) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { extractKeys, planR2ReleaseCleanup, releaseVersionFromKey };
|
||||
@@ -0,0 +1,358 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const DEFAULT_REQUIRED_PLATFORMS = [
|
||||
"darwin-aarch64",
|
||||
"darwin-x86_64",
|
||||
"windows-x86_64",
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const item = argv[index];
|
||||
if (!item.startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
const key = item.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
args[key] = "true";
|
||||
continue;
|
||||
}
|
||||
args[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function writeJson(filePath, data) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function walkFiles(rootDir) {
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
return [];
|
||||
}
|
||||
const result = [];
|
||||
const stack = [rootDir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
result.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
function normalizeVersionTag(version) {
|
||||
const normalized = String(version || "").trim();
|
||||
if (!normalized) {
|
||||
throw new Error("release version is required");
|
||||
}
|
||||
const withoutPrefix = normalized.replace(/^v/, "");
|
||||
if (withoutPrefix.includes("-")) {
|
||||
throw new Error(
|
||||
`stable updater manifest does not accept prerelease version: ${normalized}`,
|
||||
);
|
||||
}
|
||||
if (!/^\d+\.\d+\.\d+(?:\+\S+)?$/.test(withoutPrefix)) {
|
||||
throw new Error(`release version must be stable semver: ${normalized}`);
|
||||
}
|
||||
return `v${withoutPrefix.split("+")[0]}`;
|
||||
}
|
||||
|
||||
function normalizeStableVersion(version) {
|
||||
return normalizeVersionTag(version).slice(1);
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(baseUrl) {
|
||||
const normalized = String(baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
if (!normalized) {
|
||||
throw new Error("base URL is required");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function encodePathSegment(value) {
|
||||
return encodeURIComponent(value).replace(
|
||||
/[!'()*]/g,
|
||||
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
||||
);
|
||||
}
|
||||
|
||||
function basenameFromUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return decodeURIComponent(path.posix.basename(parsed.pathname));
|
||||
} catch {
|
||||
return path.basename(String(url || ""));
|
||||
}
|
||||
}
|
||||
|
||||
function collectFilesByBasename(files) {
|
||||
const byName = new Map();
|
||||
for (const file of files) {
|
||||
const name = path.basename(file);
|
||||
if (!byName.has(name)) {
|
||||
byName.set(name, []);
|
||||
}
|
||||
byName.get(name).push(file);
|
||||
}
|
||||
return byName;
|
||||
}
|
||||
|
||||
function findLatestManifestFiles(files) {
|
||||
return files.filter((file) => /^latest.*\.json$/i.test(path.basename(file)));
|
||||
}
|
||||
|
||||
function resolveReferencedAssetFile(manifestFile, candidates) {
|
||||
const manifestDir = path.dirname(manifestFile);
|
||||
const assetCandidates = candidates.filter(
|
||||
(candidate) => !/^latest.*\.json$/i.test(path.basename(candidate)),
|
||||
);
|
||||
return (
|
||||
assetCandidates.find(
|
||||
(candidate) => path.dirname(candidate) === manifestDir,
|
||||
) || assetCandidates[0]
|
||||
);
|
||||
}
|
||||
|
||||
function buildPlatformAssetPath(platformKey, assetName) {
|
||||
return `${encodePathSegment(platformKey)}/${encodePathSegment(assetName)}`;
|
||||
}
|
||||
|
||||
function collectUpdaterManifest(options) {
|
||||
const assetsDir = path.resolve(options.assetsDir);
|
||||
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
||||
const channel = options.channel || "stable";
|
||||
const versionTag = normalizeVersionTag(options.version);
|
||||
const version = versionTag.slice(1);
|
||||
const files = walkFiles(assetsDir);
|
||||
const filesByBasename = collectFilesByBasename(files);
|
||||
const latestFiles = findLatestManifestFiles(files);
|
||||
const requiredPlatforms = (
|
||||
options.requiredPlatforms || DEFAULT_REQUIRED_PLATFORMS
|
||||
)
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (latestFiles.length === 0) {
|
||||
throw new Error(`no latest*.json files found under ${assetsDir}`);
|
||||
}
|
||||
|
||||
const platforms = {};
|
||||
const referencedFiles = new Map();
|
||||
let notes = options.notes || "";
|
||||
let pubDate = options.pubDate || "";
|
||||
|
||||
for (const manifestFile of latestFiles) {
|
||||
const manifest = readJson(manifestFile);
|
||||
const manifestVersion = normalizeStableVersion(manifest.version);
|
||||
if (manifestVersion !== version) {
|
||||
throw new Error(
|
||||
`${manifestFile} version ${manifest.version} does not match release ${versionTag}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!notes && typeof manifest.notes === "string") {
|
||||
notes = manifest.notes;
|
||||
}
|
||||
if (!pubDate && typeof manifest.pub_date === "string") {
|
||||
pubDate = manifest.pub_date;
|
||||
}
|
||||
|
||||
for (const [platformKey, platform] of Object.entries(
|
||||
manifest.platforms || {},
|
||||
)) {
|
||||
const sourceUrl = String(platform?.url || "").trim();
|
||||
const signature = String(platform?.signature || "").trim();
|
||||
if (!sourceUrl || !signature) {
|
||||
throw new Error(
|
||||
`${manifestFile} platform ${platformKey} has empty url or signature`,
|
||||
);
|
||||
}
|
||||
if (platforms[platformKey]) {
|
||||
throw new Error(
|
||||
`${manifestFile} duplicates updater platform ${platformKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
const assetName = basenameFromUrl(sourceUrl);
|
||||
const candidates = filesByBasename.get(assetName) || [];
|
||||
const assetFile = resolveReferencedAssetFile(manifestFile, candidates);
|
||||
if (!assetFile) {
|
||||
throw new Error(
|
||||
`${manifestFile} platform ${platformKey} references missing asset: ${assetName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const platformAssetPath = buildPlatformAssetPath(platformKey, assetName);
|
||||
const targetUrl = `${baseUrl}/lime/${channel}/${versionTag}/${platformAssetPath}`;
|
||||
platforms[platformKey] = {
|
||||
signature,
|
||||
url: targetUrl,
|
||||
};
|
||||
referencedFiles.set(platformAssetPath, {
|
||||
assetName,
|
||||
file: assetFile,
|
||||
platformKey,
|
||||
targetPath: platformAssetPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const missing = requiredPlatforms.filter((platform) => !platforms[platform]);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`updater manifest missing required platforms: ${missing.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
version,
|
||||
notes,
|
||||
pub_date: pubDate || new Date().toISOString(),
|
||||
platforms,
|
||||
};
|
||||
|
||||
const r2UploadPlan = [
|
||||
...Array.from(referencedFiles.values()).map((item) => ({
|
||||
cacheControl: "public, max-age=31536000, immutable",
|
||||
contentType: "application/octet-stream",
|
||||
file: item.file,
|
||||
key: `lime/${channel}/${versionTag}/${item.targetPath}`,
|
||||
})),
|
||||
].sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
return {
|
||||
manifest,
|
||||
referencedFiles: Array.from(referencedFiles.values())
|
||||
.map((item) => item.file)
|
||||
.sort(),
|
||||
r2UploadPlan,
|
||||
version,
|
||||
versionTag,
|
||||
};
|
||||
}
|
||||
|
||||
function writeOutputs(result, outDir, channel) {
|
||||
const outputDir = path.resolve(outDir);
|
||||
const latestPath = path.join(outputDir, "latest.json");
|
||||
const versionedLatestPath = path.join(
|
||||
outputDir,
|
||||
result.versionTag,
|
||||
"latest.json",
|
||||
);
|
||||
const uploadPlanPath = path.join(outputDir, "r2-upload-plan.json");
|
||||
const metadataPath = path.join(outputDir, "manifest-meta.json");
|
||||
|
||||
writeJson(latestPath, result.manifest);
|
||||
writeJson(versionedLatestPath, result.manifest);
|
||||
writeJson(uploadPlanPath, [
|
||||
...result.r2UploadPlan,
|
||||
{
|
||||
cacheControl: "public, max-age=60, stale-while-revalidate=300",
|
||||
contentType: "application/json",
|
||||
file: versionedLatestPath,
|
||||
key: `lime/${channel}/${result.versionTag}/latest.json`,
|
||||
},
|
||||
{
|
||||
cacheControl: "public, max-age=60, stale-while-revalidate=300",
|
||||
contentType: "application/json",
|
||||
file: latestPath,
|
||||
key: `lime/${channel}/latest.json`,
|
||||
},
|
||||
]);
|
||||
writeJson(metadataPath, {
|
||||
platformCount: Object.keys(result.manifest.platforms).length,
|
||||
version: result.version,
|
||||
versionTag: result.versionTag,
|
||||
});
|
||||
|
||||
return {
|
||||
latestPath,
|
||||
metadataPath,
|
||||
uploadPlanPath,
|
||||
versionedLatestPath,
|
||||
};
|
||||
}
|
||||
|
||||
function loadNotes(args) {
|
||||
if (args["notes-file"]) {
|
||||
return fs.readFileSync(path.resolve(args["notes-file"]), "utf8").trim();
|
||||
}
|
||||
return process.env.RELEASE_BODY || "";
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const requiredPlatforms = (
|
||||
args["required-platforms"] ||
|
||||
process.env.REQUIRED_UPDATE_PLATFORMS ||
|
||||
""
|
||||
)
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const channel = args.channel || process.env.LIME_RELEASE_CHANNEL || "stable";
|
||||
const result = collectUpdaterManifest({
|
||||
assetsDir: args["assets-dir"] || "release-assets",
|
||||
baseUrl:
|
||||
args["base-url"] ||
|
||||
process.env.LIME_UPDATES_BASE_URL ||
|
||||
"https://updates.limecloud.com",
|
||||
channel,
|
||||
notes: loadNotes(args),
|
||||
pubDate: args["pub-date"] || process.env.RELEASE_PUB_DATE || "",
|
||||
requiredPlatforms:
|
||||
requiredPlatforms.length > 0
|
||||
? requiredPlatforms
|
||||
: DEFAULT_REQUIRED_PLATFORMS,
|
||||
version:
|
||||
args.version || process.env.RELEASE_TAG || process.env.GITHUB_REF_NAME,
|
||||
});
|
||||
const output = writeOutputs(
|
||||
result,
|
||||
args["out-dir"] || "release-updater",
|
||||
channel,
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...output,
|
||||
platformCount: Object.keys(result.manifest.platforms).length,
|
||||
version: result.version,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const isCli =
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
|
||||
if (isCli) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { collectUpdaterManifest, normalizeVersionTag, writeOutputs };
|
||||
@@ -0,0 +1,220 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { planR2ReleaseCleanup } from "./plan-r2-release-cleanup.mjs";
|
||||
import {
|
||||
collectUpdaterManifest,
|
||||
writeOutputs,
|
||||
} from "./release-updater-manifest.mjs";
|
||||
|
||||
function writeJson(filePath, data) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function writeFile(filePath, content = "asset") {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
describe("release updater manifest", () => {
|
||||
it("聚合平台 latest.json 并改写为 R2 自域名 URL", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lime-release-"));
|
||||
const assetsDir = path.join(root, "release-assets");
|
||||
|
||||
writeFile(path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"));
|
||||
writeFile(
|
||||
path.join(assetsDir, "x86_64-apple-darwin", "Lime-x64.app.tar.gz"),
|
||||
);
|
||||
writeFile(path.join(assetsDir, "x86_64-pc-windows-msvc", "Lime.nsis.zip"));
|
||||
|
||||
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
|
||||
version: "1.20.0",
|
||||
notes: "notes",
|
||||
pub_date: "2026-04-28T00:00:00Z",
|
||||
platforms: {
|
||||
"darwin-aarch64": {
|
||||
signature: "sig-arm",
|
||||
url: "https://github.com/limecloud/lime/releases/download/v1.20.0/Lime.app.tar.gz",
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJson(path.join(assetsDir, "x86_64-apple-darwin", "latest.json"), {
|
||||
version: "1.20.0",
|
||||
platforms: {
|
||||
"darwin-x86_64": {
|
||||
signature: "sig-x64",
|
||||
url: "https://github.com/limecloud/lime/releases/download/v1.20.0/Lime-x64.app.tar.gz",
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJson(path.join(assetsDir, "x86_64-pc-windows-msvc", "latest.json"), {
|
||||
version: "1.20.0",
|
||||
platforms: {
|
||||
"windows-x86_64": {
|
||||
signature: "sig-win",
|
||||
url: "https://github.com/limecloud/lime/releases/download/v1.20.0/Lime.nsis.zip",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = collectUpdaterManifest({
|
||||
assetsDir,
|
||||
baseUrl: "https://updates.limecloud.com/",
|
||||
channel: "stable",
|
||||
requiredPlatforms: ["darwin-aarch64", "darwin-x86_64", "windows-x86_64"],
|
||||
version: "v1.20.0",
|
||||
});
|
||||
|
||||
expect(result.manifest.version).toBe("1.20.0");
|
||||
expect(Object.keys(result.manifest.platforms).sort()).toEqual([
|
||||
"darwin-aarch64",
|
||||
"darwin-x86_64",
|
||||
"windows-x86_64",
|
||||
]);
|
||||
expect(result.manifest.platforms["darwin-aarch64"].url).toBe(
|
||||
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-aarch64/Lime.app.tar.gz",
|
||||
);
|
||||
expect(result.r2UploadPlan).toHaveLength(3);
|
||||
|
||||
const output = writeOutputs(result, path.join(root, "out"), "stable");
|
||||
expect(fs.existsSync(output.latestPath)).toBe(true);
|
||||
expect(fs.existsSync(output.versionedLatestPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("缺少必需平台时失败,避免发布半可用清单", () => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "lime-release-missing-"),
|
||||
);
|
||||
const assetsDir = path.join(root, "release-assets");
|
||||
writeFile(path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"));
|
||||
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
|
||||
version: "1.20.0",
|
||||
platforms: {
|
||||
"darwin-aarch64": {
|
||||
signature: "sig-arm",
|
||||
url: "https://example.com/Lime.app.tar.gz",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
collectUpdaterManifest({
|
||||
assetsDir,
|
||||
baseUrl: "https://updates.limecloud.com",
|
||||
requiredPlatforms: ["darwin-aarch64", "windows-x86_64"],
|
||||
version: "v1.20.0",
|
||||
}),
|
||||
).toThrow(/missing required platforms/);
|
||||
});
|
||||
|
||||
it("平台 latest.json 版本必须和发布 tag 一致", () => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "lime-release-version-"),
|
||||
);
|
||||
const assetsDir = path.join(root, "release-assets");
|
||||
writeFile(path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"));
|
||||
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
|
||||
version: "1.19.0",
|
||||
platforms: {
|
||||
"darwin-aarch64": {
|
||||
signature: "sig-arm",
|
||||
url: "https://example.com/Lime.app.tar.gz",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
collectUpdaterManifest({
|
||||
assetsDir,
|
||||
baseUrl: "https://updates.limecloud.com",
|
||||
requiredPlatforms: ["darwin-aarch64"],
|
||||
version: "v1.20.0",
|
||||
}),
|
||||
).toThrow(/does not match release/);
|
||||
});
|
||||
|
||||
it("同名跨平台 updater 包应写入独立 R2 路径", () => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "lime-release-same-name-"),
|
||||
);
|
||||
const assetsDir = path.join(root, "release-assets");
|
||||
|
||||
writeFile(
|
||||
path.join(assetsDir, "aarch64-apple-darwin", "Lime.app.tar.gz"),
|
||||
"arm",
|
||||
);
|
||||
writeFile(
|
||||
path.join(assetsDir, "x86_64-apple-darwin", "Lime.app.tar.gz"),
|
||||
"x64",
|
||||
);
|
||||
|
||||
writeJson(path.join(assetsDir, "aarch64-apple-darwin", "latest.json"), {
|
||||
version: "1.20.0",
|
||||
platforms: {
|
||||
"darwin-aarch64": {
|
||||
signature: "sig-arm",
|
||||
url: "https://example.com/Lime.app.tar.gz",
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJson(path.join(assetsDir, "x86_64-apple-darwin", "latest.json"), {
|
||||
version: "1.20.0",
|
||||
platforms: {
|
||||
"darwin-x86_64": {
|
||||
signature: "sig-x64",
|
||||
url: "https://example.com/Lime.app.tar.gz",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = collectUpdaterManifest({
|
||||
assetsDir,
|
||||
baseUrl: "https://updates.limecloud.com",
|
||||
channel: "stable",
|
||||
requiredPlatforms: ["darwin-aarch64", "darwin-x86_64"],
|
||||
version: "v1.20.0",
|
||||
});
|
||||
|
||||
expect(result.manifest.platforms["darwin-aarch64"].url).toBe(
|
||||
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-aarch64/Lime.app.tar.gz",
|
||||
);
|
||||
expect(result.manifest.platforms["darwin-x86_64"].url).toBe(
|
||||
"https://updates.limecloud.com/lime/stable/v1.20.0/darwin-x86_64/Lime.app.tar.gz",
|
||||
);
|
||||
expect(result.r2UploadPlan.map((item) => item.key).sort()).toEqual([
|
||||
"lime/stable/v1.20.0/darwin-aarch64/Lime.app.tar.gz",
|
||||
"lime/stable/v1.20.0/darwin-x86_64/Lime.app.tar.gz",
|
||||
]);
|
||||
expect(
|
||||
result.r2UploadPlan
|
||||
.map((item) => fs.readFileSync(item.file, "utf8"))
|
||||
.sort(),
|
||||
).toEqual(["arm", "x64"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("R2 release cleanup", () => {
|
||||
it("只删除超过保留窗口且未受保护的旧版本", () => {
|
||||
const keys = [
|
||||
"lime/stable/v1.20.0/latest.json",
|
||||
"lime/stable/v1.20.0/Lime.nsis.zip",
|
||||
"lime/stable/v1.19.0/Lime.nsis.zip",
|
||||
"lime/stable/v1.18.0/Lime.nsis.zip",
|
||||
"lime/stable/v1.17.0/Lime.nsis.zip",
|
||||
"lime/stable/v1.16.0/Lime.nsis.zip",
|
||||
];
|
||||
|
||||
const plan = planR2ReleaseCleanup({
|
||||
currentVersion: "v1.20.0",
|
||||
keep: 3,
|
||||
keys,
|
||||
minimumSupportedVersion: "v1.16.0",
|
||||
});
|
||||
|
||||
expect(plan.deleteKeys).toEqual(["lime/stable/v1.17.0/Lime.nsis.zip"]);
|
||||
expect(plan.protectedVersions).toContain("1.16.0");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user