mirror of
https://github.com/readest/readest.git
synced 2026-10-19 10:13:29 +00:00
* feat(linux): build and ship Readest on the tauri CEF runtime Linux now builds against upstream tauri `feat/cef` (git) and the plugins on plugins-workspace `feat/cef`, while macOS/Windows/mobile keep the readest fork untouched: - `pnpm tauri:cef dev|build` (scripts/tauri-cef.mjs) runs @tauri-apps/cli-cef with `--features cef`, cargo `--no-default-features` and the Linux-only `[patch.crates-io]` in src-tauri/.cargo/cef.toml, swapping the committed Cargo.cef.lock in for the run so Cargo.lock stays in the fork state. - The fork crates are no longer workspace members (a member clashes on `links` with the git copy); the tauri-plugins fork carried no changes and is removed, tauri-plugin-fs comes from crates.io at 2.5.1. - App: `cef = ["tauri/cef"]` (the fork gets a no-op stub), explicit runtime type, `cef_entry_point`, `--no-first-run` for CEF, commands generic over the runtime, xdg-portal dialogs on Linux (rfd's GTK thread crashes CEF), rangefile reads and pointer-driven move/resize on the Linux CEF runtime, Chromium reported in the About dialog, deb/rpm depend on libwebkit2gtk while plugins still pull the wry runtime. - Build-generated permission files are untracked; turso registers `batch`. - CI Linux legs use `pnpm tauri:cef`. Submodules: packages/tauri -> feat/cef-feature-stub, tauri-plugin-turso -> fix/register-batch-command (both need pushing). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(linux): let the CEF wrapper run an unpacked tauri CLI for offline builds Flatpak builds have no network, so `TAURI_CEF_CLI` points scripts/tauri-cef.mjs at an unpacked copy of @tauri-apps/cli-cef instead of `pnpm dlx`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(linux): take the CEF stack from the readest forks - src-tauri/.cargo/cef.toml: tauri and the plugins now come from the readest forks' feat/cef branches (upstream feat/cef plus the DevTools-close fix in tauri-runtime-cef); Cargo.cef.lock re-resolved against them. - tauri-plugin-turso: point at main, which merged the batch command fix. - CI: STRACE_MODE=0 for the Linux legs so the CEF AppImage bundler (quick-sharun) does not launch the app under Xvfb and hang (#4906). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(linux): make the CEF runtime the default tauri build on Linux `pnpm tauri` now goes through scripts/tauri.mjs (the former tauri-cef.mjs): on Linux its dev, build and bundle commands run the feat/cef tauri CLI with the CEF feature, cargo config and Cargo.cef.lock swap, and every other platform or command passes straight through to @tauri-apps/cli. The separate `tauri:cef` script goes away, so the release and nightly workflows, the local Linux build script and the docs use plain `pnpm tauri build` again. The tauri webdriver harness (scripts/test-tauri.sh) still calls the `tauri` binary directly and keeps the wry runtime on Linux, since tauri-driver needs WebKitWebDriver. Cargo.cef.lock picks up the quick-xml 0.41 bump from main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(nix): refresh cargoHash for the reshaped Cargo.lock Dropping the packages/tauri-plugins submodule and the tauri-plugin-fs path patch changed the vendored cargo dependency set, so the Nix fixed-output hash moves to the value the nix-deps-check job reported. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(linux): harden the CEF wrapper and pointer window drag Review follow-ups on the CEF runtime work: - scripts/tauri.mjs parks the wry Cargo.lock in Cargo.lock.wry, created exclusively, so two CEF commands in one checkout cannot swap lockfiles over each other, and a killed run leaves an obvious file to move back. - The CEF runtime alias, entry point and runtime attributes are gated on Linux as well as the `cef` feature; elsewhere the feature is a no-op. - Pointer-driven window move/resize applies the last pointer delta when the button is released before the pending animation frame, and does not start tracking when the button was already released while the window state was being read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
561 lines
26 KiB
YAML
561 lines
26 KiB
YAML
# Nightly builds. Mirrors the build matrix and build/signing steps of
|
|
# release.yml — keep cert/NDK/toolchain bumps, secret names, and the
|
|
# Linux CEF + portable-Windows steps in sync between the two.
|
|
#
|
|
# Differences from release.yml: this workflow (1) stamps a nightly version
|
|
# `<base>-<YYYYMMDDHH>` (Asia/Shanghai), (2) publishes to Cloudflare R2 only (no
|
|
# GitHub release), and (3) assembles `nightly/latest.json` race-free from
|
|
# per-leg manifest fragments so a single failing leg never clobbers the manifest.
|
|
name: Nightly Readest
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 22 * * *' # 22:00 UTC = 06:00 GMT+8
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# Serialize runs so an older run can't publish nightly/latest.json after a newer
|
|
# one (no cancel — let an in-flight build finish rather than drop artifacts).
|
|
concurrency:
|
|
group: nightly-readest
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
compute-version:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
nightly_version: ${{ steps.v.outputs.nightly_version }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: main
|
|
persist-credentials: false
|
|
- id: v
|
|
run: |
|
|
BASE=$(node -p "require('./apps/readest-app/package.json').version")
|
|
STAMP=$(TZ=Asia/Shanghai date +%Y%m%d%H)
|
|
echo "nightly_version=${BASE}-${STAMP}" >> "$GITHUB_OUTPUT"
|
|
|
|
build:
|
|
needs: compute-version
|
|
permissions:
|
|
contents: read
|
|
# Required by actions/attest-build-provenance: id-token mints the Sigstore
|
|
# OIDC identity, attestations writes the provenance to the repo's store.
|
|
id-token: write
|
|
attestations: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
config:
|
|
- os: ubuntu-latest
|
|
release: android
|
|
rust_target: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
|
|
- os: ubuntu-22.04
|
|
release: linux
|
|
arch: x86_64
|
|
rust_target: x86_64-unknown-linux-gnu
|
|
- os: ubuntu-22.04-arm
|
|
release: linux
|
|
arch: aarch64
|
|
rust_target: aarch64-unknown-linux-gnu
|
|
- os: macos-latest
|
|
release: macos
|
|
arch: aarch64
|
|
rust_target: x86_64-apple-darwin,aarch64-apple-darwin
|
|
# --config layers the iCloud overlay (Developer ID provisioning
|
|
# profile + entitlements) onto the direct-distribution build; see
|
|
# src-tauri/tauri.macos-nonestore.conf.json.
|
|
args: '--target universal-apple-darwin --config src-tauri/tauri.macos-nonestore.conf.json'
|
|
- os: windows-latest
|
|
release: windows
|
|
arch: x86_64
|
|
rust_target: x86_64-pc-windows-msvc
|
|
args: '--target x86_64-pc-windows-msvc --bundles nsis'
|
|
- os: windows-latest
|
|
release: windows
|
|
arch: aarch64
|
|
rust_target: aarch64-pc-windows-msvc
|
|
args: '--target aarch64-pc-windows-msvc --bundles nsis'
|
|
|
|
runs-on: ${{ matrix.config.os }}
|
|
# Backstop only — must stay ABOVE setup time + the per-step timeouts below,
|
|
# because a job-level timeout reports `cancelled` and skips assemble-manifest.
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: main
|
|
persist-credentials: false
|
|
|
|
- name: initialize git submodules
|
|
run: git submodule update --init --recursive
|
|
|
|
- name: setup pnpm
|
|
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
|
|
|
- name: setup node
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
|
|
- name: setup Java (for Android build only)
|
|
if: matrix.config.release == 'android'
|
|
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
|
with:
|
|
distribution: 'zulu'
|
|
java-version: '17'
|
|
|
|
- name: setup Android SDK (for Android build only)
|
|
if: matrix.config.release == 'android'
|
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4
|
|
|
|
- name: install NDK (for Android build only)
|
|
if: matrix.config.release == 'android'
|
|
run: sdkmanager "ndk;28.2.13676358"
|
|
|
|
- name: install dependencies
|
|
run: pnpm install --frozen-lockfile --prefer-offline
|
|
|
|
- name: copy pdfjs-dist and simplecc-dist to public directory
|
|
run: pnpm --filter @readest/readest-app setup-vendors
|
|
|
|
- name: install Rust stable
|
|
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
|
with:
|
|
targets: ${{ matrix.config.rust_target }}
|
|
|
|
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
|
|
with:
|
|
key: nightly-${{ matrix.config.os }}-${{ matrix.config.release }}-${{ matrix.config.arch }}-cargo
|
|
|
|
- name: install dependencies (ubuntu only)
|
|
if: contains(matrix.config.os, 'ubuntu') && matrix.config.release != 'android'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1 libwebkit2gtk-4.1-dev libjavascriptcoregtk-4.1 libjavascriptcoregtk-4.1-dev gir1.2-javascriptcoregtk-4.1 gir1.2-webkit2-4.1 libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
|
|
|
- name: create .env.local file for Next.js
|
|
run: |
|
|
echo "NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" >> .env.local
|
|
echo "NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}" >> .env.local
|
|
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
|
|
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
|
|
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
|
|
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env.local
|
|
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env.local
|
|
echo "SENTRY_ORG=readest" >> .env.local
|
|
echo "SENTRY_PROJECT=readest" >> .env.local
|
|
cp .env.local apps/readest-app/.env.local
|
|
|
|
- name: install rclone
|
|
shell: bash
|
|
run: |
|
|
if [ "$RUNNER_OS" = "Linux" ]; then
|
|
sudo apt-get update && sudo apt-get install -y rclone
|
|
elif [ "$RUNNER_OS" = "macOS" ]; then
|
|
brew install rclone
|
|
else
|
|
choco install rclone --version=1.75.0 --require-checksums -y
|
|
fi
|
|
|
|
- name: configure rclone
|
|
shell: bash
|
|
run: |
|
|
mkdir -p ~/.config/rclone
|
|
cat > ~/.config/rclone/rclone.conf <<EOF
|
|
[r2]
|
|
type = s3
|
|
provider = Cloudflare
|
|
access_key_id = ${{ secrets.RELEASE_R2_ACCESS_KEY_ID }}
|
|
secret_access_key = ${{ secrets.RELEASE_R2_SECRET_ACCESS_KEY }}
|
|
endpoint = https://${{ secrets.RELEASE_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
|
EOF
|
|
|
|
# ──────────────────────────── ANDROID ────────────────────────────
|
|
# `pnpm tauri android init` + `git checkout .` reverts tracked files
|
|
# (including package.json), so the nightly version MUST be patched AFTER
|
|
# the checkout. Mirrors release.yml's android build/signing steps.
|
|
- name: build and sign Android apks
|
|
if: matrix.config.release == 'android'
|
|
shell: bash
|
|
# Two full Android builds (universal + arm64) already run ~50min on a
|
|
# cold cache, so 55 left no headroom. Stays under the job backstop above.
|
|
timeout-minutes: 75
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/28.2.13676358
|
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
cd apps/readest-app/
|
|
rm -rf src-tauri/gen/android
|
|
pnpm tauri android init
|
|
pnpm tauri icon ../../data/icons/readest-book.png
|
|
git checkout .
|
|
# Patch the nightly version AFTER checkout so the stamp survives.
|
|
node -e "const f='package.json';const j=require('./'+f);j.version='${version}';require('fs').writeFileSync(f, JSON.stringify(j,null,2)+'\n')"
|
|
|
|
pushd src-tauri/gen/android
|
|
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
|
|
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
|
|
base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > $RUNNER_TEMP/keystore.jks
|
|
echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties
|
|
popd
|
|
|
|
apk_path=src-tauri/gen/android/app/build/outputs/apk/universal/release
|
|
universal_apk=Readest_${version}_universal.apk
|
|
arm64_apk=Readest_${version}_arm64.apk
|
|
pnpm tauri android build
|
|
cp ${apk_path}/app-universal-release.apk $universal_apk
|
|
pnpm tauri android build -t aarch64
|
|
cp ${apk_path}/app-universal-release.apk $arm64_apk
|
|
pnpm tauri signer sign $universal_apk
|
|
pnpm tauri signer sign $arm64_apk
|
|
|
|
# ──────────────────────────── DESKTOP ────────────────────────────
|
|
# Linux ships on the CEF runtime: `pnpm tauri` (mirrors release.yml; see
|
|
# apps/readest-app/scripts/tauri.mjs) runs the feat/cef tauri CLI there.
|
|
# The nightly version is patched BEFORE `tauri build` so the bundle
|
|
# filenames carry the stamp.
|
|
- name: build desktop bundles
|
|
if: matrix.config.release != 'android'
|
|
shell: bash
|
|
# A hung build must fail the STEP (step timeout -> job failure), not hit
|
|
# the job-level timeout: job timeouts report `cancelled`, which the
|
|
# assemble-manifest guard treats as run cancellation and skips promoting
|
|
# latest.json for ALL platforms (#4906).
|
|
timeout-minutes: 45
|
|
env:
|
|
# See release.yml: keep quick-sharun from running the app under Xvfb.
|
|
STRACE_MODE: '0'
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
NODE_OPTIONS: '--max-old-space-size=8192'
|
|
run: |
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
node -e "const f='apps/readest-app/package.json';const j=require('./'+f);j.version='${version}';require('fs').writeFileSync(f, JSON.stringify(j,null,2)+'\n')"
|
|
cd apps/readest-app
|
|
pnpm tauri build ${{ matrix.config.args }}
|
|
|
|
# Portable Windows build: rebuild with NEXT_PUBLIC_PORTABLE_APP=true and
|
|
# ship the raw exe (mirrors release.yml). Runs after the NSIS build above.
|
|
- name: build and sign portable binaries (Windows only)
|
|
if: matrix.config.os == 'windows-latest'
|
|
shell: bash
|
|
timeout-minutes: 30
|
|
env:
|
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
arch="${{ matrix.config.arch }}"
|
|
rust_target="${{ matrix.config.rust_target }}"
|
|
|
|
# The clean NSIS build above produced bundle/nsis/Readest_<ver>_<x64|
|
|
# arm64>-setup.exe (+ .sig). The portable rebuild below runs `tauri
|
|
# build ... --bundles nsis` AGAIN with NEXT_PUBLIC_PORTABLE_APP=true,
|
|
# which OVERWRITES that installer with a portable-flavored one. Stage
|
|
# the clean installer + its updater .sig to a safe dir FIRST so the
|
|
# collect step can read the untouched copy for the windows-* keys.
|
|
if [ "$arch" = "x86_64" ]; then
|
|
nsis_name="Readest_${version}_x64-setup.exe"
|
|
else
|
|
nsis_name="Readest_${version}_arm64-setup.exe"
|
|
fi
|
|
nsis_src="target/${rust_target}/release/bundle/nsis/${nsis_name}"
|
|
mkdir -p nsis-staged
|
|
cp "$nsis_src" "nsis-staged/${nsis_name}"
|
|
cp "${nsis_src}.sig" "nsis-staged/${nsis_name}.sig"
|
|
|
|
pushd apps/readest-app/
|
|
echo "NEXT_PUBLIC_PORTABLE_APP=true" >> .env.local
|
|
pnpm tauri build ${{ matrix.config.args }}
|
|
popd
|
|
|
|
if [ "$arch" = "x86_64" ]; then
|
|
bin_file="Readest_${version}_x64-portable.exe"
|
|
else
|
|
bin_file="Readest_${version}_arm64-portable.exe"
|
|
fi
|
|
exe_file="target/${{ matrix.config.rust_target }}/release/readest.exe"
|
|
# Browsers on Windows refuse to download zips containing exe files, so
|
|
# ship the exe directly (matches release.yml).
|
|
cp "$exe_file" "$bin_file"
|
|
pushd apps/readest-app/
|
|
pnpm tauri signer sign "../../$bin_file"
|
|
popd
|
|
|
|
# ───────────────── COLLECT ARTIFACTS + BUILD FRAGMENT ─────────────────
|
|
# Each leg copies its updater artifacts (+ .sig) into ./nightly-out and
|
|
# emits a per-leg manifest fragment keyed by the EXACT Tauri platform keys
|
|
# the client expects (see src/helpers/updater.ts::getNightlyPlatformKey and
|
|
# src/components/UpdaterWindow.tsx::TAURI_UPDATER_KEYS). The fragment
|
|
# `signature` is the .sig file CONTENTS; `url` is the download.readest.com
|
|
# URL of the uploaded artifact under nightly/<version>/.
|
|
- name: collect artifacts + build manifest fragment
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
release="${{ matrix.config.release }}"
|
|
arch="${{ matrix.config.arch }}"
|
|
rust_target="${{ matrix.config.rust_target }}"
|
|
base_url="https://download.readest.com/nightly/${version}"
|
|
out="$PWD/nightly-out"
|
|
frag_dir="$out/frag"
|
|
mkdir -p "$out" "$frag_dir"
|
|
# Cargo WORKSPACE: bundles land in the REPO-ROOT target/ dir, not
|
|
# apps/readest-app/src-tauri/target/ (matches release.yml line 371).
|
|
bundle="target"
|
|
|
|
# Stage one artifact + its .sig into $out, then append a
|
|
# platforms[<key>] = {signature, url} entry to the fragment JSON.
|
|
frag="$frag_dir/${release}-${arch:-all}.json"
|
|
echo '{"platforms":{}}' > "$frag"
|
|
add_entry() {
|
|
local src="$1"; local fname="$2"; local key="$3"
|
|
if [ ! -f "$src" ] || [ ! -f "${src}.sig" ]; then
|
|
echo "::error::missing artifact or signature for $key: $src"
|
|
exit 1
|
|
fi
|
|
cp "$src" "$out/$fname"
|
|
cp "${src}.sig" "$out/${fname}.sig"
|
|
local sig; sig=$(cat "${src}.sig")
|
|
local url="${base_url}/${fname}"
|
|
jq --arg k "$key" --arg sig "$sig" --arg url "$url" \
|
|
'.platforms[$k] = {signature: $sig, url: $url}' "$frag" > "$frag.tmp" && mv "$frag.tmp" "$frag"
|
|
echo "fragment += $key -> $fname"
|
|
}
|
|
|
|
case "$release" in
|
|
android)
|
|
# Signed in the android step; basenames already version-stamped.
|
|
add_entry "apps/readest-app/Readest_${version}_universal.apk" "Readest_${version}_universal.apk" "android-universal"
|
|
add_entry "apps/readest-app/Readest_${version}_arm64.apk" "Readest_${version}_arm64.apk" "android-arm64"
|
|
;;
|
|
macos)
|
|
# Universal updater bundle: bundle/macos/Readest.app.tar.gz (no
|
|
# version/arch on disk). Upload under the stable convention name
|
|
# Readest_universal.app.tar.gz; both darwin keys point at it.
|
|
src="${bundle}/universal-apple-darwin/release/bundle/macos/Readest.app.tar.gz"
|
|
add_entry "$src" "Readest_universal.app.tar.gz" "darwin-aarch64"
|
|
# Reuse the already-staged copy for the x86_64 key (same artifact).
|
|
x86_sig=$(cat "$out/Readest_universal.app.tar.gz.sig")
|
|
jq --arg sig "$x86_sig" --arg url "${base_url}/Readest_universal.app.tar.gz" \
|
|
'.platforms["darwin-x86_64"] = {signature: $sig, url: $url}' "$frag" > "$frag.tmp" && mv "$frag.tmp" "$frag"
|
|
echo "fragment += darwin-x86_64 -> Readest_universal.app.tar.gz"
|
|
;;
|
|
windows)
|
|
if [ "$arch" = "x86_64" ]; then
|
|
nsis_name="Readest_${version}_x64-setup.exe"
|
|
portable_name="Readest_${version}_x64-portable.exe"
|
|
nsis_key="windows-x86_64"; portable_key="windows-x86_64-portable"
|
|
else
|
|
nsis_name="Readest_${version}_arm64-setup.exe"
|
|
portable_name="Readest_${version}_arm64-portable.exe"
|
|
nsis_key="windows-aarch64"; portable_key="windows-aarch64-portable"
|
|
fi
|
|
# Read the CLEAN NSIS installer staged before the portable rebuild
|
|
# (the rebuild overwrites bundle/nsis/...-setup.exe). See the
|
|
# "build and sign portable binaries" step.
|
|
add_entry "nsis-staged/${nsis_name}" "$nsis_name" "$nsis_key"
|
|
# Portable exe was copied + signed into the repo root above.
|
|
add_entry "$portable_name" "$portable_name" "$portable_key"
|
|
;;
|
|
linux)
|
|
# CEF (sharun) AppImage: bundle/appimage/Readest_<ver>_<amd64|aarch64>.AppImage
|
|
if [ "$arch" = "x86_64" ]; then
|
|
appimage_name="Readest_${version}_amd64.AppImage"
|
|
key="linux-x86_64-appimage"
|
|
else
|
|
appimage_name="Readest_${version}_aarch64.AppImage"
|
|
key="linux-aarch64-appimage"
|
|
fi
|
|
# The Linux leg builds with `pnpm tauri build` WITHOUT `--target`
|
|
# (no matrix args), so cargo emits bundles under target/release/
|
|
# (host-target default) — NOT target/<triple>/release/ like the
|
|
# macOS/Windows legs, which DO pass `--target`. So there is no
|
|
# ${rust_target} subdir here.
|
|
add_entry "${bundle}/release/bundle/appimage/${appimage_name}" "$appimage_name" "$key"
|
|
;;
|
|
*)
|
|
echo "::error::unknown release leg: $release"; exit 1
|
|
;;
|
|
esac
|
|
|
|
# Attest the distributable binaries staged in nightly-out (apks, AppImage,
|
|
# app.tar.gz, setup/portable exe). gh attestation verify is digest-based,
|
|
# so it verifies these against readest/readest even though they ship via
|
|
# download.readest.com rather than a GitHub release. The .sig updater
|
|
# signatures are excluded — they are not binaries users run.
|
|
- name: attest nightly binaries
|
|
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
with:
|
|
subject-path: |
|
|
nightly-out/Readest*
|
|
!nightly-out/*.sig
|
|
|
|
- name: upload artifacts + fragment to R2
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
base="r2:readest-releases/nightly/${version}"
|
|
out="$PWD/nightly-out"
|
|
# Artifacts (exclude the local frag/ scratch dir).
|
|
rclone copy "$out" "$base/" --exclude "frag/**"
|
|
# Per-leg manifest fragment.
|
|
rclone copy "$out/frag" "$base/manifest-fragments/"
|
|
|
|
# Nightly KOReader plugin zip. Mirrors release.yml's build-koreader-plugin
|
|
# job (keep the helper-binary targets and zip exclusions in sync) but
|
|
# uploads to R2 under nightly/<version>/ next to the other nightly
|
|
# artifacts instead of a GitHub release. Deliberately NOT part of
|
|
# latest.json — the updater manifest carries Tauri app platforms only.
|
|
build-koreader-plugin:
|
|
needs: compute-version
|
|
permissions:
|
|
contents: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: main
|
|
persist-credentials: false
|
|
|
|
- name: Build LocalSend helper binaries for KOReader (static musl armv7 + arm64)
|
|
run: |
|
|
rustup target add armv7-unknown-linux-musleabi
|
|
rustup target add aarch64-unknown-linux-musl
|
|
pip3 install --break-system-packages --require-hashes -r .github/requirements/zigbuild.txt
|
|
node apps/readest.koplugin/scripts/build-localsend-bins.mjs --only armv7
|
|
node apps/readest.koplugin/scripts/build-localsend-bins.mjs --only arm64
|
|
|
|
- name: create KOReader plugin zip
|
|
run: |
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
plugin_zip="Readest-${version}-1.koplugin.zip"
|
|
meta_file="apps/readest.koplugin/_meta.lua"
|
|
perl -i -pe "s/^}/ version = \"${version}\",\n}/" "${meta_file}"
|
|
|
|
# Same dev-only exclusions as release.yml (keep the two lists in sync).
|
|
cd apps
|
|
zip -r ../${plugin_zip} readest.koplugin \
|
|
-x 'readest.koplugin/scripts/*' \
|
|
'readest.koplugin/docs/*' \
|
|
'readest.koplugin/spec/*' \
|
|
'readest.koplugin/.busted' \
|
|
'readest.koplugin/native/*'
|
|
|
|
- name: install rclone
|
|
run: sudo apt-get update && sudo apt-get install -y rclone
|
|
|
|
- name: configure rclone
|
|
run: |
|
|
mkdir -p ~/.config/rclone
|
|
cat > ~/.config/rclone/rclone.conf <<EOF
|
|
[r2]
|
|
type = s3
|
|
provider = Cloudflare
|
|
access_key_id = ${{ secrets.RELEASE_R2_ACCESS_KEY_ID }}
|
|
secret_access_key = ${{ secrets.RELEASE_R2_SECRET_ACCESS_KEY }}
|
|
endpoint = https://${{ secrets.RELEASE_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
|
EOF
|
|
|
|
- name: upload plugin zip to R2
|
|
run: |
|
|
set -euo pipefail
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
mkdir -p koplugin-out
|
|
mv "Readest-${version}-1.koplugin.zip" koplugin-out/
|
|
rclone copy koplugin-out "r2:readest-releases/nightly/${version}/"
|
|
echo "uploaded https://download.readest.com/nightly/${version}/Readest-${version}-1.koplugin.zip"
|
|
|
|
assemble-manifest:
|
|
needs: [compute-version, build]
|
|
if: ${{ always() && needs.build.result != 'cancelled' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: install rclone + jq
|
|
run: sudo apt-get update && sudo apt-get install -y rclone jq
|
|
|
|
- name: configure rclone
|
|
run: |
|
|
mkdir -p ~/.config/rclone
|
|
cat > ~/.config/rclone/rclone.conf <<EOF
|
|
[r2]
|
|
type = s3
|
|
provider = Cloudflare
|
|
access_key_id = ${{ secrets.RELEASE_R2_ACCESS_KEY_ID }}
|
|
secret_access_key = ${{ secrets.RELEASE_R2_SECRET_ACCESS_KEY }}
|
|
endpoint = https://${{ secrets.RELEASE_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
|
EOF
|
|
|
|
- name: assemble + atomically promote latest.json
|
|
run: |
|
|
set -euo pipefail
|
|
version="${{ needs.compute-version.outputs.nightly_version }}"
|
|
base="r2:readest-releases/nightly"
|
|
|
|
# Pull only the fragments that SUCCEEDED legs uploaded.
|
|
rclone copy "$base/${version}/manifest-fragments" ./frag || true
|
|
if [ -z "$(ls -A ./frag 2>/dev/null)" ]; then
|
|
echo "::error::no manifest fragments found — all build legs failed; manifest NOT promoted"
|
|
exit 1
|
|
fi
|
|
|
|
# Merge every fragment's .platforms into one manifest.
|
|
jq -s \
|
|
--arg version "$version" \
|
|
'{version: $version, pub_date: (now | todateiso8601), notes: "Nightly build", platforms: (map(.platforms) | add)}' \
|
|
./frag/*.json > latest.json
|
|
|
|
echo "Assembled latest.json:"
|
|
jq '{version, platforms: (.platforms | keys)}' latest.json
|
|
|
|
# Promote the manifest with a directory `rclone copy`, exactly like the
|
|
# stable release flow writes releases/latest.json (upload-to-r2.yml).
|
|
# A single-file `rclone copyto`/`moveto` first issues a CreateBucket
|
|
# probe (PUT /<bucket>) that the object-scoped R2 token can't satisfy
|
|
# (403 AccessDenied); a directory copy PUTs the object directly. R2
|
|
# PutObject is atomic, so readers never observe a half-written manifest.
|
|
mkdir -p promote && cp latest.json promote/latest.json
|
|
rclone copy promote "$base/"
|
|
|
|
- name: prune old nightly folders (keep newest 7)
|
|
run: |
|
|
set -euo pipefail
|
|
base="r2:readest-releases/nightly"
|
|
# Version dirs are <base>-<YYYYMMDDHH>. Lexicographic order is NOT
|
|
# chronological across a base-version bump (e.g. 0.2.0-* sorts after
|
|
# 0.11.0-*), so sort by the numeric stamp tail (everything after the
|
|
# last '-') to keep the newest 7 by build time.
|
|
mapfile -t dirs < <(rclone lsf "$base/" --dirs-only | sed 's:/$::' \
|
|
| sed -E 's/^(.*)-([0-9]{10})$/\2 \1-\2/' | sort -n -k1,1 | cut -d' ' -f2-)
|
|
count=${#dirs[@]}
|
|
if [ "$count" -gt 7 ]; then
|
|
for d in "${dirs[@]:0:$((count-7))}"; do
|
|
echo "pruning $d"
|
|
rclone purge "$base/$d"
|
|
done
|
|
fi
|
|
|
|
- name: notify on failure
|
|
if: failure()
|
|
run: echo "::error::Nightly assemble failed — manifest not promoted."
|