Files
goodbuddy/sites/release-index.js
T
mesalogo f95acdc000
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
Deploy website to GitHub Pages / Deploy static website (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
fix: restore packaged runtimes and ship macOS DMG only
Desktop 0.12.7 omitted OpenCode's offline node_modules because the resource
copier excludes node_modules at its source root. Copy from the parent and
verify every prepared configuration file in the final package. Bundle DSH
session projection with the unpacked Host instead of leaving an ASAR-only
external import, and reject external DeepSeek imports in shipped chunks.

Run packaged OpenCode plugin import and the real DSH UtilityProcess handshake
in Windows x64 CI before removing unpacked output. Add regressions using the
actual electron-builder copier and Vite externalization configuration.

Prepare approved Desktop 0.12.8 notes and change macOS defaults and CI to DMG
only. Update aggregation, website downloads and update readers for 12
installers and 20 assets. Current readers retain historical DMG/ZIP support;
the user accepts that older macOS GitHub readers and all older mirror readers
need manual upgrades. Agent remains 0.11.21, with no new Agent or LoongArch tag.

Validation: 3608 tests passed, 0 failed, 61 skipped across 339 files;
typecheck, lint, release-note validation, website static checks and 18 website
tests passed. No local production build or packaged-app launch was performed
at the user's request; actual packaged startup validation runs in CI.

Release note: 修复安装后 OpenCode 离线依赖缺失和 DeepSeek Harness Host 启动退出;macOS 改为仅提供 DMG,受影响旧版用户需从下载页手动升级。
2026-09-08 14:17:38 +08:00

300 lines
9.2 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";
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,
});
})();