Skip to content
Merged
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
27 changes: 27 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ runs:
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: package.json
- name: 定位下载缓存目录
id: cache-paths
shell: bash
run: |
bun run - <<'JS' >> "$GITHUB_OUTPUT"
const { homedir } = require('node:os');
const { join } = require('node:path');
console.log(`bun=${process.env.BUN_INSTALL_CACHE_DIR || join(homedir(), '.bun', 'install', 'cache')}`);
console.log(`hutch=${process.env.HUTCH_HOME || process.env.DASH_HOME || join(homedir(), '.hutch')}`);
JS
- name: 缓存 Bun 依赖下载
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.cache-paths.outputs.bun }}
key: bun-download-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'bun.lock') }}
restore-keys: |
bun-download-v1-${{ runner.os }}-${{ runner.arch }}-
- name: 缓存 Hutch 与 Electrobun 工具链
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
${{ steps.cache-paths.outputs.hutch }}/npm
${{ steps.cache-paths.outputs.hutch }}/releases
${{ steps.cache-paths.outputs.hutch }}/toolchains
key: desktop-tools-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'bun.lock', 'apps/desktop/package.json', 'apps/desktop/hutch.config.ts') }}
restore-keys: |
desktop-tools-v1-${{ runner.os }}-${{ runner.arch }}-
- name: 从根目录安装依赖
shell: bash
env:
Expand Down
108 changes: 108 additions & 0 deletions .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: 桌面安装包构建与验收

on:
workflow_call:
inputs:
sign:
description: 使用传入的签名凭据,仅发布流程开启
type: boolean
default: false
secrets:
MAC_CSC_LINK:
required: false
MAC_CSC_KEY_PASSWORD:
required: false
MAC_DEVELOPER_ID:
required: false
APPLE_ID:
required: false
APPLE_APP_SPECIFIC_PASSWORD:
required: false
APPLE_TEAM_ID:
required: false

permissions:
contents: read

jobs:
build:
name: 构建 ${{ matrix.platform }} ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: windows-2022
platform: win
arch: x64
- os: ubuntu-24.04
platform: linux
arch: x64
- os: macos-15
platform: mac
arch: arm64
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: 校验 tag 与桌面版本
if: github.ref_type == 'tag'
run: |
bun run - <<'JS'
const { version } = require('./apps/desktop/package.json');
if (process.env.GITHUB_REF_NAME !== `desktop-v${version}`) {
throw new Error(`tag 必须为 desktop-v${version}`);
}
JS
- name: 导入 macOS 签名证书
if: inputs.sign && runner.os == 'macOS'
env:
MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
run: |
if [[ -z "$MAC_CSC_LINK" ]]; then exit 0; fi
python3 - <<'PYTHON'
import base64, os, pathlib, urllib.request
value = os.environ['MAC_CSC_LINK']
data = urllib.request.urlopen(value).read() if value.startswith('https://') else base64.b64decode(value)
pathlib.Path(os.environ['RUNNER_TEMP'], 'tarp-signing.p12').write_bytes(data)
PYTHON
TARP_KEYCHAIN_PASSWORD=$(openssl rand -hex 24)
TARP_KEYCHAIN="$RUNNER_TEMP/tarp-signing.keychain-db"
security create-keychain -p "$TARP_KEYCHAIN_PASSWORD" "$TARP_KEYCHAIN"
security set-keychain-settings -lut 21600 "$TARP_KEYCHAIN"
security unlock-keychain -p "$TARP_KEYCHAIN_PASSWORD" "$TARP_KEYCHAIN"
security import "$RUNNER_TEMP/tarp-signing.p12" -k "$TARP_KEYCHAIN" -P "$MAC_CSC_KEY_PASSWORD" -T /usr/bin/codesign
security list-keychains -d user -s "$TARP_KEYCHAIN" login.keychain-db
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$TARP_KEYCHAIN_PASSWORD" "$TARP_KEYCHAIN"
- name: 构建安装包
working-directory: apps/desktop
env:
TARP_CODESIGN: ${{ inputs.sign && secrets.MAC_DEVELOPER_ID != '' && matrix.platform == 'mac' && '1' || '0' }}
TARP_NOTARIZE: ${{ inputs.sign && secrets.MAC_DEVELOPER_ID != '' && secrets.APPLE_ID != '' && matrix.platform == 'mac' && '1' || '0' }}
ELECTROBUN_DEVELOPER_ID: ${{ matrix.platform == 'mac' && secrets.MAC_DEVELOPER_ID || '' }}
ELECTROBUN_APPLEID: ${{ matrix.platform == 'mac' && secrets.APPLE_ID || '' }}
ELECTROBUN_APPLEIDPASS: ${{ matrix.platform == 'mac' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }}
ELECTROBUN_TEAMID: ${{ matrix.platform == 'mac' && secrets.APPLE_TEAM_ID || '' }}
run: bun run dist
- name: 验证打包后的 Bun、CLI、模组、Wasm 和 SQLite
run: bun scripts/release/smoke.ts desktop
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
path: |
apps/desktop/release/*.exe
apps/desktop/release/*.tar.zst
apps/desktop/release/*.tar.gz
apps/desktop/release/*.json
apps/desktop/release/*.patch
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
if-no-files-found: error
compression-level: 0
retention-days: 14
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: 仓库检查

on:
pull_request:
push:
branches: ["**"]
workflow_dispatch:
workflow_call:

Expand Down Expand Up @@ -33,3 +31,9 @@ jobs:
- run: bun run typecheck
- run: bun run test
- run: bun run build

desktop:
name: 桌面分发验收
if: github.event_name == 'pull_request'
needs: check
uses: ./.github/workflows/build-desktop.yml
83 changes: 4 additions & 79 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,86 +16,11 @@ jobs:
uses: ./.github/workflows/ci.yml

build:
name: 构建 ${{ matrix.platform }} ${{ matrix.arch }}
needs: check
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: windows-2022
platform: win
arch: x64
- os: ubuntu-24.04
platform: linux
arch: x64
- os: macos-15
platform: mac
arch: arm64
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: 校验 tag 与桌面版本
run: |
bun run - <<'JS'
const { version } = require('./apps/desktop/package.json');
if (process.env.GITHUB_REF_NAME !== `desktop-v${version}`) {
throw new Error(`tag 必须为 desktop-v${version}`);
}
JS
- name: 导入 macOS 签名证书
if: runner.os == 'macOS'
env:
MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
run: |
if [[ -z "$MAC_CSC_LINK" ]]; then exit 0; fi
python3 - <<'PYTHON'
import base64, os, pathlib, urllib.request
value = os.environ['MAC_CSC_LINK']
data = urllib.request.urlopen(value).read() if value.startswith('https://') else base64.b64decode(value)
pathlib.Path(os.environ['RUNNER_TEMP'], 'tarp-signing.p12').write_bytes(data)
PYTHON
TARP_KEYCHAIN_PASSWORD=$(openssl rand -hex 24)
TARP_KEYCHAIN="$RUNNER_TEMP/tarp-signing.keychain-db"
security create-keychain -p "$TARP_KEYCHAIN_PASSWORD" "$TARP_KEYCHAIN"
security set-keychain-settings -lut 21600 "$TARP_KEYCHAIN"
security unlock-keychain -p "$TARP_KEYCHAIN_PASSWORD" "$TARP_KEYCHAIN"
security import "$RUNNER_TEMP/tarp-signing.p12" -k "$TARP_KEYCHAIN" -P "$MAC_CSC_KEY_PASSWORD" -T /usr/bin/codesign
security list-keychains -d user -s "$TARP_KEYCHAIN" login.keychain-db
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$TARP_KEYCHAIN_PASSWORD" "$TARP_KEYCHAIN"
- name: 构建安装包
working-directory: apps/desktop
env:
TARP_CODESIGN: ${{ secrets.MAC_DEVELOPER_ID != '' && matrix.platform == 'mac' && '1' || '0' }}
TARP_NOTARIZE: ${{ secrets.MAC_DEVELOPER_ID != '' && secrets.APPLE_ID != '' && matrix.platform == 'mac' && '1' || '0' }}
ELECTROBUN_DEVELOPER_ID: ${{ matrix.platform == 'mac' && secrets.MAC_DEVELOPER_ID || '' }}
ELECTROBUN_APPLEID: ${{ matrix.platform == 'mac' && secrets.APPLE_ID || '' }}
ELECTROBUN_APPLEIDPASS: ${{ matrix.platform == 'mac' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }}
ELECTROBUN_TEAMID: ${{ matrix.platform == 'mac' && secrets.APPLE_TEAM_ID || '' }}
run: bun run dist
- name: 验证打包后的 Bun、CLI、模组、Wasm 和 SQLite
run: bun scripts/release/smoke.ts desktop
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
path: |
apps/desktop/release/*.exe
apps/desktop/release/*.tar.zst
apps/desktop/release/*.tar.gz
apps/desktop/release/*.json
apps/desktop/release/*.patch
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
if-no-files-found: error
compression-level: 0
retention-days: 14
uses: ./.github/workflows/build-desktop.yml
with:
sign: true
secrets: inherit

release:
name: 汇总安装包并创建 Release 草稿
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Git 钩子由 monorepo 根目录的 [simple-git-hooks](https://github.com/toplen

GitHub Actions 的版本 tag、跨平台构建、签名配置和 Release 草稿验收见[发布流程](../../docs/发布流程.md)。

应用图标的 1024 × 1024 完整画布主图保存在 `build/tarp-icon-source.png`,不含圆角裁切;`build/icon.png` 供 Windows、Linux 窗口与打包使用。macOS 使用已定稿的 `build/icon-mac.png`:1024 × 1024 透明画布,832 × 832 底板,四边各留白 96 像素。素材已包含圆角与透明留白,直接使用,不再缩放或补边。`build.mac.icons` 指向生成的 `out/icon.iconset`,配置与转换逻辑分别位于 `electrobun.config.ts` 和 `scripts/desktop.ts`。
应用图标的 1024 × 1024 完整画布主图保存在 `build/tarp-icon-source.png`,不含圆角裁切;`build/icon.png` 供 Linux 窗口与打包使用。Windows 使用由它缩小得到的 `build/icon-win.png`(256 × 256);Hutch 的 Windows 打包器只接受边长不超过 256 像素的正方形 PNG。更新 `build/icon.png` 时同步生成 Windows 图标。macOS 使用已定稿的 `build/icon-mac.png`:1024 × 1024 透明画布,832 × 832 底板,四边各留白 96 像素。素材已包含圆角与透明留白,直接使用,不再缩放或补边。`build.mac.icons` 指向生成的 `out/icon.iconset`,配置与转换逻辑分别位于 `electrobun.config.ts` 和 `scripts/desktop.ts`。

更新 macOS 图标时直接替换 `build/icon-mac.png`,保留定稿文件自身的圆角和透明留白。开发环境重启应用后生效,发布包需要重新打包。界面中的横版 TARP 字标继续使用原 SVG。

Expand Down
Binary file added apps/desktop/build/icon-win.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/desktop/electrobun.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default {
codesign: process.env.TARP_CODESIGN === "1",
notarize: process.env.TARP_NOTARIZE === "1",
},
win: { bundleCEF: false, icon: "build/icon.png" },
win: { bundleCEF: false, icon: "build/icon-win.png" },
linux: { bundleCEF: false, icon: "build/icon.png" },
},
runtime: { exitOnLastWindowClosed: false },
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/scripts/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ async function run(args: string[], env = process.env) {
stdin: "inherit",
});
const code = await child.exited;
if (code) throw new Error(`桌面命令执行失败(${code}):${args.slice(0, 4).join(" ")}`);
if (code) throw new Error(`桌面命令执行失败(${code}):${args.join(" ")}`);
}

await run(["bun", "run", "prepare:desktop"]);
Expand Down
18 changes: 11 additions & 7 deletions docs/发布流程.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@

## 工作流

| 入口 | 触发条件 | 行为 |
| --------------------------------------- | ---------------------- | -------------------------------------- |
| `.github/workflows/ci.yml` | PR、分支提交、手动运行 | 格式、类型、完整测试与构建 |
| `.github/workflows/release-desktop.yml` | `desktop-v*` tag | 检查 → 各平台打包与验收 → Release 草稿 |
| `.github/workflows/release-cli.yml` | `cli-v*` tag | 检查 → CLI 打包与验收 → Release 草稿 |
| 入口 | 触发条件 | 行为 |
| --------------------------------------- | -------------------------- | ----------------------------------------------- |
| `.github/workflows/ci.yml` | PR、手动运行、发布流程调用 | 格式、类型、完整测试与构建;PR 另验收桌面安装包 |
| `.github/workflows/release-desktop.yml` | `desktop-v*` tag | 检查 → 各平台打包与验收 → Release 草稿 |
| `.github/workflows/release-cli.yml` | `cli-v*` tag | 检查 → CLI 打包与验收 → Release 草稿 |

发布工作流复用 CI,共用 `.github/actions/setup/action.yml` 安装根级 `packageManager` 指定的 Bun。依赖统一从根目录按 `bun.lock` 安装;共享包随产品构建,官网和剧情模组不参与构建。CLI 兼容性检查与 CLI 发布流程另外安装 Node.js 22.12.0,验证支持的最低版本。第三方 Actions 固定到提交 SHA,升级时同步核对版本。

日常 CI 只执行仓库检查;分发包验收放在对应发布流程。`scripts/release/smoke.ts` 使用独立临时模组与配置,验证分发产物的版本、帮助、模组加载、原生公式计算和退出码。CLI 从 tgz 独立安装后分别使用 Node.js 和 Bun 运行,使用符合类型目录结构的测试资料覆盖 JSON、TOML 与 Markdown 解析,并检查已安装运行依赖的许可文件;桌面则运行各平台打包后的应用,并验证包内 SQLite 驱动与 Kysely 的加载、写入、事务回滚和重开恢复。
开发环境安装步骤缓存 Bun 的依赖下载目录,以及 Hutch 下的 `npm/`、`releases/`、`toolchains/`;按操作系统、架构、锁文件与工具链配置区分缓存。命中后仍执行 `bun install --frozen-lockfile`、构建和验收,不缓存 `node_modules`、项目构建目录、安装包或签名凭据。缓存不可用时按正常流程重新下载。

GitHub 缓存有分支和 tag 作用域限制,PR 保存的缓存不能由合并后的发布 tag 直接读取;首次发布或新的 tag 可能需要重新下载,同一 tag 重跑可复用已有缓存。默认分支已有的兼容缓存也可供其他分支及发布流程读取。

普通分支 push 不触发检查;创建或更新 PR 时执行仓库检查,通过后调用 `.github/workflows/build-desktop.yml`,构建并验收 Windows x64、Linux x64 和 macOS ARM64 安装包。PR 不传入签名凭据、不创建 Release;安装包作为 Actions 产物保留 14 天。桌面发布复用同一构建与验收流程,并按配置签名、公证;手动运行仓库检查只执行格式、类型、测试与构建。CLI 分发包验收仍在 CLI 发布流程执行。`scripts/release/smoke.ts` 使用独立临时模组与配置,验证分发产物的版本、帮助、模组加载、原生公式计算和退出码。CLI 从 tgz 独立安装后分别使用 Node.js 和 Bun 运行,使用符合类型目录结构的测试资料覆盖 JSON、TOML 与 Markdown 解析,并检查已安装运行依赖的许可文件;桌面则运行各平台打包后的应用,并验证包内 SQLite 驱动与 Kysely 的加载、写入、事务回滚和重开恢复。

## 发布版本

1. 修改目标产品的 `package.json` 版本:桌面为 `apps/desktop/`,CLI 为 `packages/module-cli/`。
2. 从根目录运行 `bun install` 更新根级锁文件,提交并合入默认分支,确认 CI 通过。影响公开行为或模组格式时同步修改官网正文。
2. 从根目录运行 `bun install` 更新根级锁文件,提交 PR,确认检查和桌面分发验收通过后合入默认分支。影响公开行为或模组格式时同步修改官网正文。
3. 从该提交创建并推送对应 tag,版本必须与目标包完全一致:

```sh
Expand Down
20 changes: 17 additions & 3 deletions packages/core/src/node/storage/file-queue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { realpath } from "node:fs/promises";
import { readlink, realpath } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";

const queues = new Map<string, Promise<void>>();
Expand All @@ -9,17 +9,31 @@ export function isMissingFile(error: unknown): boolean {
}

/** 尚未创建的文件也按真实父目录归一化,避免目录链接绕过同文件队列。 */
async function queueKey(path: string): Promise<string> {
async function directoryKey(path: string): Promise<string> {
try {
return await realpath(path);
} catch (error) {
if (!isMissingFile(error)) throw error;
const parent = dirname(path);
if (parent === path) throw error;
return join(await queueKey(parent), basename(path));
return join(await directoryKey(parent), basename(path));
}
}

async function queueKey(path: string, links = 0): Promise<string> {
// Bun 在 Linux 上通过文件描述符解析 realpath;原子替换期间旧 inode
// 可能返回带「 (deleted)」的路径,不能用会被替换的文件本身作为队列身份。
let target: string;
try {
target = await readlink(path);
} catch (error) {
if (!isMissingFile(error) && (error as NodeJS.ErrnoException).code !== "EINVAL") throw error;
return join(await directoryKey(dirname(path)), basename(path));
}
if (links >= 40) throw Object.assign(new Error("文件链接层级过多"), { code: "ELOOP" });
return queueKey(resolve(dirname(path), target), links + 1);
}

/** 保护当前进程内的一次完整文件操作;不同文件并行,失败不阻塞后续操作。 */
export async function withFileMutation<T>(path: string, operation: () => Promise<T>): Promise<T> {
// 注册顺序固定,避免异步 realpath 完成顺序改变调用顺序。
Expand Down
Loading
Loading