GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
303 lines
9.3 KiB
JavaScript
303 lines
9.3 KiB
JavaScript
(() => {
|
|
"use strict";
|
|
|
|
const mirrorIndexUrl =
|
|
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/latest.json";
|
|
const loongArchPreviewIndexUrl =
|
|
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/latest.json";
|
|
const fallbackUrl =
|
|
"https://github.com/mesalogo/goodbuddy/releases/latest";
|
|
const maximumIndexBytes = 512 * 1024;
|
|
const maximumPreviewIndexBytes = 16 * 1024;
|
|
const semVerPattern =
|
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+((?:[0-9a-zA-Z-]+)(?:\.[0-9a-zA-Z-]+)*))?$/u;
|
|
const sha256Pattern = /^[a-f0-9]{64}$/u;
|
|
const safeFileNamePattern = /^(?!\.{1,2}$)[^/\\\0]+$/u;
|
|
const targetDefinitions = Object.freeze({
|
|
"windows-x64": Object.freeze({
|
|
platform: "windows",
|
|
arch: "x64",
|
|
formats: Object.freeze(["nsis", "portable"]),
|
|
}),
|
|
"windows-arm64": Object.freeze({
|
|
platform: "windows",
|
|
arch: "arm64",
|
|
formats: Object.freeze(["nsis", "portable"]),
|
|
}),
|
|
"macos-x64": Object.freeze({
|
|
platform: "macos",
|
|
arch: "x64",
|
|
formats: Object.freeze(["dmg"]),
|
|
}),
|
|
"macos-arm64": Object.freeze({
|
|
platform: "macos",
|
|
arch: "arm64",
|
|
formats: Object.freeze(["dmg"]),
|
|
}),
|
|
"linux-x64": Object.freeze({
|
|
platform: "linux",
|
|
arch: "x64",
|
|
formats: Object.freeze(["AppImage", "deb", "rpm"]),
|
|
}),
|
|
"linux-arm64": Object.freeze({
|
|
platform: "linux",
|
|
arch: "arm64",
|
|
formats: Object.freeze(["AppImage", "deb", "rpm"]),
|
|
}),
|
|
});
|
|
const targetKeys = Object.freeze(Object.keys(targetDefinitions));
|
|
|
|
const isRecord = (value) =>
|
|
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
|
|
const hasExactKeys = (value, keys) => {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
const actualKeys = Object.keys(value);
|
|
return (
|
|
actualKeys.length === keys.length &&
|
|
keys.every((key) => Object.prototype.hasOwnProperty.call(value, key))
|
|
);
|
|
};
|
|
|
|
const assert = (condition, message) => {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
};
|
|
|
|
const canonicalFileName = (version, platform, arch, format) => {
|
|
if (platform === "windows" && format === "nsis") {
|
|
return `GoodBuddy-${version}-windows-${arch}-setup.exe`;
|
|
}
|
|
if (platform === "windows" && format === "portable") {
|
|
return `GoodBuddy-${version}-windows-${arch}-portable.zip`;
|
|
}
|
|
|
|
if (platform === "macos") {
|
|
return `GoodBuddy-${version}-mac-${arch}.${format}`;
|
|
}
|
|
|
|
const artifactArch =
|
|
arch === "x64"
|
|
? format === "deb"
|
|
? "amd64"
|
|
: "x86_64"
|
|
: format === "rpm"
|
|
? "aarch64"
|
|
: "arm64";
|
|
if (format === "AppImage") {
|
|
return `GoodBuddy-${version}-${artifactArch}.AppImage`;
|
|
}
|
|
return `GoodBuddy-${version}-linux-${artifactArch}.${format}`;
|
|
};
|
|
|
|
const assertExactUrl = (value, expected, label) => {
|
|
assert(typeof value === "string" && value.length <= 2_048, `${label} 无效`);
|
|
let url;
|
|
try {
|
|
url = new URL(value);
|
|
} catch {
|
|
throw new Error(`${label} 无效`);
|
|
}
|
|
assert(
|
|
value === expected &&
|
|
url.href === expected &&
|
|
url.protocol === "https:" &&
|
|
!url.username &&
|
|
!url.password &&
|
|
!url.port &&
|
|
!url.search &&
|
|
!url.hash,
|
|
`${label} 不是受信任的正式地址`,
|
|
);
|
|
};
|
|
|
|
const validateReleaseIndex = (index) => {
|
|
assert(
|
|
hasExactKeys(index, [
|
|
"formatVersion",
|
|
"productName",
|
|
"version",
|
|
"targets",
|
|
"checksumUrl",
|
|
"fallbackUrl",
|
|
]),
|
|
"发布索引结构无效",
|
|
);
|
|
assert(index.formatVersion === 1, "发布索引版本无效");
|
|
assert(index.productName === "GoodBuddy", "发布索引产品名称无效");
|
|
assert(
|
|
typeof index.version === "string" && index.version.length <= 256,
|
|
"发布版本无效",
|
|
);
|
|
const parsedVersion = semVerPattern.exec(index.version);
|
|
assert(parsedVersion && !parsedVersion[4], "发布索引必须指向稳定 SemVer 版本");
|
|
assert(
|
|
hasExactKeys(index.targets, targetKeys),
|
|
"发布索引必须包含且仅包含六个平台目标",
|
|
);
|
|
|
|
const releaseBase = new URL(`v${index.version}/`, mirrorIndexUrl);
|
|
const seenNames = new Set();
|
|
const seenUrls = new Set();
|
|
|
|
for (const key of targetKeys) {
|
|
const definition = targetDefinitions[key];
|
|
const target = index.targets[key];
|
|
const formats = definition.platform === "macos" && target?.files?.zip
|
|
? ["dmg", "zip"]
|
|
: definition.formats;
|
|
assert(
|
|
hasExactKeys(target, ["platform", "arch", "files"]) &&
|
|
target.platform === definition.platform &&
|
|
target.arch === definition.arch,
|
|
`发布目标与键不匹配:${key}`,
|
|
);
|
|
assert(
|
|
hasExactKeys(target.files, formats),
|
|
`发布目标文件格式无效:${key}`,
|
|
);
|
|
|
|
for (const format of formats) {
|
|
const file = target.files[format];
|
|
assert(
|
|
hasExactKeys(file, ["name", "size", "sha256", "url"]),
|
|
`发布文件结构无效:${key}/${format}`,
|
|
);
|
|
assert(
|
|
typeof file.name === "string" &&
|
|
file.name.length >= 1 &&
|
|
file.name.length <= 255 &&
|
|
safeFileNamePattern.test(file.name) &&
|
|
file.name ===
|
|
canonicalFileName(
|
|
index.version,
|
|
target.platform,
|
|
target.arch,
|
|
format,
|
|
),
|
|
`发布文件名或扩展名无效:${key}/${format}`,
|
|
);
|
|
assert(
|
|
Number.isSafeInteger(file.size) && file.size > 0,
|
|
`发布文件大小无效:${key}/${format}`,
|
|
);
|
|
assert(
|
|
typeof file.sha256 === "string" && sha256Pattern.test(file.sha256),
|
|
`发布文件校验值无效:${key}/${format}`,
|
|
);
|
|
assert(!seenNames.has(file.name), `发布文件名重复:${file.name}`);
|
|
seenNames.add(file.name);
|
|
|
|
const expectedUrl = new URL(
|
|
encodeURIComponent(file.name),
|
|
releaseBase,
|
|
).href;
|
|
assertExactUrl(file.url, expectedUrl, "发布文件地址");
|
|
assert(!seenUrls.has(file.url), `发布文件地址重复:${file.url}`);
|
|
seenUrls.add(file.url);
|
|
}
|
|
}
|
|
|
|
assertExactUrl(
|
|
index.checksumUrl,
|
|
new URL("SHA256SUMS", releaseBase).href,
|
|
"校验清单地址",
|
|
);
|
|
assertExactUrl(index.fallbackUrl, fallbackUrl, "GitHub 回退地址");
|
|
return index;
|
|
};
|
|
|
|
const validateLoongArchPreviewIndex = (index) => {
|
|
assert(
|
|
hasExactKeys(index, [
|
|
"formatVersion",
|
|
"product",
|
|
"goodBuddyVersion",
|
|
"previewVersion",
|
|
"architecture",
|
|
"format",
|
|
"artifact",
|
|
"manifestUrl",
|
|
"checksumUrl",
|
|
]),
|
|
"龙芯预览索引结构无效",
|
|
);
|
|
assert(index.formatVersion === 1, "龙芯预览索引版本无效");
|
|
assert(
|
|
index.product === "GoodBuddy LoongArch Preview",
|
|
"龙芯预览产品名称无效",
|
|
);
|
|
assert(
|
|
typeof index.goodBuddyVersion === "string" &&
|
|
index.goodBuddyVersion.length <= 256,
|
|
"龙芯预览 GoodBuddy 版本无效",
|
|
);
|
|
const parsedVersion = semVerPattern.exec(index.goodBuddyVersion);
|
|
assert(
|
|
parsedVersion && !parsedVersion[4],
|
|
"龙芯预览必须基于稳定 GoodBuddy 版本",
|
|
);
|
|
const previewPrefix = `${index.goodBuddyVersion}-loong64-preview.`;
|
|
assert(
|
|
typeof index.previewVersion === "string" &&
|
|
index.previewVersion.startsWith(previewPrefix) &&
|
|
/^[1-9]\d*$/u.test(index.previewVersion.slice(previewPrefix.length)),
|
|
"龙芯预览版本无效",
|
|
);
|
|
assert(index.architecture === "loong64", "龙芯预览架构无效");
|
|
assert(index.format === "deb", "龙芯预览格式无效");
|
|
assert(
|
|
hasExactKeys(index.artifact, ["name", "size", "sha256", "url"]),
|
|
"龙芯预览文件结构无效",
|
|
);
|
|
|
|
const expectedName =
|
|
`GoodBuddy-${index.goodBuddyVersion}-linux-loong64-preview.deb`;
|
|
assert(
|
|
index.artifact.name === expectedName &&
|
|
safeFileNamePattern.test(index.artifact.name),
|
|
"龙芯预览文件名无效",
|
|
);
|
|
assert(
|
|
Number.isSafeInteger(index.artifact.size) && index.artifact.size > 0,
|
|
"龙芯预览文件大小无效",
|
|
);
|
|
assert(
|
|
typeof index.artifact.sha256 === "string" &&
|
|
sha256Pattern.test(index.artifact.sha256),
|
|
"龙芯预览 SHA-256 无效",
|
|
);
|
|
|
|
const previewBase = new URL(
|
|
`v${index.goodBuddyVersion}/`,
|
|
loongArchPreviewIndexUrl,
|
|
);
|
|
assertExactUrl(
|
|
index.artifact.url,
|
|
new URL(encodeURIComponent(expectedName), previewBase).href,
|
|
"龙芯预览文件地址",
|
|
);
|
|
assertExactUrl(
|
|
index.manifestUrl,
|
|
new URL("preview-manifest.json", previewBase).href,
|
|
"龙芯预览 manifest 地址",
|
|
);
|
|
assertExactUrl(
|
|
index.checksumUrl,
|
|
new URL("SHA256SUMS", previewBase).href,
|
|
"龙芯预览校验清单地址",
|
|
);
|
|
return index;
|
|
};
|
|
|
|
window.GoodBuddyReleaseIndex = Object.freeze({
|
|
maximumIndexBytes,
|
|
maximumPreviewIndexBytes,
|
|
validateReleaseIndex,
|
|
validateLoongArchPreviewIndex,
|
|
});
|
|
})();
|