Skip to content

add CI

add CI #389

Workflow file for this run

name: CI
on:
pull_request:
branches: [main]
paths-ignore:
- '**/*.md'
- 'png/**'
push:
branches: [main]
paths-ignore:
- '**/*.md'
- 'png/**'
# Cancel previous runs on same branch/PR
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# ── Shell deploy assets: LF endings + syntax ───────────────────────
# Relay one-click deploy embeds these scripts into the Desktop binary and
# uploads them verbatim to a Linux host, where a single CR aborts the deploy
# with `$'\r': command not found`. .gitattributes pins LF; this is the guard.
shell-scripts:
name: Shell Deploy Scripts
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: package.json
package-manager-cache: false
- name: Reject CRLF in shell and deploy assets
run: |
bad=$(git ls-files -z \
'*.sh' '*.bash' 'Dockerfile' 'Dockerfile.*' '*.Dockerfile' 'Caddyfile' \
'docker-compose.yml' 'docker-compose.*.yml' \
| xargs -0 -r grep -lU $'\r' || true)
if [ -n "$bad" ]; then
echo "::error::CRLF line endings found; these must stay LF (see .gitattributes):"
echo "$bad"
exit 1
fi
echo "All shell and deploy assets are LF-only."
- name: bash -n every tracked shell script
run: |
rc=0
while IFS= read -r -d '' f; do
bash -n "$f" || { echo "::error file=$f::bash syntax error"; rc=1; }
done < <(git ls-files -z '*.sh' '*.bash')
exit "$rc"
- name: Verify release and version-generation contracts
run: node --test scripts/tauri-release-manifest.test.mjs scripts/linux-binaries-manifest.test.mjs scripts/version-generation.test.mjs
- name: Verify minisign download fallback
run: |
set -euo pipefail
test_root="$(mktemp -d)"
trap 'rm -rf "$test_root"' EXIT
mkdir -p "$test_root/stubs"
printf '#!/usr/bin/env bash\nexit 1\n' >"$test_root/stubs/sudo"
chmod +x "$test_root/stubs/sudo"
PATH="$test_root/stubs:/usr/bin:/bin" \
RUNNER_TEMP="$test_root" \
BITFUN_SIGNING_KEY="YQ==" \
bash scripts/sign-release-assets.sh "$test_root/missing-asset"
"$test_root/bitfun-minisign-0.12/bin/minisign" -v
# ── CLI: independent tests ─────────────────────────────────────────
cli-test:
name: CLI Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
cache_key: ubuntu
- os: macos-15
cache_key: macos
steps:
- uses: actions/checkout@v5
- name: Install Linux system dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config \
build-essential \
libssl-dev \
libxcb1-dev \
libxcb-render0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
with:
shared-key: "cli-ci-v2-${{ matrix.cache_key }}"
cache-bin: false
# PRs restore trusted caches but never publish merge-ref artifacts.
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
cache-on-failure: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Run CLI and ACP tests on macOS
if: runner.os == 'macOS'
run: cargo test --locked -p bitfun-cli -p bitfun-acp
- name: Run CLI, ACP, and agent runtime tests
if: runner.os == 'Linux'
run: cargo test --locked -p bitfun-cli -p bitfun-acp -p bitfun-agent-runtime
- name: Run SDK Host tests
if: runner.os == 'Linux'
run: cargo test --locked -p bitfun-sdk-host -p bitfun-sdk-host-app
- name: Run SDK Host terminal cleanup regressions
if: runner.os == 'Linux'
run: |
cargo test --locked -p terminal-core shutdown_returns_only_after_process_exit_is_confirmed -- --test-threads=1
cargo test --locked -p terminal-core shutdown_evicts_a_process_whose_controller_already_confirmed_exit -- --test-threads=1
cargo test --locked -p terminal-core background_only_binding_is_owned_by_the_session -- --test-threads=1
# ── Rust: build check ─────────────────────────────────────────────
rust-build-check:
name: Rust Build Check (${{ matrix.os }})
runs-on: ${{ matrix.os }}
env:
# Keep the workspace check plus desktop test profiles within hosted-runner disk limits.
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-15
- windows-latest
steps:
- uses: actions/checkout@v5
# Tauri code generation only requires its configured resource roots to
# exist during check/test; distributable assets remain frontend-build's owner.
- name: Create Tauri resource directories
shell: bash
run: mkdir -p dist src/mobile-web/dist
- name: Install Linux system dependencies (Tauri)
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
if apt-cache show libwebkit2gtk-4.1-dev >/dev/null 2>&1; then
WEBKIT_PKG=libwebkit2gtk-4.1-dev
else
WEBKIT_PKG=libwebkit2gtk-4.0-dev
fi
if apt-cache show libappindicator3-dev >/dev/null 2>&1; then
APPINDICATOR_PKG=libappindicator3-dev
else
APPINDICATOR_PKG=libayatana-appindicator3-dev
fi
sudo apt-get install -y --no-install-recommends \
pkg-config \
libglib2.0-dev \
libgtk-3-dev \
libxdo-dev \
"$WEBKIT_PKG" \
"$APPINDICATOR_PKG" \
librsvg2-dev \
patchelf \
libleptonica-dev \
libtesseract-dev \
tesseract-ocr \
tesseract-ocr-eng
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
with:
shared-key: "ci-check-v3-${{ runner.os }}-no-cargo-bin-v1"
cache-bin: false
# PR caches are scoped to merge refs; trusted main pushes own shared
# refreshes and retain completed dependency builds after late test failures.
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
cache-on-failure: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Check compilation
run: cargo check --locked --workspace
# The installer is intentionally excluded from the root Cargo workspace,
# so the workspace check above cannot catch drift in its shared Rust APIs.
- name: Check installer compilation
if: runner.os == 'Windows'
run: cargo check --manifest-path BitFun-Installer/src-tauri/Cargo.toml
- name: Run core and desktop library tests
run: cargo test --locked -p bitfun-core -p bitfun-desktop --lib
# These crates own platform-sensitive behavior that is not exercised by
# testing bitfun-core/bitfun-desktop alone. Keep their focused contract
# suites in the OS matrix so Linux success cannot hide Windows/macOS
# regressions in worker interruption, relay storage, or OAuth handling.
- name: Run Page Functions runtime tests
run: cargo test --locked -p bitfun-page-function-runtime
- name: Run Relay service tests
run: cargo test --locked -p bitfun-relay-service
- name: Run subscription authentication tests
run: cargo test --locked -p bitfun-ai-adapters --features subscription-auth --lib subscription_auth
# File watching is backed by a different OS API on every platform
# (ReadDirectoryChangesW / FSEvents / inotify), so watch registration
# regressions surface per-OS. The suite is behind a non-default feature
# and would otherwise never run anywhere.
#
# macOS is excluded: this suite already fails there before any of this
# branch's changes (the debounce/atomic-rename contracts time out under
# FSEvents coalescing), which is worth fixing separately rather than
# blocking unrelated work.
- name: Run file watch contract tests
if: runner.os != 'macOS'
run: cargo test --locked -p bitfun-services-integrations --no-default-features --features file-watch --test file_watch_contracts
# Search tools resolve paths and symlinks directly, which also differs
# across platforms. Scoped to the search module: the glob tests in this
# crate fail on Windows independently of this branch (walk-root
# derivation treats separators differently) and need their own fix.
- name: Run search tool tests
run: "cargo test --locked -p tool-runtime --lib search::"
# ── Frontend: build ────────────────────────────────────────────────
frontend-build:
name: Frontend Build
runs-on: ubuntu-latest
env:
NODE_OPTIONS: --max-old-space-size=6144
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 2
- name: Setup pnpm
uses: pnpm/action-setup@v5
- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
- name: Check repository hygiene
run: pnpm run check:repo-hygiene
- name: Check core boundaries
run: node --test scripts/check-core-boundaries.test.mjs
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Validate PPT Live generated-file contract
run: pnpm run test:ppt-live
- name: Validate GitHub config
run: pnpm run check:github-config
- name: Select i18n contract profile
shell: bash
run: |
if git rev-parse HEAD^1 >/dev/null 2>&1 &&
git diff --name-only HEAD^1 HEAD -- scripts/i18n-audit.mjs scripts/i18n-contract.test.mjs | grep -q .; then
echo "BITFUN_I18N_CONTRACT_TEST_AUDIT_INTEGRATION=1" >> "$GITHUB_ENV"
fi
- name: Validate i18n contract
run: pnpm run i18n:contract:test:ci
- name: Audit i18n resources
run: pnpm run i18n:audit
- name: Validate theme color audit contract
run: pnpm run theme:color-audit:test
- name: Audit theme color governance
run: pnpm run theme:color-audit:all
- name: Validate theme visual governance contract
run: pnpm run theme:visual-contract
- name: Lint web UI
run: pnpm run lint:web
- name: Run web UI tests
run: pnpm --dir src/web-ui run test:run
- name: Build web UI
run: pnpm run build:web
- name: Type-check mobile web
run: pnpm --dir src/mobile-web run type-check
- name: Build mobile web
run: pnpm run build:mobile-web
# ── OHOS: cargo check ──────────────────────────────────────────────
# Lightweight compile-check for the OHOS target on every PR/push.
# Uses hdx (https://github.com/chawyehsu/hdx) to download the
# HarmonyOS Command Line Tools, then extracts only the native/ NDK
# (llvm + sysroot). The full DevEco SDK, hvigor, and ohpm are not needed.
ohos-cargo-check:
name: OHOS Cargo Check (aarch64)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
CARGO_INCREMENTAL: "0"
CARGO_PROFILE_DEV_DEBUG: "0"
OHOS_CLT_VERSION: "6.1.1.300"
steps:
- uses: actions/checkout@v5
- name: Create Tauri resource directories
shell: bash
run: mkdir -p dist src/mobile-web/dist
- uses: dtolnay/rust-toolchain@stable
- name: Cache OHOS NDK
id: cache-ndk
uses: actions/cache@v4
with:
path: /opt/ohos-ndk
key: ohos-ndk-${{ env.OHOS_CLT_VERSION }}-v1
- name: Download OHOS Command Line Tools via hdx
if: steps.cache-ndk.outputs.cache-hit != 'true'
shell: bash
run: |
set -euo pipefail
# ── Install hdx ──────────────────────────────────────────────
echo "Installing hdx..."
if curl -fSL --retry 2 -o /tmp/hdx \
"https://github.com/chawyehsu/hdx/releases/latest/download/hdx-linux-x86_64" 2>/dev/null; then
chmod +x /tmp/hdx
echo "hdx binary downloaded"
else
echo "Pre-built hdx not found, building from source..."
cargo install --git https://github.com/chawyehsu/hdx --root /tmp
fi
# ── Restore hdx credentials ─────────────────────────────────
mkdir -p ~/.config/hdx
echo "${{ secrets.HDX_CREDENTIALS_B64 }}" | base64 -d > ~/.config/hdx/credentials.json
# ── Get download URL for Command Line Tools ─────────────────
echo "Fetching Command Line Tools $OHOS_CLT_VERSION download URL..."
CLT_URL=$(/tmp/hdx dget command-line-tools --version "$OHOS_CLT_VERSION" --platform linux)
echo "Download URL obtained"
# ── Download and extract ─────────────────────────────────────
echo "Downloading Command Line Tools (~2 GB)..."
curl -fSL --retry 3 --retry-delay 5 -o /tmp/command-line-tools.zip "$CLT_URL"
# The zip layout: command-line-tools/sdk/{version}/openharmony/native/
echo "Extracting native NDK..."
mkdir -p /opt/ohos-ndk
unzip -q -o /tmp/command-line-tools.zip "*/openharmony/native/*" -d /tmp/clt
NATIVE_DIR=$(find /tmp/clt -path "*/openharmony/native" -type d | head -1)
if [[ -z "$NATIVE_DIR" ]]; then
echo "::error::Could not find openharmony/native/ in Command Line Tools zip"
find /tmp/clt -maxdepth 4 -type d | head -20
exit 1
fi
mv "$NATIVE_DIR" /opt/ohos-ndk/native
echo "NDK installed: $(du -sh /opt/ohos-ndk/native | cut -f1)"
rm -rf /tmp/command-line-tools.zip /tmp/clt /tmp/hdx
- name: Install Rust nightly + rust-src
shell: bash
run: |
rustup toolchain install nightly --profile minimal
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
rustup target add aarch64-unknown-linux-ohos
- uses: swatinem/rust-cache@v2
with:
shared-key: "ohos-check-v1"
cache-bin: false
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
cache-on-failure: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Configure OHOS target linker
shell: bash
run: |
mkdir -p /usr/local/bin .cargo
# Wrapper script so clang receives --target and --sysroot flags
printf '#!/bin/sh\nexec /opt/ohos-ndk/native/llvm/bin/clang --target=aarch64-linux-ohos --sysroot=/opt/ohos-ndk/native/sysroot -D__MUSL__ "$@"\n' \
> /usr/local/bin/aarch64-unknown-linux-ohos-clang
chmod +x /usr/local/bin/aarch64-unknown-linux-ohos-clang
# .cargo/config.toml pointing to the wrapper
printf '[target.aarch64-unknown-linux-ohos]\nar = "/opt/ohos-ndk/native/llvm/bin/llvm-ar"\nlinker = "/usr/local/bin/aarch64-unknown-linux-ohos-clang"\n' \
> .cargo/config.toml
- name: Cargo check (OHOS aarch64)
shell: bash
run: |
cargo +nightly check -Z build-std=std,panic_abort \
--target aarch64-unknown-linux-ohos \
-p bitfun-desktop --lib