Skip to content

feat(koreader): add KOReader sync API for reading progress synchroniz… #256

feat(koreader): add KOReader sync API for reading progress synchroniz…

feat(koreader): add KOReader sync API for reading progress synchroniz… #256

Workflow file for this run

# Unified CI/CD workflow for Codex
#
# Based on cargo-dist with custom additions for testing, linting, frontend, and Docker.
#
# Triggers:
# - Push to main: test, lint, build artifacts, push Docker with 'main' tag
# - Push version tag: test, lint, build artifacts, push Docker with version tags, create GitHub release
name: Build
permissions:
contents: write
packages: write
attestations: write
id-token: write
on:
push:
branches:
- main
tags:
- "**[0-9]+.[0-9]+.[0-9]+*"
# Cancel in-progress runs for same branch/tag
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Build test binaries and create nextest archive
build-tests:
name: Build Tests
runs-on: ubuntu-latest
timeout-minutes: 60
env:
SCCACHE_GHA_ENABLED: "true"
SCCACHE_GHA_VERSION: test
RUSTC_WRAPPER: sccache
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v4
- name: Install mold linker
run: sudo apt-get update && sudo apt-get install -y mold
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Build and archive tests
run: cargo nextest archive --features rar --archive-file nextest-archive.tar.zst
- name: Upload test archive
uses: actions/upload-artifact@v4
with:
name: nextest-archive
path: nextest-archive.tar.zst
retention-days: 1
# Run tests in parallel partitions
test:
name: Test (${{ matrix.partition }}/5)
needs: build-tests
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
partition: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Download test archive
uses: actions/download-artifact@v4
with:
name: nextest-archive
- name: Run tests (partition ${{ matrix.partition }}/5)
run: cargo nextest run --archive-file nextest-archive.tar.zst --partition hash:${{ matrix.partition }}/5
# Run linting checks
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 30
env:
SCCACHE_GHA_ENABLED: "true"
SCCACHE_GHA_VERSION: lint
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@v4
- name: Install mold linker
run: sudo apt-get update && sudo apt-get install -y mold
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Format check
run: cargo fmt -- --check
- name: Clippy check
run: cargo clippy --features rar -- -D warnings
# Run frontend tests and build
frontend:
name: Frontend
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
# Note: We remove package-lock.json and use npm install instead of npm ci
# because Biome updates frequently and we want to use the latest compatible version
- name: Install dependencies
working-directory: web
run: |
rm -rf node_modules package-lock.json
npm install
- name: Lint check
working-directory: web
run: npm run lint
- name: Run tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: cd web && npm run test:run
- name: Build frontend
working-directory: web
run: npm run build
- name: Upload frontend build
uses: actions/upload-artifact@v4
with:
name: frontend-dist
path: web/dist
retention-days: 1
# Run plugin checks (lint, typecheck, tests)
plugins:
name: Plugins (${{ matrix.plugin }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
plugin:
- sdk-typescript
- metadata-echo
- metadata-mangabaka
- metadata-openlibrary
- recommendations-anilist
- sync-anilist
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
# SDK must be built first for other plugins to use
- name: Build SDK (dependency)
if: matrix.plugin != 'sdk-typescript'
working-directory: plugins/sdk-typescript
run: |
rm -rf node_modules package-lock.json
npm install
npm run build
- name: Install dependencies
working-directory: plugins/${{ matrix.plugin }}
run: |
rm -rf node_modules package-lock.json
npm install
- name: Lint
working-directory: plugins/${{ matrix.plugin }}
run: npm run lint
- name: Typecheck
working-directory: plugins/${{ matrix.plugin }}
run: npx tsc --noEmit
- name: Test
working-directory: plugins/${{ matrix.plugin }}
run: npm run test
- name: Build
working-directory: plugins/${{ matrix.plugin }}
run: npm run build
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
name: Plan
needs: [test, lint, frontend, plugins]
runs-on: ubuntu-22.04
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ steps.info.outputs.tag }}
tag-flag: ${{ steps.info.outputs.tag-flag }}
publishing: ${{ steps.info.outputs.publishing }}
is-release: ${{ steps.info.outputs.is-release }}
version: ${{ steps.info.outputs.version }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Determine build info
id: info
shell: bash
run: |
# Check if this is a tag push (release), main branch push, or PR
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
TAG_NAME="${{ github.ref_name }}"
VERSION="${TAG_NAME#v}"
echo "is-release=true" >> "$GITHUB_OUTPUT"
echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "tag-flag=--tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "publishing=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.ref }}" == refs/heads/main ]]; then
echo "is-release=false" >> "$GITHUB_OUTPUT"
echo "tag=" >> "$GITHUB_OUTPUT"
echo "tag-flag=" >> "$GITHUB_OUTPUT"
echo "publishing=true" >> "$GITHUB_OUTPUT"
echo "version=main" >> "$GITHUB_OUTPUT"
else
# Pull request
echo "is-release=false" >> "$GITHUB_OUTPUT"
echo "tag=" >> "$GITHUB_OUTPUT"
echo "tag-flag=" >> "$GITHUB_OUTPUT"
echo "publishing=false" >> "$GITHUB_OUTPUT"
echo "version=dev" >> "$GITHUB_OUTPUT"
fi
- name: Install dist
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh"
- name: Upload cached dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
retention-days: 1
- name: Run dist plan
id: plan
shell: bash
run: |
if [[ "${{ steps.info.outputs.is-release }}" == "true" ]]; then
dist host --steps=create --tag=${{ steps.info.outputs.tag }} --output-format=json > plan-dist-manifest.json
else
dist plan --output-format=json > plan-dist-manifest.json
fi
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: Upload dist-manifest.json
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
retention-days: 1
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
permissions:
"attestations": "write"
"contents": "read"
"id-token": "write"
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install dist
run: ${{ matrix.install_dist.run }}
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Download frontend dist
uses: actions/download-artifact@v4
with:
name: frontend-dist
path: web/dist/
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- name: Attest
uses: actions/attest-build-provenance@v2
with:
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
- id: cargo-dist
name: Post-build
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
retention-days: 5
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: ubuntu-22.04
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- name: Setup dist
run: |
chmod +x ~/.cargo/bin/dist
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
retention-days: 5
# Build and push Docker images (multi-arch) using cross-compilation
# Uses Dockerfile.cross which cross-compiles ARM on x86 (no QEMU emulation)
docker:
name: Docker
needs: [test, lint, frontend, plugins]
# Only build Docker on main branch or release tags
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-latest
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Determine build info
id: info
run: |
echo "DOCKER_REGISTRY=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "is-release=true" >> "$GITHUB_OUTPUT"
else
echo "is-release=false" >> "$GITHUB_OUTPUT"
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta (release)
if: ${{ steps.info.outputs.is-release == 'true' }}
id: meta-release
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest
type=sha,prefix=sha-
- name: Docker meta (main)
if: ${{ steps.info.outputs.is-release == 'false' }}
id: meta-main
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=main
type=raw,value=latest
type=sha,prefix=sha-
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.cross
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.info.outputs.is-release == 'true' && steps.meta-release.outputs.tags || steps.meta-main.outputs.tags }}
labels: ${{ steps.info.outputs.is-release == 'true' && steps.meta-release.outputs.labels || steps.meta-main.outputs.labels }}
cache-from: type=registry,ref=ghcr.io/${{ env.DOCKER_REGISTRY }}-buildcache:latest
cache-to: type=registry,ref=ghcr.io/${{ env.DOCKER_REGISTRY }}-buildcache:latest,mode=max
# Create GitHub Release (only for releases)
host:
name: Release
needs:
- plan
- build-local-artifacts
- build-global-artifacts
- docker
# Only run if we're "publishing" a release, and only if plan, local, global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.is-release == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-22.04
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- name: Setup dist
run: |
chmod +x ~/.cargo/bin/dist
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: Upload dist-manifest.json
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
retention-days: 5
# Create a GitHub Release while uploading all files
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > "$RUNNER_TEMP/notes.txt"
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG \
--title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
# Publish SDK to npm (only for releases)
publish-sdk:
name: Publish SDK
needs:
- plan
- host
if: ${{ always() && needs.host.result == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
# Note: We remove package-lock.json and use npm install instead of npm ci
# because Biome updates frequently and we want to use the latest compatible version
- name: Install dependencies and build
working-directory: plugins/sdk-typescript
run: |
rm -rf node_modules package-lock.json
npm install
npm run build
- name: Publish to npm
working-directory: plugins/sdk-typescript
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Publish plugins to npm (only for releases, after SDK is published)
publish-plugins:
name: Publish Plugins
needs:
- plan
- publish-sdk
if: ${{ always() && needs.publish-sdk.result == 'success' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
plugin:
- metadata-echo
- metadata-mangabaka
- metadata-openlibrary
- recommendations-anilist
- sync-anilist
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
# Update SDK dependency from file: to published npm version before install
- name: Update SDK dependency for publish
working-directory: plugins/${{ matrix.plugin }}
run: |
SDK_VERSION=$(jq -r .version ../sdk-typescript/package.json)
echo "Updating @ashdev/codex-plugin-sdk dependency to ^$SDK_VERSION"
jq --arg v "^$SDK_VERSION" '.dependencies["@ashdev/codex-plugin-sdk"] = $v' package.json > tmp.json
mv tmp.json package.json
cat package.json | grep -A2 '"dependencies"'
- name: Install plugin dependencies
working-directory: plugins/${{ matrix.plugin }}
run: |
rm -rf node_modules package-lock.json
npm install
- name: Build plugin
working-directory: plugins/${{ matrix.plugin }}
run: npm run build
- name: Publish to npm
working-directory: plugins/${{ matrix.plugin }}
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
announce:
needs:
- plan
- host
- publish-sdk
- publish-plugins
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: ubuntu-22.04
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# Clean up intermediate artifacts that are no longer needed
cleanup:
name: Cleanup
needs:
- plan
- build-local-artifacts
- build-global-artifacts
- host
- docker
- announce
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Delete intermediate artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: |
nextest-archive
frontend-dist
cargo-dist-cache
artifacts-plan-dist-manifest