diff --git a/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md b/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md index c72a3091d..a3416657e 100644 --- a/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md +++ b/.agents/notes/implemented/process/2026-09-13-ci-build-cache-and-artifact-strategy.md @@ -6,6 +6,10 @@ > 不能当作当前分支仍有这些门禁。当前 `desktop-ci.yml` 对 Draft PR 运行 Linux 格式、 > 脚本和桌面测试,非 Draft 另运行 macOS 与 Windows 测试;Core 的源码和构建缓存按 > 系统、架构、工具链与构建脚本身份区分。旧资源复用脚本、旧分片和 lane 复用尚未迁移。 +> `desktop-autobuild.yml` 与 `desktop-release.yml` 使用同一个 release 档 Core 缓存键,并共用 +> `.github/actions/setup-desktop-windows-build-tools` 安装 MinGW 和 NSIS。AutoBuild 由 `main` +> 上的定时任务运行,GitHub 允许其它分支和 tag 读取默认分支保存的缓存,所以发布时多半能直接 +> 复用前一晚编好的 Core;只在 tag 上保存的缓存,其它分支读不到。 Windows 的 Rust 静态库还会依赖 Cargo crate 提供的导入库,不能只传系统库名称给 Go 链接器。桌面构建入口读取对应 target/profile 的 Cargo 构建输出,把实际使用的 @@ -552,6 +556,8 @@ Swift 测试已经编译完整 Lithe 目标。再生成两个 DMG 会在普通 - `.github/workflows/desktop-ci.yml` - `.github/workflows/desktop-release.yml` +- `.github/workflows/desktop-autobuild.yml` +- `.github/actions/setup-desktop-windows-build-tools/action.yml` - `scripts/desktop/build-desktop-core.sh` - `scripts/desktop/desktop-go.sh` - `scripts/ci/cancel-closed-pr-runs/` diff --git a/.agents/notes/implemented/process/2026-09-13-macos-update-signing-and-release-strategy.md b/.agents/notes/implemented/process/2026-09-13-macos-update-signing-and-release-strategy.md index 37ad54eb6..955c5735d 100644 --- a/.agents/notes/implemented/process/2026-09-13-macos-update-signing-and-release-strategy.md +++ b/.agents/notes/implemented/process/2026-09-13-macos-update-signing-and-release-strategy.md @@ -2,7 +2,7 @@ 状态:已实现 -> Viva 迁移状态:本文的 Sparkle、AutoBuild、插件签名和回滚实现及验收记录属于旧客户端。Viva 当前有独立预发布打包工作流,但应用内更新页面仍是占位,尚不能据此宣称渠道、签名验证、差分更新或回滚已接入;这些安全要求必须在迁移时重新落实和验证。 +> Viva 迁移状态:本文的 Sparkle、AutoBuild、插件签名和回滚实现及验收记录属于旧客户端。Viva 当前有独立预发布打包工作流 `desktop-release.yml`,以及每天从 `preview` 滚动发布到 prerelease `desktop-AutoBuild` 的 `desktop-autobuild.yml`。后者只构建和发布,沿用正式包的应用身份 `app.lithe.viva`,没有独立的开发版身份、更新渠道和构建时间身份;两条工作流都不传公证凭据,macOS 包不会公证。应用内更新页面仍是占位,尚不能据此宣称渠道、签名验证、差分更新或回滚已接入;这些安全要求必须在迁移时重新落实和验证。 ## 先说结论 @@ -445,6 +445,8 @@ Sparkle 版本间升级、缺失/损坏的 delta、下载中断、权限不足 - `scripts/resources/worktree-resources.json` - `scripts/resources/reuse-worktree-resources.mjs` - `.github/workflows/desktop-release.yml` +- `.github/workflows/desktop-autobuild.yml` +- `scripts/ci/desktop-autobuild/publish-autobuild.sh` - `desktop/internal/settingswindow/window.go` - `shared/platform-feature-matrix/features/updates-check-download.json` - `shared/platform-feature-matrix/features/updates-failure-rollback.json` diff --git a/.github/actions/setup-desktop-windows-build-tools/action.yml b/.github/actions/setup-desktop-windows-build-tools/action.yml new file mode 100644 index 000000000..edf59bafe --- /dev/null +++ b/.github/actions/setup-desktop-windows-build-tools/action.yml @@ -0,0 +1,49 @@ +name: Set up desktop Windows build tools +description: >- + Puts MinGW-w64 gcc (for the cgo core link) and NSIS makensis (for the installer) on PATH. + Shared by the release and AutoBuild workflows so both package Windows with the same tools. + +runs: + using: composite + steps: + - name: Ensure MinGW-w64 and NSIS + shell: bash + run: | + if ! command -v gcc >/dev/null 2>&1; then + choco install mingw -y + gcc_path="$(find /c/ProgramData/chocolatey/lib/mingw /c/tools/mingw64 /c/mingw64 \ + -type f -name gcc.exe -print -quit 2>/dev/null || true)" + if [[ -z "$gcc_path" ]]; then + printf '%s\n' 'MinGW-w64 installation did not provide gcc.exe.' >&2 + exit 1 + fi + mingw_bin="$(dirname "$gcc_path")" + export PATH="$mingw_bin:$PATH" + printf '%s\n' "$(cygpath -m "$mingw_bin")" >> "$GITHUB_PATH" + fi + gcc --version + nsis_x86='/c/Program Files (x86)/NSIS' + nsis_x64='/c/Program Files/NSIS' + if ! command -v makensis >/dev/null 2>&1 && \ + [[ ! -f "$nsis_x86/makensis.exe" && ! -f "$nsis_x64/makensis.exe" ]]; then + choco install nsis -y + fi + if ! command -v makensis >/dev/null 2>&1; then + for nsis_dir in "$nsis_x86" "$nsis_x64"; do + if [[ -f "$nsis_dir/makensis.exe" ]]; then + export PATH="$nsis_dir:$PATH" + printf '%s\n' "$(cygpath -m "$nsis_dir")" >> "$GITHUB_PATH" + break + fi + done + fi + if command -v makensis >/dev/null 2>&1; then + makensis /VERSION + elif [[ -f "$nsis_x86/makensis.exe" ]]; then + "$nsis_x86/makensis.exe" /VERSION + elif [[ -f "$nsis_x64/makensis.exe" ]]; then + "$nsis_x64/makensis.exe" /VERSION + else + printf '%s\n' 'NSIS installation did not provide makensis.' >&2 + exit 1 + fi diff --git a/.github/workflows/desktop-autobuild.yml b/.github/workflows/desktop-autobuild.yml new file mode 100644 index 000000000..1a5bc15b4 --- /dev/null +++ b/.github/workflows/desktop-autobuild.yml @@ -0,0 +1,215 @@ +name: Desktop AutoBuild + +# Builds the Viva desktop client (desktop/) from the preview branch every day and publishes it to +# the rolling prerelease desktop-AutoBuild: macOS arm64, Windows x64, and Linux x64 and arm64. +# AutoBuild only builds and publishes. It runs no tests and does not inspect the packages, unlike +# Desktop Release. Its tag is separate from the AutoBuild tag that the SwiftUI and Tauri clients' +# updaters read, and the release is never marked latest. +on: + schedule: + # 10:00 UTC, which is 18:00 in China (UTC+8). + - cron: "0 10 * * *" + workflow_dispatch: + inputs: + source_branch: + description: "Branch to build" + required: true + type: string + default: preview + +permissions: + contents: read + +# One publication at a time; a run that starts while another publishes waits for it. +concurrency: + group: desktop-autobuild + cancel-in-progress: false + +env: + RELEASE_TAG: desktop-AutoBuild + SOURCE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs.source_branch || 'preview' }} + +jobs: + prepare: + name: Pin the source and build identity + if: >- + github.event_name == 'schedule' || + github.actor == github.repository_owner || + contains(format(',{0},', vars.LITHE_RELEASE_ACTOR), format(',{0},', github.actor)) + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + sha: ${{ steps.identity.outputs.sha }} + version: ${{ steps.identity.outputs.version }} + build: ${{ steps.identity.outputs.build }} + timestamp: ${{ steps.identity.outputs.timestamp }} + skip: ${{ steps.identity.outputs.skip }} + + steps: + - name: Check out the source branch + uses: actions/checkout@v7 + with: + ref: ${{ env.SOURCE_BRANCH }} + + # Every build job checks out this commit, so a push during the run cannot mix two sources. + # A scheduled run whose commit is already published builds nothing. + - name: Record the source commit and build identity + id: identity + env: + GH_TOKEN: ${{ github.token }} + run: | + sha="$(git rev-parse HEAD)" + version="$(sed -n 's/^version = "\([0-9.]*\)"$/\1/p' desktop/viva.toml)" + if [[ -z "$version" ]]; then + printf '%s\n' 'desktop/viva.toml has no [app] version.' >&2 + exit 1 + fi + read -r build timestamp < <(date -u '+%Y%m%d.%H%M%S %Y-%m-%dT%H:%M:%SZ') + skip=false + if [[ "$GITHUB_EVENT_NAME" == schedule ]]; then + published="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" --jq .object.sha 2>/dev/null || true)" + if [[ "$published" == "$sha" ]]; then + skip=true + printf '%s\n' "$RELEASE_TAG already holds $SOURCE_BRANCH at $sha; nothing to build." | tee -a "$GITHUB_STEP_SUMMARY" + fi + fi + { + printf 'sha=%s\n' "$sha" + printf 'version=%s\n' "$version" + printf 'build=%s\n' "$build" + printf 'timestamp=%s\n' "$timestamp" + printf 'skip=%s\n' "$skip" + } >> "$GITHUB_OUTPUT" + + build: + name: Package ${{ matrix.os }} ${{ matrix.arch }} + needs: prepare + if: needs.prepare.outputs.skip != 'true' + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + include: + - os: darwin + arch: arm64 + runner: macos-26 + rust-target: aarch64-apple-darwin + - os: windows + arch: amd64 + runner: windows-2022 + rust-target: x86_64-pc-windows-gnu + - os: linux + arch: amd64 + runner: ubuntu-22.04 + rust-target: x86_64-unknown-linux-gnu + - os: linux + arch: arm64 + runner: ubuntu-22.04-arm + rust-target: aarch64-unknown-linux-gnu + + steps: + - name: Check out the pinned source + uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: desktop/go.mod + cache-dependency-path: desktop/go.sum + + - name: Set up Rust + id: rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust-target }} + + - name: Ensure Windows build tools + if: matrix.os == 'windows' + uses: ./.github/actions/setup-desktop-windows-build-tools + + # The same key as Desktop Release, so the two workflows share one release build of the core. + - name: Restore the core build + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + .artifacts/desktop/lithe-core-rust + key: desktop-core-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.cachekey }}-release-${{ matrix.rust-target }}-${{ hashFiles('scripts/desktop/build-desktop-core.sh') }} + restore-keys: | + desktop-core-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.cachekey }}-release-${{ matrix.rust-target }}- + + # Each platform receives only its own signing secrets. + - name: Package + env: + APPLE_CERTIFICATE: ${{ matrix.os == 'darwin' && secrets.MACOS_SIGNING_CERTIFICATE || '' }} + APPLE_CERTIFICATE_PASSWORD: ${{ matrix.os == 'darwin' && secrets.MACOS_SIGNING_PASSWORD || '' }} + APPLE_SIGNING_IDENTITY: ${{ matrix.os == 'darwin' && vars.MACOS_SIGNING_IDENTITY || '' }} + WINDOWS_CERTIFICATE: ${{ matrix.os == 'windows' && secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 || '' }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ matrix.os == 'windows' && secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD || '' }} + WINDOWS_TIMESTAMP_URL: ${{ matrix.os == 'windows' && secrets.WINDOWS_TIMESTAMP_SERVER || '' }} + DESKTOP_ARCH: ${{ matrix.arch }} + run: bash scripts/ci/desktop-autobuild/package-autobuild.sh "$DESKTOP_ARCH" + + - name: Upload the packages + uses: actions/upload-artifact@v7 + with: + name: desktop-autobuild-${{ matrix.os }}-${{ matrix.arch }} + path: dist/autobuild/ + if-no-files-found: error + # The packages are already compressed, and the release keeps the copy users download. + compression-level: 0 + retention-days: 3 + + publish: + name: Publish the AutoBuild + needs: + - prepare + - build + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + + steps: + # The publication script comes from the workflow's own commit, not from the branch being built. + - name: Check out the publication script + uses: actions/checkout@v7 + with: + ref: ${{ github.workflow_sha }} + + - name: Download the packages + uses: actions/download-artifact@v8 + with: + pattern: desktop-autobuild-* + merge-multiple: true + path: dist/autobuild + + - name: Publish to the rolling prerelease + env: + GH_TOKEN: ${{ github.token }} + SOURCE_SHA: ${{ needs.prepare.outputs.sha }} + DESKTOP_VERSION: ${{ needs.prepare.outputs.version }} + BUILD_NUMBER: ${{ needs.prepare.outputs.build }} + BUILD_TIMESTAMP: ${{ needs.prepare.outputs.timestamp }} + # The same conditions under which package-autobuild.sh signs; a partial pair fails the build. + MACOS_SIGNED: ${{ secrets.MACOS_SIGNING_CERTIFICATE != '' }} + WINDOWS_SIGNED: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 != '' && secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD != '' }} + run: bash scripts/ci/desktop-autobuild/publish-autobuild.sh dist/autobuild + + - name: Add the release link + env: + SOURCE_SHA: ${{ needs.prepare.outputs.sha }} + BUILD_NUMBER: ${{ needs.prepare.outputs.build }} + run: | + { + printf '%s\n' "Published $SOURCE_BRANCH at \`$SOURCE_SHA\` as build $BUILD_NUMBER." + printf '%s\n' "[Download this build]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$RELEASE_TAG)" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/desktop-ci.yml b/.github/workflows/desktop-ci.yml index 572113800..6a74b6204 100644 --- a/.github/workflows/desktop-ci.yml +++ b/.github/workflows/desktop-ci.yml @@ -72,6 +72,8 @@ jobs: bash -n scripts/desktop/perf/editor-bench.sh bash -n scripts/ci/desktop-release/package-release.sh bash -n scripts/ci/desktop-release/resolve-release-version.sh + bash -n scripts/ci/desktop-autobuild/package-autobuild.sh + bash -n scripts/ci/desktop-autobuild/publish-autobuild.sh zsh -n scripts/desktop/generate-desktop-icons.sh zsh -n scripts/desktop/generate-desktop-file-icons.sh @@ -84,7 +86,9 @@ jobs: scripts/desktop/package-desktop.sh \ scripts/desktop/perf/editor-bench.sh \ scripts/ci/desktop-release/package-release.sh \ - scripts/ci/desktop-release/resolve-release-version.sh + scripts/ci/desktop-release/resolve-release-version.sh \ + scripts/ci/desktop-autobuild/package-autobuild.sh \ + scripts/ci/desktop-autobuild/publish-autobuild.sh - name: Verify shared localization run: | diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index b1a005329..ad80bac67 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -96,45 +96,7 @@ jobs: - name: Ensure Windows build tools if: matrix.os == 'windows' - run: | - if ! command -v gcc >/dev/null 2>&1; then - choco install mingw -y - gcc_path="$(find /c/ProgramData/chocolatey/lib/mingw /c/tools/mingw64 /c/mingw64 \ - -type f -name gcc.exe -print -quit 2>/dev/null || true)" - if [[ -z "$gcc_path" ]]; then - printf '%s\n' 'MinGW-w64 installation did not provide gcc.exe.' >&2 - exit 1 - fi - mingw_bin="$(dirname "$gcc_path")" - export PATH="$mingw_bin:$PATH" - printf '%s\n' "$(cygpath -m "$mingw_bin")" >> "$GITHUB_PATH" - fi - gcc --version - nsis_x86='/c/Program Files (x86)/NSIS' - nsis_x64='/c/Program Files/NSIS' - if ! command -v makensis >/dev/null 2>&1 && \ - [[ ! -f "$nsis_x86/makensis.exe" && ! -f "$nsis_x64/makensis.exe" ]]; then - choco install nsis -y - fi - if ! command -v makensis >/dev/null 2>&1; then - for nsis_dir in "$nsis_x86" "$nsis_x64"; do - if [[ -f "$nsis_dir/makensis.exe" ]]; then - export PATH="$nsis_dir:$PATH" - printf '%s\n' "$(cygpath -m "$nsis_dir")" >> "$GITHUB_PATH" - break - fi - done - fi - if command -v makensis >/dev/null 2>&1; then - makensis /VERSION - elif [[ -f "$nsis_x86/makensis.exe" ]]; then - "$nsis_x86/makensis.exe" /VERSION - elif [[ -f "$nsis_x64/makensis.exe" ]]; then - "$nsis_x64/makensis.exe" /VERSION - else - printf '%s\n' 'NSIS installation did not provide makensis.' >&2 - exit 1 - fi + uses: ./.github/actions/setup-desktop-windows-build-tools - name: Restore the core build uses: actions/cache@v6 diff --git a/docs/viva-migration.md b/docs/viva-migration.md index 2515e4408..e92bd5454 100644 --- a/docs/viva-migration.md +++ b/docs/viva-migration.md @@ -570,5 +570,5 @@ MCP 管理随 Agent 对话失去消费者后,本轮整块删除,后续重新 | 应用名 / 版本 | `CFBundleName` Lithe,版本 0.1.0(Info.plist),发布时覆盖 | `productName` Lithe,0.3.0 | `name = "Lithe Desktop"`,`version = "0.7.0"` | 否(产品名不同,版本线独立) | | macOS 更新 | Sparkle:`SUFeedURL` 与 `SUPublicEDKey` 在构建时由 `configure-sparkle-app.sh` 写入。Stable 源 `https://github.com/<仓库>/releases/latest/download/appcast-.xml`;AutoBuild 源 `https://github.com/<仓库>/releases/download/AutoBuild/appcast-preview-.xml`;公钥来自仓库变量 `SPARKLE_PUBLIC_KEY`(值未在仓库内,未找到);`SURequireSignedFeed`、`SUVerifyUpdateBeforeExtraction` 为 true;说明见 `docs/architecture/macos-updates.md` | — | `viva.toml` 与 `scripts/desktop/package-desktop.sh` 中未找到 Sparkle、appcast、feed、公钥任何配置 | 否。新客户端自建更新通道(新的 appcast 与签名公钥),不衔接旧 Sparkle 增量链;尚未实现(`updates-check-download` 为未开始) | | Windows 安装与更新 | — | Tauri 打包 `nsis`、`msi`(`tauri.conf.json` `bundle.targets`);Tauri updater 插件,`tauri.conf.json` 中 `pubkey` 为空串、`endpoints` 为空;稳定版发布工作流 `release-windows.yml` 用 secret `TAURI_SIGNING_PRIVATE_KEY` 与变量 `TAURI_UPDATER_PUBLIC_KEY` 签名,产出 `Lithe-<版本>-windows-x64-updater.exe` | `viva.toml [windows]`:`scope = "user"`、`publisher = "Lithe contributors"`;`package-desktop.sh` Windows 默认格式 `nsis,zip`;更新机制未找到 | 部分。同为 NSIS;没有 MSI、更新器与签名公钥配置 | -| 发布工作流 | `release-macos.yml`、`release-preview-macos.yml` | `release-windows.yml`、`release-preview-windows.yml` | `.github/workflows/desktop-release.yml`,标签 `desktop-v*.*.*`,作为 prerelease 发布,避免旧客户端的更新器读到 | 独立 | +| 发布工作流 | `release-macos.yml`、`release-preview-macos.yml` | `release-windows.yml`、`release-preview-windows.yml` | `.github/workflows/desktop-release.yml`,标签 `desktop-v*.*.*`,作为 prerelease 发布,避免旧客户端的更新器读到;`.github/workflows/desktop-autobuild.yml` 每天从 `preview` 构建并滚动发布到 prerelease `desktop-AutoBuild`(macOS arm64、Windows x64、Linux x64/arm64 的 deb 与 tar.gz,不跑测试),标签与旧客户端的 `AutoBuild` 分开 | 独立 | | 运行时只读边界 | 发行 bundle 运行期不可写(AGENTS.md) | 安装目录只读 | `package-desktop.sh` 注释声明运行时写入不落在 app 或安装目录;`runtime-package-integrity` 仅 macOS 夹具验证 | 一致于规则;Windows/最终包验证未完成 | diff --git a/scripts/ci/desktop-autobuild/package-autobuild.sh b/scripts/ci/desktop-autobuild/package-autobuild.sh new file mode 100755 index 000000000..5f5b42aea --- /dev/null +++ b/scripts/ci/desktop-autobuild/package-autobuild.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Packages one architecture of the host OS for the Desktop AutoBuild: +# +# package-autobuild.sh +# +# AutoBuild only builds and publishes. It runs no tests and does not inspect the packages; checking +# them is the Desktop Release's job (scripts/ci/desktop-release/package-release.sh). The version is +# the one in desktop/viva.toml. Signing follows the release: a complete certificate configuration +# signs, none leaves the package unsigned, and a partial one is refused, so a broken secret fails +# the build instead of quietly publishing unsigned packages. The packages and a .sha256 file for +# each, whose entry contains only the file name, are copied flat into dist/autobuild/, the +# directory the workflow uploads; their names are printed last. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd -P)" + +if [[ $# -ne 1 ]]; then + printf '%s\n' "Usage: $0 " >&2 + exit 2 +fi +ARCH="$1" +case "$ARCH" in + arm64|amd64) ;; + *) printf '%s\n' "The architecture must be arm64 or amd64, not $ARCH." >&2; exit 2 ;; +esac + +VERSION="$(sed -n 's/^version = "\([0-9.]*\)"$/\1/p' "$ROOT_DIR/desktop/viva.toml")" +if [[ -z "$VERSION" ]]; then + printf '%s\n' 'desktop/viva.toml has no [app] version.' >&2 + exit 1 +fi +GOOS="$(go env GOOS)" + +SIGN_ARGS=() +case "$GOOS" in + darwin) + if [[ -n "${APPLE_CERTIFICATE:-}" ]]; then + if [[ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]]; then + printf '%s\n' 'APPLE_CERTIFICATE is set without APPLE_CERTIFICATE_PASSWORD.' >&2 + exit 1 + fi + elif [[ -n "${APPLE_CERTIFICATE_PASSWORD:-}" || -n "${APPLE_SIGNING_IDENTITY:-}" ]]; then + printf '%s\n' 'Incomplete Apple signing configuration: provide APPLE_CERTIFICATE, or remove the password and identity.' >&2 + exit 1 + else + printf '%s\n' 'No Apple signing certificate is configured; the package is unsigned.' + SIGN_ARGS=(--no-sign) + fi + FORMATS='app,zip,dmg' + PACKAGES=("lithe-$VERSION-darwin-$ARCH.dmg" "lithe-$VERSION-darwin-$ARCH.zip") + ;; + windows) + if [[ -n "${WINDOWS_CERTIFICATE:-}" && -n "${WINDOWS_CERTIFICATE_PASSWORD:-}" ]]; then + : + elif [[ -n "${WINDOWS_CERTIFICATE:-}" || -n "${WINDOWS_CERTIFICATE_PASSWORD:-}" ]]; then + printf '%s\n' 'Incomplete Windows signing configuration: provide both WINDOWS_CERTIFICATE and WINDOWS_CERTIFICATE_PASSWORD, or neither.' >&2 + exit 1 + else + # A timestamp server alone signs nothing, so it does not count as a partial configuration. + printf '%s\n' 'No Windows signing certificate is configured; the package is unsigned.' + SIGN_ARGS=(--no-sign) + fi + FORMATS='nsis,zip' + PACKAGES=("lithe-$VERSION-windows-$ARCH-setup.exe" "lithe-$VERSION-windows-$ARCH.zip") + ;; + linux) + FORMATS='tar.gz,deb' + PACKAGES=("lithe_${VERSION}_${ARCH}.deb" "lithe-$VERSION-linux-$ARCH.tar.gz") + ;; + *) printf '%s\n' "Desktop packages are not supported on host OS $GOOS." >&2; exit 2 ;; +esac + +PACKAGE_DIR="$ROOT_DIR/dist/desktop/$GOOS-$ARCH" +OUT_DIR="$ROOT_DIR/dist/autobuild" +"$ROOT_DIR/scripts/desktop/package-desktop.sh" --arch "$ARCH" --format "$FORMATS" --out "$PACKAGE_DIR" \ + "${SIGN_ARGS[@]+"${SIGN_ARGS[@]}"}" + +mkdir -p "$OUT_DIR" +NAMES=() +for package in "${PACKAGES[@]}"; do + if [[ ! -s "$PACKAGE_DIR/$package" ]]; then + printf '%s\n' "viva package did not produce $PACKAGE_DIR/$package." >&2 + exit 1 + fi + cp -f "$PACKAGE_DIR/$package" "$OUT_DIR/$package" + ( + cd "$OUT_DIR" + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$package" > "$package.sha256" + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$package" > "$package.sha256" + else + printf '%s\n' 'Neither shasum nor sha256sum is available.' >&2 + exit 1 + fi + ) + NAMES+=("$package" "$package.sha256") +done + +printf '%s\n' "${NAMES[@]}" diff --git a/scripts/ci/desktop-autobuild/publish-autobuild.sh b/scripts/ci/desktop-autobuild/publish-autobuild.sh new file mode 100755 index 000000000..f665081de --- /dev/null +++ b/scripts/ci/desktop-autobuild/publish-autobuild.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# Publishes the Desktop AutoBuild packages in DIR to the rolling prerelease RELEASE_TAG: +# +# publish-autobuild.sh +# +# The environment names the build: GH_TOKEN, GITHUB_REPOSITORY, RELEASE_TAG, SOURCE_SHA, +# SOURCE_BRANCH, DESKTOP_VERSION, BUILD_NUMBER and BUILD_TIMESTAMP (ISO 8601 UTC, ending in Z); +# MACOS_SIGNED and WINDOWS_SIGNED (true or false) say whether the packages were signed. +# +# The release keeps only the latest build. Every file in DIR is uploaded over the asset of the same +# name, the tag moves to SOURCE_SHA, the title and notes describe this build, and assets this build +# did not produce (a previous version's packages, for example) are deleted. The release stays a +# prerelease that is never marked latest, so updaters reading the latest release never see it. +set -euo pipefail + +if [[ $# -ne 1 || ! -d "$1" ]]; then + printf '%s\n' "Usage: $0 " >&2 + exit 2 +fi +DIR="$1" +for name in GH_TOKEN GITHUB_REPOSITORY RELEASE_TAG SOURCE_SHA SOURCE_BRANCH DESKTOP_VERSION BUILD_NUMBER BUILD_TIMESTAMP; do + if [[ -z "${!name:-}" ]]; then + printf '%s\n' "$name is not set." >&2 + exit 2 + fi +done + +FILES=() +NAMES=() +for path in "$DIR"/*; do + if [[ -f "$path" ]]; then + FILES+=("$path") + NAMES+=("$(basename "$path")") + fi +done +if [[ ${#FILES[@]} -eq 0 ]]; then + printf '%s\n' "$DIR holds no packages to publish." >&2 + exit 1 +fi + +# The workflow reports whether the signing secrets exist; package-autobuild.sh signs exactly then. +# No notary credentials are configured, so the macOS package is never notarized. +if [[ "${MACOS_SIGNED:-false}" == true ]]; then + MACOS_SIGNING_ZH='macOS 包已用 Developer ID 签名,但没有经过 Apple 公证。' + MACOS_SIGNING_EN='The macOS package is signed with a Developer ID but is not notarized by Apple.' +else + MACOS_SIGNING_ZH='macOS 包没有用 Developer ID 签名,也没有经过 Apple 公证。' + MACOS_SIGNING_EN='The macOS package is not signed with a Developer ID and is not notarized by Apple.' +fi +if [[ "${WINDOWS_SIGNED:-false}" == true ]]; then + WINDOWS_SIGNING_ZH='Windows 安装程序和程序已签名。' + WINDOWS_SIGNING_EN='The Windows installer and program are signed.' +else + WINDOWS_SIGNING_ZH='Windows 安装程序和程序没有签名。' + WINDOWS_SIGNING_EN='The Windows installer and program are not signed.' +fi + +SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" +COMMIT_URL="$SERVER_URL/$GITHUB_REPOSITORY/commit/$SOURCE_SHA" +SHORT_SHA="${SOURCE_SHA:0:12}" +BUILT_AT="${BUILD_TIMESTAMP/T/ }" +BUILT_AT="${BUILT_AT%Z} UTC" +TITLE="Lithe Desktop AutoBuild — 构建自 $BUILT_AT" +V="$DESKTOP_VERSION" + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf -- "$WORK_DIR"' EXIT +NOTES="$WORK_DIR/notes.md" +# The Chinese text quotes button names with “ ”, which are prose, not shell quotes. +# shellcheck disable=SC1111 +cat > "$NOTES" < [!WARNING] +> 这是从 \`$SOURCE_BRANCH\` 分支自动构建的测试版本,没有经过测试和人工验证,可能有未完成的功能或严重问题,请不要用于重要工作。日常使用请下载正式版本。 +> +> This is an automatic build of the \`$SOURCE_BRANCH\` branch. It has not been tested or reviewed and may contain unfinished features or serious bugs, so do not use it for important work. For everyday use, download a stable release. + +## 中文 + +这是 Lithe Desktop 的最新自动构建。这个发布只保留最新一次构建,每次构建都会替换全部下载文件。 + +- 源码:\`$SOURCE_BRANCH\` 分支,提交 [\`$SHORT_SHA\`]($COMMIT_URL) +- 版本:$V,构建号 $BUILD_NUMBER +- 构建时间:$BUILT_AT + +### 下载 + +- macOS(Apple 芯片):\`lithe-$V-darwin-arm64.dmg\`,或 \`lithe-$V-darwin-arm64.zip\` +- Windows x64:安装程序 \`lithe-$V-windows-amd64-setup.exe\`,或免安装的 \`lithe-$V-windows-amd64.zip\` +- Linux x64:\`lithe_${V}_amd64.deb\`,或 \`lithe-$V-linux-amd64.tar.gz\` +- Linux ARM64:\`lithe_${V}_arm64.deb\`,或 \`lithe-$V-linux-arm64.tar.gz\` + +每个文件都有同名的 \`.sha256\` 校验文件。 + +### 注意事项 + +- macOS 只提供 Apple 芯片版本,不提供 Intel 版本。 +- 自动构建不会自动更新,需要新构建时请重新下载。 +- $MACOS_SIGNING_ZH如果 macOS 提示无法打开 Lithe Desktop.app,请在“应用程序”中按住 Control 点按应用并选择“打开”;如果仍被阻止,可在终端执行 \`xattr -dr com.apple.quarantine "/Applications/Lithe Desktop.app"\`。仅对可信来源的应用使用。 +- $WINDOWS_SIGNING_ZH显示 SmartScreen 提示时,确认来源可信后选择“更多信息”,再选择“仍要运行”。 + +--- + +## English + +This is the latest automatic build of Lithe Desktop. The release keeps only the latest build, and every build replaces all of its downloads. + +- Source: the \`$SOURCE_BRANCH\` branch at commit [\`$SHORT_SHA\`]($COMMIT_URL) +- Version: $V, build $BUILD_NUMBER +- Built: $BUILT_AT + +### Downloads + +- macOS (Apple silicon): \`lithe-$V-darwin-arm64.dmg\`, or \`lithe-$V-darwin-arm64.zip\` +- Windows x64: the installer \`lithe-$V-windows-amd64-setup.exe\`, or the portable \`lithe-$V-windows-amd64.zip\` +- Linux x64: \`lithe_${V}_amd64.deb\`, or \`lithe-$V-linux-amd64.tar.gz\` +- Linux ARM64: \`lithe_${V}_arm64.deb\`, or \`lithe-$V-linux-arm64.tar.gz\` + +Every file has a \`.sha256\` checksum file of the same name. + +### Notes + +- macOS builds are for Apple silicon only; there is no Intel build. +- AutoBuild does not update itself; download again to get a newer build. +- $MACOS_SIGNING_EN If macOS says it cannot open Lithe Desktop.app, Control-click it in Applications and choose Open. If it is still blocked, run \`xattr -dr com.apple.quarantine "/Applications/Lithe Desktop.app"\` in Terminal. Use this only for an app from a source you trust. +- $WINDOWS_SIGNING_EN If Windows shows a SmartScreen prompt, confirm that the source is trusted, choose More info, then choose Run anyway. +EOF + +if ! gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json tagName >/dev/null 2>&1; then + # --target only applies when the tag does not exist yet; the ref update below moves an existing one. + gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --target "$SOURCE_SHA" \ + --prerelease --latest=false --title "$TITLE" --notes-file "$NOTES" +fi + +gh release upload "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --clobber "${FILES[@]}" +gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$RELEASE_TAG" \ + -f sha="$SOURCE_SHA" -F force=true >/dev/null +gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ + --title "$TITLE" --notes-file "$NOTES" --prerelease --latest=false --draft=false >/dev/null + +CURRENT="$(printf '%s\n' "${NAMES[@]}")" +while IFS= read -r asset; do + if [[ -n "$asset" ]] && ! grep -Fxq -- "$asset" <<<"$CURRENT"; then + printf '%s\n' "Deleting $asset, which this build did not produce." + gh release delete-asset "$RELEASE_TAG" "$asset" --repo "$GITHUB_REPOSITORY" --yes + fi +done < <(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name') + +printf '%s\n' "Published ${#FILES[@]} files to $SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$RELEASE_TAG" diff --git a/shared/platform-feature-matrix/features/updates-check-download.json b/shared/platform-feature-matrix/features/updates-check-download.json index b51fab561..61112fbe7 100644 --- a/shared/platform-feature-matrix/features/updates-check-download.json +++ b/shared/platform-feature-matrix/features/updates-check-download.json @@ -30,7 +30,7 @@ ] }, "owner": "Release", - "releaseSource": "Viva 桌面构建使用独立 desktop-v 标签和预发布包;应用内更新渠道和签名验证尚未接入。", + "releaseSource": "Viva 桌面构建使用独立 desktop-v 标签和预发布包,另由 desktop-autobuild.yml 每天从 preview 滚动发布到 desktop-AutoBuild 预发布(不注入更新源,也没有独立的开发版身份);应用内更新渠道和签名验证尚未接入。", "verification": "Viva 需要分别在 macOS 和 Windows 验证更新渠道、签名、下载、安装、取消和数据保护;当前尚无对应实机验收。", "notes": "Viva:设置 › 更新页可检查、下载并安装:Viva 的更新包校验 latest.json 的 Ed25519 签名,按平台下载并校验大小与 SHA-256,下载放在用户缓存目录,安装后重启到新版本;启动时最多每天自动检查一次且只提示。更新源、公钥和渠道在链接时注入(buildinfo/update.go),打包与发布流程尚未注入也未发布签名 feed,所以发行构建目前不检查更新。没有专门的自动化测试。", "legacy": {