diff --git a/.github/workflows/release-desktop-multi-os.yml b/.github/workflows/release-desktop-multi-os.yml index 3c381926..68167154 100644 --- a/.github/workflows/release-desktop-multi-os.yml +++ b/.github/workflows/release-desktop-multi-os.yml @@ -189,6 +189,7 @@ jobs: platform_label: linux files: | src-tauri/target/release/bundle/**/*.AppImage + src-tauri/target/release/bundle/**/*.AppImage.zsync src-tauri/target/release/bundle/**/*.deb - runner: macos-latest platform_label: macos @@ -351,6 +352,11 @@ jobs: codesign --verify --strict --verbose=4 "${TARGET_BIN}" "${TARGET_BIN}" --version + - name: Remove previous AppImage update controls + if: runner.os == 'Linux' + shell: bash + run: find src-tauri -maxdepth 1 -type f -name '*.AppImage.zsync' -delete + - name: Build desktop bundle run: npm run tauri:build:mini @@ -365,6 +371,8 @@ jobs: exit 1 fi for artifact in "${artifacts[@]}"; do + # Tauri builds in src-tauri; bundled zsyncmake writes basename.zsync in that CWD. + mv -- "src-tauri/$(basename "$artifact").zsync" "${artifact}.zsync" npm run verify:appimage -- "${artifact}" src-tauri/bin/server-x86_64-unknown-linux-gnu done diff --git a/.gitignore b/.gitignore index 520dd3d2..65bc50cd 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ ref # Tauri / Rust build outputs src-tauri/target/ +src-tauri/*.AppImage.zsync src-tauri/target-dev-lowmem/ src-tauri/gen/ src/backend/wasm/target/ diff --git a/.trellis/spec/backend/quality-guidelines.md b/.trellis/spec/backend/quality-guidelines.md index 65211c2e..5cee5204 100644 --- a/.trellis/spec/backend/quality-guidelines.md +++ b/.trellis/spec/backend/quality-guidelines.md @@ -78,16 +78,22 @@ Changes to the Tauri CLI, sidecars, Linux build runner, or release workflow must ### Commands -Use `npm run tauri:build:mini`, then `npm run verify:appimage -- `. Both verifier arguments are required and must come from the same build. +Use `npm run tauri:build:mini`, move the generated `src-tauri/.zsync` next to the final image, then run `npm run verify:appimage -- `. Both verifier arguments and the adjacent control file must come from the same build. Clear previous `src-tauri/*.AppImage.zsync` before building so a missing generator cannot reuse old output. ### Boundary and environment The Linux runner selects `scripts/appimage-patchelf.js` through `PATCHELF`. `NOTE_CONNECTION_APPIMAGE_PATCHELF` identifies the original executable; `NOTE_CONNECTION_APPIMAGE_SERVER_SUFFIX` and `NOTE_CONNECTION_APPIMAGE_SERVER_SHA256` identify the protected pkg sidecar. Only its `--set-rpath` write is suppressed; dependency queries and other ELF operations remain enabled. AppRun supplies the sidecar's library search path. Never rewrite pkg's completed ELF payload offsets. +`scripts/appimage-update.js` owns the native `gh-releases-zsync` contract for the current amd64 release. The Linux runner passes it through `LDAI_UPDATE_INFORMATION`; the official bundled appimagetool/zsyncmake generates the control after the final metadata/signing writes. Tauri CLI 2.12.1 changes CWD to `src-tauri` in `crates/tauri-cli/src/build.rs:166`, and official zsyncmake 0.6.2 emits basename.zsync in that CWD. The release workflow already owns final artifact paths, so it moves each exact control beside its image and fails if absent. Do not add a second Tauri CLI parser or search guessed directories to collect it. + +Keep the product release Latest and Godot mirrors `latest=false`. Publish exactly one matching amd64 `.zsync` with its AppImage, and include both in checksums and the final asset manifest. An unsupported architecture must not pass the current release gate. Preserve v1.9.0: it lacks embedded metadata, so testing it as a delta seed requires the explicit new control URL. + ### Validation and errors The final verifier rejects inaccessible stored SquashFS modes, invalid integration symlinks, missing desktop executables/icons, invalid image decoding, and any server hash mismatch. A matching path with changed bytes makes the patchelf adapter fail. Metadata success alone does not prove that the ELF loader, WebView, or backend starts. +The same two-argument verifier requires correct embedded update information and the adjacent control file. It validates the official plain-file header, basename/relative URL, exact Length/SHA-1 and checksum table length `ceil(Length / Blocksize) * (rsum_bytes + checksum_bytes)`. The first `Hash-Lengths` field is a sequence count, not bytes per table entry. Never implement rsync weak hashes or MD4 to duplicate the official updater; table-content correctness is accepted through official reconstruction followed by full target SHA-256 equality. + ### Acceptance cases - Good: the Ubuntu 22.04 artifact launches as a normal user, serves authenticated graph/reader requests, and shuts down its sidecars. @@ -96,7 +102,7 @@ The final verifier rejects inaccessible stored SquashFS modes, invalid integrati ### Required tests -Run the portability and patchelf behavioral suites. Verify the final image on the baseline OS and a newer host, retaining its SHA-256, logs, and screenshots. The installed Markdown worker must be discovered under Tauri's suffixless name and report `engine: pulldown` without a missing-worker fallback. +Run the update, portability and patchelf behavioral suites. Update tests must cover a good official control fixture, missing/stale controls, same-size image corruption, malformed headers/tables and wrong owner/repository/channel/architecture/URL. Verify the final image on the baseline OS and a newer host, retaining its SHA-256, logs, and screenshots. The installed Markdown worker must be discovered under Tauri's suffixless name and report `engine: pulldown` without a missing-worker fallback. Run the offline simulation worker suite and load a graph with external networking disabled, keeping loopback available for the sidecar. Graph layout dependencies must be bundled; an initial window and successful graph API do not establish that worker-produced node positions render. diff --git a/docs/diataxis-map.json b/docs/diataxis-map.json index c4b2bad1..3fa05041 100644 --- a/docs/diataxis-map.json +++ b/docs/diataxis-map.json @@ -126,11 +126,11 @@ "id": "release-and-governance", "category": "reference", "en": { - "canonical": ["docs/en/release_v1.6.0_report.md", "docs/release_notes_v1.6.0.md"], + "canonical": ["docs/en/release_v1.6.0_report.md", "docs/release_notes_v1.6.0.md", "docs/release_notes_v1.9.1.md"], "diataxis": "docs/diataxis/en/reference/release-and-governance.md" }, "zh": { - "canonical": ["docs/zh/release_v1.6.0_report.md", "docs/release_notes_v1.6.0.md"], + "canonical": ["docs/zh/release_v1.6.0_report.md", "docs/release_notes_v1.6.0.md", "docs/release_notes_v1.9.1.md"], "diataxis": "docs/diataxis/zh/reference/release-and-governance.md" } }, diff --git a/docs/diataxis/en/how-to/test-linux-appimage.md b/docs/diataxis/en/how-to/test-linux-appimage.md index 3294391f..ed4a751e 100644 --- a/docs/diataxis/en/how-to/test-linux-appimage.md +++ b/docs/diataxis/en/how-to/test-linux-appimage.md @@ -9,21 +9,34 @@ Install the usual Tauri Linux build dependencies and the artifact verifier tools ```bash sudo apt-get install desktop-file-utils libgdk-pixbuf2.0-bin squashfs-tools npm ci +find src-tauri -maxdepth 1 -type f -name '*.AppImage.zsync' -delete npm run tauri:build:mini -npm run verify:appimage -- src-tauri/target/release/bundle/appimage/NoteConnection_1.8.0_amd64.AppImage src-tauri/bin/server-x86_64-unknown-linux-gnu -sha256sum src-tauri/target/release/bundle/appimage/*.AppImage +appimage=src-tauri/target/release/bundle/appimage/NoteConnection_1.9.1_amd64.AppImage +mv -- "src-tauri/$(basename "$appimage").zsync" "${appimage}.zsync" +npm run verify:appimage -- "$appimage" src-tauri/bin/server-x86_64-unknown-linux-gnu +sha256sum "$appimage" "${appimage}.zsync" ``` -Substitute the actual release filename. `verify:appimage` reads the final SquashFS manifest, checks that packaged files are readable and executables/directories usable by users other than the build owner, then extracts into a fresh temporary directory with `unsquashfs`. This preserves stored permissions; the AppImage runtime's `--appimage-extract` can replace directory modes with 0700. +Substitute the actual release filename and output directory, including any custom Cargo target directory. The official bundled `zsyncmake` writes `.zsync` in Tauri's `src-tauri` working directory even when the image output is absolute. Move that exact file beside the image; a missing source is a build failure. The release workflow clears previous controls before building and performs this move before verification and either upload. + +`verify:appimage` requires the adjacent `.zsync`, checks native update information, filename/relative URL, file length, SHA-1 and checksum-table structure against the exact final image. It then reads the final SquashFS manifest, checks that packaged files are readable and executables/directories usable by users other than the build owner, and extracts into a fresh temporary directory with `unsquashfs`. This preserves stored permissions; the AppImage runtime's `--appimage-extract` can replace directory modes with 0700. The verifier rejects absolute, escaping, dangling, or cyclic integration links, checks `.DirIcon`, the root desktop entry, `AppRun`, `AppRun.wrapped`, the desktop `Exec` target and themed icon, validates the desktop file with `desktop-file-validate`, and decodes the icon with GDK Pixbuf. The required second argument is the original server sidecar from the same build: the packaged server must match its complete SHA-256, including the appended pkg payload. It deletes its temporary extraction on success or failure. The Linux release workflow runs it before either artifact upload. Run the regression suite with: ```bash -npm test -- --runInBand src/appimage.portability.test.ts src/appimage.patchelf.test.ts src/simulation.worker.offline.test.ts +npm test -- --runInBand src/appimage.update.test.ts src/appimage.portability.test.ts src/appimage.patchelf.test.ts src/simulation.worker.offline.test.ts ``` +## Verify native updates and publish the pair + +The Linux release contract currently supports amd64. Its embedded information is `gh-releases-zsync|Jacobinwwey|NoteConnection|latest|NoteConnection_*_amd64.AppImage.zsync`, owned by `scripts/appimage-update.js`. Keep exactly one matching control asset in the product release. Other architectures need their own explicit release contract before publication. Product releases must be marked Latest when the accepted draft is published; Godot mirror releases remain `latest=false`. + +Use the plugin's bundled official generator. It runs after appimagetool finishes metadata and signing. Do not alter the AppImage after generation. Publish the AppImage and its adjacent `.zsync` together in the same GitHub release, include both in `SHA256SUMS.txt` and the asset manifest, and verify downloaded bytes against the accepted CI build. The zsync `URL` is the AppImage basename, resolved relative to its control-file URL. Do not replace it with a build-host path or generate a control file from an earlier image. + +The artifact gate checks the control header and table structure. Establish actual delta reconstruction separately with an official AppImageUpdate/zsync client, preserving its version, command, logs and target SHA-256 comparison. A v1.9.0 AppImage can supply local seed bytes when the new control URL is explicitly provided; it cannot discover updates by itself because v1.9.0 contains no update information. Before publication, serve the exact CI candidate and its unchanged control file together from a local HTTP server that supports Range requests, and provide that control URL explicitly to the client. Require reconstructed SHA-256 to equal the candidate. After publication, repeat against `https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/NoteConnection_1.9.1_amd64.AppImage.zsync` and verify the public download matches the accepted candidate. Test automatic discovery with a metadata-bearing image after the product release is Latest. + ## Verify application behavior The artifact gate checks packaging. Also launch the actual final AppImage on Ubuntu 22.04 from a directory outside the repository as a normal user. Use isolated XDG config/data directories, `NOTE_CONNECTION_CONFIG_PATH`, and disposable Markdown notes; keep the operating-system home and toolchain caches in their normal locations. Confirm a real visible window, successful graph loading, note reading, switching into and out of Path mode, and clean shutdown of the server and Godot sidecars. Retain the exact artifact SHA-256, source commit, installed Tauri CLI version, command output, and screenshots with the test report. diff --git a/docs/diataxis/en/reference/release-and-governance.md b/docs/diataxis/en/reference/release-and-governance.md index 01e24781..f0bd058f 100644 --- a/docs/diataxis/en/reference/release-and-governance.md +++ b/docs/diataxis/en/reference/release-and-governance.md @@ -16,6 +16,7 @@ This page is the governance index for release pipelines, docs delivery, and lear - [docs/release_notes_v1.6.0.md](../../../release_notes_v1.6.0.md) - [docs/release_notes_v1.6.7.md](../../../release_notes_v1.6.7.md) - [docs/release_notes_v1.7.0.md](../../../release_notes_v1.7.0.md) +- [docs/release_notes_v1.9.1.md](../../../release_notes_v1.9.1.md) - [Knowledge Mastery Evolution Roadmap](../explanation/knowledge-mastery-evolution-roadmap.md) - [Development Progress Dashboard](../explanation/development-progress-dashboard.md) diff --git a/docs/diataxis/zh/reference/release-and-governance.md b/docs/diataxis/zh/reference/release-and-governance.md index 35986fb7..a640e3f8 100644 --- a/docs/diataxis/zh/reference/release-and-governance.md +++ b/docs/diataxis/zh/reference/release-and-governance.md @@ -16,6 +16,7 @@ - [docs/release_notes_v1.6.0.md](../../../release_notes_v1.6.0.md) - [docs/release_notes_v1.6.7.md](../../../release_notes_v1.6.7.md) - [docs/release_notes_v1.7.0.md](../../../release_notes_v1.7.0.md) +- [docs/release_notes_v1.9.1.md](../../../release_notes_v1.9.1.md) - [知识彻底掌握演进路线图](../explanation/knowledge-mastery-evolution-roadmap.md) - [开发进度看板](../explanation/development-progress-dashboard.md) diff --git a/docs/release_notes_v1.9.1.md b/docs/release_notes_v1.9.1.md new file mode 100644 index 00000000..69bd86bf --- /dev/null +++ b/docs/release_notes_v1.9.1.md @@ -0,0 +1,113 @@ +# NoteConnection v1.9.1 + +## English + +Comparison baseline: [v1.9.0...v1.9.1](https://github.com/Jacobinwwey/NoteConnection/compare/v1.9.0...v1.9.1). + +### Linux AppImage native updates + +- Addresses the remaining missing-update-information warning from [AppImage catalog PR #8838](https://github.com/AppImage/appimage.github.io/pull/8838). The Linux build now supplies native `gh-releases-zsync` metadata through linuxdeploy's supported `LDAI_UPDATE_INFORMATION`, targeting the product's Latest release and its amd64 `.AppImage.zsync` asset. +- Uses the official bundled appimagetool/zsyncmake to generate a control file after the AppImage's metadata and signing writes are complete. The release workflow moves the actual control file from Tauri's `src-tauri` working directory beside the final image. Missing generation fails the release gate; old controls are removed before the build. +- Preserves the published v1.9.0 release. Its AppImage can provide local bytes for a delta update when the new `.zsync` URL is supplied explicitly. It cannot automatically discover this update because its embedded update information is absent. + +### Release integrity and workflow + +- Extends the existing two-argument AppImage verifier to require the adjacent control file, exact embedded update target, matching filename/relative URL, file length, SHA-1 and structurally valid checksum table. Existing stored-permission, desktop/icon, portable-link and original-server payload checks remain mandatory. +- Includes `.AppImage.zsync` in the shared Linux asset list used by both workflow artifacts and the draft GitHub release. The current release contract accepts amd64; other architectures require a separate explicit contract. Accepted product releases are published as Latest, while Godot mirror releases remain `latest=false`. +- Updates the product version to 1.9.1 in npm metadata and Tauri configuration. The native About dialog already reads that configured version. Documents the exact local collection/verification procedure, paired checksums/manifests and official-client reconstruction requirement. + +### Verified source and release artifacts + +- Release build source: [`cd6548f315e7712bd5034e1025cb603174795528`](https://github.com/Jacobinwwey/NoteConnection/commit/cd6548f315e7712bd5034e1025cb603174795528), based on `origin/main` at `72eac8981f434d7c7e37f0f4ba064f3b81555367`. [Source PR #3](https://github.com/Jacobinwwey/NoteConnection/pull/3). The v1.9.1 tag and binaries retain this build source; the final evidence documentation is a separate follow-up with no production-code changes. +- Final unfiltered Jest passed **177 suites / 1,830 tests**, with no failures, skipped tests or todos and a natural exit code of 0. Targeted AppImage coverage passed **3 suites / 71 tests**, including 33 update cases. Strict Rust `--locked` passed **34 tests**, with one pre-existing mobile probe ignored. +- TypeScript/build, 40 frontend runtime assets, actionlint, actual workflow shell success/failure cases, version/lock consistency, Diataxis and the documentation site passed independent review. +- [Release CI 37266034722](https://github.com/Jacobinwwey/NoteConnection/actions/runs/37266034722) passed Linux, Windows and macOS packaging. All four workflow artifact archives match GitHub's SHA-256 digests; six draft release assets match the accepted file sizes and hashes. The public source archive's 15 changed files match the build commit byte for byte. +- Final AppImage: **201,521,656 bytes**, SHA-256 `4f843ceffdf3c7c0596d901dd91604d0fa70a755ff9bea2ccd44e3eb76c0977b`. +- Companion `.zsync`: **344,642 bytes**, SHA-256 `4b9a4944b3b37ae6dd3dcca173a7803963bfd4e5b0400e4cad91a991a3ce82cb`. +- The full AppImage gate passed locally using the original server from that same CI run, SHA-256 `9f7e877652d9a42e0564d58095bff0143a7a56677e36c1433b26bf8ac621465e`. + +### Actual differential update and startup + +Both official clients reconstructed the final v1.9.1 artifact from an unchanged copy of the public v1.9.0 AppImage. Each output's complete SHA-256 exactly matches the final CI artifact. The unmodified plugin-generated control file was served from an isolated HTTP server with real Range/206 responses. + +| Official client | Local bytes reused | Target content downloaded | Fraction of complete target | Exit | +| --- | ---: | ---: | ---: | ---: | +| AppImageUpdate 2.0.0-alpha-1-20251018 | 189,865,984 | 13,699,576 | 6.80% | 0 | +| zsync2 2.0.0-alpha-1 | 189,865,984 | 11,655,672 | 5.78% | 0 | + +These download counts exclude control-file and transport overhead. Independent accounting of HTTP byte ranges matches the client statistics. A 404 failure test retained the seed, exited 1 and left only an incomplete `.part` file. No old-version metadata or public artifact was rewritten. Because v1.9.0 has no metadata, this test supplied the new control URL explicitly; users can manually download v1.9.1 as the first version with native update discovery. + +The reconstructed application also passed ordinary-user native FUSE startup on Ubuntu 24.04.2. Its real initial language-selection window rendered, a normal window close exited 0, and the application, test processes and mounts were cleaned up. The pre-existing host service remained unchanged. + +### Linux KVM acceptance + +The exact final AppImage passed complete acceptance in the local Ubuntu 22.04.5 guest, with glibc 2.35 and QMP-confirmed KVM acceleration. The unchanged catalog worker exited 0, recorded the correct native update target and emitted no missing-update-information warning. + +- Both native FUSE and extract-and-run passed offline authenticated APIs, including unauthenticated rejection, diagnostics, knowledge folders/root, graph build, Markdown content/index/chunks, cache and knowledge state. The packaged pulldown worker ran without fallback. +- Both native About dialogs displayed **1.9.1**. Both modes rendered the three-node Force graph, DAG arrows, Markdown/Mermaid and the Godot learning path, then returned to the main window. +- Both normal window closes exited **0**. All 26 observed application-related processes and five test support processes were gone; the FUSE mount and isolated network namespace were removed. Runtime manifests remained as test evidence. The extraction cache also remained after application exit and was manually removed only after checking that no process held it open. +- The VM shut down normally. QEMU, its PID file and QGA/QMP sockets were gone; the host service and all 11 monitored macOS disk/firmware/startup files retained their recorded state. + +The screenshots also preserve existing visual limitations: reader loading/outline placeholders, clipped Mermaid labels, DAG label overlap and overlap/clipping in parts of the Godot UI. This release does not claim to fix those independent UI issues. See the [KVM acceptance summary and 12 screenshots](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/NoteConnection_1.9.1_linux-kvm-validation.zip). + +Windows/macOS results above establish successful packaging, not complete application GUI acceptance. Android assets are excluded from this desktop release; existing signing/device gates remain in force. + +### Download verification + +[SHA256SUMS](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/SHA256SUMS.txt) · [CI provenance](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/public-ci-manifest.json) · [Source validation](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/source-validation.json) · [Differential update evidence](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/NoteConnection_1.9.1_update-validation.md). + +## 中文 + +对比基线:[v1.9.0...v1.9.1](https://github.com/Jacobinwwey/NoteConnection/compare/v1.9.0...v1.9.1)。 + +### Linux AppImage 原生更新 + +- 修复 [AppImage 目录 PR #8838](https://github.com/AppImage/appimage.github.io/pull/8838) 剩余的更新信息缺失警告。Linux 构建现在通过 linuxdeploy 支持的 `LDAI_UPDATE_INFORMATION` 写入原生 `gh-releases-zsync` 元数据,指向产品 Latest 发布中的 amd64 `.AppImage.zsync` 资产。 +- 使用官方内置 appimagetool/zsyncmake,在 AppImage 元数据和签名写入结束后生成控制文件。发布工作流从 Tauri 的 `src-tauri` 工作目录收拢实际生成的控制文件,将其移动到最终镜像旁;生成缺失会使发布门禁失败,构建前清除旧控制文件。 +- 保留已公开的 v1.9.0 发布。显式提供新的 `.zsync` URL 时,其 AppImage 可以作为差分更新的本地种子;由于该版本没有内嵌更新信息,无法自动发现本次更新。 + +### 发布完整性与工作流 + +- 扩展现有双参数 AppImage 校验器:必须存在相邻控制文件,并校验精确的内嵌更新目标、文件名/相对 URL、文件长度、SHA-1 和校验表结构。既有的存储权限、desktop/图标、可移植链接和原始 server payload 检查继续作为必需门禁。 +- 将 `.AppImage.zsync` 加入 workflow artifact 与 GitHub 草稿发布共用的 Linux 资产列表。当前发布契约只接受 amd64,其他架构需要明确的独立契约。合格产品发布设为 Latest,Godot 镜像继续保持 `latest=false`。 +- 将 npm 元数据与 Tauri 配置中的产品版本更新到 1.9.1。原生 About 对话框已从该配置读取版本。补充本地精确收拢/校验步骤、成对散列清单及 manifest 要求,并明确官方客户端重建验收。 + +### 已验证源码与发布制品 + +- 构建源码:[`cd6548f315e7712bd5034e1025cb603174795528`](https://github.com/Jacobinwwey/NoteConnection/commit/cd6548f315e7712bd5034e1025cb603174795528),基于 `origin/main` 的 `72eac8981f434d7c7e37f0f4ba064f3b81555367`。[源码 PR #3](https://github.com/Jacobinwwey/NoteConnection/pull/3)。v1.9.1 标签和二进制保持该构建源码;最终验收说明是独立文档补充,不改变生产代码。 +- 最终完整未筛选 Jest **177 套件 / 1,830 测试通过**,无失败、跳过或 todo,正常退出 0。AppImage 定向测试 **3 套件 / 71 测试通过**,其中新增更新检查 33 例。严格 Rust `--locked` **34 测试通过**,1 个既有移动端探针忽略。 +- TypeScript/构建、40 项前端资源、actionlint、工作流实际 shell 成功/失败场景、版本/锁文件一致性、Diataxis 和文档站均通过独立审查。 +- [发布 CI 37266034722](https://github.com/Jacobinwwey/NoteConnection/actions/runs/37266034722) 的 Linux、Windows、macOS 打包全部成功。4 个 workflow artifact 归档的 SHA-256 与 GitHub 一致,6 个草稿资产大小和散列与验收文件一致。公开源码归档的 15 个变更文件逐字节匹配构建提交。 +- 最终 AppImage:**201,521,656 字节**,SHA-256 `4f843ceffdf3c7c0596d901dd91604d0fa70a755ff9bea2ccd44e3eb76c0977b`。 +- 配套 `.zsync`:**344,642 字节**,SHA-256 `4b9a4944b3b37ae6dd3dcca173a7803963bfd4e5b0400e4cad91a991a3ce82cb`。 +- 本地完整 AppImage 门禁通过,使用同次 CI 的原始 server,SHA-256 `9f7e877652d9a42e0564d58095bff0143a7a56677e36c1433b26bf8ac621465e`。 + +### 真实差分更新与启动 + +两个官方客户端都从未改写的公开 v1.9.0 AppImage 副本重建出最终 v1.9.1;完整输出 SHA-256 均精确匹配 CI 制品。未改动的官方控制文件由隔离 HTTP 服务提供,抓包确认真实 Range/206 响应。 + +| 官方客户端 | 复用本地字节 | 下载目标内容 | 占完整目标 | 退出码 | +| --- | ---: | ---: | ---: | ---: | +| AppImageUpdate 2.0.0-alpha-1-20251018 | 189,865,984 | 13,699,576 | 6.80% | 0 | +| zsync2 2.0.0-alpha-1 | 189,865,984 | 11,655,672 | 5.78% | 0 | + +下载量不包含控制文件和传输开销;按 HTTP 实际字节区间独立核算,与工具统计精确一致。404 失败测试保留种子文件,退出 1,仅留下未完成的 `.part`。未改写旧版元数据或公开制品。由于 v1.9.0 没有元数据,本次测试显式提供新的控制 URL;用户可以手动下载 v1.9.1,作为首次支持原生更新发现的版本。 + +重建程序还通过普通用户在 Ubuntu 24.04.2 上的原生 FUSE 启动检查:真实首次语言选择界面正常显示,正常关闭窗口后退出 0,应用、辅助进程和挂载均清理,宿主原有服务保持不变。 + +### Linux KVM 验收 + +最终同一份 AppImage 已在本机 Ubuntu 22.04.5 客体中完成验收,glibc 为 2.35,QMP 确认真正启用 KVM 加速。未修改的目录 worker 退出 0,记录了正确的原生更新目标,未再发出更新信息缺失警告。 + +- 原生 FUSE 与解包运行均通过离线认证 API,包括未认证拒绝、诊断、知识库目录/根路径、图构建、Markdown 内容/索引/分块、缓存和知识状态;打包的 pulldown worker 正常运行,没有回退。 +- 两种模式的原生 About 均显示 **1.9.1**,实际验证三节点 Force 图、DAG 箭头、Markdown/Mermaid、Godot 学习路径,以及返回主窗口。 +- 两次正常关闭窗口均退出 **0**。26 个观测到的应用相关进程和 5 个测试辅助进程全部消失,FUSE 挂载及隔离网络命名空间已移除。运行时 manifest 保留作为测试证据;解包缓存也会在应用退出后保留,确认无进程占用后才由测试人员精确手动删除。 +- 虚拟机正常关机,QEMU、PID 文件及 QGA/QMP socket 均消失;宿主原有服务以及 11 个受监测的 macOS 磁盘、固件和启动文件保持记录中的状态。 + +截图如实保留既有视觉限制:阅读器加载/目录占位提示、Mermaid 标签裁切、DAG 标签重叠,以及 Godot 部分界面的重叠或裁切。本次发布不宣称解决这些独立界面问题。详见 [KVM 验收摘要与 12 张截图](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/NoteConnection_1.9.1_linux-kvm-validation.zip)。 + +上述 Windows/macOS 结果证明打包成功,不代表完整应用 GUI 验收。Android 资产不在本次桌面发布范围,已有签名和真机门禁继续保留。 + +### 下载校验 + +[SHA256SUMS](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/SHA256SUMS.txt) · [CI 来源清单](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/public-ci-manifest.json) · [源码测试](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/source-validation.json) · [差分更新证据](https://github.com/Jacobinwwey/NoteConnection/releases/download/v1.9.1/NoteConnection_1.9.1_update-validation.md)。 diff --git a/package-lock.json b/package-lock.json index f57b0ef6..b647f78b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "noteconnection", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "noteconnection", - "version": "1.9.0", + "version": "1.9.1", "license": "ISC", "dependencies": { "@capacitor/android": "^8.0.0", diff --git a/package.json b/package.json index 64901ed7..5be17c3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "noteconnection", - "version": "1.9.0", + "version": "1.9.1", "description": "Hierarchical Knowledge Graph Visualization System", "main": "dist/src/server.js", "bin": { diff --git a/scripts/appimage-update.js b/scripts/appimage-update.js new file mode 100644 index 00000000..8c78056b --- /dev/null +++ b/scripts/appimage-update.js @@ -0,0 +1,63 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const { productName } = require('../src-tauri/tauri.conf.json'); + +const APPIMAGE_SUFFIX = '_amd64.AppImage'; +const APPIMAGE_UPDATE_INFORMATION = `gh-releases-zsync|Jacobinwwey|NoteConnection|latest|${productName}_*${APPIMAGE_SUFFIX}.zsync`; + +function verifyAppImageUpdate(artifact) { + const filename = path.basename(artifact); + if (!filename.startsWith(`${productName}_`) || !filename.endsWith(APPIMAGE_SUFFIX)) { + throw new Error('AppImage update assets must use the supported product amd64 filename'); + } + const updateInformation = execFileSync(artifact, ['--appimage-updateinformation'], { + encoding: 'utf8', timeout: 30000, + }).trim(); + if (updateInformation !== APPIMAGE_UPDATE_INFORMATION) { + throw new Error(`AppImage update information must be ${APPIMAGE_UPDATE_INFORMATION}`); + } + + const zsyncPath = `${artifact}.zsync`; + const control = fs.readFileSync(zsyncPath); + const headerEnd = control.indexOf('\n\n'); + if (headerEnd < 0) throw new Error('zsync header is missing its checksum table separator'); + const fields = new Map(); + const allowedFields = new Set(['zsync', 'Filename', 'MTime', 'Blocksize', 'Length', 'Hash-Lengths', 'URL', 'SHA-1']); + for (const line of control.subarray(0, headerEnd).toString('latin1').split('\n')) { + const match = line.match(/^([A-Za-z0-9-]+): (.+)$/); + if (!match || !allowedFields.has(match[1]) || fields.has(match[1]) || !/^[\x20-\x7e]+$/.test(match[2])) { + throw new Error('zsync header contains a malformed, unsupported or duplicate field'); + } + fields.set(match[1], match[2]); + } + if (!/^0\.6\.\d+$/.test(fields.get('zsync') || '')) throw new Error('Unsupported zsync format version'); + if (fields.get('Filename') !== filename || fields.get('URL') !== filename) { + throw new Error('zsync Filename and relative URL must name the adjacent AppImage'); + } + const length = Number(fields.get('Length')); + if (!/^[1-9]\d*$/.test(fields.get('Length') || '') || !Number.isSafeInteger(length) + || length !== fs.statSync(artifact).size) { + throw new Error('zsync Length does not match the final AppImage'); + } + const blocksize = Number(fields.get('Blocksize')); + if (!/^[1-9]\d*$/.test(fields.get('Blocksize') || '') || !Number.isSafeInteger(blocksize) + || 2 ** Math.round(Math.log2(blocksize)) !== blocksize) { + throw new Error('zsync Blocksize must be a positive power of two'); + } + const hashLengths = (fields.get('Hash-Lengths') || '').match(/^([12]),([1-4]),([3-9]|1[0-6])$/); + if (!hashLengths) throw new Error('zsync Hash-Lengths are outside the supported format bounds'); + // The first field is the sequence-match count, not bytes stored in each table entry. + const tableLength = Math.ceil(length / blocksize) * (Number(hashLengths[2]) + Number(hashLengths[3])); + if (control.length - headerEnd - 2 !== tableLength) { + throw new Error('zsync checksum table has an invalid length'); + } + // Coreutils streams the large image; do not retain a second complete AppImage in memory. + const sha1 = execFileSync('sha1sum', ['--', artifact], { encoding: 'utf8', timeout: 120000 }).slice(0, 40); + if (!/^[a-f0-9]{40}$/.test(fields.get('SHA-1') || '') || fields.get('SHA-1') !== sha1) { + throw new Error('zsync SHA-1 does not match the final AppImage'); + } + return { updateInformation, zsyncPath, sha1 }; +} + +module.exports = { APPIMAGE_UPDATE_INFORMATION, verifyAppImageUpdate }; diff --git a/scripts/run-tauri-build.js b/scripts/run-tauri-build.js index e5b141d1..0e9f9682 100644 --- a/scripts/run-tauri-build.js +++ b/scripts/run-tauri-build.js @@ -6,6 +6,7 @@ const { execFileSync, spawnSync } = require('child_process'); const { prepareDesktopGodotPack } = require('./prepare-desktop-godot-pack'); const { resolveHostServerBinaryName } = require('./tauri-sidecar-utils'); const { sha256File } = require('./sidecar-build-fingerprint'); +const { APPIMAGE_UPDATE_INFORMATION } = require('./appimage-update'); function extractFrontendBuildMode(argv) { const passthroughArgs = []; @@ -74,6 +75,7 @@ function main() { }).trim()); if (originalPatchelf === adapter) throw new Error('PATCHELF must name the original patchelf executable'); const tauriConfig = require('../src-tauri/tauri.conf.json'); + appImageEnvironment.LDAI_UPDATE_INFORMATION = APPIMAGE_UPDATE_INFORMATION; appImageEnvironment.PATCHELF = adapter; appImageEnvironment.NOTE_CONNECTION_APPIMAGE_PATCHELF = originalPatchelf; appImageEnvironment.NOTE_CONNECTION_APPIMAGE_SERVER_SUFFIX = path.join(`${tauriConfig.productName}.AppDir`, 'usr', 'bin', 'server'); diff --git a/scripts/verify-appimage.js b/scripts/verify-appimage.js index 4733f65f..24b626a8 100644 --- a/scripts/verify-appimage.js +++ b/scripts/verify-appimage.js @@ -5,6 +5,7 @@ const os = require('node:os'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); const { sha256File } = require('./sidecar-build-fingerprint'); +const { verifyAppImageUpdate } = require('./appimage-update'); function verifySquashfsPermissions(manifest) { const entries = manifest.split('\n').filter((line) => /^[dl-][rwxstST-]{9}\s/.test(line)); @@ -104,6 +105,7 @@ function verifyAppDir(appDirPath, sourceServerPath) { function verifyAppImage(artifactPath, sourceServerPath) { const artifact = fs.realpathSync(artifactPath); const sourceServer = fs.realpathSync(sourceServerPath); + const update = verifyAppImageUpdate(artifact); const extractionDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'noteconnection-appimage-')); try { const offset = execFileSync(artifact, ['--appimage-offset'], { encoding: 'utf8', timeout: 30000 }).trim(); @@ -126,7 +128,7 @@ function verifyAppImage(artifactPath, sourceServerPath) { stdio: 'pipe', timeout: 30000, }); - return { artifact, name: integration.name, executable: path.basename(integration.executablePath), serverSha256: integration.serverSha256 }; + return { artifact, name: integration.name, executable: path.basename(integration.executablePath), serverSha256: integration.serverSha256, ...update }; } finally { fs.rmSync(extractionDirectory, { recursive: true, force: true }); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ee1f7a24..b80a7498 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "NoteConnection", - "version": "1.9.0", + "version": "1.9.1", "identifier": "com.jacobinwwey.noteconnection", "build": { "beforeDevCommand": "", diff --git a/src/appimage.update.test.ts b/src/appimage.update.test.ts new file mode 100644 index 00000000..3eb63bd3 --- /dev/null +++ b/src/appimage.update.test.ts @@ -0,0 +1,111 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +const { APPIMAGE_UPDATE_INFORMATION, verifyAppImageUpdate } = require('../scripts/appimage-update'); + +const describeLinux = process.platform === 'linux' ? describe : describe.skip; + +describeLinux('AppImage update artifact boundary', () => { + let directory: string; + let artifact: string; + const filename = 'NoteConnection_fixture_amd64.AppImage'; + const updateInformation = 'gh-releases-zsync|Jacobinwwey|NoteConnection|latest|NoteConnection_*_amd64.AppImage.zsync'; + const runtime = `#!/bin/sh\nprintf '%s\\n' '${updateInformation}'\n`; + // Generated by bundled official zsyncmake 0.6.2: zsyncmake -u "$filename" "$filename". + // The shell fixture exposes the runtime metadata boundary; its table is not synthesized by this test. + const control = Buffer.from( + 'enN5bmM6IDAuNi4yCkZpbGVuYW1lOiBOb3RlQ29ubmVjdGlvbl9maXh0dXJlX2FtZDY0LkFwcEltYWdlCk1UaW1lOiBNb24sIDA1IE9jdCAyMDI2IDA0OjM4OjAwICswMDAwCkJsb2Nrc2l6ZTogMjA0OApMZW5ndGg6IDExNgpIYXNoLUxlbmd0aHM6IDEsMiw0ClVSTDogTm90ZUNvbm5lY3Rpb25fZml4dHVyZV9hbWQ2NC5BcHBJbWFnZQpTSEEtMTogYjY3NDMyZTc0ZDM1ZjI5MmNmOTA1ZWMzZDk2MGI5MDg5YjFkYjQwMQoK3tOgETED', + 'base64' + ); + + beforeEach(() => { + directory = fs.mkdtempSync(path.join(os.tmpdir(), 'appimage update ')); + artifact = path.join(directory, filename); + fs.writeFileSync(artifact, runtime, { mode: 0o755 }); + fs.writeFileSync(`${artifact}.zsync`, control); + }); + + afterEach(() => fs.rmSync(directory, { recursive: true, force: true })); + + test('accepts an official control file for the exact final bytes and Latest amd64 target', () => { + expect(APPIMAGE_UPDATE_INFORMATION).toBe(updateInformation); + expect(verifyAppImageUpdate(artifact)).toEqual({ + updateInformation, + zsyncPath: `${artifact}.zsync`, + sha1: 'b67432e74d35f292cf905ec3d960b9089b1db401', + }); + }); + + test('requires the companion file at the adjacent release path', () => { + fs.renameSync(`${artifact}.zsync`, path.join(directory, 'elsewhere.zsync')); + expect(() => verifyAppImageUpdate(artifact)).toThrow(/ENOENT/); + }); + + test.each(['', updateInformation.replace('Jacobinwwey', 'other-owner'), + updateInformation.replace('|NoteConnection|', '|other-repository|'), + updateInformation.replace('|latest|', '|v1.9.0|'), + updateInformation.replace('_amd64.', '_aarch64.'), + updateInformation.replace('_amd64.', '_*.'), + ])('rejects missing or wrong embedded update target: %s', (metadata) => { + fs.writeFileSync(artifact, `#!/bin/sh\nprintf '%s\\n' '${metadata}'\n`); + expect(() => verifyAppImageUpdate(artifact)).toThrow(/update information must be/); + }); + + test.each(['Other_fixture_amd64.AppImage', 'NoteConnection_fixture_aarch64.AppImage'])( + 'rejects unsupported product or architecture: %s', (wrongName) => { + const moved = path.join(directory, wrongName); + fs.renameSync(artifact, moved); + fs.renameSync(`${artifact}.zsync`, `${moved}.zsync`); + expect(() => verifyAppImageUpdate(moved)).toThrow(/supported product amd64 filename/); + } + ); + + test('rejects a stale control file after AppImage bytes are appended', () => { + fs.appendFileSync(artifact, '# rebuilt\n'); + expect(() => verifyAppImageUpdate(artifact)).toThrow(/Length does not match/); + }); + + test('rejects same-size AppImage corruption even when metadata still prints correctly', () => { + fs.writeFileSync(artifact, runtime.replace('printf ', 'printf\t')); + expect(() => verifyAppImageUpdate(artifact)).toThrow(/SHA-1 does not match/); + }); + + test.each([ + ['Filename: NoteConnection_fixture_amd64.AppImage', 'Filename: old.AppImage', /Filename and relative URL/], + ['URL: NoteConnection_fixture_amd64.AppImage', 'URL: https://example.com/image.AppImage', /Filename and relative URL/], + ['Length: 116', 'Length: 117', /Length does not match/], + ['Length: 116', 'Length: 1.16e2', /Length does not match/], + ['Blocksize: 2048', 'Blocksize: 0', /Blocksize/], + ['Blocksize: 2048', 'Blocksize: 2047', /Blocksize/], + ['Blocksize: 2048', 'Blocksize: 9007199254740991', /Blocksize/], + ['Hash-Lengths: 1,2,4', 'Hash-Lengths: 3,2,4', /Hash-Lengths/], + ['Hash-Lengths: 1,2,4', 'Hash-Lengths: 1,5,4', /Hash-Lengths/], + ['Hash-Lengths: 1,2,4', 'Hash-Lengths: 1,2,17', /Hash-Lengths/], + ['Hash-Lengths: 1,2,4', 'Hash-Lengths: 1,2,3', /checksum table/], + ['SHA-1: b67432e74d35f292cf905ec3d960b9089b1db401', 'SHA-1: 0000000000000000000000000000000000000000', /SHA-1 does not match/], + ['zsync: 0.6.2', 'zsync: unknown', /format version/], + ['MTime:', 'Min-Version:', /unsupported/], + ['MTime:', 'Unknown:', /unsupported/], + ['MTime:', 'Z-URL:', /unsupported/], + ['MTime:', 'Filename:', /duplicate/], + ['Filename: Note', 'Filename: \xceote', /malformed/], + ])('rejects malformed or mismatched control field: %s', (before, after, message) => { + fs.writeFileSync(`${artifact}.zsync`, Buffer.from(control.toString('latin1').replace(before as string, after as string), 'latin1')); + expect(() => verifyAppImageUpdate(artifact)).toThrow(message as RegExp); + }); + + test('rejects a truncated checksum table', () => { + fs.writeFileSync(`${artifact}.zsync`, control.subarray(0, -1)); + expect(() => verifyAppImageUpdate(artifact)).toThrow(/checksum table/); + }); + + test('rejects trailing bytes after the checksum table', () => { + fs.appendFileSync(`${artifact}.zsync`, Buffer.from([0])); + expect(() => verifyAppImageUpdate(artifact)).toThrow(/checksum table/); + }); + + test('rejects a header without the checksum table separator', () => { + fs.writeFileSync(`${artifact}.zsync`, control.subarray(0, control.indexOf('\n\n'))); + expect(() => verifyAppImageUpdate(artifact)).toThrow(/separator/); + }); +});