mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-11 21:41:02 +08:00
Merge branch 'ci/github-actions'
This commit is contained in:
commit
cd158f6785
5 files changed
+261
-7
No files matched your search
@@ -0,0 +1,124 @@
|
||||
# Build and test: server (Node/TypeScript), web client, UI review, Android shell.
|
||||
#
|
||||
# Runs on pull requests and on pushes to any branch except main. Pushes to main and
|
||||
# release tags run it through release.yml (workflow_call), before any image is
|
||||
# published, so main is tested exactly once per push.
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches-ignore: [main]
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# A workflow_call run shares the caller's ref; keep it apart from a direct run.
|
||||
group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24
|
||||
|
||||
jobs:
|
||||
server:
|
||||
name: Server · typecheck, test, build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
cache-dependency-path: server/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm run typecheck
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
|
||||
web:
|
||||
name: Web · typecheck, test, build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
web/package-lock.json
|
||||
server/package-lock.json
|
||||
# The web suite imports server code (search parity), so both are installed.
|
||||
- run: npm ci --prefix server
|
||||
- run: npm ci --prefix web
|
||||
- run: npm run typecheck --prefix web
|
||||
- run: npm test --prefix web
|
||||
- run: npm run build --prefix web
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: web/dist
|
||||
retention-days: 7
|
||||
|
||||
ui-review:
|
||||
# The production bundle rendered in a real Chromium and measured; see docs/ui.md §7.
|
||||
name: Web · UI review
|
||||
runs-on: ubuntu-latest
|
||||
needs: web
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: npm ci --prefix web
|
||||
- run: npx playwright install --with-deps chromium
|
||||
working-directory: web
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: web/dist
|
||||
- run: npm run ui:review:only --prefix web
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: ui-review
|
||||
path: artifacts/ui-review/react-app
|
||||
retention-days: 14
|
||||
|
||||
android:
|
||||
name: Android · shell contract, debug APK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: sh android/scripts/check-shell.sh
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
# The APK carries the current web build; verify-web-assets checks it is packed.
|
||||
- run: sh android/scripts/build-web-assets.sh
|
||||
- run: sh android/scripts/verify-web-assets.sh
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
# The hosted runner ships the Android SDK; this only accepts and adds what the
|
||||
# build asks for (platform 35, build-tools 35.0.1).
|
||||
- uses: android-actions/setup-android@v3
|
||||
- run: ./gradlew --no-daemon :app:assembleDebug
|
||||
working-directory: android
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reader-debug-apk
|
||||
path: android/app/build/outputs/apk/debug/*.apk
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,123 @@
|
||||
# Release the single-container image (server + web) to Docker Hub for amd64 and arm64.
|
||||
#
|
||||
# push to main -> hectorqin/reader-pro:main, :sha-<short>
|
||||
# tag v1.2.3 -> hectorqin/reader-pro:1.2.3, :1.2, :latest, :sha-<short>
|
||||
# manual run -> tags for the chosen ref, as above
|
||||
#
|
||||
# CI (ci.yml) runs first; nothing is published from a red build. Each architecture
|
||||
# is built on its own native runner — the arm64 image under QEMU would run npm ci
|
||||
# and the Vite build emulated, which is many times slower — and pushed by digest,
|
||||
# then one job stitches the digests into the multi-arch tags.
|
||||
#
|
||||
# Needs repository secrets DOCKERHUB_USERNAME and DOCKERHUB_TOKEN (a Docker Hub
|
||||
# access token with read/write on hectorqin/reader-pro).
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
IMAGE: hectorqin/reader-pro
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: ./.github/workflows/ci.yml
|
||||
|
||||
build:
|
||||
name: Image · ${{ matrix.platform }}
|
||||
needs: ci
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Platform slug
|
||||
run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV"
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- uses: actions/checkout@v5
|
||||
- id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE }}
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
|
||||
- name: Record digest
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/digests"
|
||||
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"
|
||||
env:
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
publish:
|
||||
name: Image · multi-arch tags
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
tags=()
|
||||
while read -r tag; do tags+=(-t "$tag"); done < <(jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||
refs=()
|
||||
for digest in *; do refs+=("$IMAGE@sha256:$digest"); done
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
- name: Inspect
|
||||
run: docker buildx imagetools inspect "$IMAGE:$VERSION"
|
||||
env:
|
||||
VERSION: ${{ steps.meta.outputs.version }}
|
||||
@@ -14,7 +14,7 @@ Docs (mostly Chinese) live in `docs/`; `docs/architecture.md` explains *why* thi
|
||||
|
||||
## Commands
|
||||
|
||||
Run from repo root (or `cd` into the package). CI (`.cnb.yml`) runs install → typecheck → test → build for each package, only when that package changed.
|
||||
Run from repo root (or `cd` into the package). CI runs install → typecheck → test → build per package: GitHub Actions `.github/workflows/ci.yml` (PRs and non-main pushes; also server, UI review, Android shell check + debug APK) and CNB `.cnb.yml`. `.github/workflows/release.yml` runs CI on main pushes and `v*` tags, then publishes the multi-arch (amd64 + arm64, native runners) image to Docker Hub `hectorqin/reader-pro` (`main`/`sha-*`, or `x.y.z`/`x.y`/`latest` for tags); it needs the `DOCKERHUB_USERNAME`/`DOCKERHUB_TOKEN` secrets. `android/scripts/check-shell.sh` checks the JS bridge contract: only `@JavascriptInterface` methods count, and each must be declared in `web/src/android-bridge.d.ts` (method syntax, `name?(): T` for optional).
|
||||
|
||||
```sh
|
||||
npm ci --prefix server && npm ci --prefix web
|
||||
|
||||
@@ -21,7 +21,8 @@ err() { printf 'FAIL: %s\n' "$1" >&2; fail=1; }
|
||||
note "==> checking package declarations match directory layout"
|
||||
for file in $(find "$android_dir/app/src" -name '*.kt'); do
|
||||
pkg=$(sed -n 's/^package \(.*\)$/\1/p' "$file" | head -1)
|
||||
rel=${file#"$android_dir"/app/src/main/java/}
|
||||
# Any source set (main, test, androidTest): the package is the path under its java/.
|
||||
rel=${file#"$android_dir"/app/src/*/java/}
|
||||
expected=$(dirname "$rel" | tr '/' '.')
|
||||
if [ "$pkg" != "$expected" ]; then
|
||||
err "$file declares '$pkg' but lives in '$expected'"
|
||||
@@ -58,10 +59,16 @@ bridge_file="$android_dir/app/src/main/java/cool/cnb/reader/bridge/ReaderBridge.
|
||||
speech_file="$android_dir/app/src/main/java/cool/cnb/reader/bridge/SpeechBridge.kt"
|
||||
types_file="$repo_dir/web/src/android-bridge.d.ts"
|
||||
|
||||
# A method is "implemented" if either bridge class has it at method indentation.
|
||||
# Both files are searched for both interfaces: the Kotlin class a method happens
|
||||
# to live in is an implementation detail, not part of the contract.
|
||||
implemented=$(sed -n 's/^ fun \([A-Za-z][A-Za-z0-9_]*\)(.*/\1/p' "$bridge_file" "$speech_file" | sort -u)
|
||||
# A method is "implemented" if either bridge class exposes it to the page: a
|
||||
# method-level `fun` directly under `@JavascriptInterface`. Both files are searched
|
||||
# for both interfaces: the Kotlin class a method happens to live in is an
|
||||
# implementation detail, not part of the contract. A `fun` without the annotation
|
||||
# is Kotlin-only (SpeechBridge.interruptForMedia is called by the media bridge,
|
||||
# never by the page), and the page cannot call it, so declaring it would be wrong.
|
||||
implemented=$(awk '
|
||||
/^ fun [A-Za-z][A-Za-z0-9_]*\(/ && exposed { name = $2; sub(/\(.*/, "", name); print name }
|
||||
NF { exposed = ($0 ~ /^[[:space:]]*@JavascriptInterface[[:space:]]*$/) }
|
||||
' "$bridge_file" "$speech_file" | sort -u)
|
||||
|
||||
# The declaration side has two shapes, and the difference matters:
|
||||
#
|
||||
|
||||
Vendored
+1
-1
@@ -11,7 +11,7 @@ export interface AndroidBridge {
|
||||
/** Version of the native shell, so the client can require a minimum. */
|
||||
shellVersion(): number;
|
||||
/** Optional Reader server origin configured by the native APK. */
|
||||
configuredServerUrl?: () => string;
|
||||
configuredServerUrl?(): string;
|
||||
/** Device label recorded on progress rows, e.g. `Pixel 7`. */
|
||||
deviceLabel(): string;
|
||||
/** 'online' | 'offline' from the platform connectivity manager. */
|
||||
|
||||
Reference in new issue
Block a user