diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml index 9c013c25..6b3a856c 100644 --- a/.gitea/workflows/build-and-test.yml +++ b/.gitea/workflows/build-and-test.yml @@ -89,6 +89,15 @@ jobs: # sit on master until somebody cut a tag. These three steps are what make # those configs load-bearing. All are project deps installed by # `bun install`; nothing is fetched at job time. + # Cheap tripwire for a class of defect this repo kept hitting: tooling on + # a rarely-taken path. scripts/build-android.sh ran `npm install` on its + # clean-build branch -- in a bun project, ignoring bun.lock and + # re-resolving the tree, which is how the Tauri plugin crate/package + # versions drifted apart and broke a release build. It survived because + # clean builds are rare. + - name: Check build tooling + run: bash scripts/check-tooling.sh + - name: Check formatting run: bun run format:check @@ -107,6 +116,28 @@ jobs: bunx svelte-kit sync bun run check + # Tauri refuses to build when a plugin's Rust crate and npm package are on + # different minor versions. Nothing here runs `tauri build` -- that only + # happens on a tag -- so a mismatch introduced on master stayed invisible + # until the release build, which is where it was found: v0.10.0 prep hit + # `tauri-plugin-log (v2.8.0) : @tauri-apps/plugin-log (v2.9.0)`. `cargo + # check`, clippy, the tests and svelte-check had all passed. + # + # `tauri info` performs the same comparison the bundler does, without a + # build. Grepping its output is crude, but the alternative is discovering + # this at tag time again. + - name: Check Tauri plugin versions match + run: | + set -e + if bunx tauri info 2>&1 | tee /tmp/tauri-info.txt | grep -q "version mismatched"; then + echo "::error::A Tauri plugin's Rust crate and npm package versions disagree." + echo "::error::The release build will refuse to start. Align them in" + echo "::error::src-tauri/Cargo.toml and package.json (both are pinned exactly)." + grep -A6 "version mismatched" /tmp/tauri-info.txt || true + exit 1 + fi + echo "βœ… Tauri plugin crate/package versions agree." + # Coverage rather than a bare `bun run test`: same suite, plus the # thresholds in vitest.config.ts, so a large untested module or a deleted # test fails here instead of being noticed months later. diff --git a/.gitea/workflows/build-release.yml b/.gitea/workflows/build-release.yml index 2ed80ae1..8c24fe92 100644 --- a/.gitea/workflows/build-release.yml +++ b/.gitea/workflows/build-release.yml @@ -146,9 +146,29 @@ jobs: # If TAURI_SIGNING_PRIVATE_KEY is ever absent the build fails loudly rather # than quietly shipping an unsigned release that no client will accept -- # which is the behaviour we want. + # Same hazard as the Windows job: the bundle directory is never cleaned by + # cargo and the runner reuses src-tauri/target, while the copy step below + # globs bundle/deb/*.deb and friends. Windows is where this actually bit + # (v0.8.2 shipped thirteen stale installers), but only because Linux + # packaging is newer -- the glob is identical. Remove the directory so a + # stale artifact cannot exist to be copied. + - name: Clear previous bundle output + run: rm -rf src-tauri/target/release/bundle + - name: Build for Linux run: bun run tauri build env: + # linuxdeploy's bundled `strip` cannot parse the `.relr.dyn` section + # modern toolchains emit, and fails on every bundled library: + # strip: libzstd.so.1: unknown type [0x13] section `.relr.dyn' + # failed to bundle project `failed to run linuxdeploy` + # Ubuntu 23.10+ links with -z pack-relative-relocs by default, so this + # image hits it. Skipping strip is linuxdeploy's documented escape + # hatch; the cost is a larger AppImage. Found by building the target + # locally before tagging -- nothing in CI builds the app, so a release + # would have been the first time anyone discovered the AppImage target + # does not work. + NO_STRIP: "true" TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} @@ -357,8 +377,12 @@ jobs: keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }} EOF + # `--apk` is a boolean flag, not `--apk true`. tauri-cli took a value here + # until 2.10; from 2.11 the stray `true` is parsed as a positional and the + # command fails with "unexpected argument 'true' found" before building. + # This line and scripts/build-android.sh must agree. - name: Build signed Android APK - run: bun run tauri android build --apk true --target aarch64 + run: bun run tauri android build --apk --target aarch64 - name: Collect & verify signed APK run: | @@ -413,6 +437,19 @@ jobs: name: jellytau-android path: artifacts/android/ + # Runs before the SBOM, the checksums and the upload -- everything + # downstream describes this set of files, so a stale artifact must be + # caught before it gets hashed into SHA256SUMS and published as though it + # belonged to this release. + # + # See the script for the eight months of releases that shipped their + # predecessors' Windows installers. + - name: Verify artifacts belong to this release + run: | + ./scripts/check-release-artifacts.sh \ + "${{ steps.tag_name.outputs.VERSION }}" \ + artifacts/linux artifacts/windows artifacts/android + # Software Bill of Materials, one per half of the app. Without it there is # no answer to "does this release contain ?" other than # rebuilding the tag and re-resolving it. cargo-cyclonedx is in the builder @@ -482,9 +519,11 @@ jobs: exit 1 fi - # Release notes for the update prompt come from the traceability graph, - # same source as the release body. - NOTES="$(bun run release:notes 2>/dev/null | head -c 4000 || echo "See the release page for details.")" + # What the in-app update prompt shows. Same reviewed source as the + # release body -- the CHANGELOG section for this version, not the + # traceability draft. + NOTES="$(awk -v ver="## $VERSION" '$0==ver{f=1;next} /^## /{if(f)exit} f' CHANGELOG.md | head -c 4000)" + [ -n "$NOTES" ] || NOTES="See the release page for details." jq -n \ --arg version "$PLAIN" \ @@ -549,24 +588,37 @@ jobs: # release rather than shipping and failing for users. sha256sum -c SHA256SUMS - # Release notes come from the traceability graph, not from a hardcoded - # heredoc. scripts/release-notes.ts resolves the commit range's changed - # files to their TRACES ids and then to requirement descriptions, grouping - # UR into Features and DR/IR into Improvements -- which is what CLAUDE.md - # has asked for all along, while this workflow pasted a fixed block of - # install instructions and a line saying "see CHANGELOG.md for detailed - # changes". It also linked "GitHub Issues" on a Gitea-hosted project. + # The published body is the hand-written CHANGELOG.md section for this + # version. `bun run release:notes` is printed into the job log as a + # drafting aid, but is NOT published: CLAUDE.md is explicit that its + # output is "a reviewed draft, not a final changelog", and publishing it + # unreviewed proved the point -- a range containing a repo-wide prettier + # sweep resolved to nearly the whole requirement matrix and produced notes + # claiming one release had added the entire application. + # + # A missing CHANGELOG section fails the release. A release whose notes say + # nothing is worse than one that waits for a maintainer to write two + # sentences, and the checklist already requires that entry. - name: Prepare release notes id: release_notes run: | set -e VERSION="${{ steps.tag_name.outputs.VERSION }}" + + echo "πŸ“‹ Traceability draft (for reference; not published):" + bun run release:notes 2>/dev/null || echo "(could not derive a draft)" + echo "" + + # The section between this version's heading and the next one. + CHANGES=$(awk -v ver="## $VERSION" '$0==ver{f=1;next} /^## /{if(f)exit} f' CHANGELOG.md) + if [ -z "$(echo "$CHANGES" | tr -d '[:space:]')" ]; then + echo "::error::CHANGELOG.md has no '## $VERSION' section." + echo "::error::Add the entry for this version and re-tag; see docs/release-checklist.md." + exit 1 + fi + { - echo "## JellyTau $VERSION" - echo "" - # A generated summary of what actually changed; falls back to a - # pointer rather than failing the release if the range is odd. - bun run release:notes 2>/dev/null || echo "See the commit log for changes in this release." + echo "$CHANGES" echo "" echo "### Downloads" echo "" @@ -578,8 +630,8 @@ jobs: echo "| Windows | \`*-setup.exe\` (NSIS). Unsigned β€” SmartScreen may warn on first run. |" echo "| Android | \`*.apk\` sideload, or \`*.aab\` for Play Console |" echo "" - echo "Desktop builds update themselves from here on: JellyTau checks this" - echo "release feed and can install a new version in place." + echo "Desktop builds check for updates from here and can install a new" + echo "version in place, verifying its signature first." echo "" echo "### Verifying your download" echo "" @@ -599,6 +651,7 @@ jobs: echo "---" echo "Report a problem: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/issues" } > release_notes.md + echo "πŸ“ Release notes:" cat release_notes.md diff --git a/bun.lock b/bun.lock index 661e1e14..066f4a7d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,12 +5,12 @@ "": { "name": "jellytau", "dependencies": { - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-log": "^2.9.0", - "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/api": "^2.11.1", + "@tauri-apps/plugin-log": "2.9.0", + "@tauri-apps/plugin-opener": "^2.5.4", "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-process": "^2.3.1", - "@tauri-apps/plugin-updater": "^2.10.1", + "@tauri-apps/plugin-updater": "2.10.1", "hls.js": "^1.6.15", "svelte-dnd-action": "^0.9.69", }, @@ -20,7 +20,7 @@ "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.1.18", - "@tauri-apps/cli": "^2", + "@tauri-apps/cli": "^2.11.4", "@testing-library/svelte": "^5.3.1", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.16", @@ -255,35 +255,35 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], - "@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], + "@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], - "@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="], + "@tauri-apps/cli": ["@tauri-apps/cli@2.11.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.4", "@tauri-apps/cli-darwin-x64": "2.11.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", "@tauri-apps/cli-linux-arm64-musl": "2.11.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-musl": "2.11.4", "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", "@tauri-apps/cli-win32-x64-msvc": "2.11.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ=="], - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ=="], + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ=="], - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw=="], + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A=="], - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.6", "", { "os": "linux", "cpu": "arm" }, "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg=="], + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.4", "", { "os": "linux", "cpu": "arm" }, "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ=="], - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg=="], + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA=="], - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw=="], + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw=="], - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.6", "", { "os": "linux", "cpu": "none" }, "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ=="], + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.4", "", { "os": "linux", "cpu": "none" }, "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ=="], - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA=="], + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ=="], - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ=="], + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A=="], - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ=="], + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ=="], - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg=="], + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA=="], - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="], + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="], "@tauri-apps/plugin-log": ["@tauri-apps/plugin-log@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-Ql8okrnsguk0eDq1GvRfttFV5KaeW/7vcao6bdbkXCRJ1+2sWE15ZJvJVEKVANrOKy1mRngqC3IFIAP+wP5qSw=="], - "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="], "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="], @@ -761,9 +761,9 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tauri-apps/plugin-log/@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], + "@tauri-apps/plugin-os/@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], - "@tauri-apps/plugin-updater/@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], + "@tauri-apps/plugin-process/@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], diff --git a/docs/build/ci-operations.md b/docs/build/ci-operations.md index 94837c2d..fd058075 100644 --- a/docs/build/ci-operations.md +++ b/docs/build/ci-operations.md @@ -80,6 +80,29 @@ docker run --rm gitea.tourolle.paris/dtourolle/jellytau-builder:2026.09 \ toolchain inside the job β€” a toolchain install in CI. Bump both, rebuild, push, then merge. +## Tauri plugin versions are pinned in pairs + +Every Tauri plugin exists twice: a Rust crate in `src-tauri/Cargo.toml` and an +npm package in `package.json`. **The Tauri CLI refuses to build when the two are +on different minor versions** β€” not a warning, a hard stop before compilation. + +Both sides are therefore pinned *exactly* (`"2.8.0"`, not `"^2.8.0"`). A caret +range is what let them drift apart in the first place: `bun add` took the latest +npm package while cargo held an older crate, and nothing noticed until a release +build refused to start. + +Nothing in `build-and-test.yml` runs `tauri build` β€” that happens only on a tag β€” +so this class of breakage used to be invisible until release day. The +`Check Tauri plugin versions match` step runs `tauri info`, which performs the +same comparison without building. + +To upgrade a plugin, move **both** sides together and re-run that step. Expect +the Rust side to be the constraint: a newer plugin crate may pull a large +transitive upgrade (bumping `tauri-plugin-log` to 2.9.0 also moved `wry`, +`wasm-bindgen`, `web-sys` and `webkit2gtk`), which touches the webview and +therefore video playback. That is a change to make deliberately, with a full +build and a playback check β€” not one to slip into a release. + ## Secrets Managed with the `tea` CLI (`tea actions secrets list`) or the repo settings UI. diff --git a/docs/requirements.md b/docs/requirements.md index 84b1e044..093eac08 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -410,6 +410,11 @@ Internal architecture, components, and application logic. | DR-216 | Dependencies are gated on known vulnerabilities and on licence compatibility, and the build graph is pinned to what is actually shipped. The project had no scanning of any kind: nothing checked the ~500-crate Rust graph or the JS packages against an advisory feed, and nothing checked that everything redistributed inside an MIT-licensed bundle permits it. The first run found eight vulnerabilities and one unsoundness β€” `bytes`, four in `rustls-webpki`, `time`, two in `quick-xml`, `rand` β€” every one closed by a `cargo update` nobody had reason to run. `cargo deny` (src-tauri/deny.toml) now runs in CI over advisories, licences, bans and sources. Two structural fixes matter as much as the gate: the graph is scoped to the targets actually shipped, so an advisory against an Apple-only path is correctly absent rather than ignored by ID; and the one git dependency (`libmpv`) is pinned by revision instead of by branch, since a branch means any `cargo update` silently substitutes new upstream code in the one dependency that is unsigned and links a C library into the player. Licence findings are recorded rather than waved through β€” `libmpv`/`libmpv-sys` are LGPL-2.1, which the app satisfies by dynamic linking, and that carries obligations (keep the linkage dynamic; ship libmpv's licence text with any bundle carrying the .so) | Tooling | - | Done | | DR-217 | In-app update, desktop only, over a manifest we control. `tauri-plugin-updater` and `tauri-plugin-process` are compiled for everything except Android/iOS β€” spelled as a target-triple cfg rather than `cfg(desktop)`, which Cargo does not evaluate in a `[target.'cfg(…)']` table and which therefore drops the dependency silently, surfacing much later as "Permission updater:default not found". The release workflow signs updater artifacts with a minisign key held in Gitea secrets and publishes `latest.json` to a dedicated `updater` branch, read over Gitea's raw-file URL: this instance serves `/releases/download//` but returns 404 for `/releases/latest/download/`, so there is no stable latest-release URL to point at, and the docs branch is force-pushed by publish-docs.yml so it cannot host the manifest either. Bundle targets gain `appimage`, which the release notes had been advertising for months while `tauri.conf.json` never built it β€” the artifact step globbed for `*.AppImage`, found nothing, and said nothing | Tooling | UR-077 | Done | | DR-218 | Persistent, redacted logging and a diagnostics export. `tauri-plugin-log` replaces the `env_logger` stdout-only init, giving a rotating 5 MB file, a webview target in dev, and β€” the single largest gain β€” logcat on Android, where `env_logger`'s stdout went nowhere. **Redaction runs in the log formatter, not at export**: a credential in a file on the device is already a disclosure, so stripping it on the way out would be too late; the exporter redacts a second time to cover files written by older builds. `api_key`/`X-Emby-Token`/`Authorization`/`"AccessToken"`/`Token="…"` all reduce to `[REDACTED]` while host, item ids and filenames are deliberately kept β€” a bundle scrubbed of those is one nobody can debug from. The server URL is reduced to scheme and host, dropping any embedded `user:pass@`. The panic hook chains to the previous hook rather than replacing it, because `utils/lock.rs` installs a silencing hook around tests that provoke poisoned locks on purpose. The chosen level persists to disk and is re-applied at startup, since reproducing a bug usually means restarting into it. The frontend facade keeps its untouched `console.*` pass-through (DR-204) and additionally forwards a stringified copy at info and above, so one file holds both halves of the app in order β€” which is what makes a race between them legible after the fact | Tooling | UR-078 | Done | +| DR-219 | Release notes are the reviewed CHANGELOG entry, not a generated draft. Every release from v0.0.1 to v0.9.1 published the same ~1,050 bytes of generic install instructions whose "What's New" section said "See CHANGELOG.md" β€” a link that does not resolve from a release page. Thirty-five releases, byte-identical, telling a reader nothing about what changed. The workflow now publishes the `## ` section of CHANGELOG.md and fails the release if that section is absent, since notes that say nothing are worse than a build that waits for two sentences. `release:notes` is printed into the job log as a drafting aid but is deliberately *not* published: CLAUDE.md calls its output "a reviewed draft, not a final changelog", and publishing it unreviewed proved why β€” a range containing a repo-wide formatting sweep resolved to nearly the entire requirement matrix and produced notes claiming one release had added the whole application. The script now skips cosmetic commits (`chore(format)`, `chore(deps)`, `style`) when deriving a range's files, and says how many it skipped rather than silently reporting a smaller set | Tooling | - | Done | +| DR-220 | A release ships only its own artifacts. `src-tauri/target/*/release/bundle/` is not versioned, cargo never cleans it, and the CI runner reuses the target directory β€” so the copy step's `bundle/**/*-setup.exe` glob collected every installer ever built there. Every release from v0.1.0 to v0.8.2 shipped its predecessors': sixteen Windows installers on v0.8.2, thirteen of them stale, and a download list on v0.5.0 reaching back to 0.1.0. It went unnoticed for eight months because there was nothing to notice β€” the upload loop reported success, the files were real, and the page looked busy rather than wrong. It stopped only when an unrelated cache change wiped the runner's target dir, leaving the defect dormant rather than fixed. Both desktop builds now clear the bundle directory first, so a stale file cannot exist to be copied β€” filtering the copy by version would have hidden it instead. `scripts/check-release-artifacts.sh` is the backstop for the next route nobody predicts: it runs before the SBOM, the checksums and the upload, and refuses to publish when any artifact's embedded version disagrees with the tag | Tooling | - | Done | +| DR-221 | The release path is exercised before a tag exists. Nothing in `build-and-test.yml` runs `tauri build` β€” only a tag does β€” so a whole class of breakage was invisible until release day, and two instances of it were sitting on master at once. Tauri refuses to build when a plugin's Rust crate and npm package differ by minor version, which the updater and logging work had introduced (`tauri-plugin-log 2.8.0` against `@tauri-apps/plugin-log 2.9.0`) while `cargo check`, clippy, the tests and `svelte-check` all passed; both sides are now pinned exactly rather than by caret, since a caret is what let them separate, and CI runs `tauri info` to compare them without building. The AppImage target had never once been built: linuxdeploy carries a `strip` too old to parse the `.relr.dyn` section modern toolchains emit, so bundling failed on every library β€” and Ubuntu 23.10+ links with `-z pack-relative-relocs` by default, so the builder image fails the same way a modern Arch host does. `NO_STRIP=true` is linuxdeploy's documented escape hatch; the cost is a larger, unstripped bundle. Both were found by building the target locally before tagging rather than by publishing a release that could not build | Tooling | - | Done | +| DR-222 | Build tooling matches the package manager the project declares. `scripts/build-android.sh` ran `npm install` on its clean-build path β€” in a bun project, where `packageManager` says bun and `bun.lock` is the committed lockfile. npm ignores that lockfile, re-resolves the whole tree from package.json, and writes a `package-lock.json` that `.gitignore` then hides. That is not a style preference: the JS halves of the Tauri plugins are pinned exactly against Cargo.lock because the CLI refuses to build when a plugin's crate and package differ by minor version, and a silent re-resolve is precisely how they drift apart. It survived because clean builds are rare β€” the shape shared by nearly every defect found preparing v0.10.0, where the code running on every commit was healthy and the code running on a release, a tag or a clean build had no guard at all. `scripts/check-tooling.sh` fails on any npm/yarn/pnpm invocation or foreign lockfile | Tooling | - | Done | +| DR-223 | The Android JavaVM and Application are published into `ndk_context` by this crate, not by a transitive dependency. Seven call sites (five in credentials.rs, two in lib.rs) read that process-global to reach JNI, and nothing here ever set it β€” `tao` did, three levels below anything this project names in Cargo.toml. tao 0.35.3 moved those pointers into a private struct and stopped publishing them, so the Tauri 2.11 upgrade made the first credential read abort the process on every launch: `PANIC ... android context was not initialized`. Our code had not changed; an undocumented side effect of the windowing layer had gone. The invariant is now owned here rather than assumed: `JNI_OnLoad` captures the JavaVM as the shared library loads, and the Application is resolved lazily via `ActivityThread.currentApplication()` and pinned as a global reference for the process lifetime β€” the Application rather than the Activity, since that is what `SecureStorage.initialize()` immediately reduces its argument to. Failure degrades to the encrypted-file credential path and is logged, rather than aborting. Found only by installing on a device: nothing in CI runs the app | Security | UR-012 | Done | | DR-198 | The webview runs under a real Content-Security-Policy, and the asset protocol is scoped to the one directory it still serves. `csp` was `null`, which disables CSP entirely: any script that reached the web layer β€” through a future `{@html}`, a dependency, or a devtools paste β€” would have inherited the whole IPC surface, and with it the user's session. `script-src 'self'` (Tauri injects a nonce for SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed) plus `object-src`/`frame-src 'none'` and `base-uri 'self'` is the part that is genuinely restrictive. `img-src`/`media-src`/`connect-src` cannot be: the Jellyfin origin is typed in by the user at run time and is commonly plain `http` on a LAN, so they allow `http:`/`https:` β€” a wide grant for *data*, but one that still bars `file:`, `filesystem:` and scripting schemes, and leaves `script-src` untouched. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes (including `app.html`'s `display: contents` wrapper) into markup; this is safe only while no `