Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,38 @@ jobs:
if: runner.os == 'macOS' && always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true

# 各 build slot 并行都写了 latest.json,last-writer-wins 会丢平台条目。
# 这步等 build 全部完成,基于 release 上各 target 的 .sig 合并一份正确且跨平台的 latest.json 并覆盖。
publish-json:
needs: [build]
if: always() && needs.build.result == 'success' && (needs.release.outputs.tag != '' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Determine tag
id: get-tag
shell: bash
run: |
if [ "${{ github.event_name }}" = 'workflow_dispatch' ]; then
echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ needs.release.outputs.tag }}" >> "$GITHUB_OUTPUT"
fi

- name: Download all .sig artifacts from release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.get-tag.outputs.tag }}
shell: bash
run: |
mkdir -p sigs
gh release download "$TAG" --pattern '*.sig' --dir sigs

- name: Build and upload merged latest.json
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.get-tag.outputs.tag }}
shell: bash
run: node scripts/publish-update-json.cjs
81 changes: 81 additions & 0 deletions scripts/publish-update-json.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* 合并 GitHub Release 上各 build target 的 .sig 产物,
* 生成一份覆盖所有平台的 latest.json 并上传覆盖旧的 latest.json。
*
* 解决 `tauri-action` 在 matrix 并行下 last-writer-wins 让 macOS 条目被吞的问题。
*
* 输入:环境变量 TAG (GitHub Release tag)、sigs/ 目录里有 `gh release download --pattern '*.sig'` 拉下来的签名。
* 输出:在 release 上覆盖 latest.json。
* 不需要 macOS 签名/notarization 凭证 —— 只读签名文本,不再构建。
*/
const { readFileSync, writeFileSync, readdirSync } = require("node:fs");
const { execSync } = require("node:child_process");
const path = require("node:path");

const TAG = process.env.TAG;
if (!TAG) {
console.error("TAG env required");
process.exit(1);
}

const root = path.resolve(__dirname, "..");
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
const version = pkg.version;

const sigsDir = path.join(root, "sigs");
const sigs = new Map();
for (const f of readdirSync(sigsDir)) {
if (f.endsWith(".sig")) sigs.set(f, readFileSync(path.join(sigsDir, f), "utf8").trim());
}

const repo = process.env.GITHUB_REPOSITORY || "lovstudio/lovcode";
const baseUrl = `https://github.com/${repo}/releases/download/${TAG}`;

function entryFor(file) {
const sigName = `${file}.sig`;
const sig = sigs.get(sigName);
if (!sig) return null;
return { url: `${baseUrl}/${file}`, signature: sig };
}

// 平台 key → 上传文件名。同一 .tar.gz 会映射到两个 key(如 darwin-aarch64 / -app),
// 见 tauri-plugin-updater 2.10 的候选列表 {os}-{arch}-{installer} 与 {os}-{arch}。
// 顺序与 tauri-action 默认输出保持一致。
const mappings = [
["darwin-aarch64", () => entryFor("lovcode_aarch64.app.tar.gz")],
["darwin-aarch64-app", () => entryFor("lovcode_aarch64.app.tar.gz")],
["darwin-x86_64", () => entryFor("lovcode_x64.app.tar.gz")],
["darwin-x86_64-app", () => entryFor("lovcode_x64.app.tar.gz")],
["linux-x86_64", () => entryFor(`lovcode_${version}_amd64.AppImage`)],
["linux-x86_64-appimage", () => entryFor(`lovcode_${version}_amd64.AppImage`)],
["linux-x86_64-deb", () => entryFor(`lovcode_${version}_amd64.deb`)],
["linux-x86_64-rpm", () => entryFor(`lovcode-${version}-1.x86_64.rpm`)],
["windows-x86_64", () => entryFor(`lovcode_${version}_x64_en-US.msi`)],
["windows-x86_64-msi", () => entryFor(`lovcode_${version}_x64_en-US.msi`)],
["windows-x86_64-nsis", () => entryFor(`lovcode_${version}_x64-setup.exe`)],
];

const platforms = {};
for (const [key, fn] of mappings) {
if (platforms[key]) continue; // 同一 tarball 已用裸 key 占位,-app / -nsis 等后缀共用 entry
const e = fn();
if (e) platforms[key] = e;
else console.warn(`skip ${key}: missing ${fn.toString().match(/entryFor\("([^"]+)"\)/)?.[1] || "unknown"}.sig`);
}

const release = JSON.parse(
execSync(`gh release view "${TAG}" --json publishedAt`, { encoding: "utf8" })
);

const latest = {
version,
notes: "", // tauri-action 原从 CHANGELOG 提取;客户端 UpdateChecker 不读,后续可加
pub_date: release.publishedAt,
platforms,
};

const outPath = path.join(root, "latest.json");
writeFileSync(outPath, JSON.stringify(latest, null, 2));
execSync(`gh release upload "${TAG}" "${outPath}" --clobber`, { stdio: "inherit" });
console.log(`latest.json uploaded: keys=${Object.keys(platforms).join(", ")}`);
20 changes: 19 additions & 1 deletion src/components/UpdateChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import { useI18n } from "@/i18n";

export type UpdateStage = "checking" | "latest" | "available" | "downloading" | "done" | "error" | "disabled";

// "Platform missing" 不是真正的检查失败 —— 该 release 没为当前平台打包。
// tauri-plugin-updater 2.10 抛的两类:
// Error::TargetNotFound → "the platform `X` was not found in the response `platforms` object"
// Error::TargetsNotFound → "None of the fallback platforms `[..]` were found in the response `platforms` object"
function isUpdaterPlatformMissingError(e: unknown): boolean {
if (!(e instanceof Error)) return false;
const m = e.message;
return (
m.includes("was not found in the response `platforms` object") ||
m.includes("fallback platforms")
);
}

interface UpdateState {
stage: UpdateStage;
update: Update | null;
Expand Down Expand Up @@ -91,8 +104,13 @@ export function UpdateChecker() {
.catch((e) => {
window.clearTimeout(timeout);
if (checkId.current !== currentCheckId) return;
const msg = e instanceof Error ? e.message : String(e);
if (isUpdaterPlatformMissingError(e)) {
// 该 release 没为当前平台出包,UI 不再尖叫
setState({ stage: "latest", update: null, error: "" });
return;
}
console.error("[UpdateChecker]", e);
const msg = e instanceof Error ? e.message : String(e);
setState({ stage: "error", update: null, error: msg });
});
}, [autoUpdateEnabled, setState, updaterProxy]);
Expand Down