diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 62e2078c9..959797644 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -78,6 +78,8 @@ updates: update-types: ["minor", "patch"] ignore: # The Rust side of the Tauri pairs above; bumped with them, by hand. + # Build the Linux installer before taking a plugin bump: for example, + # tauri-plugin-deep-link 2.6.2 does not compile against rust-ini 0.21.3. - dependency-name: "tauri" - dependency-name: "tauri-*" - package-ecosystem: "github-actions" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 640d536e4..6d4e5b044 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -24,6 +24,7 @@ jobs: outputs: run_sidecar: ${{ steps.scope.outputs.run_sidecar }} run_native: ${{ steps.scope.outputs.run_native }} + native_matrix: ${{ steps.scope.outputs.native_matrix }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -62,10 +63,36 @@ jobs: fi fi fi + if [ "${EVENT_NAME}" = "pull_request" ]; then + # The checkout is the merge commit; its first parent is the base. + git diff --relative --name-only HEAD^1 HEAD > "${changed_paths}" + fi python scripts/classify_build_changes.py \ --event "${EVENT_NAME}" \ --paths-file "${changed_paths}" \ --github-output "${GITHUB_OUTPUT}" + # A pull request builds the Linux installer only; main and dispatches + # build every platform. + python - "${EVENT_NAME}" >> "${GITHUB_OUTPUT}" <<'PY' + import json, sys + native = [ + {"label": "macOS arm64", "os": "macos-15", "platform": "macos", + "architecture": "arm64", "target": "aarch64-apple-darwin", + "bundles": "dmg", "signing": "adhoc"}, + {"label": "macOS Intel", "os": "macos-15-intel", "platform": "macos", + "architecture": "x86_64", "target": "x86_64-apple-darwin", + "bundles": "dmg", "signing": "adhoc"}, + {"label": "Windows x86_64", "os": "windows-2022", "platform": "windows", + "architecture": "x86_64", "target": "x86_64-pc-windows-msvc", + "bundles": "msi,nsis", "signing": "unsigned"}, + {"label": "Linux x86_64", "os": "ubuntu-22.04", "platform": "linux", + "architecture": "x86_64", "target": "x86_64-unknown-linux-gnu", + "bundles": "deb,appimage", "signing": "unsigned"}, + ] + if sys.argv[1] == "pull_request": + native = [entry for entry in native if entry["platform"] == "linux"] + print("native_matrix=" + json.dumps(native)) + PY frontend: name: Frontend behavior and build @@ -133,10 +160,11 @@ jobs: strategy: fail-fast: false matrix: - # PRs prove the frozen-engine contract once on Linux. Main repeats the - # complete matrix for dependency, toolchain, packaging, and native - # changes. Immutable native release tags rebuild every supported OS. - os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-22.04"]' || '["ubuntu-22.04", "macos-14", "windows-latest"]') }} + # PRs prove the frozen-engine contract on Linux and macOS (macOS freezes + # the PyObjC wheels, whose license notices the build must find). Main + # repeats the complete matrix for dependency, toolchain, packaging, and + # native changes. Immutable native release tags rebuild every supported OS. + os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-22.04", "macos-14"]' || '["ubuntu-22.04", "macos-14", "windows-latest"]') }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -182,7 +210,8 @@ jobs: name: Native installer (${{ matrix.label }}) needs: artifact-scope # Main runs this evidence for dependency, toolchain, packaging, and native - # changes. Application-only merges already pass the PR checks and the + # changes, and a pull request that changes the native shell builds the + # Linux installer. Application-only merges already pass the PR checks and the # exact-main frontend, distribution, and cross-platform test jobs. Every # immutable desktop-v* release tag rebuilds and tests all installers in the # separate Native Installer Release workflow. @@ -190,35 +219,9 @@ jobs: strategy: fail-fast: false matrix: - include: - - label: macOS arm64 - os: macos-15 - platform: macos - architecture: arm64 - target: aarch64-apple-darwin - bundles: dmg - signing: adhoc - - label: macOS Intel - os: macos-15-intel - platform: macos - architecture: x86_64 - target: x86_64-apple-darwin - bundles: dmg - signing: adhoc - - label: Windows x86_64 - os: windows-2022 - platform: windows - architecture: x86_64 - target: x86_64-pc-windows-msvc - bundles: msi,nsis - signing: unsigned - - label: Linux x86_64 - os: ubuntu-22.04 - platform: linux - architecture: x86_64 - target: x86_64-unknown-linux-gnu - bundles: deb,appimage - signing: unsigned + # Built by the artifact-scope job: every platform on main and dispatch, + # Linux only on a pull request that changes the native shell. + include: ${{ fromJSON(needs.artifact-scope.outputs.native_matrix) }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/desktop-test.yml b/.github/workflows/desktop-test.yml index d91bd7fc9..a79737e91 100644 --- a/.github/workflows/desktop-test.yml +++ b/.github/workflows/desktop-test.yml @@ -2,9 +2,10 @@ # # Called from packages.yml when a change touches packages/desktop, so its jobs # feed the required packages-gate. Pull requests run the Linux lane on Python -# 3.11 and 3.12. A push to main repeats the same contract on macOS and Windows, -# so a release always rests on the complete cross-platform matrix. Ported from -# openadapt-desktop's test.yml. +# 3.11 and 3.12 and one Windows lane, because Windows checks files out with CRLF +# and has its own paths. A push to main repeats the same contract on macOS and +# Windows, so a release always rests on the complete cross-platform matrix. +# Ported from openadapt-desktop's test.yml. name: Desktop test on: @@ -23,8 +24,9 @@ jobs: strategy: fail-fast: false matrix: - os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || '["ubuntu-latest", "macos-latest", "windows-latest"]') }} + os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest", "windows-latest"]' || '["ubuntu-latest", "macos-latest", "windows-latest"]') }} python-version: ["3.11", "3.12"] + exclude: ${{ fromJSON(github.event_name == 'pull_request' && '[{"os":"windows-latest","python-version":"3.11"}]' || '[]') }} runs-on: ${{ matrix.os }} timeout-minutes: 45 steps: diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 9ebd38c1c..c6851892c 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -1,6 +1,18 @@ # CHANGELOG +## v0.21.1 (2026-10-19) + +0.21.0 was never published, because main's macOS, Windows and Linux builds of it failed. This release ships the 0.21.0 changes below together with these fixes. + +### Bug Fixes + +- **desktop**: go back to tauri-plugin-deep-link 2.6.1 so the Linux shell compiles +- **desktop**: supply the reviewed PyObjC license for the 12.2.2 wheels +- **desktop**: prepare a release on a CRLF checkout + + + ## v0.21.0 (2026-10-18) ### Features diff --git a/packages/desktop/openadapt_desktop/__init__.py b/packages/desktop/openadapt_desktop/__init__.py index 601b30681..bd0497bcd 100644 --- a/packages/desktop/openadapt_desktop/__init__.py +++ b/packages/desktop/openadapt_desktop/__init__.py @@ -21,4 +21,4 @@ +-- backends/ Storage backend plugins """ -__version__ = "0.21.0" +__version__ = "0.21.1" diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json index a35042ee8..6a0a57428 100644 --- a/packages/desktop/package-lock.json +++ b/packages/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "openadapt-desktop", - "version": "0.21.0", + "version": "0.21.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openadapt-desktop", - "version": "0.21.0", + "version": "0.21.1", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-notification": "^2", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index bd94fc447..32a33a5d7 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "openadapt-desktop", - "version": "0.21.0", + "version": "0.21.1", "description": "Installed companion for OpenAdapt authoring, teaching, and local pairing", "private": true, "type": "module", diff --git a/packages/desktop/pyproject.toml b/packages/desktop/pyproject.toml index 29faee9cc..e06f4b378 100644 --- a/packages/desktop/pyproject.toml +++ b/packages/desktop/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "openadapt-desktop" -version = "0.21.0" +version = "0.21.1" description = "The OpenAdapt desktop app and local engine: record a data-entry task, run it on a list, and check each saved record" readme = "README.md" requires-python = ">=3.11" diff --git a/packages/desktop/scripts/classify_build_changes.py b/packages/desktop/scripts/classify_build_changes.py index ec27f69e3..2c41eb576 100644 --- a/packages/desktop/scripts/classify_build_changes.py +++ b/packages/desktop/scripts/classify_build_changes.py @@ -1,6 +1,8 @@ """Classify whether a Desktop change needs full native artifact builds. -Pull requests keep the representative Linux frozen-sidecar build. Release +Pull requests keep the representative frozen-sidecar builds, and a pull +request that changes the Tauri shell or its JavaScript locks also builds the +Linux installer, so a native dependency bump compiles before it merges. Release candidates and explicit dispatches keep the complete platform matrix. A push to protected ``main`` repeats the expensive matrix only when a dependency, toolchain, packaging, or native-runtime input changed. The immutable native @@ -51,6 +53,12 @@ "scripts/verify_build_artifact.py", } +# A pull request that changes one of these also builds the Linux installer. +# The Rust shell and its plugins compile per platform, and the npm and cargo +# sides of each Tauri plugin must move together. +NATIVE_PR_FILES = {"package-lock.json", "package.json"} +NATIVE_PR_PREFIXES = ("src-tauri/",) + # These paths get the normal PR checks and the exact-main frontend, Python # distribution, and cross-platform test jobs. They do not change the native # packaging contract. Everything not named here fails toward the full matrix. @@ -105,12 +113,19 @@ def classify_build_scope(event: str, changed_paths: Iterable[str]) -> BuildScope if event == "workflow_dispatch": return BuildScope(True, True, "explicit release-candidate build") + paths = tuple(path for raw in changed_paths if (path := _normalize(raw))) if event == "pull_request": + native = tuple( + path + for path in paths + if path in NATIVE_PR_FILES or path.startswith(NATIVE_PR_PREFIXES) + ) + if native: + return BuildScope(True, True, f"pull request changes the native shell: {native[0]}") return BuildScope(True, False, "representative pull-request sidecar") if event != "push": return BuildScope(True, True, f"unknown event {event!r}; fail toward full evidence") - paths = tuple(path for raw in changed_paths if (path := _normalize(raw))) if not paths: return BuildScope(True, True, "no changed paths; fail toward full evidence") diff --git a/packages/desktop/scripts/frozen_notices.py b/packages/desktop/scripts/frozen_notices.py index d0ccb7aa9..05c58a792 100644 --- a/packages/desktop/scripts/frozen_notices.py +++ b/packages/desktop/scripts/frozen_notices.py @@ -268,6 +268,48 @@ def capture_floor_is_satisfied(candidate: str, floor: str) -> bool: "e5dcffe836b6ec8a58e492419b550e65fb8cbdc308503979e5dacb33ac7ea3b7" ), }, + ("pyobjc-core", "12.2.2"): { + "relative_path": "pyobjc/LICENSE.txt", + "source_url": ( + "https://github.com/ronaldoussoren/pyobjc/blob/" + "38cdf73fc049ef2d176973eff4ac8d5c76b0a34b/" + "pyobjc-core/License.txt" + ), + "source_commit": "38cdf73fc049ef2d176973eff4ac8d5c76b0a34b", + "sha256": "0ca04b07928d4872b9d9bb22187ca0426dd8bfab08f26eada0999a71dc81aaff", + "license_expression": "MIT", + "license_evidence_sha256": ( + "e5dcffe836b6ec8a58e492419b550e65fb8cbdc308503979e5dacb33ac7ea3b7" + ), + }, + ("pyobjc-framework-coremedia", "12.2.2"): { + "relative_path": "pyobjc/LICENSE.txt", + "source_url": ( + "https://github.com/ronaldoussoren/pyobjc/blob/" + "38cdf73fc049ef2d176973eff4ac8d5c76b0a34b/" + "pyobjc-framework-CoreMedia/License.txt" + ), + "source_commit": "38cdf73fc049ef2d176973eff4ac8d5c76b0a34b", + "sha256": "0ca04b07928d4872b9d9bb22187ca0426dd8bfab08f26eada0999a71dc81aaff", + "license_expression": "MIT", + "license_evidence_sha256": ( + "e5dcffe836b6ec8a58e492419b550e65fb8cbdc308503979e5dacb33ac7ea3b7" + ), + }, + ("pyobjc-framework-screencapturekit", "12.2.2"): { + "relative_path": "pyobjc/LICENSE.txt", + "source_url": ( + "https://github.com/ronaldoussoren/pyobjc/blob/" + "38cdf73fc049ef2d176973eff4ac8d5c76b0a34b/" + "pyobjc-framework-ScreenCaptureKit/License.txt" + ), + "source_commit": "38cdf73fc049ef2d176973eff4ac8d5c76b0a34b", + "sha256": "0ca04b07928d4872b9d9bb22187ca0426dd8bfab08f26eada0999a71dc81aaff", + "license_expression": "MIT", + "license_evidence_sha256": ( + "e5dcffe836b6ec8a58e492419b550e65fb8cbdc308503979e5dacb33ac7ea3b7" + ), + }, ("rapidocr-onnxruntime", "1.4.4"): { "relative_path": "rapidocr/LICENSE", "source_url": ( diff --git a/packages/desktop/scripts/native_release.py b/packages/desktop/scripts/native_release.py index 35a7d6767..d3baf61c9 100644 --- a/packages/desktop/scripts/native_release.py +++ b/packages/desktop/scripts/native_release.py @@ -271,7 +271,9 @@ def set_native_version(version: str, root: Path = ROOT) -> dict[str, str]: result with the Git blobs of the candidate tag. Text-mode I/O would break that: on Windows it rewrites every ``\\n`` as ``\\r\\n``, and an omitted encoding decodes with the locale default. All reads and writes below are - therefore explicit UTF-8 bytes with LF endings. + therefore explicit UTF-8 bytes with LF endings. The Cargo patterns also + accept CRLF, because Git on Windows checks files out with CRLF by default, + and they keep whatever ending each line already has. """ if not VERSION_PATTERN.fullmatch(version): @@ -301,7 +303,10 @@ def set_lock_versions(lock: dict) -> None: cargo_toml = root / "src-tauri" / "Cargo.toml" text, replaced = re.subn( - r'(?m)^version = "[^"]+"$', f'version = "{version}"', read_source(cargo_toml), count=1 + r'(?m)^version = "[^"]+"(?=\r?$)', + f'version = "{version}"', + read_source(cargo_toml), + count=1, ) if replaced != 1: raise ValueError(f"could not rewrite package version in {cargo_toml}") @@ -309,7 +314,7 @@ def set_lock_versions(lock: dict) -> None: cargo_lock = root / "src-tauri" / "Cargo.lock" text, replaced = re.subn( - r'(name = "openadapt-desktop"\nversion = ")[^"]+(")', + r'(name = "openadapt-desktop"\r?\nversion = ")[^"]+(")', rf"\g<1>{version}\g<2>", read_source(cargo_lock), count=1, diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 56ecbc222..b7e27df90 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2502,7 +2502,7 @@ dependencies = [ [[package]] name = "openadapt-desktop" -version = "0.21.0" +version = "0.21.1" dependencies = [ "hex", "reqwest", @@ -3941,9 +3941,9 @@ dependencies = [ [[package]] name = "tauri-plugin-deep-link" -version = "2.6.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4" +checksum = "4ac7a92a46ab5c88f44532ca50906d6e448a948d4ddf8c5376ab125e3260f736" dependencies = [ "dunce", "plist", @@ -4005,9 +4005,9 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.5.4" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561cd99484561bc39c954768e7b41a66cebbe1ac3d9d081c34361f740c489e2c" +checksum = "3c142ed88deee222bd2d979269d35c73b6c1c0f6ebd5b79b4ff80066fcad6af1" dependencies = [ "serde", "serde_json", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 7b1dcd390..fe9612c3c 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openadapt-desktop" -version = "0.21.0" +version = "0.21.1" description = "Installed companion for OpenAdapt authoring, teaching, and local pairing" authors = ["OpenAdapt AI"] license = "MIT" diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index aea2772db..fad2fc57b 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicoverbruggen/tauri-v2-schema/main/schema.json", "productName": "OpenAdapt Desktop", - "version": "0.21.0", + "version": "0.21.1", "identifier": "ai.openadapt.desktop", "build": { "frontendDist": "../dist", diff --git a/packages/desktop/tests/test_build_change_classifier.py b/packages/desktop/tests/test_build_change_classifier.py index 51e5df0e4..8e87ddd4b 100644 --- a/packages/desktop/tests/test_build_change_classifier.py +++ b/packages/desktop/tests/test_build_change_classifier.py @@ -13,6 +13,15 @@ def test_pull_request_keeps_one_representative_sidecar() -> None: ) +@pytest.mark.parametrize( + "path", ["src-tauri/Cargo.lock", "src-tauri/src/main.rs", "package.json", "package-lock.json"] +) +def test_pull_request_that_changes_the_native_shell_builds_an_installer(path: str) -> None: + scope = classify_build_scope("pull_request", ["src/App.tsx", path]) + assert scope.run_sidecar and scope.run_native + assert path in scope.reason + + def test_explicit_release_candidate_keeps_the_complete_matrix() -> None: assert classify_build_scope("workflow_dispatch", []) == BuildScope( run_sidecar=True, diff --git a/packages/desktop/tests/test_release_intent.py b/packages/desktop/tests/test_release_intent.py index 3d949247b..79c79022c 100644 --- a/packages/desktop/tests/test_release_intent.py +++ b/packages/desktop/tests/test_release_intent.py @@ -119,6 +119,24 @@ def test_prepare_sets_every_version_source_and_the_changelog(repo: Path) -> None assert "explain the portal" not in changelog +def test_prepare_works_on_a_crlf_checkout(repo: Path) -> None: + """Git on Windows checks files out with CRLF; every version source must still move.""" + + for relative in VERSION_FILES: + path = repo / relative + path.write_bytes(path.read_bytes().replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")) + # On Windows the checkout (and so the fixture) is already CRLF, so the + # rewrite can leave nothing to commit; the test is the prepare() below. + _git(repo, "add", "-A") + _git(repo, "commit", "-q", "--allow-empty", "-m", "chore: check out with CRLF") + _commit(repo, "fix(portal): keep the card", touch="src/portal.txt") + + version = prepare_release.prepare(repo, today=datetime.date(2026, 10, 19)) + + assert set(check_release_consistency.release_versions(repo).values()) == {version} + assert _load("native_release").native_version(repo) == version + + def test_prepare_refuses_without_a_releasable_commit(repo: Path) -> None: _commit(repo, "docs: only words", touch="docs/words.md") with pytest.raises(ValueError, match="no feat, fix, perf"): diff --git a/packages/desktop/third_party/README.md b/packages/desktop/third_party/README.md index 458711607..171c54bd1 100644 --- a/packages/desktop/third_party/README.md +++ b/packages/desktop/third_party/README.md @@ -91,20 +91,25 @@ version, metadata, commit, and byte-hash gate used for FlatBuffers. ## PyObjC macOS runtime -- Distributions: `pyobjc-core==12.2.1` and - `pyobjc-framework-ApplicationServices==12.2.1` +- Distributions: `pyobjc-core` (`12.2.1` and `12.2.2`), + `pyobjc-framework-ApplicationServices==12.2.1`, + `pyobjc-framework-CoreMedia==12.2.2` and + `pyobjc-framework-ScreenCaptureKit==12.2.2` - Upstream repository: `https://github.com/ronaldoussoren/pyobjc` -- Upstream tag and commit: `v12.2.1`, - `cb525f3e7f433c851b68725dea54bc70d35681b8` -- Source paths: `pyobjc-core/License.txt` and - `pyobjc-framework-ApplicationServices/License.txt` +- Upstream tags and commits: `v12.2.1`, + `cb525f3e7f433c851b68725dea54bc70d35681b8`; `v12.2.2`, + `38cdf73fc049ef2d176973eff4ac8d5c76b0a34b` +- Source paths: `pyobjc-core/License.txt`, + `pyobjc-framework-ApplicationServices/License.txt`, + `pyobjc-framework-CoreMedia/License.txt` and + `pyobjc-framework-ScreenCaptureKit/License.txt` - Pinned license SHA-256: `0ca04b07928d4872b9d9bb22187ca0426dd8bfab08f26eada0999a71dc81aaff` - License: MIT -- Modification status: the two source files are byte-identical; copied without - modification as `pyobjc/LICENSE.txt` +- Modification status: the source files are byte-identical at both tags; + copied without modification as `pyobjc/LICENSE.txt` -The two applicable macOS wheels omit a concrete license file. The -frozen-runtime build supplies their shared exact upstream license only when -both package version and license metadata match the reviewed records. Symbol -files whose names merely contain `copying` are not treated as license notices. +These macOS wheels omit a concrete license file. The frozen-runtime build +supplies their shared exact upstream license only when both package version +and license metadata match the reviewed records. Symbol files whose names +merely contain `copying` are not treated as license notices. diff --git a/packages/desktop/uv.lock b/packages/desktop/uv.lock index 307424ab4..61e69db20 100644 --- a/packages/desktop/uv.lock +++ b/packages/desktop/uv.lock @@ -1159,7 +1159,7 @@ wheels = [ [[package]] name = "openadapt-desktop" -version = "0.21.0" +version = "0.21.1" source = { editable = "." } dependencies = [ { name = "cryptography" },