Four gates that were documented but unenforced, plus the flaky test that made a full-suite run untrustworthy. Rust lint/format: CLAUDE.md has required `cargo fmt` and `cargo clippy` before every commit for as long as the rule existed, yet neither ran anywhere in CI — the requirement rested on memory alone. Both now run in build-and-test.yml and build-release.yml. rustfmt and clippy are already baked into the builder image, so nothing is installed at job time. `cargo fmt --all -- --check` is strict immediately (the tree is clean). Clippy is advisory for now: ~51 pre-existing warnings mean `-D warnings` would fail on unrelated work, so the step carries a TODO to flip the flag once the backlog clears. A compile error still fails it, so it is not a no-op. Traceability threshold: MIN_THRESHOLD sat at 50 while real coverage was 86%, so nearly half the matrix could rot before the gate objected. Ratcheted to 82 with the policy written down — it only ever goes up, and is never lowered to make a red build pass. The same figure lives in MIN_COVERAGE_PERCENT so `traces:coverage` gates locally on the same bar, and a test fails if the two drift. Dangling IDs: a TRACES comment could name any well-formed ID and the extractor accepted it silently, so typos and renames that missed a call site passed unnoticed. `bun run traces:validate` cross-checks every traced ID against the table rows in requirements.md and fails with the referencing files listed. It spans UT/IT as well, which the coverage orphan list ignores by design. This currently reports DR-189 and UT-188, which are being defined separately. Flaky offlineCatalog test: the first dynamic import of the service paid ~1s to transform its dependency graph, charged to a test body against vitest's 5s default. Alone it passed; under suite-wide contention it timed out. The import is now warmed at collection time, so no test is timing the compiler — the timeout is deliberately unchanged. The store shim also drops subscribers from module instances discarded by resetModules, which previously leaked across tests.
431 lines
16 KiB
YAML
431 lines
16 KiB
YAML
name: Build & Release
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*'
|
||
workflow_dispatch:
|
||
inputs:
|
||
version:
|
||
description: 'Version to build (e.g., v1.0.0)'
|
||
required: false
|
||
|
||
env:
|
||
RUST_BACKTRACE: 1
|
||
CARGO_TERM_COLOR: always
|
||
|
||
jobs:
|
||
test:
|
||
name: Run Tests
|
||
runs-on: linux/amd64
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.cargo/registry
|
||
~/.cargo/git
|
||
src-tauri/target
|
||
key: ${{ runner.os }}-cargo-host-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-host-
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
- name: Install dependencies
|
||
run: bun install
|
||
|
||
- name: Run frontend tests
|
||
run: |
|
||
bunx svelte-kit sync
|
||
bun run test --run
|
||
continue-on-error: false
|
||
|
||
# Same gate as build-and-test.yml. A release must not ship from a tree
|
||
# that would fail the per-commit checks. rustfmt/clippy come from the
|
||
# builder image; nothing is installed here.
|
||
- name: Check Rust formatting
|
||
run: |
|
||
cd src-tauri
|
||
cargo fmt --all -- --check
|
||
continue-on-error: false
|
||
|
||
# Advisory until the ~51 pre-existing warnings are cleared; see the longer
|
||
# note in build-and-test.yml. Tighten both to `-- -D warnings` together.
|
||
- name: Run clippy (advisory)
|
||
run: |
|
||
cd src-tauri
|
||
cargo clippy --all-targets
|
||
|
||
- name: Run Rust tests
|
||
run: bun run test:rust
|
||
continue-on-error: false
|
||
|
||
- name: Check TypeScript
|
||
run: bun run check
|
||
continue-on-error: false
|
||
|
||
build-linux:
|
||
name: Build Linux
|
||
runs-on: linux/amd64
|
||
needs: test
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.cargo/registry
|
||
~/.cargo/git
|
||
src-tauri/target
|
||
key: ${{ runner.os }}-cargo-host-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-host-
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
- name: Install dependencies
|
||
run: bun install
|
||
|
||
# The Linux job previously had no version step at all, so a tagged release
|
||
# built Linux packages from whatever version happened to be committed.
|
||
- name: Set app version from tag
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
|
||
- name: Build for Linux
|
||
run: bun run tauri build
|
||
env:
|
||
TAURI_SKIP_UPDATER: true
|
||
|
||
- name: Prepare Linux artifacts
|
||
run: |
|
||
mkdir -p dist/linux
|
||
# Copy AppImage
|
||
if [ -f "src-tauri/target/release/bundle/appimage/jellytau_"*.AppImage ]; then
|
||
cp src-tauri/target/release/bundle/appimage/jellytau_*.AppImage dist/linux/
|
||
fi
|
||
# Copy .deb if built
|
||
if [ -f "src-tauri/target/release/bundle/deb/jellytau_"*.deb ]; then
|
||
cp src-tauri/target/release/bundle/deb/jellytau_*.deb dist/linux/
|
||
fi
|
||
ls -lah dist/linux/
|
||
|
||
- name: Upload Linux build artifact
|
||
uses: actions/upload-artifact@v3
|
||
with:
|
||
name: jellytau-linux
|
||
path: dist/linux/
|
||
retention-days: 30
|
||
|
||
build-windows:
|
||
name: Build Windows
|
||
runs-on: linux/amd64
|
||
needs: test
|
||
# Cross-compiled from Linux via the official Tauri path (MSVC + cargo-xwin),
|
||
# baked into the builder image. No toolchain installs here — the image has
|
||
# cargo-xwin, clang/clang-cl, lld, llvm, nsis and the msvc target.
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.cargo/registry
|
||
~/.cargo/git
|
||
~/.cache/cargo-xwin
|
||
src-tauri/target
|
||
key: ${{ runner.os }}-cargo-windows-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-windows-
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
# The tag is the single source of truth for a release version; the script
|
||
# stamps every file that carries it (package.json, tauri.conf.json,
|
||
# Cargo.toml, Cargo.lock). This step used to sed only tauri.conf.json, so
|
||
# the other three shipped whatever was committed.
|
||
- name: Set app version from tag
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
|
||
- name: Build Windows (NSIS installer + exe)
|
||
run: OUTPUT_DIR="$PWD/dist/windows" WIN_BUNDLES=nsis ./scripts/build-windows-cross.sh
|
||
|
||
- name: List Windows artifacts
|
||
run: ls -lah dist/windows/
|
||
|
||
- name: Upload Windows build artifact
|
||
uses: actions/upload-artifact@v3
|
||
with:
|
||
name: jellytau-windows
|
||
path: dist/windows/
|
||
retention-days: 30
|
||
|
||
build-android:
|
||
name: Build Android
|
||
runs-on: linux/amd64
|
||
needs: test
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
env:
|
||
ANDROID_HOME: /opt/android-sdk
|
||
ANDROID_SDK_ROOT: /opt/android-sdk
|
||
ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.cargo/registry
|
||
~/.cargo/git
|
||
src-tauri/target
|
||
key: ${{ runner.os }}-cargo-android-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-android-
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
- name: Install dependencies
|
||
run: bun install
|
||
|
||
# Stamp before `android init`: it derives its generated project (including
|
||
# the initial versionCode) from tauri.conf.json.
|
||
- name: Set app version from tag
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
|
||
- name: Initialize Android project
|
||
run: bun run tauri android init
|
||
|
||
# Re-run after init: tauri.properties only exists now, and its
|
||
# autogenerated versionCode (0.0.15 -> 15) is both tiny and NOT monotonic
|
||
# against the 1000 floor already shipped in the field. The script rewrites
|
||
# it as 1000 + major*10000 + minor*100 + patch. Runs unconditionally so
|
||
# untagged builds get a sane code too, derived from git describe.
|
||
- name: Pin a monotonic Android versionCode
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
|
||
- name: Sync custom Android sources & gradle config
|
||
run: ./scripts/sync-android-sources.sh
|
||
|
||
- name: Write signing keystore
|
||
run: |
|
||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/jellytau-release.jks"
|
||
cat > src-tauri/gen/android/keystore.properties <<EOF
|
||
storeFile=$RUNNER_TEMP/jellytau-release.jks
|
||
storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
|
||
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
EOF
|
||
|
||
- name: Build signed Android APK
|
||
run: bun run tauri android build --apk true --target aarch64
|
||
|
||
- name: Collect & verify signed APK
|
||
run: |
|
||
mkdir -p dist/android
|
||
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name '*-release.apk' | head -1)
|
||
if [ -z "$APK" ]; then echo "❌ No release APK produced"; exit 1; fi
|
||
cp "$APK" dist/android/jellytau-release.apk
|
||
APKSIGNER=$(find "$ANDROID_SDK_ROOT/build-tools" -name apksigner | sort -V | tail -1)
|
||
echo "🔏 Verifying signature with $APKSIGNER"
|
||
"$APKSIGNER" verify --print-certs dist/android/jellytau-release.apk
|
||
ls -lah dist/android/
|
||
|
||
- name: Upload Android build artifact
|
||
uses: actions/upload-artifact@v3
|
||
with:
|
||
name: jellytau-android
|
||
path: dist/android/
|
||
retention-days: 30
|
||
|
||
create-release:
|
||
name: Create Release
|
||
runs-on: linux/amd64
|
||
needs: [build-linux, build-windows, build-android]
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Get version from tag
|
||
id: tag_name
|
||
run: |
|
||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||
echo "RELEASE_NAME=JellyTau ${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Download Linux artifacts
|
||
uses: actions/download-artifact@v3
|
||
with:
|
||
name: jellytau-linux
|
||
path: artifacts/linux/
|
||
|
||
- name: Download Windows artifacts
|
||
uses: actions/download-artifact@v3
|
||
with:
|
||
name: jellytau-windows
|
||
path: artifacts/windows/
|
||
|
||
- name: Download Android artifacts
|
||
uses: actions/download-artifact@v3
|
||
with:
|
||
name: jellytau-android
|
||
path: artifacts/android/
|
||
|
||
- name: Prepare release notes
|
||
id: release_notes
|
||
run: |
|
||
VERSION="${{ steps.tag_name.outputs.VERSION }}"
|
||
echo "## JellyTau $VERSION Release" > release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Downloads" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Linux" >> release_notes.md
|
||
echo "- **AppImage** - Run directly on most Linux distributions" >> release_notes.md
|
||
echo "- **DEB** - Install via \`sudo dpkg -i jellytau_*.deb\` (Ubuntu/Debian)" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Windows" >> release_notes.md
|
||
echo "- **Installer (.exe)** - Run \`jellytau_*-setup.exe\` (NSIS). Unsigned — SmartScreen may warn on first run." >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Android" >> release_notes.md
|
||
echo "- **APK** - Install via \`adb install jellytau-release.apk\` or sideload via file manager" >> release_notes.md
|
||
echo "- **AAB** - Upload to Google Play Console or testing platforms" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### What's New" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "See [CHANGELOG.md](CHANGELOG.md) for detailed changes." >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Installation" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Linux (AppImage)" >> release_notes.md
|
||
echo "\`\`\`bash" >> release_notes.md
|
||
echo "chmod +x jellytau_*.AppImage" >> release_notes.md
|
||
echo "./jellytau_*.AppImage" >> release_notes.md
|
||
echo "\`\`\`" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Linux (DEB)" >> release_notes.md
|
||
echo "\`\`\`bash" >> release_notes.md
|
||
echo "sudo dpkg -i jellytau_*.deb" >> release_notes.md
|
||
echo "jellytau" >> release_notes.md
|
||
echo "\`\`\`" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Android" >> release_notes.md
|
||
echo "- Sideload: Download APK and install via file manager or ADB" >> release_notes.md
|
||
echo "- Play Store: Coming soon" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Known Issues" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "See [GitHub Issues](../../issues) for reported bugs." >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Requirements" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "**Linux:**" >> release_notes.md
|
||
echo "- 64-bit Linux system" >> release_notes.md
|
||
echo "- GLIBC 2.29+" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "**Android:**" >> release_notes.md
|
||
echo "- Android 8.0 or higher" >> release_notes.md
|
||
echo "- 50MB free storage" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "---" >> release_notes.md
|
||
echo "Built with Tauri, SvelteKit, and Rust" >> release_notes.md
|
||
|
||
- name: Publish Gitea release & upload assets
|
||
env:
|
||
# GITEA_TOKEN (a PAT) is preferred; falls back to the auto-provided token.
|
||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -e
|
||
command -v jq >/dev/null || { echo "❌ jq is required on the runner"; exit 1; }
|
||
VERSION="${{ steps.tag_name.outputs.VERSION }}"
|
||
API="${GITHUB_SERVER_URL}/api/v1"
|
||
REPO="${GITHUB_REPOSITORY}"
|
||
TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}"
|
||
case "$VERSION" in *rc*|*beta*|*alpha*) PRE=true;; *) PRE=false;; esac
|
||
|
||
PAYLOAD=$(jq -n \
|
||
--arg tag "$VERSION" \
|
||
--arg name "JellyTau $VERSION" \
|
||
--rawfile body release_notes.md \
|
||
--argjson pre "$PRE" \
|
||
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:$pre}')
|
||
|
||
echo "📦 Creating release $VERSION on $REPO"
|
||
# -f drops on HTTP error; capture status so an existing release (409) is handled gracefully.
|
||
HTTP=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$API/repos/$REPO/releases" \
|
||
-H "Authorization: token $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d "$PAYLOAD")
|
||
if [ "$HTTP" = "201" ]; then
|
||
RELEASE_ID=$(jq -r '.id' resp.json)
|
||
elif [ "$HTTP" = "409" ]; then
|
||
echo "ℹ️ Release $VERSION already exists; fetching its id to upload assets"
|
||
RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$VERSION" \
|
||
-H "Authorization: token $TOKEN" | jq -r '.id')
|
||
else
|
||
echo "❌ Failed to create release (HTTP $HTTP):"; cat resp.json; exit 1
|
||
fi
|
||
echo "Release id=$RELEASE_ID"
|
||
|
||
for f in artifacts/android/* artifacts/linux/* artifacts/windows/*; do
|
||
[ -f "$f" ] || continue
|
||
echo "⬆️ Uploading $(basename "$f")"
|
||
curl -fsS -X POST \
|
||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
|
||
-H "Authorization: token $TOKEN" \
|
||
-F "attachment=@$f" >/dev/null
|
||
done
|
||
echo "✅ Release $VERSION published with assets"
|