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
21 changes: 15 additions & 6 deletions .github/workflows/android-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,7 @@ jobs:
working-directory: openless-all/app
env:
OPENLESS_UPDATE_APK_DIR: ${{ steps.stage.outputs.out_dir }}
OPENLESS_UPDATE_REPO: Open-Less/openless
OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com
OPENLESS_UPDATE_CDN_BASE_URL: https://dl.aeoform.com
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
OPENLESS_RELEASE_TAG: ${{ github.ref_name }}
run: |
Expand All @@ -526,15 +525,25 @@ jobs:
node scripts/write-updater-manifest.mjs
done

- name: Append release files (manifests + signatures)
# The manifests are not release assets any more (the in-app updater reads them from the
# project CDN), but they must still leave the run so the CDN sync can publish them.
- name: Upload Android updater manifests
uses: actions/upload-artifact@v4
with:
name: openless-android-updater-manifests
if-no-files-found: error
path: |
${{ steps.stage.outputs.out_dir }}/latest-android-*.json

- name: Append release files (signatures + aliases)
id: release_assets
shell: bash
run: |
set -euo pipefail
out_dir="${{ steps.stage.outputs.out_dir }}"
{
echo "${{ steps.stage.outputs.release_files }}"
for f in "$out_dir"/*.apk.sig "$out_dir"/latest-android-*.json "$out_dir"/OpenLess_android_*.apk; do
for f in "$out_dir"/*.apk.sig "$out_dir"/OpenLess_android_*.apk; do
[ -e "$f" ] && echo "$f"
done
} > "$RUNNER_TEMP/android-release-files.txt"
Expand All @@ -555,8 +564,8 @@ jobs:

### Android 应用内更新

- Stable 用户检查 `latest-android-aarch64.json`(及 mirror 变体)。
- Beta 用户(设置 → 高级 → 加入 Beta)检查带 `-beta` 后缀的 manifest。
- 清单由项目 CDN 提供:Stable 读 `/updates/stable/latest-android-<arch>.json`,Beta 读 `/updates/beta/latest-android-<arch>-beta.json`。
- 应用内会自动检查并拉起系统安装器,无需手动下载清单。
EOF

- name: Attach Android assets to GitHub Release
Expand Down
21 changes: 11 additions & 10 deletions .github/workflows/release-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -531,15 +531,11 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
OPENLESS_UPDATE_TARGET: ${{ matrix.updater-target }}
OPENLESS_UPDATE_ARCH: ${{ matrix.updater-arch }}
OPENLESS_UPDATE_REPO: ${{ github.repository }}
OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com
OPENLESS_UPDATE_CDN_BASE_URL: https://dl.aeoform.com
# On the beta channel output latest-{tgt}-{arch}-beta.json; stable keeps the old filename.
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
# On the beta channel the script must write manifest.url as releases/download/<tag>/...
# rather than releases/latest (the latter is always = Stable, so beta users downloading
# via url would fetch the wrong file).
# On workflow_dispatch github.ref_name is not a tag — but then channel=stable and the
# script does not read this field, so it is safe.
# Manifest URLs use the CDN's tag-scoped `releases/<tag>/<asset>` path. Stable
# workflow validation without a release tag falls back to the CDN `latest` path.
OPENLESS_RELEASE_TAG: ${{ env.OPENLESS_RELEASE_TAG }}
run: node scripts/write-updater-manifest.mjs

Expand Down Expand Up @@ -704,8 +700,9 @@ jobs:
# The administrator explicitly publishes the shared release afterwards.
draft: true
# Beta releases must be marked prerelease=true: GitHub UI collapses them and regular
# users do not see them; only latest-*-beta.json is uploaded, so stable users'
# endpoint (latest-*.json) is never overwritten and beta never leaks into stable.
# users do not see them. Channel isolation for the updater is a CDN URL path
# (`/updates/stable/` vs `/updates/beta/`, see src-tauri/src/update_source.rs), not a
# filename suffix in these assets.
prerelease: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
# Non-leader jobs have OPENLESS_RELEASE_BODY_PATH as an empty string; softprops keeps
# the existing content via the `body || existing.body` branch when body is empty.
Expand All @@ -715,6 +712,11 @@ jobs:
body_path: ${{ env.OPENLESS_RELEASE_BODY_PATH }}
# generate_release_notes also runs only on the leader, avoiding duplicates from matrix jobs.
generate_release_notes: ${{ matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' }}
# `latest-*.json` is deliberately NOT attached: the in-app updater reads its manifests
# from the project CDN (`/updates/{stable,beta}/`, see src-tauri/src/update_source.rs),
# and cloning them here added fourteen near-identical files to every release. The
# manifests still leave the workflow through the `*-updater` artifacts above, which is
# where the CDN sync job picks them up.
files: |
openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz
Expand All @@ -723,7 +725,6 @@ jobs:
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig
openless-all/app/src-tauri/target/release/bundle/msi/*.msi
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
openless-all/app/src-tauri/target/release/bundle/latest-*.json
openless-all/app/src-tauri/target/release/downloads/*

# ── Hand the finished tag to the platform workflows that ride the same Release ──
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ Go to [Releases](https://github.com/Open-Less/openless/releases) and download:
- **Older 32-bit ARM**: `OpenLess_<version>_armeabi-v7a.apk`
- **64-bit emulator**: `OpenLess_<version>_x86_64.apk`
- **32-bit emulator**: `OpenLess_<version>_x86.apk`
- In-app updates (Settings → About) use `latest-android-{arch}.json` manifests; Beta users join Beta in Advanced settings.
- In-app updates (Settings → About) read `https://dl.aeoform.com/updates/stable/latest-android-{arch}.json`; Beta users join Beta in Advanced settings and read the `.../updates/beta/latest-android-{arch}-beta.json` counterpart.
- Debug smoke builds: `OpenLess-android-debug-{abi}-*.apk` from workflow_dispatch artifacts.
- If unsure, run `adb shell getprop ro.product.cpu.abi` and pick the matching APK.
- **Linux**: the Tauri/WebView build has been retired. The native `eframe` host uses the shared Core 2.0 services. After CI and real Ubuntu audio, input, install, upgrade and rollback acceptance, the shared release includes deb/rpm packages and `SHA256SUMS`. Linux requires glibc ≥ 2.39 (Ubuntu 24.04 or a comparable distribution); there is no AppImage or in-app updater.
Expand Down Expand Up @@ -330,7 +330,16 @@ Rules of thumb:
- **Beta work must not leak to Stable.** `main` receives merges only from `beta`, performed by maintainers after a successful two-platform smoke build. No direct pushes to `main`.
- **Stable releases are cut from `main`** by pushing a `v<version>-tauri` tag — see the maintainer release checklist below.

Beta release distribution (manual-download opt-in): the in-app updater always reads the Stable manifest, so regular users never receive Beta builds via auto-update. Users who want to try Beta open **Settings → About**, enable "Join Beta channel", and download the latest Beta installer manually from the link the app fetches from GitHub. Tag convention: `v<version>-beta-tauri` produces the Beta release (marked as a GitHub pre-release; manifest written as `latest-{tgt}-{arch}-beta.json`), while `v<version>-tauri` produces the Stable release. The two manifest files never overlap, so Stable users' updater feed cannot pick up Beta releases.
Beta release distribution (manual-download opt-in): the in-app updater's Stable channel only ever reads the Stable manifest path, so regular users never receive Beta builds via auto-update. Users who want to try Beta open **Settings → About**, enable "Join Beta channel", and download the latest Beta installer manually from the link the app fetches from GitHub. Tag convention: `v<version>-beta-tauri` produces the Beta release (marked as a GitHub pre-release), while `v<version>-tauri` produces the Stable release.

Updater manifests are served from the project CDN, one rolling file per platform and channel:

```text
https://dl.aeoform.com/updates/stable/latest-{target}-{arch}.json
https://dl.aeoform.com/updates/beta/latest-{target}-{arch}-beta.json
```

The channel split is the URL path (`/updates/stable/` vs `/updates/beta/`), so a Stable client cannot reach a Beta manifest even if it asked for the wrong URL. Artifact URLs inside the manifest point at the same CDN (`https://dl.aeoform.com/releases/{tag}/{asset}`), which is what mainland-China users download from. The manifests are generated by the release workflows as run artifacts and are **not** attached to the GitHub Release, so maintainers must upload them to the CDN after a build — see `RELEASING.md`. Clients built before this switch read the old `releases/latest/download/latest-*.json` assets and therefore cannot auto-update past the last release that still carries them; they need one manual download.

## Credentials

Expand Down
13 changes: 11 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ OpenLess 只做一件事:**把语音变成可用的书面文字(尤其是 AI 提
- **旧款 32 位 ARM**:`OpenLess_<version>_armeabi-v7a.apk`
- **64 位模拟器**:`OpenLess_<version>_x86_64.apk`
- **32 位模拟器**:`OpenLess_<version>_x86.apk`
- 应用内更新(设置 → 关于)读取 `latest-android-{arch}.json`;Beta 用户在高级设置加入 Beta 渠道。
- 应用内更新(设置 → 关于)读取 `https://dl.aeoform.com/updates/stable/latest-android-{arch}.json`;Beta 用户在高级设置加入 Beta 渠道,读取 `.../updates/beta/latest-android-{arch}-beta.json`。
- 调试包:`OpenLess-android-debug-{abi}-*.apk`(workflow_dispatch 产物)。
- 不确定时执行 `adb shell getprop ro.product.cpu.abi`,下载对应 ABI 的包。
- **Linux**:原 Tauri/WebView 构建已退出 Linux 路线。基于共享 Core 2.0 服务的原生 `eframe` 宿主经 CI 和 Ubuntu 真实音频、输入、安装、升级、回滚验收后,随共用 Release 提供 deb/rpm 和 `SHA256SUMS`。要求 glibc ≥ 2.39(Ubuntu 24.04 或同等级发行版);没有 AppImage 或应用内更新。
Expand Down Expand Up @@ -337,7 +337,16 @@ your fork / topic branch
- **Beta 的工作不得泄漏到 Stable。** `main` 只接受来自 `beta` 的合并,由维护者在一次成功的双平台冒烟构建之后执行。不允许直接向 `main` 推送。
- **Stable 发布从 `main` 切出**,通过推送 `v<version>-tauri` 标签完成——见下方的维护者发布清单。

Beta 发布的分发(手动下载、可选加入):应用内的更新器始终读取 Stable 清单,因此普通用户绝不会通过自动更新拿到 Beta 构建。想尝试 Beta 的用户打开 **设置 → 关于**,启用“加入 Beta 频道”,再从应用从 GitHub 获取的链接处手动下载最新的 Beta 安装包。标签约定:`v<version>-beta-tauri` 产出 Beta 发布(标记为 GitHub pre-release;清单写为 `latest-{tgt}-{arch}-beta.json`),而 `v<version>-tauri` 产出 Stable 发布。两份清单文件互不重叠,因此 Stable 用户的更新器源不会拾取到 Beta 发布。
Beta 发布的分发(手动下载、可选加入):应用内更新器的 Stable 渠道只读 Stable 清单路径,因此普通用户绝不会通过自动更新拿到 Beta 构建。想尝试 Beta 的用户打开 **设置 → 关于**,启用“加入 Beta 频道”,再从应用从 GitHub 获取的链接处手动下载最新的 Beta 安装包。标签约定:`v<version>-beta-tauri` 产出 Beta 发布(标记为 GitHub pre-release),而 `v<version>-tauri` 产出 Stable 发布。

更新清单由项目 CDN 提供,每个平台/渠道一个滚动文件:

```text
https://dl.aeoform.com/updates/stable/latest-{target}-{arch}.json
https://dl.aeoform.com/updates/beta/latest-{target}-{arch}-beta.json
```

渠道隔离就是 URL 路径(`/updates/stable/` vs `/updates/beta/`),Stable 客户端即使请求错地址也拿不到 Beta 清单。清单里的下载地址同样指向 CDN(`https://dl.aeoform.com/releases/{tag}/{asset}`),国内用户直接从这里下载。清单由发布 workflow 生成为运行产物(run artifact),**不再挂到 GitHub Release**;因此发版后需要维护者手动把清单上传到 CDN,见 `RELEASING.md`。改动前装的客户端读的是旧的 `releases/latest/download/latest-*.json`,因此无法自动更新到最后一个还带这些文件的版本之后,需要手动下载一次。

## 凭据

Expand Down
31 changes: 28 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,34 @@ The script takes a plain `X.Y.Z`; for a prerelease version such as
4. The applicable [desktop feature and device acceptance](docs/2.0-desktop-acceptance.md), signing, and distribution requirements are met; green builds alone do not establish product readiness. Linux acceptance below is required before including Linux assets.
5. Then, and only then, push the release tag.
6. Both Beta and Stable tag workflows upload Tauri and Android assets to a shared
draft. Wait for **both** workflows to succeed, verify the packages and channel
updater manifests, then publish that draft (Beta as a prerelease; Stable as latest).
draft. Wait for **both** workflows to succeed, verify the packages, then publish that
draft (Beta as a prerelease; Stable as latest).
Do not rerun an asset workflow after publication without first returning the release to draft.

The workflows also write the channel updater manifests (`latest-{target}-{arch}.json` and
`latest-{target}-{arch}-beta.json`) but deliberately keep them **out** of the release assets —
current clients read the CDN, not the release. Download them from the run and publish them to
the CDN as part of publishing the release:

| platform | workflow artifact |
| --- | --- |
| macOS | `openless-macos-<arch>-updater` |
| Windows | `openless-windows-x64-updater` |
| Android | `openless-android-updater-manifests` |

Destination (overwrite in place; the file name is per platform and channel, never per
version):

```text
https://dl.aeoform.com/updates/stable/latest-{target}-{arch}.json
https://dl.aeoform.com/updates/beta/latest-{target}-{arch}-beta.json
```

Their `url` fields must point at the CDN artifacts (`.../releases/{tag}/{asset}`).
Manifest upload is the step that makes a release reachable by auto-update: a published
release whose manifests were not synced is downloadable by hand but invisible to the updater.
Clients built before the CDN switch read the old `releases/latest/download/latest-*.json`
assets, which new releases no longer carry, so they need one manual download.
7. Publishing Stable with the administrator's CLI/UI credential automatically
triggers `sync-homebrew-cask.yml`. It accepts only the published latest Stable
tag, preserves application build metadata in DMG filenames, verifies matching
Expand Down Expand Up @@ -341,6 +366,6 @@ change the published channel. The same format applies to subsequent releases.
2. For a Stable release, a maintainer merges `beta → main`.
3. An **admin** bumps the Tauri version (five-location sync), verifies CI is green,
and pushes the release tag, which triggers the macOS/Windows/Android asset
workflows and their channel-specific updater manifests.
workflows and their channel updater manifests.
4. After both release workflows pass, an admin verifies and publishes the shared
draft. Linux packages are attached independently only after Linux acceptance.
9 changes: 5 additions & 4 deletions openless-all/app/crates/openless-core/src/shared_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ pub enum MacosNewlineMode {
}

/// Auto-update channel. Picks which manifest the background AutoUpdateGate
/// fetches. `Stable` = `latest-android-{arch}.json` (or the desktop
/// plugin-updater stable endpoints). `Beta` = `latest-android-{arch}-beta.json`
/// (or the desktop beta endpoints). The Settings manual "check stable / check
/// beta" buttons pass the channel explicitly and ignore this pref.
/// fetches. `Stable` = `/updates/stable/latest-{target}-{arch}.json` on the CDN
/// (`latest-android-{arch}.json` for the Android client). `Beta` = the
/// `/updates/beta/` counterpart with a `-beta` filename suffix. The Settings
/// manual "check stable / check beta" buttons pass the channel explicitly and
/// ignore this pref.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum UpdateChannel {
Expand Down
25 changes: 16 additions & 9 deletions openless-all/app/scripts/android-apk-workflow-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,16 +169,15 @@ try {
const apk = `OpenLess_${version}_${gradleAbi}.apk`;
const alias = `OpenLess_android_${gradleAbi}.apk`;
originals.push(`${directory}/${apk}`);
return [
apk,
`${apk}.sig`,
alias,
`${alias}.sig`,
`latest-android-${abi}.json`,
`latest-android-${abi}-mirror.json`,
];
return [apk, `${apk}.sig`, alias, `${alias}.sig`];
});
for (const name of names) writeFileSync(join(releaseTmp, name), 'fixture');
// The manifests are still written (they feed the CDN sync through a workflow artifact) but
// must never be re-attached to the release: the in-app updater reads them from the CDN.
const manifests = ANDROID_ABI_MATRIX.flatMap(({ abi }) => [
`latest-android-${abi}.json`,
`latest-android-${abi}-mirror.json`,
]);
for (const name of [...names, ...manifests]) writeFileSync(join(releaseTmp, name), 'fixture');
const step = parsed.jobs['publish-android-release'].steps.find(
(step) => step.id === 'release_assets',
);
Expand All @@ -199,6 +198,14 @@ try {
.trim()
.split(/\r?\n/);
assert.deepEqual(assets.sort(), names.map((name) => `${directory}/${name}`).sort());
const manifestUpload = parsed.jobs['publish-android-release'].steps.find((step) =>
step.uses?.startsWith('actions/upload-artifact@'),
);
assert.match(
manifestUpload?.with.path,
/latest-android-\*\.json/,
'manifests must leave the run as an artifact so the CDN sync can publish them',
);
} finally {
rmSync(releaseTmp, { recursive: true, force: true });
}
Expand Down
18 changes: 18 additions & 0 deletions openless-all/app/scripts/release-publication-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ for (const name of ['release-tauri.yml', 'android-apk.yml']) {
`${name}: no channel may publish a partially built release`,
);
assert.match(publishers[0].with.prerelease, /OPENLESS_RELEASE_CHANNEL == 'beta'/);
// Updater manifests belong to the CDN, not to the release assets: current clients read
// `/updates/{stable,beta}/` (see src-tauri/src/update_source.rs), and cloning them into the
// release produced fourteen near-identical files per tag.
assert.doesNotMatch(
publishers[0].with.files,
/latest-[^\s]*\.json/,
`${name}: updater manifests must not be attached to the release`,
);
const manifestUploads = steps.filter(
(step) =>
step.uses?.startsWith('actions/upload-artifact@') &&
/latest-[^\s]*\.json/.test(step.with?.path ?? ''),
);
assert.equal(
manifestUploads.length,
1,
`${name}: exactly one artifact must carry the updater manifests for the CDN sync`,
);
}
const sync = workflow('sync-homebrew-cask.yml');
assert.deepEqual(sync.on.release.types, ['published']);
Expand Down
Loading
Loading