readest/.github/workflows/nightly.yml
Huang Xin 655c72ad5a
feat(linux): ship Readest on the tauri CEF runtime (#6074)
* 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>
2026-09-05 20:09:06 +02:00

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."