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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions .github/actions/setup-desktop-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ inputs:
description: pnpm version.
required: false
default: 10.28.2
tauri-cli-version:
description: tauri-cli version.
required: false
default: 2.10.0

runs:
using: composite
steps:
Expand All @@ -42,16 +37,6 @@ runs:
with:
toolchain: ${{ inputs.rust-toolchain }}

- name: Install Tauri CLI (non-Windows)
if: runner.os != 'Windows'
shell: bash
run: cargo install tauri-cli --version "${{ inputs.tauri-cli-version }}" --locked

- name: Install Tauri CLI (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: cargo install tauri-cli --version "${{ inputs.tauri-cli-version }}" --locked

- name: Install dependencies (non-Windows)
if: runner.os != 'Windows'
shell: bash
Expand Down
194 changes: 189 additions & 5 deletions .github/workflows/build-desktop-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ env:
ASTRBOT_NIGHTLY_SCHEDULE_CRON: ${{ vars.ASTRBOT_NIGHTLY_SCHEDULE_CRON || '7 3 * * *' }}
ASTRBOT_NIGHTLY_UTC_HOUR: ${{ vars.ASTRBOT_NIGHTLY_UTC_HOUR || '3' }}
ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY: ${{ vars.ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY || '' }}
R2_ACCOUNT_ID: ${{ vars.R2_ACCOUNT_ID || '' }}
R2_BUCKET: ${{ vars.R2_BUCKET || 'astrbot-desktop-releases' }}
R2_PUBLIC_BASE_URL: ${{ vars.R2_PUBLIC_BASE_URL || 'https://releases.astrbot.app' }}

jobs:
resolve_build_context:
Expand Down Expand Up @@ -213,8 +216,28 @@ jobs:
shell: bash
run: |
set -euo pipefail
echo "Building Linux release bundles (deb and rpm only)."
cargo tauri build --bundles deb,rpm
echo "Building Linux release bundles (deb, rpm, and AppImage)."
pnpm exec tauri build --bundles deb,rpm,appimage

- name: Verify Linux AppImage updater artifacts
shell: bash
run: |
set -euo pipefail
appimage_dir="src-tauri/target/release/bundle/appimage"
shopt -s nullglob
appimages=("${appimage_dir}"/*.AppImage)
if [ "${#appimages[@]}" -ne 1 ]; then
echo "Expected exactly one AppImage in ${appimage_dir}, found ${#appimages[@]}." >&2
ls -la "${appimage_dir}" || true
exit 1
fi
updater_signature="${appimages[0]}.sig"
if [ ! -s "${updater_signature}" ]; then
echo "Expected a non-empty AppImage updater signature: ${updater_signature}" >&2
ls -la "${appimage_dir}" || true
exit 1
fi
echo "Verified ${appimages[0]} and ${updater_signature}"

- name: Smoke test backend startup (Linux)
shell: bash
Expand All @@ -230,6 +253,8 @@ jobs:
path: |
src-tauri/target/release/bundle/**/*.deb
src-tauri/target/release/bundle/**/*.rpm
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig

build-macos:
needs:
Expand Down Expand Up @@ -387,7 +412,7 @@ jobs:
retry_sleep_seconds="${ASTRBOT_MACOS_BUILD_RETRY_SLEEP_SECONDS}"
# Resources are already prepared and, when available, pre-signed in earlier steps.
tauri_config_override='{"build":{"beforeBuildCommand":""}}'
# Retry only for known transient cargo/crates network failures.
# Retry known transient cargo/crates network failures.
# Spurious retry hints emitted by cargo.
transient_retry_spurious='spurious network error|network failure seems to have happened'
# HTTP-layer transient fetch failures (rate limits and 5xx responses).
Expand All @@ -411,7 +436,7 @@ jobs:

for attempt in $(seq 1 "${max_attempts}"); do
build_log="$(mktemp -t tauri-macos-build.XXXXXX.log)"
if cargo tauri build --verbose --target ${{ matrix.target }} --bundles app --config "${tauri_config_override}" 2>&1 | tee "${build_log}"; then
if pnpm exec tauri build --verbose --target ${{ matrix.target }} --bundles app --config "${tauri_config_override}" 2>&1 | tee "${build_log}"; then
rm -f "${build_log}" || true
break
fi
Expand Down Expand Up @@ -439,7 +464,7 @@ jobs:
set -euo pipefail
node scripts/ci/backend-smoke-test.mjs --label "macos-${{ matrix.arch }}"

- name: Collect macOS updater artifacts
- name: Collect macOS release artifacts
env:
ASTRBOT_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }}
RESOLVED_APP_BUNDLE_NAME: ${{ steps.resolve_macos_app_bundle.outputs.app_bundle_name }}
Expand Down Expand Up @@ -495,6 +520,31 @@ jobs:
cp "${updater_signature}" "${release_dir}/${release_base}.sig"
echo "Collected ${release_dir}/${release_base}"

app_bundle="${bundle_dir}/${app_bundle_name}.app"
if [ ! -d "${app_bundle}" ]; then
echo "Expected signed macOS app bundle not found: ${app_bundle}" >&2
ls -la "${bundle_dir}" || true
exit 1
fi

# create-dmg mounts and mutates a writable image, which is prone to
# DiskArbitration detach timeouts on hosted Intel runners. Building
# directly from a prepared source folder produces the same drag-to-
# Applications layout without an explicit attach/detach cycle.
dmg_staging_dir="$(mktemp -d "${RUNNER_TEMP}/astrbot-dmg-${{ matrix.arch }}.XXXXXX")"
ditto "${app_bundle}" "${dmg_staging_dir}/${app_bundle_name}.app"
ln -s /Applications "${dmg_staging_dir}/Applications"
dmg_release_name="AstrBot_${ASTRBOT_VERSION}_macos_${{ matrix.arch }}.dmg"
dmg_release_path="${release_dir}/${dmg_release_name}"
hdiutil create \
-volname "${app_bundle_name}" \
-srcfolder "${dmg_staging_dir}" \
-ov \
-format UDZO \
"${dmg_release_path}"
hdiutil verify "${dmg_release_path}"
echo "Collected ${release_dir}/${dmg_release_name}"

- name: Upload artifacts
uses: actions/upload-artifact@v7.0.1
with:
Expand All @@ -503,6 +553,7 @@ jobs:
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/release-artifacts/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/release-artifacts/*.app.tar.gz.sig
src-tauri/target/${{ matrix.target }}/release/bundle/release-artifacts/*.dmg

build-windows:
needs:
Expand Down Expand Up @@ -644,11 +695,14 @@ jobs:
python3 scripts/ci/validate-release-artifacts.py release-artifacts

- name: Generate Tauri updater manifest
id: updater_manifest
if: ${{ needs.resolve_build_context.outputs.build_mode != 'custom' }}
env:
RELEASE_TAG: ${{ needs.resolve_build_context.outputs.release_tag }}
RELEASE_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }}
BUILD_MODE: ${{ needs.resolve_build_context.outputs.build_mode }}
R2_PUBLIC_BASE_URL: ${{ env.R2_PUBLIC_BASE_URL }}
R2_RELEASE_ID: ${{ github.run_id }}-${{ github.run_attempt }}
shell: bash
run: |
set -euo pipefail
Expand All @@ -666,8 +720,94 @@ jobs:
--tag "${RELEASE_TAG}" \
--version "${RELEASE_VERSION}" \
--channel "${manifest_channel}" \
--asset-base-url "${R2_PUBLIC_BASE_URL%/}/desktop/releases/${RELEASE_VERSION}/${R2_RELEASE_ID}" \
--output "${manifest_output}"

printf 'channel=%s\n' "${manifest_channel}" >> "${GITHUB_OUTPUT}"
printf 'path=%s\n' "${manifest_output}" >> "${GITHUB_OUTPUT}"

- name: Upload immutable release objects to Cloudflare R2
if: ${{ needs.resolve_build_context.outputs.build_mode != 'custom' }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
RELEASE_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }}
R2_RELEASE_ID: ${{ github.run_id }}-${{ github.run_attempt }}
R2_ACCOUNT_ID: ${{ env.R2_ACCOUNT_ID }}
R2_BUCKET: ${{ env.R2_BUCKET }}
UPDATER_CHANNEL: ${{ steps.updater_manifest.outputs.channel }}
UPDATER_MANIFEST: ${{ steps.updater_manifest.outputs.path }}
shell: bash
run: |
set -euo pipefail

if [ -z "${AWS_ACCESS_KEY_ID}" ] || [ -z "${AWS_SECRET_ACCESS_KEY}" ]; then
echo "::error::R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY repository secrets are required."
exit 1
fi
if [ -z "${R2_ACCOUNT_ID}" ]; then
echo "::error::R2_ACCOUNT_ID repository variable is required."
exit 1
fi

aws --version
python3 -m scripts.ci.publish_r2_release \
--artifacts-root release-artifacts \
--manifest "${UPDATER_MANIFEST}" \
--version "${RELEASE_VERSION}" \
--release-id "${R2_RELEASE_ID}" \
--channel "${UPDATER_CHANNEL}" \
--phase artifacts \
--bucket "${R2_BUCKET}" \
--endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"

- name: Verify immutable R2 updater objects on public origin
if: ${{ needs.resolve_build_context.outputs.build_mode != 'custom' }}
env:
R2_PUBLIC_BASE_URL: ${{ env.R2_PUBLIC_BASE_URL }}
UPDATER_MANIFEST: ${{ steps.updater_manifest.outputs.path }}
shell: bash
run: |
set -euo pipefail

updater_urls_file="${RUNNER_TEMP}/r2-updater-urls.txt"
python3 - "${UPDATER_MANIFEST}" "${R2_PUBLIC_BASE_URL%/}" > "${updater_urls_file}" <<'PY'
import json
import sys
from urllib.parse import urlsplit

manifest_path, expected_origin = sys.argv[1:]
with open(manifest_path, encoding="utf-8") as file:
manifest = json.load(file)

expected = urlsplit(expected_origin)
if expected.scheme != "https" or not expected.netloc:
raise SystemExit(f"Invalid R2 public origin: {expected_origin!r}")

urls = sorted(
platform.get("url", "")
for platform in manifest.get("platforms", {}).values()
)
if not urls or any(not url for url in urls):
raise SystemExit("Updater manifest has no complete platform URLs")

for url in urls:
parsed = urlsplit(url)
if (parsed.scheme, parsed.netloc) != (expected.scheme, expected.netloc):
raise SystemExit(
f"Updater artifact URL is outside the configured R2 origin: {url!r}"
)
print(url)
PY

while IFS= read -r updater_url; do
echo "Waiting for public R2 object: ${updater_url}"
curl --fail --silent --show-error --location --head \
--retry 20 --retry-delay 10 --retry-max-time 300 --retry-all-errors \
"${updater_url}" >/dev/null
done < "${updater_urls_file}"

- name: Remove existing assets from target release
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -694,6 +834,50 @@ jobs:
files: release-artifacts/**/*
fail_on_unmatched_files: true

- name: Promote Cloudflare R2 updater channel
if: ${{ needs.resolve_build_context.outputs.build_mode != 'custom' }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
RELEASE_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }}
R2_RELEASE_ID: ${{ github.run_id }}-${{ github.run_attempt }}
R2_ACCOUNT_ID: ${{ env.R2_ACCOUNT_ID }}
R2_BUCKET: ${{ env.R2_BUCKET }}
R2_PUBLIC_BASE_URL: ${{ env.R2_PUBLIC_BASE_URL }}
UPDATER_CHANNEL: ${{ steps.updater_manifest.outputs.channel }}
UPDATER_MANIFEST: ${{ steps.updater_manifest.outputs.path }}
shell: bash
run: |
set -euo pipefail

python3 -m scripts.ci.publish_r2_release \
--artifacts-root release-artifacts \
--manifest "${UPDATER_MANIFEST}" \
--version "${RELEASE_VERSION}" \
--release-id "${R2_RELEASE_ID}" \
--channel "${UPDATER_CHANNEL}" \
--phase channel \
--bucket "${R2_BUCKET}" \
--endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"

public_manifest_url="${R2_PUBLIC_BASE_URL%/}/desktop/channels/${UPDATER_CHANNEL}/latest.json"
curl --fail --silent --show-error \
--retry 20 --retry-delay 10 --retry-max-time 300 --retry-all-errors \
"${public_manifest_url}?version=${RELEASE_VERSION}" \
--output published-latest.json
python3 - "${RELEASE_VERSION}" <<'PY'
import json
import sys

with open("published-latest.json", encoding="utf-8") as file:
published = json.load(file)
if published.get("version") != sys.argv[1]:
raise SystemExit(
f"Published updater version mismatch: {published.get('version')!r}"
)
PY

- name: Demote previous prerelease marker
if: ${{ needs.resolve_build_context.outputs.release_prerelease == 'true' && needs.resolve_build_context.outputs.build_mode == 'nightly' }}
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.DS_Store
.claude/
.pnpm-store/
.wrangler/

# dependencies/cache
node_modules/
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ If you only want to use the app and do not need to build it locally, download th
- [Stable](https://github.com/AstrBotDevs/AstrBot-desktop/releases/latest): recommended for most users.
- [Nightly](https://github.com/AstrBotDevs/AstrBot-desktop/releases/tag/nightly): automatically built from newer upstream changes for early access to fixes and features.
- Choose the package that matches your operating system and CPU architecture.
- On macOS, download the `.dmg`, open it, and drag AstrBot into Applications.
- On Linux, use the AppImage for in-app self-updates; `deb` and `rpm` remain system-package installs with manual update prompts.

<!-- section: data-and-configuration-location -->
## Data and Configuration Location
Expand Down
2 changes: 2 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ AstrBot Desktop 是面向本地桌面使用的 AstrBot 打包发行版。它内
- [Stable](https://github.com/AstrBotDevs/AstrBot-desktop/releases/latest):适合大多数用户日常使用。
- [Nightly](https://github.com/AstrBotDevs/AstrBot-desktop/releases/tag/nightly):基于较新的上游改动自动构建,适合提前体验新功能或修复。
- 下载时请按操作系统与 CPU 架构选择对应安装包。
- macOS 请下载 `.dmg`,打开后将 AstrBot 拖入 Applications(应用程序)目录。
- Linux 如需应用内热更新请使用 AppImage;`deb` / `rpm` 仍由系统包方式安装,更新时会提示手动下载。

<!-- section: data-and-configuration-location -->
## 数据与配置位置
Expand Down
6 changes: 5 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ beforeBuildCommand = pnpm run prepare:resources

- 定时构建(`schedule`)检测到上游新 tag 时,会先自动同步版本文件并提交,再继续构建。
- 手动触发(`workflow_dispatch`)默认只构建,不自动回写版本文件。
- 发布与 updater 相关行为依赖 `src-tauri/tauri.conf.json`、GitHub Actions workflow 以及资源准备脚本共同完成。
- Desktop 使用独立 updater manifest,不复用 AstrBot Core 的 `api.soulter.top/releases` 更新索引。
- stable/nightly 的更新入口分别为 `https://releases.astrbot.app/desktop/channels/stable/latest.json` 和 `https://releases.astrbot.app/desktop/channels/nightly/latest.json`。
- GitHub Actions 会把完整安装包继续发布到 GitHub Releases,同时将 updater 产物上传到 Cloudflare R2:先上传并校验不可变的版本目录,再更新 GitHub Release,最后原子提升通道 manifest。
- R2 bucket、repository variables/secrets 和对象目录约定见 [`docs/environment-variables.md`](./environment-variables.md#4-发布ci-github-actions)。
- 发布与 updater 相关行为依赖 `src-tauri/tauri.conf.json`、GitHub Actions workflow 以及 `scripts/ci/generate_tauri_latest_json.py`、`scripts/ci/publish_r2_release.py` 共同完成。

## 8. 相关文档

Expand Down
15 changes: 15 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@ Windows 对应路径通常为 `C:\Users\<用户名>\.astrbot\data\cmd_config.jso
| `ASTRBOT_DESKTOP_UPDATER_PUBLIC_KEY` | updater 公钥透传到构建步骤 | 默认空;当前由 `.github/workflows/build-desktop-tauri.yml` 传递,Rust 运行时不直接解析 |
| `ASTRBOT_DESKTOP_TARGET_ARCH` | 透传矩阵目标架构给资源准备脚本 | 默认空;Windows workflow 当前会传 `matrix.arch`,避免在 WOA 上误用仿真层 Node 的 `process.arch` |
| `ASTRBOT_DESKTOP_WINDOWS_ARM_BACKEND_ARCH` | 透传 Windows ARM64 backend runtime 架构覆盖配置到构建步骤 | 默认空;具体取值与默认行为见第 2 节 |
| `R2_ACCOUNT_ID` | Cloudflare 账户 ID,用于拼接 R2 S3 endpoint | GitHub Actions repository variable;发布 stable/nightly 时必填 |
| `R2_BUCKET` | Desktop 发布对象所在的 R2 bucket | GitHub Actions repository variable;默认 `astrbot-desktop-releases` |
| `R2_PUBLIC_BASE_URL` | manifest 和 updater 产物的公网基地址 | GitHub Actions repository variable;默认 `https://releases.astrbot.app` |
| `R2_ACCESS_KEY_ID` | R2 S3 API Access Key ID | GitHub Actions repository secret;仅授予发布 bucket 的 Object Read & Write |
| `R2_SECRET_ACCESS_KEY` | R2 S3 API Secret Access Key | GitHub Actions repository secret;禁止写入仓库、日志或构建产物 |

R2 发布目录约定:

```text
desktop/releases/<version>/<github-run-id>-<run-attempt>/<artifact>
desktop/releases/<version>/<github-run-id>-<run-attempt>/latest-<channel>.json
desktop/channels/<channel>/latest.json
```

`desktop/releases/` 下只保存原生 updater 使用的安装包、签名与版本 manifest,对象不可变并使用长期缓存;每次 workflow attempt 使用独立目录,安全支持失败重试。`desktop/channels/` 下仅保留通道指针并使用 `no-store`。CI 先上传并校验不可变对象,再发布 GitHub Release,最后替换通道 manifest,避免客户端观察到尚未上传完整的版本。

## 5. 维护约定

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
"prepare:webui": "node scripts/prepare-resources.mjs webui",
"prepare:backend": "node scripts/prepare-resources.mjs backend",
"prepare:resources": "pnpm run prepare:webui && pnpm run prepare:backend",
"dev": "cargo tauri dev",
"build": "cargo tauri build"
"dev": "tauri dev",
"build": "tauri build"
},
"devDependencies": {
"@tauri-apps/cli": "2.10.0",
"yaml": "^2.8.1"
}
}
Loading
Loading