From 1d7a0d6bd86d1ded6a2d39a15c130d9fcdfea51e Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 3 Apr 2026 19:13:25 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B7=20build(desktop):=20remove=20night?= =?UTF-8?q?ly=20release=20channel=20(#13480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 👷 build(desktop): remove nightly release channel * 🐛 fix(database): remove invalid tool_call_id from messages inserts in tests * 🧪 test(desktop): fix updater channel migration mocks * ♻️ refactor(desktop): migrate update channel in bootstrap * ♻️ refactor(desktop): extract store migrations * 🐛 fix(desktop): use custom store migration runner * ♻️ refactor(desktop): split store migrations into files * update Signed-off-by: Innei --------- Signed-off-by: Innei Co-authored-by: codex-514 --- .github/workflows/manual-build-desktop.yml | 16 +- .github/workflows/release-desktop-beta.yml | 6 +- .github/workflows/release-desktop-canary.yml | 87 +++- .github/workflows/release-desktop-nightly.yml | 415 ------------------ .../src/main/controllers/UpdaterCtr.ts | 8 +- .../controllers/__tests__/UpdaterCtr.test.ts | 47 ++ .../main/core/infrastructure/StoreManager.ts | 2 + .../core/infrastructure/UpdaterManager.ts | 6 +- .../__tests__/StoreManager.test.ts | 54 ++- .../migration/001-normalize-update-channel.ts | 15 + .../migration/defineMigration.ts | 10 + .../core/infrastructure/migration/index.ts | 55 +++ .../src/main/modules/updater/configs.ts | 13 +- .../__tests__/messages/message.update.test.ts | 5 - .../electron-client-ipc/src/types/update.ts | 2 +- src/locales/default/setting.ts | 2 +- src/routes/(main)/settings/advanced/index.tsx | 3 +- 17 files changed, 268 insertions(+), 478 deletions(-) delete mode 100644 .github/workflows/release-desktop-nightly.yml create mode 100644 apps/desktop/src/main/core/infrastructure/migration/001-normalize-update-channel.ts create mode 100644 apps/desktop/src/main/core/infrastructure/migration/defineMigration.ts create mode 100644 apps/desktop/src/main/core/infrastructure/migration/index.ts diff --git a/.github/workflows/manual-build-desktop.yml b/.github/workflows/manual-build-desktop.yml index 974f21cc01..1abbf82cec 100644 --- a/.github/workflows/manual-build-desktop.yml +++ b/.github/workflows/manual-build-desktop.yml @@ -6,10 +6,10 @@ on: channel: description: 'Release channel for desktop build (affects version suffix and workflow:set-desktop-version)' required: true - default: nightly + default: canary type: choice options: - - nightly + - canary - beta - stable build_macos: @@ -118,8 +118,8 @@ jobs: KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ inputs.channel == 'beta' && secrets.UMAMI_BETA_DESKTOP_PROJECT_ID || secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }} - NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ inputs.channel == 'beta' && secrets.UMAMI_BETA_DESKTOP_BASE_URL || secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }} + NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ inputs.channel == 'stable' && secrets.UMAMI_STABLE_DESKTOP_PROJECT_ID || secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }} + NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ inputs.channel == 'stable' && secrets.UMAMI_STABLE_DESKTOP_BASE_URL || secrets.UMAMI_BETA_DESKTOP_BASE_URL }} CSC_FOR_PULL_REQUEST: true APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} @@ -184,8 +184,8 @@ jobs: APP_URL: http://localhost:3015 DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' - NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ inputs.channel == 'beta' && secrets.UMAMI_BETA_DESKTOP_PROJECT_ID || secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }} - NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ inputs.channel == 'beta' && secrets.UMAMI_BETA_DESKTOP_BASE_URL || secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }} + NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ inputs.channel == 'stable' && secrets.UMAMI_STABLE_DESKTOP_PROJECT_ID || secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }} + NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ inputs.channel == 'stable' && secrets.UMAMI_STABLE_DESKTOP_BASE_URL || secrets.UMAMI_BETA_DESKTOP_BASE_URL }} TEMP: C:\temp TMP: C:\temp @@ -228,8 +228,8 @@ jobs: APP_URL: http://localhost:3015 DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' - NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ inputs.channel == 'beta' && secrets.UMAMI_BETA_DESKTOP_PROJECT_ID || secrets.UMAMI_NIGHTLY_DESKTOP_PROJECT_ID }} - NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ inputs.channel == 'beta' && secrets.UMAMI_BETA_DESKTOP_BASE_URL || secrets.UMAMI_NIGHTLY_DESKTOP_BASE_URL }} + NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ inputs.channel == 'stable' && secrets.UMAMI_STABLE_DESKTOP_PROJECT_ID || secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }} + NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ inputs.channel == 'stable' && secrets.UMAMI_STABLE_DESKTOP_BASE_URL || secrets.UMAMI_BETA_DESKTOP_BASE_URL }} - name: Upload artifact uses: actions/upload-artifact@v6 diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 0d174a8287..4a45e5a86d 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -7,7 +7,7 @@ name: Release Desktop Beta # 如: v2.0.0-beta.1, v2.0.0-alpha.1, v2.0.0-rc.1 # # 注意: Stable 版本 (如 v2.0.0) 由 release-desktop-stable.yml 处理 -# 注意: Nightly 版本 (如 v2.1.0-nightly.xxx) 由 release-desktop-nightly.yml 处理 +# 注意: Nightly 版本已停用,不再参与 Desktop 发布流程 # ============================================ on: @@ -41,10 +41,10 @@ jobs: version="${version#v}" echo "version=${version}" >> $GITHUB_OUTPUT - # Beta 版本包含 beta/alpha/rc (nightly 由 release-desktop-nightly.yml 处理) + # Beta 版本包含 beta/alpha/rc;nightly 标签已停用 if [[ "$version" == *"nightly"* ]]; then echo "is_beta=false" >> $GITHUB_OUTPUT - echo "⏭️ Skipping: $version is a nightly release (handled by release-desktop-nightly.yml)" + echo "⏭️ Skipping: $version is a disabled nightly release tag" elif [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]] || [[ "$version" == *"rc"* ]]; then echo "is_beta=true" >> $GITHUB_OUTPUT echo "✅ Beta release detected: $version" diff --git a/.github/workflows/release-desktop-canary.yml b/.github/workflows/release-desktop-canary.yml index 77b9253e6e..01e7a94536 100644 --- a/.github/workflows/release-desktop-canary.yml +++ b/.github/workflows/release-desktop-canary.yml @@ -45,6 +45,7 @@ jobs: name: Calculate Canary Version runs-on: ubuntu-latest outputs: + release_notes: ${{ steps.release-notes.outputs.release_notes }} version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} should_build: ${{ steps.check.outputs.should_build }} @@ -121,6 +122,66 @@ jobs: echo "✅ Canary version: ${version}" echo "🏷️ Tag: ${tag}" + - name: Generate canary release notes + if: steps.check.outputs.should_build == 'true' + id: release-notes + env: + TAG: ${{ steps.version.outputs.tag }} + run: | + previous_canary=$(git tag --sort=-creatordate | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-canary\.[0-9]+$' | head -n 1) + latest_stable=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) + + if [ -n "$previous_canary" ]; then + compare_from="$previous_canary" + compare_range="${previous_canary}..HEAD" + elif [ -n "$latest_stable" ]; then + compare_from="$latest_stable" + compare_range="${latest_stable}..HEAD" + else + compare_from="initial commit" + compare_range="HEAD" + fi + + commit_count=$(git rev-list --count "$compare_range") + commits=$(git log --no-merges --pretty='- `%h` %s (%an)' "$compare_range") + + if [ -z "$commits" ]; then + commits='- No new commits recorded.' + fi + + { + echo "release_notes< Automated canary build from \`canary\` branch." + echo + echo "### Commit Information" + echo + echo "- Based on changes since \`${compare_from}\`" + echo "- Commit count: ${commit_count}" + echo + printf '%s\n' "$commits" + echo + echo "### ⚠️ Important Notes" + echo + echo "- **This is an automated canary build and is NOT intended for production use.**" + echo "- Canary builds are triggered by \`build\`/\`fix\`/\`style\` commits on the \`canary\` branch." + echo "- May contain **unstable or incomplete changes**. **Use at your own risk.**" + echo "- It is strongly recommended to **back up your data** before using a canary build." + echo + echo "### 📦 Installation" + echo + echo "Download the appropriate installer for your platform from the assets below." + echo + echo "| Platform | File |" + echo "|----------|------|" + echo "| macOS (Apple Silicon) | \`.dmg\` (arm64) |" + echo "| macOS (Intel) | \`.dmg\` (x64) |" + echo "| Windows | \`.exe\` |" + echo "| Linux | \`.AppImage\` / \`.deb\` |" + echo "EOF" + } >> $GITHUB_OUTPUT + # ============================================ # 代码质量检查 # ============================================ @@ -182,6 +243,7 @@ jobs: env: UPDATE_CHANNEL: canary UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} + RELEASE_NOTES: ${{ needs.calculate-version.outputs.release_notes }} APP_URL: http://localhost:3015 DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' @@ -201,6 +263,7 @@ jobs: env: UPDATE_CHANNEL: canary UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} + RELEASE_NOTES: ${{ needs.calculate-version.outputs.release_notes }} APP_URL: http://localhost:3015 DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' @@ -216,6 +279,7 @@ jobs: env: UPDATE_CHANNEL: canary UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} + RELEASE_NOTES: ${{ needs.calculate-version.outputs.release_notes }} APP_URL: http://localhost:3015 DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' @@ -299,28 +363,7 @@ jobs: tag_name: ${{ needs.calculate-version.outputs.tag }} name: 'Desktop Canary ${{ needs.calculate-version.outputs.tag }}' prerelease: true - body: | - ## 🐤 Canary Build — ${{ needs.calculate-version.outputs.tag }} - - > Automated canary build from `canary` branch. - - ### ⚠️ Important Notes - - - **This is an automated canary build and is NOT intended for production use.** - - Canary builds are triggered by `build`/`fix`/`style` commits on the `canary` branch. - - May contain **unstable or incomplete changes**. **Use at your own risk.** - - It is strongly recommended to **back up your data** before using a canary build. - - ### 📦 Installation - - Download the appropriate installer for your platform from the assets below. - - | Platform | File | - |----------|------| - | macOS (Apple Silicon) | `.dmg` (arm64) | - | macOS (Intel) | `.dmg` (x64) | - | Windows | `.exe` | - | Linux | `.AppImage` / `.deb` | + body: ${{ needs.calculate-version.outputs.release_notes }} files: | release/latest* release/*.dmg* diff --git a/.github/workflows/release-desktop-nightly.yml b/.github/workflows/release-desktop-nightly.yml deleted file mode 100644 index c50a8460ff..0000000000 --- a/.github/workflows/release-desktop-nightly.yml +++ /dev/null @@ -1,415 +0,0 @@ -name: Release Desktop Nightly - -# ============================================ -# Nightly 自动发版工作流 -# ============================================ -# 触发条件: -# 1. 定时: 每天 UTC+8 14:00 (UTC 06:00) -# 2. 手动触发 (workflow_dispatch) -# -# 版本策略: -# 基于最新 tag 的 minor+1, 格式: X.(Y+1).0-nightly.YYYYMMDDHHMM -# 例: 当前 tag v2.0.12 → v2.1.0-nightly.202502091400 -# 使用精确到分钟的时间戳避免同一天多次触发时 tag 冲突 -# ============================================ - -on: - schedule: - - cron: '0 6 * * *' - workflow_dispatch: - inputs: - force: - description: 'Force build (skip diff check)' - required: false - type: boolean - default: false - -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: true - -permissions: read-all - -env: - NODE_VERSION: '24.11.1' - -jobs: - # ============================================ - # 计算 Nightly 版本号 - # ============================================ - calculate-version: - name: Calculate Nightly Version - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - tag: ${{ steps.version.outputs.tag }} - has_changes: ${{ steps.changes.outputs.has_changes }} - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Check for code changes since last nightly - id: changes - run: | - # 手动触发 + force 时跳过 diff 检查 - if [ "${{ inputs.force }}" == "true" ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "🔧 Force build requested, skipping diff check" - exit 0 - fi - - # 查找上一个 nightly tag - last_nightly=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-nightly\.' | head -n 1) - - if [ -z "$last_nightly" ]; then - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "📦 No previous nightly tag found, proceeding with first nightly build" - exit 0 - fi - - echo "📌 Last nightly tag: $last_nightly" - - # 对比指定目录是否有变更 - changes=$(git diff --name-only "$last_nightly"..HEAD -- package.json src/ packages/ apps/desktop/) - - if [ -z "$changes" ]; then - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "⏭️ No code changes since $last_nightly, skipping nightly build" - else - echo "has_changes=true" >> $GITHUB_OUTPUT - change_count=$(echo "$changes" | wc -l | tr -d ' ') - echo "✅ ${change_count} file(s) changed since $last_nightly:" - echo "$changes" | head -20 - [ "$change_count" -gt 20 ] && echo " ... and $((change_count - 20)) more" - fi - - - name: Calculate nightly version - if: steps.changes.outputs.has_changes == 'true' - id: version - run: | - # 获取最新的 tag (排除 nightly tag) - latest_tag=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) - - if [ -z "$latest_tag" ]; then - echo "❌ No stable tag found" - exit 1 - fi - - echo "📌 Latest stable tag: $latest_tag" - - # 去掉 v 前缀 - base_version="${latest_tag#v}" - - # 解析 major.minor.patch - IFS='.' read -r major minor patch <<< "$base_version" - - # minor + 1, patch 归零 - new_minor=$((minor + 1)) - timestamp=$(date -u +"%Y%m%d%H%M") - - version="${major}.${new_minor}.0-nightly.${timestamp}" - tag="v${version}" - - echo "version=${version}" >> $GITHUB_OUTPUT - echo "tag=${tag}" >> $GITHUB_OUTPUT - echo "✅ Nightly version: ${version}" - echo "🏷️ Tag: ${tag}" - - # ============================================ - # 代码质量检查 - # ============================================ - test: - name: Code quality check - needs: [calculate-version] - if: needs.calculate-version.outputs.has_changes == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout base - uses: actions/checkout@v6 - - - name: Setup environment - uses: ./.github/actions/setup-env - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install deps - run: pnpm install - - - name: Lint - run: bun run lint - - # ============================================ - # 多平台构建 - # ============================================ - build: - needs: [calculate-version, test] - if: needs.calculate-version.outputs.has_changes == 'true' - name: Build Desktop App - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-15, macos-15-intel, windows-2025, ubuntu-latest] - steps: - - uses: actions/checkout@v6 - - - name: Setup build environment - uses: ./.github/actions/desktop-build-setup - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Set package version - run: npm run workflow:set-desktop-version ${{ needs.calculate-version.outputs.version }} nightly - - # macOS 构建前清理 (修复 hdiutil 问题 https://github.com/electron-userland/electron-builder/issues/8415) - - name: Clean previous build artifacts (macOS) - if: runner.os == 'macOS' - run: | - sudo rm -rf apps/desktop/release || true - sudo rm -rf apps/desktop/dist || true - sudo rm -rf /tmp/electron-builder* || true - - # macOS 构建 - - name: Build artifact on macOS - if: runner.os == 'macOS' - run: npm run desktop:package:app - env: - UPDATE_CHANNEL: nightly - UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} - APP_URL: http://localhost:3015 - DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' - KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' - CSC_LINK: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} - CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - CSC_FOR_PULL_REQUEST: true - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }} - NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }} - - # Windows 构建 - - name: Build artifact on Windows - if: runner.os == 'Windows' - run: npm run desktop:package:app - env: - UPDATE_CHANNEL: nightly - UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} - APP_URL: http://localhost:3015 - DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' - KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' - NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }} - NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }} - TEMP: C:\temp - TMP: C:\temp - - # Linux 构建 - - name: Build artifact on Linux - if: runner.os == 'Linux' - run: npm run desktop:package:app - env: - UPDATE_CHANNEL: nightly - UPDATE_SERVER_URL: ${{ secrets.UPDATE_SERVER_URL }} - APP_URL: http://localhost:3015 - DATABASE_URL: 'postgresql://postgres@localhost:5432/postgres' - KEY_VAULTS_SECRET: 'oLXWIiR/AKF+rWaqy9lHkrYgzpATbW3CtJp3UfkVgpE=' - NEXT_PUBLIC_DESKTOP_PROJECT_ID: ${{ secrets.UMAMI_BETA_DESKTOP_PROJECT_ID }} - NEXT_PUBLIC_DESKTOP_UMAMI_BASE_URL: ${{ secrets.UMAMI_BETA_DESKTOP_BASE_URL }} - - - name: Upload artifacts - uses: ./.github/actions/desktop-upload-artifacts - with: - artifact-name: release-${{ matrix.os }} - retention-days: 3 - - # ============================================ - # 合并 macOS 多架构 latest-mac.yml 文件 - # ============================================ - merge-mac-files: - needs: [build] - name: Merge macOS Release Files - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup environment - uses: ./.github/actions/setup-env - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Download artifacts - uses: actions/download-artifact@v7 - with: - path: release - pattern: release-* - merge-multiple: true - - - name: List downloaded artifacts - run: ls -R release - - - name: Install yaml only for merge step - run: | - cd scripts/electronWorkflow - if [ ! -f package.json ]; then - echo '{"name":"merge-mac-release","private":true}' > package.json - fi - bun add --no-save yaml@2.8.1 - - - name: Merge latest-mac.yml files - run: bun run scripts/electronWorkflow/mergeMacReleaseFiles.js - - - name: Upload artifacts with merged macOS files - uses: actions/upload-artifact@v6 - with: - name: merged-release - path: release/ - retention-days: 1 - - # ============================================ - # 创建 Nightly Release - # ============================================ - publish-release: - needs: [merge-mac-files, calculate-version] - name: Publish Nightly Release - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Download merged artifacts - uses: actions/download-artifact@v7 - with: - name: merged-release - path: release - - - name: List final artifacts - run: ls -R release - - - name: Create Nightly Release - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ needs.calculate-version.outputs.tag }} - name: 'Desktop Nightly ${{ needs.calculate-version.outputs.tag }}' - prerelease: true - body: | - ## 🌙 Nightly Build — ${{ needs.calculate-version.outputs.tag }} - - > Automated nightly build from `main` branch. - - ### ⚠️ Important Notes - - - **This is an automated nightly build and is NOT intended for production use.** - - Nightly builds are generated from the latest `main` branch and may contain **unstable, untested, or incomplete features**. - - **No guarantees** are made regarding stability, data integrity, or backward compatibility. - - Bugs, crashes, and breaking changes are expected. **Use at your own risk.** - - **Do NOT report bugs** from nightly builds unless you can reproduce them on the latest beta or stable release. - - Nightly builds may have **different update channels** — they will not auto-update to/from stable or beta versions. - - It is strongly recommended to **back up your data** before using a nightly build. - - ### 📦 Installation - - Download the appropriate installer for your platform from the assets below. - - | Platform | File | - |----------|------| - | macOS (Apple Silicon) | `.dmg` (arm64) | - | macOS (Intel) | `.dmg` (x64) | - | Windows | `.exe` | - | Linux | `.AppImage` / `.deb` | - files: | - release/latest* - release/*.dmg* - release/*.zip* - release/*.exe* - release/*.AppImage - release/*.deb* - release/*.snap* - release/*.rpm* - release/*.tar.gz* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # ============================================ - # 发布到 S3 更新服务器 - # ============================================ - publish-s3: - needs: [merge-mac-files, calculate-version] - name: Publish to S3 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: ./.github/actions/desktop-publish-s3 - with: - channel: nightly - version: ${{ needs.calculate-version.outputs.version }} - aws-access-key-id: ${{ secrets.UPDATE_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.UPDATE_AWS_SECRET_ACCESS_KEY }} - s3-bucket: ${{ secrets.UPDATE_S3_BUCKET }} - s3-region: ${{ secrets.UPDATE_S3_REGION }} - s3-endpoint: ${{ secrets.UPDATE_S3_ENDPOINT }} - - # ============================================ - # 清理旧的 Nightly Releases (保留最近 7 个) - # ============================================ - cleanup-old-nightlies: - needs: [publish-release, publish-s3] - name: Cleanup Old Nightly Releases - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v6 - - - name: Delete old nightly GitHub releases - uses: actions/github-script@v7 - with: - script: | - const { data: releases } = await github.rest.repos.listReleases({ - owner: context.repo.owner, - repo: context.repo.repo, - per_page: 100, - }); - - const nightlyReleases = releases - .filter(r => r.tag_name.includes('-nightly.')) - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - - const toDelete = nightlyReleases.slice(7); - - for (const release of toDelete) { - console.log(`🗑️ Deleting old nightly release: ${release.tag_name}`); - - // Delete the release - await github.rest.repos.deleteRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: release.id, - }); - - // Delete the tag - try { - await github.rest.git.deleteRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: `tags/${release.tag_name}`, - }); - } catch (e) { - console.log(`⚠️ Could not delete tag ${release.tag_name}: ${e.message}`); - } - } - - console.log(`✅ Cleanup complete. Kept ${Math.min(nightlyReleases.length, 7)} nightly releases, deleted ${toDelete.length}.`); - - - name: Cleanup old S3 versions - uses: ./.github/actions/desktop-cleanup-s3 - with: - channel: nightly - keep-count: '15' - aws-access-key-id: ${{ secrets.UPDATE_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.UPDATE_AWS_SECRET_ACCESS_KEY }} - s3-bucket: ${{ secrets.UPDATE_S3_BUCKET }} - s3-region: ${{ secrets.UPDATE_S3_REGION }} - s3-endpoint: ${{ secrets.UPDATE_S3_ENDPOINT }} diff --git a/apps/desktop/src/main/controllers/UpdaterCtr.ts b/apps/desktop/src/main/controllers/UpdaterCtr.ts index c28e6ebd96..375f4808f8 100644 --- a/apps/desktop/src/main/controllers/UpdaterCtr.ts +++ b/apps/desktop/src/main/controllers/UpdaterCtr.ts @@ -1,5 +1,6 @@ import type { UpdateChannel, UpdaterState } from '@lobechat/electron-client-ipc'; +import { UPDATE_CHANNEL } from '@/modules/updater/configs'; import { createLogger } from '@/utils/logger'; import { ControllerModule, IpcMethod } from './index'; @@ -46,11 +47,11 @@ export default class UpdaterCtr extends ControllerModule { @IpcMethod() async getUpdateChannel(): Promise { - return this.app.storeManager.get('updateChannel') ?? 'stable'; + return this.app.storeManager.get('updateChannel') ?? UPDATE_CHANNEL; } /** - * Get the build-time channel (stable, nightly, canary, beta). + * Get the build-time channel (stable, canary, beta, or legacy nightly). * Used for display in About page to distinguish pre-release builds. */ @IpcMethod() @@ -61,11 +62,12 @@ export default class UpdaterCtr extends ControllerModule { @IpcMethod() async setUpdateChannel(channel: UpdateChannel): Promise { - const validChannels = new Set(['stable', 'nightly', 'canary']); + const validChannels = new Set(['stable', 'canary']); if (!validChannels.has(channel)) { logger.warn(`Invalid update channel: ${channel}, ignoring`); return; } + logger.info(`Set update channel requested: ${channel}`); this.app.storeManager.set('updateChannel', channel); this.app.updaterManager.switchChannel(channel); diff --git a/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts b/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts index 4f29aeea4f..b5c2148b1a 100644 --- a/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +++ b/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts @@ -8,9 +8,14 @@ import UpdaterCtr from '../UpdaterCtr'; vi.mock('@/utils/logger', () => ({ createLogger: () => ({ info: vi.fn(), + warn: vi.fn(), }), })); +vi.mock('@/modules/updater/configs', () => ({ + UPDATE_CHANNEL: 'stable', +})); + const { ipcMainHandleMock } = vi.hoisted(() => ({ ipcMainHandleMock: vi.fn(), })); @@ -26,13 +31,23 @@ const mockCheckForUpdates = vi.fn(); const mockDownloadUpdate = vi.fn(); const mockInstallNow = vi.fn(); const mockInstallLater = vi.fn(); +const mockGetUpdaterState = vi.fn(); +const mockSwitchChannel = vi.fn(); +const mockStoreGet = vi.fn(); +const mockStoreSet = vi.fn(); const mockApp = { + storeManager: { + get: mockStoreGet, + set: mockStoreSet, + }, updaterManager: { checkForUpdates: mockCheckForUpdates, downloadUpdate: mockDownloadUpdate, + getUpdaterState: mockGetUpdaterState, installNow: mockInstallNow, installLater: mockInstallLater, + switchChannel: mockSwitchChannel, }, } as unknown as App; @@ -42,6 +57,8 @@ describe('UpdaterCtr', () => { beforeEach(() => { vi.clearAllMocks(); ipcMainHandleMock.mockClear(); + mockStoreGet.mockReset(); + mockStoreSet.mockReset(); updaterCtr = new UpdaterCtr(mockApp); }); @@ -73,6 +90,36 @@ describe('UpdaterCtr', () => { }); }); + describe('update channel', () => { + it('should return stored update channel', async () => { + mockStoreGet.mockReturnValueOnce('canary'); + + await expect(updaterCtr.getUpdateChannel()).resolves.toBe('canary'); + }); + + it('should return default update channel when store is empty', async () => { + mockStoreGet.mockReturnValueOnce(undefined); + + await expect(updaterCtr.getUpdateChannel()).resolves.toBe('stable'); + }); + + it('should keep canary input unchanged', async () => { + await updaterCtr.setUpdateChannel('canary'); + + expect(mockStoreSet).toHaveBeenCalledWith('updateChannel', 'canary'); + expect(mockSwitchChannel).toHaveBeenCalledWith('canary'); + }); + + it('should ignore invalid legacy input', async () => { + await updaterCtr.setUpdateChannel( + 'nightly' as unknown as Parameters[0], + ); + + expect(mockStoreSet).not.toHaveBeenCalled(); + expect(mockSwitchChannel).not.toHaveBeenCalled(); + }); + }); + // 测试错误处理 describe('error handling', () => { it('should handle errors when checking for updates', async () => { diff --git a/apps/desktop/src/main/core/infrastructure/StoreManager.ts b/apps/desktop/src/main/core/infrastructure/StoreManager.ts index db01805592..799f183f9c 100644 --- a/apps/desktop/src/main/core/infrastructure/StoreManager.ts +++ b/apps/desktop/src/main/core/infrastructure/StoreManager.ts @@ -6,6 +6,7 @@ import { makeSureDirExist } from '@/utils/file-system'; import { createLogger } from '@/utils/logger'; import type { App } from '../App'; +import { runStoreMigrations } from './migration'; // Create logger const logger = createLogger('core:StoreManager'); @@ -27,6 +28,7 @@ export class StoreManager { defaults: STORE_DEFAULTS, name: STORE_NAME, }); + runStoreMigrations(this.store); logger.info('StoreManager initialized with store name:', STORE_NAME); const storagePath = this.store.get('storagePath'); diff --git a/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts b/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts index a5a1b5eb39..d9ed61df2a 100644 --- a/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +++ b/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts @@ -139,9 +139,7 @@ export class UpdaterManager { public switchChannel = (channel: UpdateChannel) => { logger.info(`Switching update channel: ${this.currentChannel} -> ${channel}`); - const isDowngrade = - (this.currentChannel === 'canary' && channel !== 'canary') || - (this.currentChannel === 'nightly' && channel === 'stable'); + const isDowngrade = this.currentChannel === 'canary' && channel === 'stable'; this.currentChannel = channel; autoUpdater.allowDowngrade = isDowngrade; @@ -366,7 +364,7 @@ export class UpdaterManager { /** * Strip trailing channel path from URL so we can re-append the correct channel. - * Handles both base URL (https://cdn.example.com) and legacy URL with channel (https://cdn.example.com/stable) + * Handles both base URL (https://cdn.example.com) and legacy URLs with channel suffixes. */ private getBaseUpdateUrl(): string | undefined { if (!UPDATE_SERVER_URL) return undefined; diff --git a/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts b/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts index 347ddfbeae..cf006d8851 100644 --- a/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +++ b/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { App as AppCore } from '../../App'; +import { APPLIED_STORE_MIGRATIONS_KEY, getStoreMigrations, runStoreMigrations } from '../migration'; import { StoreManager } from '../StoreManager'; // Use vi.hoisted to define mocks before hoisting @@ -46,6 +47,11 @@ vi.mock('@/utils/file-system', () => ({ makeSureDirExist: mockMakeSureDirExist, })); +vi.mock('@/modules/updater/configs', () => ({ + coerceStoredUpdateChannel: (channel?: string | null) => + channel === 'canary' ? 'canary' : 'stable', +})); + // Mock store constants vi.mock('@/const/store', () => ({ STORE_DEFAULTS: { @@ -77,18 +83,52 @@ describe('StoreManager', () => { describe('constructor', () => { it('should create electron-store with correct options', () => { - expect(MockStore).toHaveBeenCalledWith({ - defaults: { - locale: 'auto', - storagePath: '/default/storage/path', - }, - name: 'test-config', - }); + expect(MockStore).toHaveBeenCalledWith( + expect.objectContaining({ + defaults: { + locale: 'auto', + storagePath: '/default/storage/path', + }, + name: 'test-config', + }), + ); }); it('should ensure storage directory exists', () => { expect(mockMakeSureDirExist).toHaveBeenCalledWith('/mock/storage/path'); }); + + it('should migrate legacy nightly channel and record applied migration ids', () => { + const store = { + get: vi.fn((key: string) => { + if (key === APPLIED_STORE_MIGRATIONS_KEY) return undefined; + if (key === 'updateChannel') return 'nightly'; + }), + set: vi.fn(), + } as any; + + runStoreMigrations(store); + + expect(store.set).toHaveBeenCalledWith('updateChannel', 'stable'); + expect(store.set).toHaveBeenCalledWith(APPLIED_STORE_MIGRATIONS_KEY, [ + getStoreMigrations()[0].id, + ]); + }); + + it('should skip already applied migrations', () => { + const appliedMigrationId = getStoreMigrations()[0].id; + const store = { + get: vi.fn((key: string) => { + if (key === APPLIED_STORE_MIGRATIONS_KEY) return [appliedMigrationId]; + if (key === 'updateChannel') return 'nightly'; + }), + set: vi.fn(), + } as any; + + runStoreMigrations(store); + + expect(store.set).not.toHaveBeenCalled(); + }); }); describe('get', () => { diff --git a/apps/desktop/src/main/core/infrastructure/migration/001-normalize-update-channel.ts b/apps/desktop/src/main/core/infrastructure/migration/001-normalize-update-channel.ts new file mode 100644 index 0000000000..4d471d81aa --- /dev/null +++ b/apps/desktop/src/main/core/infrastructure/migration/001-normalize-update-channel.ts @@ -0,0 +1,15 @@ +import { coerceStoredUpdateChannel } from '@/modules/updater/configs'; + +import { defineMigration } from './defineMigration'; + +export default defineMigration({ + id: '001-normalize-update-channel', + up: (store) => { + const storedChannel = store.get('updateChannel'); + const normalizedChannel = coerceStoredUpdateChannel(storedChannel); + + if (storedChannel && storedChannel !== normalizedChannel) { + store.set('updateChannel', normalizedChannel); + } + }, +}); diff --git a/apps/desktop/src/main/core/infrastructure/migration/defineMigration.ts b/apps/desktop/src/main/core/infrastructure/migration/defineMigration.ts new file mode 100644 index 0000000000..719856ab12 --- /dev/null +++ b/apps/desktop/src/main/core/infrastructure/migration/defineMigration.ts @@ -0,0 +1,10 @@ +import type Store from 'electron-store'; + +import type { ElectronMainStore } from '@/types/store'; + +export interface StoreMigration { + id: string; + up: (store: Store) => void; +} + +export const defineMigration = (migration: StoreMigration): StoreMigration => migration; diff --git a/apps/desktop/src/main/core/infrastructure/migration/index.ts b/apps/desktop/src/main/core/infrastructure/migration/index.ts new file mode 100644 index 0000000000..aa3f3808e0 --- /dev/null +++ b/apps/desktop/src/main/core/infrastructure/migration/index.ts @@ -0,0 +1,55 @@ +import type Store from 'electron-store'; + +import type { ElectronMainStore } from '@/types/store'; +import { createLogger } from '@/utils/logger'; + +import normalizeUpdateChannelMigration from './001-normalize-update-channel'; +import type { StoreMigration } from './defineMigration'; + +export const APPLIED_STORE_MIGRATIONS_KEY = 'lobeDesktopAppliedStoreMigrations'; + +const logger = createLogger('core:storeMigration'); + +const migrations: StoreMigration[] = [normalizeUpdateChannelMigration]; + +const getAppliedMigrationIds = (store: Store): string[] => { + return ( + (store.get(APPLIED_STORE_MIGRATIONS_KEY as keyof ElectronMainStore) as string[] | undefined) ?? + [] + ); +}; + +const setAppliedMigrationIds = (store: Store, ids: string[]) => { + store.set( + APPLIED_STORE_MIGRATIONS_KEY as keyof ElectronMainStore, + ids as ElectronMainStore[keyof ElectronMainStore], + ); +}; + +export const getStoreMigrations = () => migrations; + +export const runStoreMigrations = (store: Store) => { + logger.info('Store migrations started'); + + const appliedMigrationIds = new Set(getAppliedMigrationIds(store)); + let hasNewMigrationApplied = false; + + for (const migration of migrations) { + if (appliedMigrationIds.has(migration.id)) continue; + + logger.info(`Running store migration: ${migration.id}`); + migration.up(store); + appliedMigrationIds.add(migration.id); + hasNewMigrationApplied = true; + } + + if (hasNewMigrationApplied) { + setAppliedMigrationIds(store, [...appliedMigrationIds]); + } + + logger.info( + hasNewMigrationApplied + ? 'Store migrations finished (updates applied)' + : 'Store migrations finished (nothing pending)', + ); +}; diff --git a/apps/desktop/src/main/modules/updater/configs.ts b/apps/desktop/src/main/modules/updater/configs.ts index 51ecd56ae0..c086ec38a7 100644 --- a/apps/desktop/src/main/modules/updater/configs.ts +++ b/apps/desktop/src/main/modules/updater/configs.ts @@ -5,14 +5,13 @@ import { getDesktopEnv } from '@/env'; // Build-time default channel, can be overridden at runtime via store const rawChannel = getDesktopEnv().UPDATE_CHANNEL || 'stable'; -const VALID_CHANNELS = new Set(['stable', 'nightly', 'canary']); -/** Raw build channel for display (stable, nightly, canary, beta) */ +export const coerceStoredUpdateChannel = (channel?: string | null): UpdateChannel => + channel === 'canary' ? 'canary' : 'stable'; + +/** Raw build channel for display (stable, canary, beta, or legacy nightly). */ export const BUILD_CHANNEL: string = rawChannel; -export const UPDATE_CHANNEL: UpdateChannel = VALID_CHANNELS.has(rawChannel as UpdateChannel) - ? (rawChannel as UpdateChannel) - : rawChannel === 'beta' - ? 'nightly' - : 'stable'; +export const UPDATE_CHANNEL: UpdateChannel = + rawChannel === 'canary' || rawChannel === 'beta' ? 'canary' : 'stable'; // S3 base URL for all channels // e.g., https://releases.lobehub.com diff --git a/packages/database/src/models/__tests__/messages/message.update.test.ts b/packages/database/src/models/__tests__/messages/message.update.test.ts index dc121a815b..6e8895932a 100644 --- a/packages/database/src/models/__tests__/messages/message.update.test.ts +++ b/packages/database/src/models/__tests__/messages/message.update.test.ts @@ -990,7 +990,6 @@ describe('MessageModel Update Tests', () => { role: 'tool', content: 'search result', parentId: 'assistant-msg-1', - tool_call_id: 'tool-call-1', }); // Create plugin record @@ -1058,7 +1057,6 @@ describe('MessageModel Update Tests', () => { role: 'tool', content: 'search result', parentId: 'assistant-msg-2', - tool_call_id: 'tool-call-search', }, { id: 'tool-msg-calc', @@ -1066,7 +1064,6 @@ describe('MessageModel Update Tests', () => { role: 'tool', content: 'calc result', parentId: 'assistant-msg-2', - tool_call_id: 'tool-call-calc', }, ]); @@ -1175,7 +1172,6 @@ describe('MessageModel Update Tests', () => { role: 'tool' as const, content: '', parentId: `perf-assistant-${i}`, - tool_call_id: `perf-tool-call-${i}`, })); await serverDB.insert(messages).values(toolMessagesData); @@ -1382,7 +1378,6 @@ describe('MessageModel Update Tests', () => { role: 'tool', content: 'tool result', parentId: 'assistant-no-tools', - tool_call_id: 'orphan-tool-call', }); // Create plugin record diff --git a/packages/electron-client-ipc/src/types/update.ts b/packages/electron-client-ipc/src/types/update.ts index 74fb83e1d9..3f75a8ba0d 100644 --- a/packages/electron-client-ipc/src/types/update.ts +++ b/packages/electron-client-ipc/src/types/update.ts @@ -1,4 +1,4 @@ -export type UpdateChannel = 'stable' | 'nightly' | 'canary'; +export type UpdateChannel = 'stable' | 'canary'; export interface ReleaseNoteInfo { /** diff --git a/src/locales/default/setting.ts b/src/locales/default/setting.ts index 3dc702de4e..c2ed93a9db 100644 --- a/src/locales/default/setting.ts +++ b/src/locales/default/setting.ts @@ -910,7 +910,7 @@ When I am ___, I need ___ 'tab.advanced.updateChannel.canaryDesc': 'Triggered on every PR merge, multiple builds per day. Most unstable.', 'tab.advanced.updateChannel.desc': - 'By default, get notifications for stable updates. Nightly and Canary channels receive pre-release builds that may be unstable for production work.', + 'By default, get notifications for stable updates. The Canary channel receives pre-release builds that may be unstable for production work.', 'tab.advanced.updateChannel.nightly': 'Nightly', 'tab.advanced.updateChannel.nightlyDesc': 'Automated daily builds with the latest changes.', 'tab.advanced.updateChannel.stable': 'Stable', diff --git a/src/routes/(main)/settings/advanced/index.tsx b/src/routes/(main)/settings/advanced/index.tsx index 8c99b9417d..06933996cd 100644 --- a/src/routes/(main)/settings/advanced/index.tsx +++ b/src/routes/(main)/settings/advanced/index.tsx @@ -16,7 +16,7 @@ import { autoUpdateService } from '@/services/electron/autoUpdate'; import { useUserStore } from '@/store/user'; import { labPreferSelectors, preferenceSelectors, settingsSelectors } from '@/store/user/selectors'; -type UpdateChannelValue = 'canary' | 'nightly' | 'stable'; +type UpdateChannelValue = 'canary' | 'stable'; const styles = createStaticStyles(({ css }) => ({ labItem: css` @@ -74,7 +74,6 @@ const Page = memo(() => { const channelOptions = [ { label: t('tab.advanced.updateChannel.stable'), value: 'stable' as const }, - { label: t('tab.advanced.updateChannel.nightly'), value: 'nightly' as const }, { label: t('tab.advanced.updateChannel.canary'), value: 'canary' as const }, ];