From 4c2b70c1852271c1d7f5c4a82e40b7770b1e5a09 Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:24:50 +0800 Subject: [PATCH 1/8] =?UTF-8?q?ci(desktop):=20=E6=A1=8C=E9=9D=A2=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=8F=91=E8=A1=8C=E6=89=8B=E5=8A=A8=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=20/=20add=20manual=20desktop=20preview=20release=20wo?= =?UTF-8?q?rkflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 #18 确认约束:仅 workflow_dispatch,不自动发布;windows-2022 / macos-15 矩阵,job 开始断言 uname -m 与 process.arch 与目标一致; 复用现有打包与验证脚本,并从解包 ZIP 直接启动做原生冒烟; 上传 ZIP / 安装包 / SHA256SUMS,runner 数据全部隔离于临时目录; 汇总 job 如实记录 darwin-x64 未覆盖,禁止 Rosetta 代产。 --- .github/workflows/desktop-release-preview.yml | 161 ++++++++++++++++++ docs/construction-plan.md | 2 + docs/desktop-release-preview-plan.md | 23 +++ 3 files changed, 186 insertions(+) create mode 100644 .github/workflows/desktop-release-preview.yml create mode 100644 docs/desktop-release-preview-plan.md diff --git a/.github/workflows/desktop-release-preview.yml b/.github/workflows/desktop-release-preview.yml new file mode 100644 index 0000000..bc590da --- /dev/null +++ b/.github/workflows/desktop-release-preview.yml @@ -0,0 +1,161 @@ +name: Desktop preview release + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - os: windows-2022 + target: windows-x64 + - os: macos-15 + target: macos-arm64 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Assert native architecture matches target + shell: bash + run: | + echo "runner.arch=${RUNNER_ARCH}" + echo "uname -m: $(uname -m)" + node -v + case "${{ matrix.target }}" in + windows-x64) + [ "$(node -p process.arch)" = x64 ] || { echo "node arch mismatch" >&2; exit 1; } + [ "$PROCESSOR_ARCHITECTURE" = AMD64 ] || { echo "PROCESSOR_ARCHITECTURE mismatch" >&2; exit 1; } + ;; + macos-arm64) + [ "$(uname -m)" = arm64 ] || { echo "uname mismatch" >&2; exit 1; } + [ "$(node -p process.arch)" = arm64 ] || { echo "node arch mismatch" >&2; exit 1; } + ;; + esac + + - name: Install dependencies + shell: bash + run: | + corepack enable + corepack yarn install --immutable + + - name: Install Godot 4.7.2 (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $archive = Join-Path $env:RUNNER_TEMP 'godot.zip' + $destination = Join-Path $env:RUNNER_TEMP 'godot-4.7.2' + Invoke-WebRequest 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_win64.exe.zip' -OutFile $archive + if ((Get-FileHash $archive -Algorithm SHA256).Hash -ne '731980f9608d61333e5baf54a2ef17210acc7a538446c0cb9969f002aca1e953') { + throw 'Godot archive checksum mismatch' + } + Expand-Archive $archive -DestinationPath $destination + Add-Content -Path $env:GITHUB_ENV -Value "GODOT_BIN=$(Join-Path $destination 'Godot_v4.7.2-stable_win64_console.exe')" + + - name: Install Godot 4.7.2 (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + archive="$RUNNER_TEMP/godot.zip" + curl -fsSL -o "$archive" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_universal.zip' + echo 'c58a24e31d720be9d62f60cb5627c4e695fb72f21b0cfe1bc9ccaa9a3b3ba63e godot.zip' | shasum -a 256 -c - + unzip -q "$archive" -d "$RUNNER_TEMP/godot-4.7.2" + echo "GODOT_BIN=$RUNNER_TEMP/godot-4.7.2/Godot.app/Contents/MacOS/Godot" >> "$GITHUB_ENV" + + - name: Build Web plugin and world + shell: bash + run: | + "$GODOT_BIN" --headless --path games/mosslight --editor --import + corepack yarn build:world + corepack yarn build:web + + - name: Build portable package (Windows) + if: runner.os == 'Windows' + shell: bash + run: node apps/desktop/build.mjs + + - name: Verify installer and run from distribution (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $build = (Get-Content dist/desktop-latest.txt -Raw).Trim() + pwsh -NoProfile -File apps/desktop/verify.ps1 -BuildDirectory $build + $work = Join-Path $env:RUNNER_TEMP ('zip-smoke-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) + Expand-Archive -Path (Join-Path $build 'qcode-windows-x64.zip') -DestinationPath $work + $exe = Get-ChildItem -LiteralPath $work -Recurse -Filter qcode.exe | Select-Object -First 1 + if (-not $exe) { throw 'Portable launcher executable missing from zip' } + $data = Join-Path $env:RUNNER_TEMP ('zip-smoke-data-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) + $env:QCODE_DATA_HOME = $data + try { + $proc = Start-Process -FilePath $exe.FullName -WindowStyle Hidden -PassThru + $urlFile = Join-Path $data 'browser-url.txt' + $deadline = (Get-Date).AddMinutes(4) + while (-not (Test-Path $urlFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 } + if (-not (Test-Path $urlFile)) { throw 'Service did not become ready from unpacked zip' } + $url = [uri](Get-Content $urlFile -Raw) + $response = Invoke-WebRequest $url.AbsoluteUri -UseBasicParsing -MaximumRedirection 0 -ErrorAction SilentlyContinue + if ($response.StatusCode -notin 200, 303) { throw "Unpacked zip smoke got HTTP $($response.StatusCode)" } + Stop-Process -Id $proc.Id -Force + $proc.WaitForExit() + Start-Sleep -Seconds 2 + if (Get-NetTCPConnection -LocalPort $url.Port -State Listen -ErrorAction SilentlyContinue) { throw 'Service survived launcher termination' } + } finally { + Remove-Item Env:QCODE_DATA_HOME -ErrorAction SilentlyContinue + } + + - name: Build portable package (macOS) + if: runner.os == 'macOS' + shell: bash + run: corepack yarn build:desktop:darwin + + - name: Verify from unpacked distribution (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + build="$(cat dist/desktop-darwin-latest.txt)" + work="$RUNNER_TEMP/zip-smoke" + mkdir -p "$work" + ditto -x -k "$build/qcode-darwin-arm64.zip" "$work" + corepack yarn verify:desktop:darwin "$work/app" + (cd "$build" && shasum -a 256 -c SHA256SUMS.txt) + + - name: Checksums + shell: bash + run: | + build="$(cat dist/desktop-latest.txt 2>/dev/null || cat dist/desktop-darwin-latest.txt)" + echo "distribution dir: $build" + ls -la "$build" + + - name: Upload distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.target }}-preview + path: | + dist/desktop-*/qcode-*.zip + dist/desktop-*/qcode-setup-x64.exe + dist/desktop-*/SHA256SUMS.txt + if-no-files-found: error + + summary: + name: Coverage summary + needs: build + if: always() + runs-on: ubuntu-latest + steps: + - name: Record matrix coverage + shell: bash + run: | + { + echo "## Desktop preview release coverage" + echo "- windows-x64: ${{ needs.build.result }}" + echo "- macos-arm64: ${{ needs.build.result }}" + echo "- **darwin-x64: 未覆盖** — GitHub 无真实 Intel macOS runner;按约定不使用 Rosetta 代产。" + echo "- 触发方式:workflow_dispatch 手动,不自动发布;冒烟通过不替代 #19 真实模型与干净机器人工验收。" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/construction-plan.md b/docs/construction-plan.md index 07e9890..3540d04 100644 --- a/docs/construction-plan.md +++ b/docs/construction-plan.md @@ -4,6 +4,8 @@ ## 产品目标 +桌面预览发行 CI(2026-09-22,实施中):新增 `desktop-release-preview.yml`,仅手动触发、不自动发布;`windows-2022` / `macos-15` 矩阵在 job 开始断言 `uname -m` 与 `process.arch` 与目标一致,复用各平台现有打包与验证脚本,并从解包后的 ZIP 直接启动做原生冒烟(token 化 URL、退出清端口),上传 ZIP、安装包与 SHA256SUMS;darwin-x64 在汇总中如实标记未覆盖,不以 Rosetta 代产。自动冒烟不替代 #19 真实模型与干净机器验收。阶段、约束与首跑证据要求见 [桌面预览发行 CI 施工](desktop-release-preview-plan.md)。 + macOS 原生架构门禁(2026-09-20,已实现待验收):`build:desktop:darwin` 与 `verify-darwin.sh` 在产出构建目录或启动验证进程前,按硬件级 `hw.optional.arm64` 与 Node 自身架构判定执行环境,拒绝 Apple Silicon 上的 Rosetta 翻译执行并给出手工切换指引,不静默更改输出架构;架构判定收敛到 `apps/desktop/darwin-target.mjs` 单一来源,验证脚本经其 `--print-shell-env` 复用同一规则。11 项目标解析测试(含注入式 sysctl 分支)通过;Apple Silicon 实机覆盖原生通过、翻译 x86_64 Node 拒绝(官方 darwin-x64 Node 实测)与 Rosetta shell 内原生 arm64 Node 正常放行三条路径;Intel Mac 真机与完整重新打包未覆盖。详见 [桌面启动器说明](../apps/desktop/README.md)。 QCode 重命名(2026-09-18,已实现待验收):产品与公开仓库已改名为 QCode;Mosslight / 苔光之屿继续作为世界名称。workspace、Web 插件目录与包名、TypeScript / Godot 自有符号、DOM 命名空间、Web/Godot Bridge、桌面应用、产品文档和发布链接已改用 QCode;新路由和消息使用 `/qcode/*`、`qcode-host`、`qcode-world`,迁移期兼容旧路由、请求头、消息来源和 Bridge 名称。浏览器存储、开发目录、Host 状态文件及桌面用户数据均采用无覆盖迁移,教程存储 domain 保持旧稳定身份以读取既有进度,世界导出状态改用 `.qcode-world-export.json`;`check:qcode-naming` 已接入 CI,开发目录迁移测试也已加入 CI。immutable 安装、类型检查、Web 构建、61 项插件测试、23 项本轮启动/迁移/文档及辅助测试、Godot 4.7.2 的 7 个 CI 场景、世界导出及哈希校验通过;隔离临时数据目录在 3081 端口启动后,认证入口、首页、QCode 图标与世界 HTML/WASM/PCK 的 HTTP 冒烟通过。Windows QCode 安装包与便携包完成安装提取、认证页面、世界资源、退出清理、单实例、原生模块、旧数据迁移与双目录冲突验收。介绍页通过 1440px 桌面截图检查,真实 390px 浏览器视口无横向溢出,动态标题正确。GitHub API 确认公开仓库为 `Qiuner/QCode`、remote 和 Pages 地址已更新且主分支保护保持不变,新 Pages 路径返回 200。macOS 脚本已同步但未在原生环境执行;真实浏览器 WebGL 与模型请求未在本轮验收,远端 CI 与线上页面的新内容仍待验收。本地 checkout 路径暂不改名,以免使活跃任务工作区失效。详见 [QCode 重命名施工方案](qcode-renaming-plan.md)。 diff --git a/docs/desktop-release-preview-plan.md b/docs/desktop-release-preview-plan.md new file mode 100644 index 0000000..bb09abe --- /dev/null +++ b/docs/desktop-release-preview-plan.md @@ -0,0 +1,23 @@ +# 桌面预览发行 CI 施工 + +状态:实施中。跟踪 [issue #18](https://github.com/Qiuner/QCode/issues/18)。工作流已编写,待 fork 与上游首跑证据后转"已实现待验收"。 + +## 阶段与范围 + +- `.github/workflows/desktop-release-preview.yml`:仅 `workflow_dispatch` 手动触发,不自动发布、不创建 GitHub Release、不推送任何渠道。 +- 矩阵:`windows-2022` → `windows-x64`;`macos-15` → `macos-arm64`。`fail-fast: false`,两平台独立判定,任何一方的通过都不替代另一方。 +- 架构守卫:job 开始即以 `uname -m`、`node -p process.arch`(Windows 另加 `PROCESSOR_ARCHITECTURE` 与 `RUNNER_ARCH`)断言与目标矩阵一致,不一致立即失败;不凭 runner 标签推断 CPU。macOS 标签的实际架构以首跑守卫结果为准,通过后在本文件与 workflow 注释中固定。 +- 构建:immutable 安装 → Godot 4.7.2(win64 / universal 固定 SHA-256)→ `build:world` + `build:web` → 各平台现有打包脚本(`apps/desktop/build.mjs` / `build:desktop:darwin`)。 +- 冒烟:Windows 运行现有 `verify.ps1`(安装提取、外部链接检查、隔离数据、单实例、退出清端口),并额外从解包后的 `qcode-windows-x64.zip` 直接启动 `QCode.exe`,轮询 token 化 `browser-url.txt` 请求完整 URL(200/303),终止后断言端口释放;macOS 解包 zip 后把 `app/` 交给 `verify-darwin.sh`(含 Rosetta 拒绝门禁),并 `shasum -c SHA256SUMS.txt`。 +- 产物:白名单上传 `qcode-*.zip`、`qcode-setup-x64.exe`、`SHA256SUMS.txt`;失败日志仅来自 runner 临时目录(`RUNNER_TEMP`),`QCODE_DATA_HOME` / 隔离数据全部位于临时目录。 +- 汇总 job:输出各矩阵结果,并如实记录 **darwin-x64 未覆盖**(无真实 Intel runner,禁止 Rosetta 代产)。 + +## 验收证据(待首跑) + +- 首跑需人工核对:守卫输出的实际 `uname -m` / `process.arch` 与矩阵一致;两平台产物 ZIP + SHA256SUMS 下载校验;冒烟日志。 +- 结果与限制记录在 `docs/construction-plan.md` 对应条目。 + +## 限制 + +- 自动冒烟不替代 #19 的真实模型端到端与干净机器人工验收。 +- Dependabot / 第三方 action 版本固定为当前 major(checkout@v4、upload-artifact@v4)。 From 18f6a5fad00028cdd53286ec07516f7b50c2a49e Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:27:40 +0800 Subject: [PATCH 2/8] =?UTF-8?q?ci(desktop):=20=E5=8F=91=E8=A1=8C=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E8=A1=A5=20Godot=20Web=20=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=20/=20add=20web=20export=20templates=20to=20?= =?UTF-8?q?release=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub runner 默认不带 Godot 导出模板,build:world 会失败; Windows/macOS 两侧均下载官方 sha256 校验的 tpz 并只解出 web debug/release 与 nothreads 变体,另断言 index.wasm 存在。 --- .github/workflows/desktop-release-preview.yml | 25 +++++++++++++++++++ docs/desktop-release-preview-plan.md | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/desktop-release-preview.yml b/.github/workflows/desktop-release-preview.yml index bc590da..845f054 100644 --- a/.github/workflows/desktop-release-preview.yml +++ b/.github/workflows/desktop-release-preview.yml @@ -58,6 +58,22 @@ jobs: } Expand-Archive $archive -DestinationPath $destination Add-Content -Path $env:GITHUB_ENV -Value "GODOT_BIN=$(Join-Path $destination 'Godot_v4.7.2-stable_win64_console.exe')" + $sha = (Invoke-WebRequest 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz.sha256' -UseBasicParsing).Content + $expected = ($sha -split '\s')[0] + $tpz = Join-Path $env:RUNNER_TEMP 'godot-templates.tpz' + Invoke-WebRequest 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz' -OutFile $tpz + if ((Get-FileHash $tpz -Algorithm SHA256).Hash.ToLower() -ne $expected) { throw 'Godot export templates checksum mismatch' } + $templates = Join-Path $env:APPDATA 'Godot\export_templates\4.7.2.stable' + New-Item -ItemType Directory -Force -Path $templates | Out-Null + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [IO.Compression.ZipFile]::OpenRead($tpz) + try { + foreach ($name in @('web_debug.zip', 'web_release.zip', 'web_nothreads_debug.zip', 'web_nothreads_release.zip')) { + $entry = $zip.Entries | Where-Object { $_.FullName -eq "templates/$name" } + if (-not $entry) { throw "template entry $name missing from tpz" } + [IO.Compression.ZipFileExtensions]::ExtractToFile($entry, (Join-Path $templates $name), $true) + } + } finally { $zip.Dispose() } - name: Install Godot 4.7.2 (macOS) if: runner.os == 'macOS' @@ -68,6 +84,14 @@ jobs: echo 'c58a24e31d720be9d62f60cb5627c4e695fb72f21b0cfe1bc9ccaa9a3b3ba63e godot.zip' | shasum -a 256 -c - unzip -q "$archive" -d "$RUNNER_TEMP/godot-4.7.2" echo "GODOT_BIN=$RUNNER_TEMP/godot-4.7.2/Godot.app/Contents/MacOS/Godot" >> "$GITHUB_ENV" + tpz="$RUNNER_TEMP/godot-templates.tpz" + curl -fsSL -o "$tpz.sha256" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz.sha256' + expected="$(cut -d' ' -f1 "$tpz.sha256")" + curl -fsSL -o "$tpz" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz' + echo "$expected $tpz" | shasum -a 256 -c - + templates="$HOME/Library/Application Support/Godot/export_templates/4.7.2.stable" + mkdir -p "$templates" + unzip -q -j "$tpz" 'templates/web_debug.zip' 'templates/web_release.zip' 'templates/web_nothreads_debug.zip' 'templates/web_nothreads_release.zip' -d "$templates" - name: Build Web plugin and world shell: bash @@ -75,6 +99,7 @@ jobs: "$GODOT_BIN" --headless --path games/mosslight --editor --import corepack yarn build:world corepack yarn build:web + test -f games/mosslight/build/web/index.wasm - name: Build portable package (Windows) if: runner.os == 'Windows' diff --git a/docs/desktop-release-preview-plan.md b/docs/desktop-release-preview-plan.md index bb09abe..8805891 100644 --- a/docs/desktop-release-preview-plan.md +++ b/docs/desktop-release-preview-plan.md @@ -7,7 +7,7 @@ - `.github/workflows/desktop-release-preview.yml`:仅 `workflow_dispatch` 手动触发,不自动发布、不创建 GitHub Release、不推送任何渠道。 - 矩阵:`windows-2022` → `windows-x64`;`macos-15` → `macos-arm64`。`fail-fast: false`,两平台独立判定,任何一方的通过都不替代另一方。 - 架构守卫:job 开始即以 `uname -m`、`node -p process.arch`(Windows 另加 `PROCESSOR_ARCHITECTURE` 与 `RUNNER_ARCH`)断言与目标矩阵一致,不一致立即失败;不凭 runner 标签推断 CPU。macOS 标签的实际架构以首跑守卫结果为准,通过后在本文件与 workflow 注释中固定。 -- 构建:immutable 安装 → Godot 4.7.2(win64 / universal 固定 SHA-256)→ `build:world` + `build:web` → 各平台现有打包脚本(`apps/desktop/build.mjs` / `build:desktop:darwin`)。 +- 构建:immutable 安装 → Godot 4.7.2(win64 / universal 固定 SHA-256)+ 官方校验和的 Web 导出模板(tpz 仅解出四个 web 变体,runner 默认不带模板)→ `build:world` + `build:web` → 各平台现有打包脚本(`apps/desktop/build.mjs` / `build:desktop:darwin`)。 - 冒烟:Windows 运行现有 `verify.ps1`(安装提取、外部链接检查、隔离数据、单实例、退出清端口),并额外从解包后的 `qcode-windows-x64.zip` 直接启动 `QCode.exe`,轮询 token 化 `browser-url.txt` 请求完整 URL(200/303),终止后断言端口释放;macOS 解包 zip 后把 `app/` 交给 `verify-darwin.sh`(含 Rosetta 拒绝门禁),并 `shasum -c SHA256SUMS.txt`。 - 产物:白名单上传 `qcode-*.zip`、`qcode-setup-x64.exe`、`SHA256SUMS.txt`;失败日志仅来自 runner 临时目录(`RUNNER_TEMP`),`QCODE_DATA_HOME` / 隔离数据全部位于临时目录。 - 汇总 job:输出各矩阵结果,并如实记录 **darwin-x64 未覆盖**(无真实 Intel runner,禁止 Rosetta 代产)。 From 3354f45d815c4aa26254ecb333c60503f938df50 Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:32:18 +0800 Subject: [PATCH 3/8] =?UTF-8?q?ci(desktop):=20=E6=A8=A1=E6=9D=BF=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=94=B9=E7=94=A8=E5=AE=98=E6=96=B9=20SHA512=20?= =?UTF-8?q?=E9=92=89=E6=AD=BB=E5=80=BC=20/=20pin=20official=20SHA512=20for?= =?UTF-8?q?=20export=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Godot 4.7.2 GitHub release 无 tpz 的 .sha256 伴生资产(404 返回网页), 改用 release 附件 SHA512-SUMS.txt 中导出模板包的官方哈希直接钉死。 --- .github/workflows/desktop-release-preview.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/desktop-release-preview.yml b/.github/workflows/desktop-release-preview.yml index 845f054..84aba52 100644 --- a/.github/workflows/desktop-release-preview.yml +++ b/.github/workflows/desktop-release-preview.yml @@ -58,11 +58,9 @@ jobs: } Expand-Archive $archive -DestinationPath $destination Add-Content -Path $env:GITHUB_ENV -Value "GODOT_BIN=$(Join-Path $destination 'Godot_v4.7.2-stable_win64_console.exe')" - $sha = (Invoke-WebRequest 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz.sha256' -UseBasicParsing).Content - $expected = ($sha -split '\s')[0] $tpz = Join-Path $env:RUNNER_TEMP 'godot-templates.tpz' Invoke-WebRequest 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz' -OutFile $tpz - if ((Get-FileHash $tpz -Algorithm SHA256).Hash.ToLower() -ne $expected) { throw 'Godot export templates checksum mismatch' } + if ((Get-FileHash $tpz -Algorithm SHA512).Hash.ToLower() -ne 'ca4d71c4d7b81dfc15d1a98baa07534aa95b03fdda78a0075b06672e1648d2e5f40980c9adc28d23e1b92e732ee7bf3461997aa804af74ec2fcd7a93ccb84079') { throw 'Godot export templates checksum mismatch' } $templates = Join-Path $env:APPDATA 'Godot\export_templates\4.7.2.stable' New-Item -ItemType Directory -Force -Path $templates | Out-Null Add-Type -AssemblyName System.IO.Compression.FileSystem @@ -85,10 +83,8 @@ jobs: unzip -q "$archive" -d "$RUNNER_TEMP/godot-4.7.2" echo "GODOT_BIN=$RUNNER_TEMP/godot-4.7.2/Godot.app/Contents/MacOS/Godot" >> "$GITHUB_ENV" tpz="$RUNNER_TEMP/godot-templates.tpz" - curl -fsSL -o "$tpz.sha256" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz.sha256' - expected="$(cut -d' ' -f1 "$tpz.sha256")" curl -fsSL -o "$tpz" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz' - echo "$expected $tpz" | shasum -a 256 -c - + echo 'ca4d71c4d7b81dfc15d1a98baa07534aa95b03fdda78a0075b06672e1648d2e5f40980c9adc28d23e1b92e732ee7bf3461997aa804af74ec2fcd7a93ccb84079 $tpz' | shasum -a 512 -c - templates="$HOME/Library/Application Support/Godot/export_templates/4.7.2.stable" mkdir -p "$templates" unzip -q -j "$tpz" 'templates/web_debug.zip' 'templates/web_release.zip' 'templates/web_nothreads_debug.zip' 'templates/web_nothreads_release.zip' -d "$templates" From 7e8dedf78d7dc65fbc64f28b1145e765abb132cf Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:58:50 +0800 Subject: [PATCH 4/8] =?UTF-8?q?ci(desktop):=20=E4=BF=AE=E6=AD=A3=20macOS?= =?UTF-8?q?=20=E8=B5=84=E4=BA=A7=E5=90=8D=E4=B8=8E=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E5=B9=B6=E5=AE=B9=E5=BF=8D=20303=20/=20fix?= =?UTF-8?q?=20macos=20asset=20name,=20hash=20expansion=20and=20303?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首跑矩阵暴露三处:Godot macOS 资产实名带 macos. 前缀(404); shasum -c 行误用单引号阻止变量展开且校验相对路径(本就不可用), 统一改为双引号全路径并以官方 SHA512-SUMS.txt 钉死; pwsh 7 Invoke-WebRequest 对 303 抛异常,加 -SkipHttpErrorCheck 后按状态码判定。verify.ps1 与安装提取冒烟本身已全过。 --- .github/workflows/desktop-release-preview.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/desktop-release-preview.yml b/.github/workflows/desktop-release-preview.yml index 84aba52..4068006 100644 --- a/.github/workflows/desktop-release-preview.yml +++ b/.github/workflows/desktop-release-preview.yml @@ -78,13 +78,13 @@ jobs: shell: bash run: | archive="$RUNNER_TEMP/godot.zip" - curl -fsSL -o "$archive" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_universal.zip' - echo 'c58a24e31d720be9d62f60cb5627c4e695fb72f21b0cfe1bc9ccaa9a3b3ba63e godot.zip' | shasum -a 256 -c - + curl -fsSL -o "$archive" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_macos.universal.zip' + echo "38aa16e5bba2083941fc5b3e54be0089bd4cc35e32415f5b9fd9a8a6a7b9818255d44532ea8ef94b5aef56c4b407c2d634fa4f657e4ebe681ebbf59b7bac69ca $archive" | shasum -a 512 -c - unzip -q "$archive" -d "$RUNNER_TEMP/godot-4.7.2" echo "GODOT_BIN=$RUNNER_TEMP/godot-4.7.2/Godot.app/Contents/MacOS/Godot" >> "$GITHUB_ENV" tpz="$RUNNER_TEMP/godot-templates.tpz" curl -fsSL -o "$tpz" 'https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz' - echo 'ca4d71c4d7b81dfc15d1a98baa07534aa95b03fdda78a0075b06672e1648d2e5f40980c9adc28d23e1b92e732ee7bf3461997aa804af74ec2fcd7a93ccb84079 $tpz' | shasum -a 512 -c - + echo "ca4d71c4d7b81dfc15d1a98baa07534aa95b03fdda78a0075b06672e1648d2e5f40980c9adc28d23e1b92e732ee7bf3461997aa804af74ec2fcd7a93ccb84079 $tpz" | shasum -a 512 -c - templates="$HOME/Library/Application Support/Godot/export_templates/4.7.2.stable" mkdir -p "$templates" unzip -q -j "$tpz" 'templates/web_debug.zip' 'templates/web_release.zip' 'templates/web_nothreads_debug.zip' 'templates/web_nothreads_release.zip' -d "$templates" @@ -121,7 +121,7 @@ jobs: while (-not (Test-Path $urlFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 } if (-not (Test-Path $urlFile)) { throw 'Service did not become ready from unpacked zip' } $url = [uri](Get-Content $urlFile -Raw) - $response = Invoke-WebRequest $url.AbsoluteUri -UseBasicParsing -MaximumRedirection 0 -ErrorAction SilentlyContinue + $response = Invoke-WebRequest $url.AbsoluteUri -UseBasicParsing -MaximumRedirection 0 -SkipHttpErrorCheck if ($response.StatusCode -notin 200, 303) { throw "Unpacked zip smoke got HTTP $($response.StatusCode)" } Stop-Process -Id $proc.Id -Force $proc.WaitForExit() From 3844be83d276e043ff168b81dbe82c4ca2dd1633 Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:07:55 +0800 Subject: [PATCH 5/8] =?UTF-8?q?ci(desktop):=20=E4=BF=AE=E6=AD=A3=E8=A7=A3?= =?UTF-8?q?=E5=8C=85=E7=9B=AE=E5=BD=95=E5=B1=82=E7=BA=A7=E4=B8=8E=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F=20303=20=E8=AE=A4=E8=AF=81=E8=B7=B3=E8=BD=AC=20/=20fi?= =?UTF-8?q?x=20extracted=20layout=20and=20redirect=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify-darwin.sh 期望含 app/ 的 out 目录,而 zip 根即 app/, 传 $work 而非 $work/app;Windows 冒烟改为跟随 token 认证 303 跳转并断言最终 200,顺带验证完整认证流。 --- .github/workflows/desktop-release-preview.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/desktop-release-preview.yml b/.github/workflows/desktop-release-preview.yml index 4068006..eb843a1 100644 --- a/.github/workflows/desktop-release-preview.yml +++ b/.github/workflows/desktop-release-preview.yml @@ -121,8 +121,8 @@ jobs: while (-not (Test-Path $urlFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 } if (-not (Test-Path $urlFile)) { throw 'Service did not become ready from unpacked zip' } $url = [uri](Get-Content $urlFile -Raw) - $response = Invoke-WebRequest $url.AbsoluteUri -UseBasicParsing -MaximumRedirection 0 -SkipHttpErrorCheck - if ($response.StatusCode -notin 200, 303) { throw "Unpacked zip smoke got HTTP $($response.StatusCode)" } + $response = Invoke-WebRequest $url.AbsoluteUri -UseBasicParsing + if ($response.StatusCode -ne 200) { throw "Unpacked zip smoke got HTTP $($response.StatusCode)" } Stop-Process -Id $proc.Id -Force $proc.WaitForExit() Start-Sleep -Seconds 2 @@ -144,7 +144,7 @@ jobs: work="$RUNNER_TEMP/zip-smoke" mkdir -p "$work" ditto -x -k "$build/qcode-darwin-arm64.zip" "$work" - corepack yarn verify:desktop:darwin "$work/app" + corepack yarn verify:desktop:darwin "$work" (cd "$build" && shasum -a 256 -c SHA256SUMS.txt) - name: Checksums From 69e863fa8511bba6bb3aca0f381d7ebba757cd74 Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:16:31 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix(desktop):=20darwin=20=E4=BE=BF=E6=90=BA?= =?UTF-8?q?=E5=8C=85=E9=9A=8F=E5=B8=A6=E5=8A=A8=E6=80=81=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E7=9A=84=20libnode=20/=20bundle=20libnode=20dylib=20on=20darwi?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS 发行 CI 首跑发现:embedNodeRuntime 只拷 process.execPath, 官方 darwin 发行版 node 动态链接 libnode..dylib(rpath 含 @loader_path),单独拷出的二进制在包内 Abort trap。 构建用 node 静态链接时 lib 目录无匹配自然跳过(本机 v26 验证零拷贝)。 修复后由 desktop-release-preview macOS job 复验。 --- apps/desktop/pack-app.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/desktop/pack-app.mjs b/apps/desktop/pack-app.mjs index e05a767..4fe77fc 100644 --- a/apps/desktop/pack-app.mjs +++ b/apps/desktop/pack-app.mjs @@ -1,5 +1,5 @@ // 桌面发行:把 Web 插件、世界资源与 node_modules 打进安装根目录(Win/Mac 共用)。 -import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { assertWorldExportCurrent } from '../../scripts/world-export-state.mjs' @@ -56,6 +56,16 @@ export function materializeAppTree(app) { export async function embedNodeRuntime(app, { binaryName }) { mkdirSync(path.join(app, 'runtime'), { recursive: true }) cpSync(process.execPath, path.join(app, 'runtime', binaryName)) + // 官方 darwin 发行版 node 动态链接 libnode(rpath 含 @loader_path), + // 只拷二进制会得到坏包;构建用 node 静态链接时该目录不存在,自然跳过。 + if (process.platform === 'darwin') { + const libDir = path.join(path.dirname(realpathSync(process.execPath)), '..', 'lib') + if (existsSync(libDir)) { + for (const lib of readdirSync(libDir).filter(name => /^libnode\.\d+\.dylib$/.test(name))) { + cpSync(path.join(libDir, lib), path.join(app, 'runtime', lib)) + } + } + } const licenseCache = path.join(root, 'dist', `node-${process.version}-LICENSE`) if (!existsSync(licenseCache)) { mkdirSync(path.join(root, 'dist'), { recursive: true }) From f778d5613e91f347303890f9c11d9a16283f31f1 Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:26:09 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix(desktop):=20=E5=86=92=E7=83=9F=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=95=B0=E6=8D=AE=E5=86=B2=E7=AA=81=E4=BB=A5=E9=9D=9E?= =?UTF-8?q?=E9=9B=B6=E9=80=80=E5=87=BA=20/=20fail=20smoke=20with=20nonzero?= =?UTF-8?q?=20exit=20on=20data=20conflict?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS 发行 CI 复验暴露:resolveDataHome 抛错路径统一走 NSApp.terminate(nil),冒烟模式下冲突检测退出码为 0, verify-darwin 的迁移冲突断言形同虚设。 smoke 分支改为 stderr 输出原因并 exit(1);GUI 模式行为不变。 本机 swiftc 编译复现验证:冲突场景由静默 0 修复为 1 + 中文错误。 --- apps/desktop/macos/Launcher.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/desktop/macos/Launcher.swift b/apps/desktop/macos/Launcher.swift index 5ce91f5..1fdfebe 100644 --- a/apps/desktop/macos/Launcher.swift +++ b/apps/desktop/macos/Launcher.swift @@ -39,7 +39,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { let alert = NSAlert() alert.messageText = "QCode 无法启动" alert.informativeText = error.localizedDescription - if !smoke { alert.runModal() } + if smoke { + // NSApp.terminate 会以 0 退出:冒烟模式必须把失败如实传给调用方,否则冲突检测形同虚设。 + FileHandle.standardError.write(Data(("QCode 无法启动: " + error.localizedDescription + "\n").utf8)) + exit(1) + } + alert.runModal() NSApp.terminate(nil) return } From c1638b046328fe861d77150cccee621462e032a6 Mon Sep 17 00:00:00 2001 From: Jay Feng <153891754+ruijayfeng@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:35:09 +0800 Subject: [PATCH 8/8] =?UTF-8?q?docs(desktop):=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=8F=91=E8=A1=8C=20CI=20fork=20=E9=A6=96=E8=B7=91=E8=AF=81?= =?UTF-8?q?=E6=8D=AE=E4=B8=8E=E7=8A=B6=E6=80=81=20/=20record=20fork=20firs?= =?UTF-8?q?t-run=20evidence=20for=20release=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 矩阵两 job 全绿(架构守卫、双平台 verify、解包冒烟、产物上传), 状态转已实现待验收;如实记录首跑发现并修复的两个缺陷。 --- docs/construction-plan.md | 2 +- docs/desktop-release-preview-plan.md | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/construction-plan.md b/docs/construction-plan.md index 3540d04..3a4896c 100644 --- a/docs/construction-plan.md +++ b/docs/construction-plan.md @@ -4,7 +4,7 @@ ## 产品目标 -桌面预览发行 CI(2026-09-22,实施中):新增 `desktop-release-preview.yml`,仅手动触发、不自动发布;`windows-2022` / `macos-15` 矩阵在 job 开始断言 `uname -m` 与 `process.arch` 与目标一致,复用各平台现有打包与验证脚本,并从解包后的 ZIP 直接启动做原生冒烟(token 化 URL、退出清端口),上传 ZIP、安装包与 SHA256SUMS;darwin-x64 在汇总中如实标记未覆盖,不以 Rosetta 代产。自动冒烟不替代 #19 真实模型与干净机器验收。阶段、约束与首跑证据要求见 [桌面预览发行 CI 施工](desktop-release-preview-plan.md)。 +桌面预览发行 CI(2026-09-22,已实现待验收):新增 `desktop-release-preview.yml`,仅手动触发、不自动发布;`windows-2022` / `macos-15` 矩阵在 job 开始断言 `uname -m` 与 `process.arch` 与目标一致,复用各平台现有打包与验证脚本,并从解包后的 ZIP 直接启动做原生冒烟(token 化 URL、退出清端口),上传 ZIP、安装包与 SHA256SUMS;darwin-x64 在汇总中如实标记未覆盖,不以 Rosetta 代产。fork 矩阵首跑两平台全绿,并暴露修复了官方动态链接 Node 坏包与冒烟冲突静默退出 0 两个真实缺陷;上游内首跑与评审待验收。自动冒烟不替代 #19 真实模型与干净机器验收。阶段、约束与首跑证据见 [桌面预览发行 CI 施工](desktop-release-preview-plan.md)。 macOS 原生架构门禁(2026-09-20,已实现待验收):`build:desktop:darwin` 与 `verify-darwin.sh` 在产出构建目录或启动验证进程前,按硬件级 `hw.optional.arm64` 与 Node 自身架构判定执行环境,拒绝 Apple Silicon 上的 Rosetta 翻译执行并给出手工切换指引,不静默更改输出架构;架构判定收敛到 `apps/desktop/darwin-target.mjs` 单一来源,验证脚本经其 `--print-shell-env` 复用同一规则。11 项目标解析测试(含注入式 sysctl 分支)通过;Apple Silicon 实机覆盖原生通过、翻译 x86_64 Node 拒绝(官方 darwin-x64 Node 实测)与 Rosetta shell 内原生 arm64 Node 正常放行三条路径;Intel Mac 真机与完整重新打包未覆盖。详见 [桌面启动器说明](../apps/desktop/README.md)。 diff --git a/docs/desktop-release-preview-plan.md b/docs/desktop-release-preview-plan.md index 8805891..f923dcc 100644 --- a/docs/desktop-release-preview-plan.md +++ b/docs/desktop-release-preview-plan.md @@ -1,6 +1,6 @@ # 桌面预览发行 CI 施工 -状态:实施中。跟踪 [issue #18](https://github.com/Qiuner/QCode/issues/18)。工作流已编写,待 fork 与上游首跑证据后转"已实现待验收"。 +状态:已实现待验收。工作流已落地并在 fork 完成 windows-2022 / macos-15 矩阵首跑(见下文证据);上游仓库内的首跑需维护者(admin)触发,评审与合并待验收。 ## 阶段与范围 @@ -12,10 +12,15 @@ - 产物:白名单上传 `qcode-*.zip`、`qcode-setup-x64.exe`、`SHA256SUMS.txt`;失败日志仅来自 runner 临时目录(`RUNNER_TEMP`),`QCODE_DATA_HOME` / 隔离数据全部位于临时目录。 - 汇总 job:输出各矩阵结果,并如实记录 **darwin-x64 未覆盖**(无真实 Intel runner,禁止 Rosetta 代产)。 -## 验收证据(待首跑) +## 验收证据 -- 首跑需人工核对:守卫输出的实际 `uname -m` / `process.arch` 与矩阵一致;两平台产物 ZIP + SHA256SUMS 下载校验;冒烟日志。 -- 结果与限制记录在 `docs/construction-plan.md` 对应条目。 +fork 首跑(https://github.com/ruijayfeng/agent-isles/actions/runs/35699586487 ,head `f778d5613e91f347303890f9c11d9a16283f31f1`): + +- 架构守卫:macos-15 输出 `runner.arch=ARM64`、`uname -m: arm64`、`process.arch=arm64`;windows-2022 输出 `runner.arch=X64`、`uname -m: x86_64`、`PROCESSOR_ARCHITECTURE=AMD64`。macOS arm64 标签按首跑结果固定为 `macos-15`。 +- Windows(Microsoft Windows Server 2022):`verify.ps1` 全过(安装提取、无外部链接、认证页面、正常 / 崩溃清理、单实例、原生模块、旧数据迁移 / 冲突),并从解包 ZIP 启动 `QCode.exe` 跟随 token 认证跳转得到 200,终止后端口释放。 +- macOS:`verify-darwin.sh` 对解包 ZIP 全过(原生架构、冒烟就绪、退出清理、单实例、原生模块、迁移 / 冲突),`shasum -c SHA256SUMS.txt` 通过。 +- 产物:`windows-x64-preview` 287,523,735 字节、`macos-arm64-preview` 139,974,843 字节(ZIP / 安装包 / SHA256SUMS 白名单)。 +- 首跑发现并修复两个真实缺陷:`embedNodeRuntime` 只拷二进制导致官方动态链接 Node 产出坏包(补 `libnode.*.dylib`);`Launcher.swift` 冒烟模式数据冲突经 `NSApp.terminate` 静默以 0 退出(改 `exit(1)`,否则 verify 冲突断言形同虚设)。 ## 限制