From 34d8190348a38753ff476df7ed4a5a859f575a1b Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 17:47:32 +0000 Subject: [PATCH 01/23] Implement update channels --- .github/workflows/nightly.yml | 44 -------- Backend/build.rs | 70 +++++++++++- Backend/src/app_identity.rs | 102 +++++++++++++++++ Backend/src/lib.rs | 1 + Backend/src/plugin_paths.rs | 3 +- Backend/src/plugin_runtime/settings_store.rs | 3 +- Backend/src/settings.rs | 3 +- Backend/src/state.rs | 3 +- Justfile | 29 +++-- README.md | 4 + docs/channel-branding-execplan.md | 113 +++++++++++++++++++ install.sh | 101 +++++++++++++---- packaging/flatpak/README.md | 1 + 13 files changed, 389 insertions(+), 88 deletions(-) create mode 100644 Backend/src/app_identity.rs create mode 100644 docs/channel-branding-execplan.md diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2655b5e2..3cb65286 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -20,8 +20,6 @@ permissions: env: # Scheduled runs should always build from Dev. Manual runs should build from the selected branch. TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || 'Dev' }} - # Flatpak is enabled in CI. - OPENVCS_ENABLE_FLATPAK: ${{ vars.ENABLE_FLATPAK }} jobs: check-changes: @@ -187,42 +185,6 @@ jobs: sudo apt-get install -y libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libwebkit2gtk-4.1-dev || sudo apt-get install -y libwebkit2gtk-4.0-dev - # ---------- Flatpak (Linux only; artifact) ---------- - - name: Install Flatpak tooling - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - run: | - set -euxo pipefail - sudo apt-get update - sudo apt-get install -y --no-install-recommends flatpak flatpak-builder appstream elfutils - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak install --user -y flathub org.gnome.Platform//49 org.gnome.Sdk//49 - # Cargo/rustc must come from the Flatpak SDK environment (not the host toolchain). - # Try common branches to avoid coupling this workflow to the host runner image. - flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 \ - || flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable//24.08 \ - || flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable - - - name: Verify Flatpak manifest exists - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - run: | - set -euxo pipefail - ls -la packaging/flatpak - test -f packaging/flatpak/io.github.jordonbc.OpenVCS.yml - - - name: Build Flatpak bundle - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - run: | - set -euxo pipefail - flatpak-builder --user --force-clean --repo=repo build-flatpak packaging/flatpak/io.github.jordonbc.OpenVCS.yml - flatpak build-bundle repo OpenVCS.flatpak io.github.jordonbc.OpenVCS - - - name: Upload Flatpak bundle (artifact) - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - uses: actions/upload-artifact@v7 - with: - name: OpenVCS-flatpak-nightly - path: OpenVCS.flatpak - # ---------- Cache ---------- - name: Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -279,9 +241,3 @@ jobs: releaseDraft: false prerelease: true args: ${{ matrix.args }} - - - name: Upload Flatpak bundle to GitHub Release - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release upload openvcs-nightly OpenVCS.flatpak --clobber diff --git a/Backend/build.rs b/Backend/build.rs index 21372f0f..fb7c029f 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -2,6 +2,52 @@ // SPDX-License-Identifier: GPL-3.0-or-later use std::{env, fs, path::PathBuf, process::Command}; +/// Channel-specific metadata used for generated desktop bundles. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ChannelConfig { + /// Normalized channel slug used across build and runtime. + slug: &'static str, + /// Human-facing desktop product name. + product_name: &'static str, + /// Tauri bundle identifier. + identifier: &'static str, + /// Main window title. + window_title: &'static str, +} + +impl ChannelConfig { + /// Resolves normalized channel metadata from an arbitrary environment value. + /// + /// # Parameters + /// - `raw`: Raw environment value. + /// + /// # Returns + /// - Stable metadata when the input is missing or unknown. + /// - Beta or nightly metadata for recognized channel names. + fn from_env_value(raw: &str) -> Self { + match raw.trim().to_ascii_lowercase().as_str() { + "beta" => Self { + slug: "beta", + product_name: "OpenVCS Beta", + identifier: "dev.jordon.openvcs.beta", + window_title: "OpenVCS Beta", + }, + "nightly" => Self { + slug: "nightly", + product_name: "OpenVCS Nightly", + identifier: "dev.jordon.openvcs.nightly", + window_title: "OpenVCS Nightly", + }, + _ => Self { + slug: "stable", + product_name: "OpenVCS", + identifier: "dev.jordon.openvcs", + window_title: "OpenVCS", + }, + } + } +} + /// Returns whether the build is running for Flatpak packaging. fn is_flatpak_build() -> bool { matches!( @@ -131,8 +177,10 @@ fn main() { let data = fs::read_to_string(&base).expect("read tauri.conf.json"); let mut json: serde_json::Value = serde_json::from_str(&data).expect("parse tauri.conf.json"); - // Compute channel based on environment; default to stable - let chan = env::var("OPENVCS_UPDATE_CHANNEL").unwrap_or_else(|_| "stable".into()); + // Compute channel based on environment; default to stable. + let channel = ChannelConfig::from_env_value( + &env::var("OPENVCS_UPDATE_CHANNEL").unwrap_or_else(|_| "stable".into()), + ); // Repository URL (can be overridden via env var for forks) let repo = @@ -153,7 +201,7 @@ fn main() { // Navigate: plugins.updater.endpoints if let Some(plugins) = json.get_mut("plugins") { if let Some(updater) = plugins.get_mut("updater") { - let endpoints = match chan.as_str() { + let endpoints = match channel.slug { // Beta: check beta first, then stable "beta" => serde_json::Value::Array(vec![beta.clone(), stable.clone()]), // Nightly: check nightly first, then stable @@ -165,6 +213,19 @@ fn main() { } } + json["productName"] = serde_json::Value::String(channel.product_name.into()); + json["identifier"] = serde_json::Value::String(channel.identifier.into()); + if let Some(app) = json.get_mut("app") { + if let Some(windows) = app + .get_mut("windows") + .and_then(|value| value.as_array_mut()) + { + if let Some(main_window) = windows.first_mut() { + main_window["title"] = serde_json::Value::String(channel.window_title.into()); + } + } + } + // The app should only ever point at a dev server when running `cargo tauri dev`. // In all other cases (including `cargo build`, `cargo run`, `cargo tauri build`, Flatpak, CI), // we must ship/load the prebuilt frontend assets from `frontendDist`. @@ -196,6 +257,7 @@ fn main() { // Provide the generated config via inline JSON env var (must be single-line) let inline = serde_json::to_string(&json).unwrap(); println!("cargo:rustc-env=TAURI_CONFIG={}", inline); + println!("cargo:rustc-env=OPENVCS_APP_CHANNEL={}", channel.slug); // Also persist a copy alongside OUT_DIR for debugging (non-fatal if it fails) if let Ok(out_dir) = env::var("OUT_DIR") { @@ -266,7 +328,7 @@ fn main() { pkg_version.clone() } else { let branch_ident = sanitize_semver_ident(&branch); - let channel_suffix = match chan.as_str() { + let channel_suffix = match channel.slug { "beta" => "-beta", "nightly" => "-nightly", _ => "", diff --git a/Backend/src/app_identity.rs b/Backend/src/app_identity.rs new file mode 100644 index 00000000..2f6e3446 --- /dev/null +++ b/Backend/src/app_identity.rs @@ -0,0 +1,102 @@ +// Copyright © 2025-2026 OpenVCS Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Channel-aware desktop identity and persistence paths. + +use directories::ProjectDirs; + +/// Known desktop release channels. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AppChannel { + /// Stable desktop builds that preserve the legacy app identity. + Stable, + /// Beta desktop builds that use separate branding and persistence. + Beta, + /// Nightly desktop builds that use separate branding and persistence. + Nightly, +} + +impl AppChannel { + /// Parses the compile-time channel slug emitted by `build.rs`. + /// + /// # Parameters + /// - `raw`: Raw channel string. + /// + /// # Returns + /// - [`AppChannel::Stable`] for missing or unknown values. + /// - The matching prerelease channel for known values. + pub fn from_build_channel(raw: &str) -> Self { + match raw.trim().to_ascii_lowercase().as_str() { + "beta" => Self::Beta, + "nightly" => Self::Nightly, + _ => Self::Stable, + } + } + + /// Returns the user-facing desktop product name for this channel. + /// + /// # Returns + /// - Product name used by bundles and channel-specific docs. + pub fn product_name(self) -> &'static str { + match self { + Self::Stable => "OpenVCS", + Self::Beta => "OpenVCS Beta", + Self::Nightly => "OpenVCS Nightly", + } + } +} + +/// Returns the current desktop release channel compiled into the binary. +/// +/// # Returns +/// - Current build channel, defaulting to stable when unspecified. +pub fn current_channel() -> AppChannel { + AppChannel::from_build_channel(option_env!("OPENVCS_APP_CHANNEL").unwrap_or("stable")) +} + +/// Returns channel-aware project directories for app config and data. +/// +/// Stable builds preserve the legacy `OpenVCS` application name so existing +/// users keep the same config and data roots. Beta and nightly use their +/// distinct product names so they can coexist with stable. +/// +/// # Returns +/// - `Some(ProjectDirs)` when the platform exposes standard app directories. +/// - `None` when no platform-specific directories are available. +pub fn project_dirs() -> Option { + ProjectDirs::from("dev", "OpenVCS", current_channel().product_name()) +} + +#[cfg(test)] +mod tests { + use super::AppChannel; + + /// Verifies unknown channel strings fall back to stable. + #[test] + fn defaults_unknown_channels_to_stable() { + assert_eq!(AppChannel::from_build_channel(""), AppChannel::Stable); + assert_eq!( + AppChannel::from_build_channel("preview"), + AppChannel::Stable + ); + } + + /// Verifies known prerelease channels parse successfully. + #[test] + fn parses_known_prerelease_channels() { + assert_eq!(AppChannel::from_build_channel("beta"), AppChannel::Beta); + assert_eq!( + AppChannel::from_build_channel("nightly"), + AppChannel::Nightly + ); + assert_eq!(AppChannel::from_build_channel("BETA"), AppChannel::Beta); + } + + /// Verifies each channel exposes the expected desktop product name. + #[test] + fn exposes_product_names() { + assert_eq!(AppChannel::Stable.product_name(), "OpenVCS"); + assert_eq!(AppChannel::Beta.product_name(), "OpenVCS Beta"); + assert_eq!(AppChannel::Nightly.product_name(), "OpenVCS Nightly"); + } +} diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 475422d8..61120038 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -14,6 +14,7 @@ use tauri_plugin_updater::UpdaterExt; use crate::core::BackendId; +mod app_identity; mod core; mod logging; mod output_log; diff --git a/Backend/src/plugin_paths.rs b/Backend/src/plugin_paths.rs index acf25bda..d6152b18 100644 --- a/Backend/src/plugin_paths.rs +++ b/Backend/src/plugin_paths.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! Path resolution helpers for installed and built-in plugins. -use directories::ProjectDirs; use log::{info, warn}; use std::{ env, @@ -36,7 +35,7 @@ static LOGGED_BUILTIN_DIRS: AtomicBool = AtomicBool::new(false); /// # Returns /// - The absolute config-directory path used to store installed plugins. pub fn plugins_dir() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.config_dir().join("plugins") } else { PathBuf::from("plugins") diff --git a/Backend/src/plugin_runtime/settings_store.rs b/Backend/src/plugin_runtime/settings_store.rs index deebc455..605e0455 100644 --- a/Backend/src/plugin_runtime/settings_store.rs +++ b/Backend/src/plugin_runtime/settings_store.rs @@ -3,14 +3,13 @@ //! Filesystem persistence for plugin settings JSON. -use directories::ProjectDirs; use serde_json::{Map, Value}; use std::fs; use std::path::{Path, PathBuf}; /// Returns the root plugin data directory under the app config directory. fn plugin_data_root() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.config_dir().join("plugin-data") } else { PathBuf::from("plugin-data") diff --git a/Backend/src/settings.rs b/Backend/src/settings.rs index 2dd721a6..b9390f86 100644 --- a/Backend/src/settings.rs +++ b/Backend/src/settings.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! Global application configuration types and persistence helpers. -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::{fs, io}; @@ -669,7 +668,7 @@ impl AppConfig { /// # Returns /// - Filesystem path to the global OpenVCS config file. pub fn path() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.config_dir().join("openvcs.conf") } else { PathBuf::from("openvcs.conf") diff --git a/Backend/src/state.rs b/Backend/src/state.rs index e475097c..46452e94 100644 --- a/Backend/src/state.rs +++ b/Backend/src/state.rs @@ -13,7 +13,6 @@ use crate::plugin_runtime::PluginRuntimeManager; use crate::repo::Repo; use crate::repo_settings::RepoConfig; use crate::settings::AppConfig; -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; /// Default number of recent repositories stored when settings are missing or invalid. @@ -287,7 +286,7 @@ struct RecentFileEntry { /// # Returns /// - Recents file path. fn recents_file_path() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.data_dir().join("recents.json") } else { PathBuf::from("recents.json") diff --git a/Justfile b/Justfile index edd20051..9d13e970 100644 --- a/Justfile +++ b/Justfile @@ -1,17 +1,26 @@ default: @just --list -build target="all": - @just {{ if target == "plugins" { "_build_plugins" } else if target == "client" { "_build_client" } else if target == "all" { "_build_all" } else { "_build_usage" } }} - -_build_all: _build_client - -_build_client: +build target="stable": + @case "{{target}}" in \ + plugins) just _build_plugins ;; \ + client|stable|beta|nightly) just _build_client "{{target}}" ;; \ + all) just _build_all stable ;; \ + *) just _build_usage ;; \ + esac + +_build_all channel="stable": + @just _build_client {{channel}} + +_build_client channel="stable": npm --prefix Frontend run build - cargo build + FRONTEND_SKIP_BUILD=1 NO_STRIP=true OPENVCS_UPDATE_CHANNEL={{channel}} node scripts/tauri-build.js + +_build_plugins: + @echo "Plugin-only builds are not currently implemented by this Justfile" _build_usage: - @echo "Unknown build target. Use: just build, just build plugins, or just build client" + @echo "Unknown build target. Use: just build, just build stable, just build beta, just build nightly, just build client, or just build plugins" @exit 2 test: @@ -19,8 +28,8 @@ test: cd Frontend && npm exec tsc -- -p tsconfig.json --noEmit || true cd Frontend && npx vitest run || true -tauri-build: - node scripts/tauri-build.js +tauri-build channel="stable": + FRONTEND_SKIP_BUILD=1 NO_STRIP=true OPENVCS_UPDATE_CHANNEL={{channel}} node scripts/tauri-build.js build-flatpak install="": @if [ "{{install}}" = "-i" ]; then \ diff --git a/README.md b/README.md index f096c10d..8c101809 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ curl -fsSL https://raw.githubusercontent.com/Jordonbc/OpenVCS/stable/install.sh The script targets Linux, leaves existing configuration untouched, and can be re-run to pull the newest release. +Stable desktop builds keep the legacy `OpenVCS` app identity and reuse the existing config and plugin directories. + **Install pre-release (nightly):** ```bash @@ -42,6 +44,8 @@ curl -fsSL https://raw.githubusercontent.com/Jordonbc/OpenVCS/stable/install.sh Swap `stable` for `dev` in the URL if you want the bleeding-edge installer. +Pre-release AppImage installs use a separate launcher and install path when the selected release is branded as beta or nightly, so they can coexist with stable on the same machine. + --- ## Key Goals diff --git a/docs/channel-branding-execplan.md b/docs/channel-branding-execplan.md new file mode 100644 index 00000000..d8589430 --- /dev/null +++ b/docs/channel-branding-execplan.md @@ -0,0 +1,113 @@ +# Implement channel-aware desktop branding + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. + +This document must be maintained in accordance with `PLANS.md` from the repository root. + +## Purpose / Big Picture + +OpenVCS desktop builds should clearly identify whether they are stable, beta, or nightly, and prerelease builds should be able to live alongside stable without sharing configuration or plugin state. After this change, stable desktop bundles still look and behave like the existing app, while beta and nightly bundles show distinct names, use distinct bundle identifiers, and persist settings in separate directories. Flatpak remains a stable-only package that continues to present itself as plain `OpenVCS`. + +## Progress + +- [x] (2026-03-24 17:45Z) Confirmed channel inputs already come from CI via `OPENVCS_UPDATE_CHANNEL` and documented the compatibility rule: stable keeps legacy identity and paths; beta/nightly get separate branding and persistence. +- [x] (2026-03-24 17:46Z) Identified implementation surfaces: `Backend/build.rs`, backend persistence helpers, `install.sh`, GitHub workflows, README, and Flatpak notes. +- [x] (2026-03-24 17:54Z) Centralized desktop channel metadata in `Backend/build.rs`, exported `OPENVCS_APP_CHANNEL`, and removed the unused `Backend/tauri.stable.conf.json`, `Backend/tauri.beta.conf.json`, and `Backend/tauri.nightly.conf.json` files. +- [x] (2026-03-24 17:55Z) Added `Backend/src/app_identity.rs` and moved all backend config/data/plugin persistence call sites onto channel-aware `ProjectDirs` resolution. +- [x] (2026-03-24 17:57Z) Updated `install.sh`, `.github/workflows/nightly.yml`, `README.md`, and `packaging/flatpak/README.md` for side-by-side prerelease installs and stable-only Flatpak packaging. +- [x] (2026-03-24 17:59Z) Ran `cargo fmt --all`, `npm exec tsc -- -p tsconfig.json --noEmit` in `Frontend/`, and `cargo check --package openvcs --lib` for stable, beta, and nightly. +- [x] (2026-03-24 18:00Z) Ran `cargo test --package openvcs --lib` for stable, beta, and nightly; all three runs compiled the new channel-aware code and failed only in existing plugin runtime tests that require an unavailable bundled Node runtime. + +## Surprises & Discoveries + +- Observation: The repository already contains `Backend/tauri.stable.conf.json`, `Backend/tauri.beta.conf.json`, and `Backend/tauri.nightly.conf.json`, but no build step merges them into Tauri config. + Evidence: `Backend/build.rs` only rewrites updater endpoints and version strings before exporting `TAURI_CONFIG`. +- Observation: Desktop coexistence is blocked by runtime persistence paths as much as by bundle metadata. + Evidence: `Backend/src/settings.rs`, `Backend/src/state.rs`, `Backend/src/plugin_paths.rs`, and `Backend/src/plugin_runtime/settings_store.rs` all hardcode `ProjectDirs::from("dev", "OpenVCS", "OpenVCS")`. +- Observation: The installer script currently always writes `~/Applications/openvcs.AppImage` and `~/.local/share/applications/openvcs.desktop`, so prerelease installs overwrite stable regardless of bundle metadata. + Evidence: `install.sh` constants `TARGET_BASENAME="openvcs.AppImage"` and `DESKTOP_PATH="${DESKTOP_DIR}/openvcs.desktop"`. +- Observation: Backend library tests are currently not fully hermetic because several plugin runtime tests expect an app-bundled Node runtime to exist in the test environment. + Evidence: `cargo test --package openvcs --lib` failed in `plugin_runtime::manager::{start_and_stop_are_idempotent,sync_ignores_plugins_without_module_component,sync_tracks_enabled_state}` with `bundled node runtime is unavailable; plugin execution requires app-bundled node`. + +## Decision Log + +- Decision: Stable desktop builds preserve the legacy `OpenVCS` product name, bundle identifier, and existing config/data/plugin directories. + Rationale: This avoids migrating or stranding existing stable users. + Date/Author: 2026-03-24 / OpenCode +- Decision: Beta and nightly desktop builds derive branding and identifiers from a centralized channel map in `Backend/build.rs` instead of separate Tauri config files. + Rationale: One source of truth keeps updater endpoints, branding, and generated config aligned. + Date/Author: 2026-03-24 / OpenCode +- Decision: Flatpak remains stable-only and visually plain `OpenVCS`. + Rationale: User explicitly narrowed the scope to desktop/Tauri variants while leaving Flatpak unchanged. + Date/Author: 2026-03-24 / OpenCode + +## Outcomes & Retrospective + +Implemented the planned desktop channel split. Stable retains the historical identity and persistence roots, beta and nightly now derive distinct Tauri branding plus separate config/data/plugin directories from one shared channel map, prerelease AppImage installs no longer overwrite stable, and nightly CI no longer publishes Flatpak artifacts. Validation succeeded for formatting, frontend type-checking, and stable/beta/nightly backend compilation; existing plugin runtime tests still fail because the test environment lacks the bundled Node runtime they expect. + +## Context and Orientation + +`Backend/build.rs` is the build script that reads `Backend/tauri.conf.json`, mutates it, and exports the final one-line JSON through the `TAURI_CONFIG` environment variable for Tauri to consume at compile time. Today it already knows the release channel through `OPENVCS_UPDATE_CHANNEL`, but it only uses that value to choose updater endpoints and prerelease version suffixes. + +Runtime persistence currently bypasses Tauri path resolution and instead uses `directories::ProjectDirs`. The hardcoded application tuple appears in `Backend/src/settings.rs` for the global config file, `Backend/src/state.rs` for the recent-repositories file, `Backend/src/plugin_paths.rs` for installed plugins, and `Backend/src/plugin_runtime/settings_store.rs` for plugin-local settings. Those paths must stay unchanged for stable and diverge for beta/nightly. + +`install.sh` installs AppImage releases for end users. It currently treats every install as plain `OpenVCS`, so prerelease installs overwrite the stable AppImage and desktop entry. GitHub workflow files in `.github/workflows/` drive stable, beta, nightly, and Flatpak packaging. Nightly currently still builds a Flatpak artifact even though Flatpak is meant to remain stable-only. + +## Plan of Work + +First, extend `Backend/build.rs` with a small channel metadata model that normalizes the channel string and exposes the product name, bundle identifier, window title, updater endpoint list, and version suffix behavior. Apply those values directly to the parsed `tauri.conf.json` object before emitting `TAURI_CONFIG`, and export the normalized channel as a Rust compile-time environment variable so runtime code can make matching persistence decisions. Once this mapping exists, delete the unused `Backend/tauri.stable.conf.json`, `Backend/tauri.beta.conf.json`, and `Backend/tauri.nightly.conf.json` files. + +Next, add a new backend helper module that owns channel-aware identity and `ProjectDirs` resolution. It should parse the compile-time channel value, preserve the legacy stable directories, and return alternate application names for beta and nightly. Update the four current persistence call sites to use this helper instead of constructing `ProjectDirs` directly. + +Then, update the AppImage installer so it chooses stable, beta, nightly, or generic prerelease install paths based on the selected release tag or asset name. The desktop entry name and executable path should follow the detected variant so stable and prerelease installs do not overwrite each other. Update nightly CI so it stops building and uploading Flatpak artifacts, leaving Flatpak only in the stable workflow. + +Finally, refresh user-facing documentation in `README.md` and `packaging/flatpak/README.md` so channel behavior and Flatpak scope are explicit, then run formatting and validation commands. + +## Concrete Steps + +From `/projects/OpenVCS/Client`, perform these commands as the implementation progresses. + + cargo fmt --all + cargo test --workspace + OPENVCS_UPDATE_CHANNEL=stable cargo test --package openvcs_lib + OPENVCS_UPDATE_CHANNEL=beta cargo test --package openvcs_lib + OPENVCS_UPDATE_CHANNEL=nightly cargo test --package openvcs_lib + +If a full workspace test run becomes too expensive during iteration, run `cargo test --package openvcs_lib` after backend changes and finish with the full workspace run before stopping. + +## Validation and Acceptance + +Acceptance is reached when these behaviors are observable: + +Stable builds still present as `OpenVCS` and continue using the historical settings and plugin directories. Beta builds present as `OpenVCS Beta` with identifier `dev.jordon.openvcs.beta`, and nightly builds present as `OpenVCS Nightly` with identifier `dev.jordon.openvcs.nightly`. Backend tests or direct assertions must prove that beta and nightly compute different `ProjectDirs` roots than stable. The installer must no longer write prerelease installs to the same AppImage or desktop entry path as stable. Nightly CI configuration must no longer publish Flatpak artifacts. + +## Idempotence and Recovery + +The build-script and runtime-helper changes are additive and safe to rerun. Deleting the unused `tauri.*.conf.json` stubs is safe once the generated config path is in place because no workflow references them. Installer changes should remain idempotent by writing deterministic paths per variant. If a validation command fails, fix the code and rerun the same command; no manual cleanup beyond the usual build artifacts should be necessary. + +## Artifacts and Notes + +Important files in scope: + + Backend/build.rs + Backend/tauri.conf.json + Backend/src/settings.rs + Backend/src/state.rs + Backend/src/plugin_paths.rs + Backend/src/plugin_runtime/settings_store.rs + install.sh + .github/workflows/nightly.yml + README.md + packaging/flatpak/README.md + +## Interfaces and Dependencies + +Add a small helper module under `Backend/src/` that exposes a stable Rust API for channel-aware identity and data directories. At minimum, the final code should provide functions equivalent to these signatures: + + pub enum AppChannel { Stable, Beta, Nightly } + pub fn current_channel() -> AppChannel + pub fn project_dirs() -> Option + +`Backend/build.rs` should define an internal channel metadata structure that can mutate the generated Tauri config object without needing external config merge files. Runtime call sites should depend only on the shared helper module, not duplicate channel-specific strings. + +Revision note (2026-03-24): Created this ExecPlan to guide implementation after the user approved the full desktop-variant scope, stable-compatibility behavior, and removal of unused Tauri channel config stubs. diff --git a/install.sh b/install.sh index f0c48231..aadad564 100755 --- a/install.sh +++ b/install.sh @@ -13,11 +13,7 @@ REPO_OWNER="Jordonbc" REPO_NAME="OpenVCS" INSTALL_DIR="${HOME}/Applications" -TARGET_BASENAME="openvcs.AppImage" -TARGET_PATH="${INSTALL_DIR%/}/${TARGET_BASENAME}" - DESKTOP_DIR="${HOME}/.local/share/applications" -DESKTOP_PATH="${DESKTOP_DIR}/openvcs.desktop" ICON_NAME="openvcs" ICON_SOURCE_PATH="docs/images/logos/OpenVCS-256.png" ICON_URL_BRANCH="Dev" @@ -25,6 +21,13 @@ ICON_THEME_DIR="${HOME}/.local/share/icons/hicolor" ICON_TARGET_DIR="${ICON_THEME_DIR}/256x256/apps" ICON_PATH="${ICON_TARGET_DIR}/${ICON_NAME}.png" +APP_VARIANT="stable" +APP_DISPLAY_NAME="OpenVCS" +TARGET_BASENAME="openvcs.AppImage" +DESKTOP_BASENAME="openvcs.desktop" +TARGET_PATH="${INSTALL_DIR%/}/${TARGET_BASENAME}" +DESKTOP_PATH="${DESKTOP_DIR}/${DESKTOP_BASENAME}" + # --- State --- INCLUDE_PRERELEASE=false UNINSTALL=false @@ -104,6 +107,65 @@ refresh_desktop_entries() { fi } +set_install_variant() { # $1: stable|beta|nightly|prerelease + case "$1" in + beta) + APP_VARIANT="beta" + APP_DISPLAY_NAME="OpenVCS Beta" + TARGET_BASENAME="openvcs-beta.AppImage" + DESKTOP_BASENAME="openvcs-beta.desktop" + ;; + nightly) + APP_VARIANT="nightly" + APP_DISPLAY_NAME="OpenVCS Nightly" + TARGET_BASENAME="openvcs-nightly.AppImage" + DESKTOP_BASENAME="openvcs-nightly.desktop" + ;; + prerelease) + APP_VARIANT="prerelease" + APP_DISPLAY_NAME="OpenVCS Pre-release" + TARGET_BASENAME="openvcs-prerelease.AppImage" + DESKTOP_BASENAME="openvcs-prerelease.desktop" + ;; + *) + APP_VARIANT="stable" + APP_DISPLAY_NAME="OpenVCS" + TARGET_BASENAME="openvcs.AppImage" + DESKTOP_BASENAME="openvcs.desktop" + ;; + esac + + TARGET_PATH="${INSTALL_DIR%/}/${TARGET_BASENAME}" + DESKTOP_PATH="${DESKTOP_DIR}/${DESKTOP_BASENAME}" +} + +detect_release_variant() { # $1: release tag, $2: asset name + local combined + combined="${1,,} ${2,,}" + + case "$combined" in + *nightly*) printf 'nightly' ;; + *beta*) printf 'beta' ;; + *stable*|*openvcs-v*) printf 'stable' ;; + *) + if $INCLUDE_PRERELEASE; then + printf 'prerelease' + else + printf 'stable' + fi + ;; + esac +} + +remove_file_if_present() { # $1: path, $2: label + if [[ -f "$1" ]]; then + echo "Removing $2: $1" + rm "$1" + else + echo "No $2 found at $1" + fi +} + # --- Parse flags --- for arg in "${@:-}"; do case "$arg" in @@ -190,20 +252,12 @@ trap 'if $INTERACTIVE_MODE; then show_error "Installation failed. Check network # --- Uninstall mode --- if $UNINSTALL; then - echo "Uninstalling OpenVCS..." - if [[ -f "${TARGET_PATH}" ]]; then - echo "Removing AppImage: ${TARGET_PATH}" - rm "${TARGET_PATH}" - else - echo "No AppImage found at ${TARGET_PATH}" - fi - - if [[ -f "${DESKTOP_PATH}" ]]; then - echo "Removing desktop entry: ${DESKTOP_PATH}" - rm "${DESKTOP_PATH}" - else - echo "No desktop entry found at ${DESKTOP_PATH}" - fi + echo "Uninstalling OpenVCS variants..." + for variant in stable beta nightly prerelease; do + set_install_variant "$variant" + remove_file_if_present "${TARGET_PATH}" "${APP_DISPLAY_NAME} AppImage" + remove_file_if_present "${DESKTOP_PATH}" "${APP_DISPLAY_NAME} desktop entry" + done if [[ -f "${ICON_PATH}" ]]; then echo "Removing icon: ${ICON_PATH}" @@ -217,9 +271,9 @@ if $UNINSTALL; then refresh_desktop_entries - echo "✅ OpenVCS uninstalled." + echo "✅ OpenVCS variants uninstalled." if $INTERACTIVE_MODE; then - show_info "✅ OpenVCS was uninstalled." + show_info "✅ OpenVCS variants were uninstalled." fi exit 0 fi @@ -294,6 +348,9 @@ fi echo "Selected release tag: ${RELEASE_TAG:-unknown}" $INCLUDE_PRERELEASE && echo "(including pre-releases)" +set_install_variant "$(detect_release_variant "${RELEASE_TAG:-}" "${ASSET_NAME:-}")" +echo "Installing variant: ${APP_DISPLAY_NAME}" + # --- Download safely (atomic on same filesystem) --- TMP_FILE="$(mktemp --tmpdir="${INSTALL_DIR}" ".openvcs.XXXXXXXX")" trap '[[ -f "${TMP_FILE:-}" ]] && rm "${TMP_FILE}"' EXIT @@ -314,7 +371,7 @@ echo "Writing desktop entry: ${DESKTOP_PATH}" cat > "${DESKTOP_PATH}" < Date: Tue, 24 Mar 2026 17:52:56 +0000 Subject: [PATCH 02/23] Update tauri-build.js --- scripts/tauri-build.js | 82 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index 04823ae5..bbfeeb2b 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -1,8 +1,82 @@ #!/usr/bin/env node const fs = require('fs'); +const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); +/** + * Returns normalized desktop channel metadata for Tauri CLI config overrides. + * + * @param {string | undefined} raw + * @returns {{slug: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} + */ +function resolveChannelConfig(raw) { + const slug = (raw || 'stable').trim().toLowerCase(); + const repo = process.env.OPENVCS_REPO || 'https://github.com/Jordonbc/OpenVCS'; + const stableEndpoint = `${repo}/releases/latest/download/latest.json`; + const betaEndpoint = `${repo}/releases/download/openvcs-beta/latest.json`; + const nightlyEndpoint = `${repo}/releases/download/openvcs-nightly/latest.json`; + + if (slug === 'beta') { + return { + slug: 'beta', + productName: 'OpenVCS Beta', + identifier: 'dev.jordon.openvcs.beta', + windowTitle: 'OpenVCS Beta', + updaterEndpoints: [betaEndpoint, stableEndpoint], + }; + } + + if (slug === 'nightly') { + return { + slug: 'nightly', + productName: 'OpenVCS Nightly', + identifier: 'dev.jordon.openvcs.nightly', + windowTitle: 'OpenVCS Nightly', + updaterEndpoints: [nightlyEndpoint, stableEndpoint], + }; + } + + return { + slug: 'stable', + productName: 'OpenVCS', + identifier: 'dev.jordon.openvcs', + windowTitle: 'OpenVCS', + updaterEndpoints: [stableEndpoint], + }; +} + +/** + * Writes a temporary Tauri merge config matching the requested channel. + * + * @param {string} repoRoot + * @param {{productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} channel + * @returns {string} + */ +function writeChannelConfigOverride(repoRoot, channel) { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openvcs-tauri-config-')); + const configPath = path.join(tmpDir, 'tauri.channel.conf.json'); + const override = { + productName: channel.productName, + identifier: channel.identifier, + app: { + windows: [ + { + title: channel.windowTitle, + }, + ], + }, + plugins: { + updater: { + endpoints: channel.updaterEndpoints, + }, + }, + }; + + fs.writeFileSync(configPath, JSON.stringify(override, null, 2)); + return configPath; +} + function loadLocalEnv(filePath) { if (!fs.existsSync(filePath)) return; const content = fs.readFileSync(filePath, 'utf8'); @@ -69,6 +143,8 @@ function promptHidden(question) { async function main() { const repoRoot = process.cwd(); loadLocalEnv(path.join(repoRoot, '.env.tauri.local')); + const channel = resolveChannelConfig(process.env.OPENVCS_UPDATE_CHANNEL); + const channelConfigPath = writeChannelConfigOverride(repoRoot, channel); if (process.env.TAURI_SIGNING_PRIVATE_KEY_FILE && !process.env.TAURI_SIGNING_PRIVATE_KEY) { console.log('Signing: loading key from TAURI_SIGNING_PRIVATE_KEY_FILE'); @@ -88,13 +164,17 @@ async function main() { } process.env.NO_STRIP = process.env.NO_STRIP || 'true'; + console.log(`Tauri channel: ${channel.slug} (${channel.productName})`); - const child = spawn('cargo', ['tauri', 'build'], { + const child = spawn('cargo', ['tauri', 'build', '--config', channelConfigPath], { stdio: 'inherit', env: process.env, }); child.on('exit', (code, signal) => { + try { + fs.rmSync(path.dirname(channelConfigPath), { recursive: true, force: true }); + } catch {} if (signal) process.kill(process.pid, signal); process.exit(code ?? 1); }); From 9223d0787178bfe36c2e8529aeec3da66a442ea3 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 17:54:57 +0000 Subject: [PATCH 03/23] Update build.rs --- Backend/build.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Backend/build.rs b/Backend/build.rs index fb7c029f..f58dc2d9 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -7,6 +7,8 @@ use std::{env, fs, path::PathBuf, process::Command}; struct ChannelConfig { /// Normalized channel slug used across build and runtime. slug: &'static str, + /// Binary and bundle stem without spaces. + main_binary_name: &'static str, /// Human-facing desktop product name. product_name: &'static str, /// Tauri bundle identifier. @@ -28,18 +30,21 @@ impl ChannelConfig { match raw.trim().to_ascii_lowercase().as_str() { "beta" => Self { slug: "beta", + main_binary_name: "openvcs-beta", product_name: "OpenVCS Beta", identifier: "dev.jordon.openvcs.beta", window_title: "OpenVCS Beta", }, "nightly" => Self { slug: "nightly", + main_binary_name: "openvcs-nightly", product_name: "OpenVCS Nightly", identifier: "dev.jordon.openvcs.nightly", window_title: "OpenVCS Nightly", }, _ => Self { slug: "stable", + main_binary_name: "openvcs", product_name: "OpenVCS", identifier: "dev.jordon.openvcs", window_title: "OpenVCS", @@ -213,6 +218,7 @@ fn main() { } } + json["mainBinaryName"] = serde_json::Value::String(channel.main_binary_name.into()); json["productName"] = serde_json::Value::String(channel.product_name.into()); json["identifier"] = serde_json::Value::String(channel.identifier.into()); if let Some(app) = json.get_mut("app") { From 41527312d3a2d9b0f1bd11d75d1dfb622e3e6223 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 17:54:59 +0000 Subject: [PATCH 04/23] Update tauri-build.js --- scripts/tauri-build.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index bbfeeb2b..f56764c8 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -8,7 +8,7 @@ const { spawn } = require('child_process'); * Returns normalized desktop channel metadata for Tauri CLI config overrides. * * @param {string | undefined} raw - * @returns {{slug: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} + * @returns {{slug: string, mainBinaryName: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} */ function resolveChannelConfig(raw) { const slug = (raw || 'stable').trim().toLowerCase(); @@ -20,6 +20,7 @@ function resolveChannelConfig(raw) { if (slug === 'beta') { return { slug: 'beta', + mainBinaryName: 'openvcs-beta', productName: 'OpenVCS Beta', identifier: 'dev.jordon.openvcs.beta', windowTitle: 'OpenVCS Beta', @@ -30,6 +31,7 @@ function resolveChannelConfig(raw) { if (slug === 'nightly') { return { slug: 'nightly', + mainBinaryName: 'openvcs-nightly', productName: 'OpenVCS Nightly', identifier: 'dev.jordon.openvcs.nightly', windowTitle: 'OpenVCS Nightly', @@ -39,6 +41,7 @@ function resolveChannelConfig(raw) { return { slug: 'stable', + mainBinaryName: 'openvcs', productName: 'OpenVCS', identifier: 'dev.jordon.openvcs', windowTitle: 'OpenVCS', @@ -50,13 +53,14 @@ function resolveChannelConfig(raw) { * Writes a temporary Tauri merge config matching the requested channel. * * @param {string} repoRoot - * @param {{productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} channel + * @param {{mainBinaryName: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} channel * @returns {string} */ function writeChannelConfigOverride(repoRoot, channel) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openvcs-tauri-config-')); const configPath = path.join(tmpDir, 'tauri.channel.conf.json'); const override = { + mainBinaryName: channel.mainBinaryName, productName: channel.productName, identifier: channel.identifier, app: { From 32a51e0e50a0793eb2ef5d48837e2b242f1062e2 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 18:00:11 +0000 Subject: [PATCH 05/23] Delete channel-branding-execplan.md --- docs/channel-branding-execplan.md | 113 ------------------------------ 1 file changed, 113 deletions(-) delete mode 100644 docs/channel-branding-execplan.md diff --git a/docs/channel-branding-execplan.md b/docs/channel-branding-execplan.md deleted file mode 100644 index d8589430..00000000 --- a/docs/channel-branding-execplan.md +++ /dev/null @@ -1,113 +0,0 @@ -# Implement channel-aware desktop branding - -This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. - -This document must be maintained in accordance with `PLANS.md` from the repository root. - -## Purpose / Big Picture - -OpenVCS desktop builds should clearly identify whether they are stable, beta, or nightly, and prerelease builds should be able to live alongside stable without sharing configuration or plugin state. After this change, stable desktop bundles still look and behave like the existing app, while beta and nightly bundles show distinct names, use distinct bundle identifiers, and persist settings in separate directories. Flatpak remains a stable-only package that continues to present itself as plain `OpenVCS`. - -## Progress - -- [x] (2026-03-24 17:45Z) Confirmed channel inputs already come from CI via `OPENVCS_UPDATE_CHANNEL` and documented the compatibility rule: stable keeps legacy identity and paths; beta/nightly get separate branding and persistence. -- [x] (2026-03-24 17:46Z) Identified implementation surfaces: `Backend/build.rs`, backend persistence helpers, `install.sh`, GitHub workflows, README, and Flatpak notes. -- [x] (2026-03-24 17:54Z) Centralized desktop channel metadata in `Backend/build.rs`, exported `OPENVCS_APP_CHANNEL`, and removed the unused `Backend/tauri.stable.conf.json`, `Backend/tauri.beta.conf.json`, and `Backend/tauri.nightly.conf.json` files. -- [x] (2026-03-24 17:55Z) Added `Backend/src/app_identity.rs` and moved all backend config/data/plugin persistence call sites onto channel-aware `ProjectDirs` resolution. -- [x] (2026-03-24 17:57Z) Updated `install.sh`, `.github/workflows/nightly.yml`, `README.md`, and `packaging/flatpak/README.md` for side-by-side prerelease installs and stable-only Flatpak packaging. -- [x] (2026-03-24 17:59Z) Ran `cargo fmt --all`, `npm exec tsc -- -p tsconfig.json --noEmit` in `Frontend/`, and `cargo check --package openvcs --lib` for stable, beta, and nightly. -- [x] (2026-03-24 18:00Z) Ran `cargo test --package openvcs --lib` for stable, beta, and nightly; all three runs compiled the new channel-aware code and failed only in existing plugin runtime tests that require an unavailable bundled Node runtime. - -## Surprises & Discoveries - -- Observation: The repository already contains `Backend/tauri.stable.conf.json`, `Backend/tauri.beta.conf.json`, and `Backend/tauri.nightly.conf.json`, but no build step merges them into Tauri config. - Evidence: `Backend/build.rs` only rewrites updater endpoints and version strings before exporting `TAURI_CONFIG`. -- Observation: Desktop coexistence is blocked by runtime persistence paths as much as by bundle metadata. - Evidence: `Backend/src/settings.rs`, `Backend/src/state.rs`, `Backend/src/plugin_paths.rs`, and `Backend/src/plugin_runtime/settings_store.rs` all hardcode `ProjectDirs::from("dev", "OpenVCS", "OpenVCS")`. -- Observation: The installer script currently always writes `~/Applications/openvcs.AppImage` and `~/.local/share/applications/openvcs.desktop`, so prerelease installs overwrite stable regardless of bundle metadata. - Evidence: `install.sh` constants `TARGET_BASENAME="openvcs.AppImage"` and `DESKTOP_PATH="${DESKTOP_DIR}/openvcs.desktop"`. -- Observation: Backend library tests are currently not fully hermetic because several plugin runtime tests expect an app-bundled Node runtime to exist in the test environment. - Evidence: `cargo test --package openvcs --lib` failed in `plugin_runtime::manager::{start_and_stop_are_idempotent,sync_ignores_plugins_without_module_component,sync_tracks_enabled_state}` with `bundled node runtime is unavailable; plugin execution requires app-bundled node`. - -## Decision Log - -- Decision: Stable desktop builds preserve the legacy `OpenVCS` product name, bundle identifier, and existing config/data/plugin directories. - Rationale: This avoids migrating or stranding existing stable users. - Date/Author: 2026-03-24 / OpenCode -- Decision: Beta and nightly desktop builds derive branding and identifiers from a centralized channel map in `Backend/build.rs` instead of separate Tauri config files. - Rationale: One source of truth keeps updater endpoints, branding, and generated config aligned. - Date/Author: 2026-03-24 / OpenCode -- Decision: Flatpak remains stable-only and visually plain `OpenVCS`. - Rationale: User explicitly narrowed the scope to desktop/Tauri variants while leaving Flatpak unchanged. - Date/Author: 2026-03-24 / OpenCode - -## Outcomes & Retrospective - -Implemented the planned desktop channel split. Stable retains the historical identity and persistence roots, beta and nightly now derive distinct Tauri branding plus separate config/data/plugin directories from one shared channel map, prerelease AppImage installs no longer overwrite stable, and nightly CI no longer publishes Flatpak artifacts. Validation succeeded for formatting, frontend type-checking, and stable/beta/nightly backend compilation; existing plugin runtime tests still fail because the test environment lacks the bundled Node runtime they expect. - -## Context and Orientation - -`Backend/build.rs` is the build script that reads `Backend/tauri.conf.json`, mutates it, and exports the final one-line JSON through the `TAURI_CONFIG` environment variable for Tauri to consume at compile time. Today it already knows the release channel through `OPENVCS_UPDATE_CHANNEL`, but it only uses that value to choose updater endpoints and prerelease version suffixes. - -Runtime persistence currently bypasses Tauri path resolution and instead uses `directories::ProjectDirs`. The hardcoded application tuple appears in `Backend/src/settings.rs` for the global config file, `Backend/src/state.rs` for the recent-repositories file, `Backend/src/plugin_paths.rs` for installed plugins, and `Backend/src/plugin_runtime/settings_store.rs` for plugin-local settings. Those paths must stay unchanged for stable and diverge for beta/nightly. - -`install.sh` installs AppImage releases for end users. It currently treats every install as plain `OpenVCS`, so prerelease installs overwrite the stable AppImage and desktop entry. GitHub workflow files in `.github/workflows/` drive stable, beta, nightly, and Flatpak packaging. Nightly currently still builds a Flatpak artifact even though Flatpak is meant to remain stable-only. - -## Plan of Work - -First, extend `Backend/build.rs` with a small channel metadata model that normalizes the channel string and exposes the product name, bundle identifier, window title, updater endpoint list, and version suffix behavior. Apply those values directly to the parsed `tauri.conf.json` object before emitting `TAURI_CONFIG`, and export the normalized channel as a Rust compile-time environment variable so runtime code can make matching persistence decisions. Once this mapping exists, delete the unused `Backend/tauri.stable.conf.json`, `Backend/tauri.beta.conf.json`, and `Backend/tauri.nightly.conf.json` files. - -Next, add a new backend helper module that owns channel-aware identity and `ProjectDirs` resolution. It should parse the compile-time channel value, preserve the legacy stable directories, and return alternate application names for beta and nightly. Update the four current persistence call sites to use this helper instead of constructing `ProjectDirs` directly. - -Then, update the AppImage installer so it chooses stable, beta, nightly, or generic prerelease install paths based on the selected release tag or asset name. The desktop entry name and executable path should follow the detected variant so stable and prerelease installs do not overwrite each other. Update nightly CI so it stops building and uploading Flatpak artifacts, leaving Flatpak only in the stable workflow. - -Finally, refresh user-facing documentation in `README.md` and `packaging/flatpak/README.md` so channel behavior and Flatpak scope are explicit, then run formatting and validation commands. - -## Concrete Steps - -From `/projects/OpenVCS/Client`, perform these commands as the implementation progresses. - - cargo fmt --all - cargo test --workspace - OPENVCS_UPDATE_CHANNEL=stable cargo test --package openvcs_lib - OPENVCS_UPDATE_CHANNEL=beta cargo test --package openvcs_lib - OPENVCS_UPDATE_CHANNEL=nightly cargo test --package openvcs_lib - -If a full workspace test run becomes too expensive during iteration, run `cargo test --package openvcs_lib` after backend changes and finish with the full workspace run before stopping. - -## Validation and Acceptance - -Acceptance is reached when these behaviors are observable: - -Stable builds still present as `OpenVCS` and continue using the historical settings and plugin directories. Beta builds present as `OpenVCS Beta` with identifier `dev.jordon.openvcs.beta`, and nightly builds present as `OpenVCS Nightly` with identifier `dev.jordon.openvcs.nightly`. Backend tests or direct assertions must prove that beta and nightly compute different `ProjectDirs` roots than stable. The installer must no longer write prerelease installs to the same AppImage or desktop entry path as stable. Nightly CI configuration must no longer publish Flatpak artifacts. - -## Idempotence and Recovery - -The build-script and runtime-helper changes are additive and safe to rerun. Deleting the unused `tauri.*.conf.json` stubs is safe once the generated config path is in place because no workflow references them. Installer changes should remain idempotent by writing deterministic paths per variant. If a validation command fails, fix the code and rerun the same command; no manual cleanup beyond the usual build artifacts should be necessary. - -## Artifacts and Notes - -Important files in scope: - - Backend/build.rs - Backend/tauri.conf.json - Backend/src/settings.rs - Backend/src/state.rs - Backend/src/plugin_paths.rs - Backend/src/plugin_runtime/settings_store.rs - install.sh - .github/workflows/nightly.yml - README.md - packaging/flatpak/README.md - -## Interfaces and Dependencies - -Add a small helper module under `Backend/src/` that exposes a stable Rust API for channel-aware identity and data directories. At minimum, the final code should provide functions equivalent to these signatures: - - pub enum AppChannel { Stable, Beta, Nightly } - pub fn current_channel() -> AppChannel - pub fn project_dirs() -> Option - -`Backend/build.rs` should define an internal channel metadata structure that can mutate the generated Tauri config object without needing external config merge files. Runtime call sites should depend only on the shared helper module, not duplicate channel-specific strings. - -Revision note (2026-03-24): Created this ExecPlan to guide implementation after the user approved the full desktop-variant scope, stable-compatibility behavior, and removal of unused Tauri channel config stubs. From d5a34817bddfd7c9e61e5253d417f31cbb479190 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 18:00:47 +0000 Subject: [PATCH 06/23] Implement ci changes --- .github/workflows/beta.yml | 8 ++- .github/workflows/nightly.yml | 7 +- .github/workflows/publish-stable.yml | 7 +- README.md | 4 ++ scripts/tauri-build.js | 92 ++++-------------------- scripts/write-tauri-channel-config.js | 100 ++++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 84 deletions(-) create mode 100644 scripts/write-tauri-channel-config.js diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index cf2b8ae6..97e36ef9 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -105,6 +105,11 @@ jobs: if (e.status !== 422) throw e; } + - name: Write channel Tauri config override + env: + OPENVCS_UPDATE_CHANNEL: beta + run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json" + - name: Build and publish Beta prerelease uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 env: @@ -124,5 +129,4 @@ jobs: Runner: ${{ runner.os }} • Run #${{ github.run_number }} releaseDraft: true prerelease: true - args: ${{ matrix.args }} - + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args, runner.temp) }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3cb65286..f808cb3a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -214,6 +214,11 @@ jobs: if (e.status !== 422) throw e; } + - name: Write channel Tauri config override + env: + OPENVCS_UPDATE_CHANNEL: nightly + run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json" + # ---------- Build & publish ---------- - name: Build and publish Nightly prerelease uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 @@ -240,4 +245,4 @@ jobs: ${{ steps.meta.outputs.changelog }} releaseDraft: false prerelease: true - args: ${{ matrix.args }} + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args, runner.temp) }} diff --git a/.github/workflows/publish-stable.yml b/.github/workflows/publish-stable.yml index fbf203e9..e2e19cee 100644 --- a/.github/workflows/publish-stable.yml +++ b/.github/workflows/publish-stable.yml @@ -132,6 +132,11 @@ jobs: - name: Cargo clippy run: cargo clippy --all-targets -- -D warnings + - name: Write channel Tauri config override + env: + OPENVCS_UPDATE_CHANNEL: stable + run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json" + # ---------- Build & publish with Tauri action ---------- - name: Build and create GitHub Release (draft) uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 @@ -155,7 +160,7 @@ jobs: releaseBody: 'See the assets to download this version and install.' releaseDraft: true prerelease: false - args: ${{ matrix.args }} + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args, runner.temp) }} includeDebug: false # default; set true if you want debug archives too # bundles: '' # e.g. 'deb,appimage,msi,nsis' if you want to restrict output diff --git a/README.md b/README.md index 8c101809..0d9740a2 100644 --- a/README.md +++ b/README.md @@ -163,11 +163,15 @@ cargo tauri dev ```bash just tauri-build +just tauri-build beta ``` This wraps `cargo tauri build` with `NO_STRIP=true` to avoid AppImage linuxdeploy strip failures on newer Linux toolchains. +`just build`, `just build stable`, `just build beta`, and `just build nightly` +use the same channel-aware Tauri build flow. + The Tauri precommands in `Backend/tauri.conf.json` intentionally set an explicit hook `cwd` to `Backend/` so Tauri does not resolve them from nested plugin directories, and built-in plugins are bundled through each plugin's own diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index f56764c8..0ef74044 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -3,83 +3,7 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); - -/** - * Returns normalized desktop channel metadata for Tauri CLI config overrides. - * - * @param {string | undefined} raw - * @returns {{slug: string, mainBinaryName: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} - */ -function resolveChannelConfig(raw) { - const slug = (raw || 'stable').trim().toLowerCase(); - const repo = process.env.OPENVCS_REPO || 'https://github.com/Jordonbc/OpenVCS'; - const stableEndpoint = `${repo}/releases/latest/download/latest.json`; - const betaEndpoint = `${repo}/releases/download/openvcs-beta/latest.json`; - const nightlyEndpoint = `${repo}/releases/download/openvcs-nightly/latest.json`; - - if (slug === 'beta') { - return { - slug: 'beta', - mainBinaryName: 'openvcs-beta', - productName: 'OpenVCS Beta', - identifier: 'dev.jordon.openvcs.beta', - windowTitle: 'OpenVCS Beta', - updaterEndpoints: [betaEndpoint, stableEndpoint], - }; - } - - if (slug === 'nightly') { - return { - slug: 'nightly', - mainBinaryName: 'openvcs-nightly', - productName: 'OpenVCS Nightly', - identifier: 'dev.jordon.openvcs.nightly', - windowTitle: 'OpenVCS Nightly', - updaterEndpoints: [nightlyEndpoint, stableEndpoint], - }; - } - - return { - slug: 'stable', - mainBinaryName: 'openvcs', - productName: 'OpenVCS', - identifier: 'dev.jordon.openvcs', - windowTitle: 'OpenVCS', - updaterEndpoints: [stableEndpoint], - }; -} - -/** - * Writes a temporary Tauri merge config matching the requested channel. - * - * @param {string} repoRoot - * @param {{mainBinaryName: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} channel - * @returns {string} - */ -function writeChannelConfigOverride(repoRoot, channel) { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openvcs-tauri-config-')); - const configPath = path.join(tmpDir, 'tauri.channel.conf.json'); - const override = { - mainBinaryName: channel.mainBinaryName, - productName: channel.productName, - identifier: channel.identifier, - app: { - windows: [ - { - title: channel.windowTitle, - }, - ], - }, - plugins: { - updater: { - endpoints: channel.updaterEndpoints, - }, - }, - }; - - fs.writeFileSync(configPath, JSON.stringify(override, null, 2)); - return configPath; -} +const { writeChannelConfig } = require('./write-tauri-channel-config.js'); function loadLocalEnv(filePath) { if (!fs.existsSync(filePath)) return; @@ -147,8 +71,16 @@ function promptHidden(question) { async function main() { const repoRoot = process.cwd(); loadLocalEnv(path.join(repoRoot, '.env.tauri.local')); - const channel = resolveChannelConfig(process.env.OPENVCS_UPDATE_CHANNEL); - const channelConfigPath = writeChannelConfigOverride(repoRoot, channel); + const channelSlug = (process.env.OPENVCS_UPDATE_CHANNEL || 'stable').trim().toLowerCase(); + const channelNames = { + stable: 'OpenVCS', + beta: 'OpenVCS Beta', + nightly: 'OpenVCS Nightly', + }; + const channelProductName = channelNames[channelSlug] || 'OpenVCS'; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openvcs-tauri-config-')); + const channelConfigPath = path.join(tmpDir, 'tauri.channel.conf.json'); + writeChannelConfig(channelConfigPath); if (process.env.TAURI_SIGNING_PRIVATE_KEY_FILE && !process.env.TAURI_SIGNING_PRIVATE_KEY) { console.log('Signing: loading key from TAURI_SIGNING_PRIVATE_KEY_FILE'); @@ -168,7 +100,7 @@ async function main() { } process.env.NO_STRIP = process.env.NO_STRIP || 'true'; - console.log(`Tauri channel: ${channel.slug} (${channel.productName})`); + console.log(`Tauri channel: ${channelSlug} (${channelProductName})`); const child = spawn('cargo', ['tauri', 'build', '--config', channelConfigPath], { stdio: 'inherit', diff --git a/scripts/write-tauri-channel-config.js b/scripts/write-tauri-channel-config.js new file mode 100644 index 00000000..04678a31 --- /dev/null +++ b/scripts/write-tauri-channel-config.js @@ -0,0 +1,100 @@ +// Copyright © 2025-2026 OpenVCS Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +const fs = require('fs'); +const path = require('path'); + +/** + * Returns normalized desktop channel metadata for Tauri CLI config overrides. + * + * @param {string | undefined} raw + * @returns {{mainBinaryName: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} + */ +function resolveChannelConfig(raw) { + const slug = (raw || 'stable').trim().toLowerCase(); + const repo = process.env.OPENVCS_REPO || 'https://github.com/Jordonbc/OpenVCS'; + const stableEndpoint = `${repo}/releases/latest/download/latest.json`; + const betaEndpoint = `${repo}/releases/download/openvcs-beta/latest.json`; + const nightlyEndpoint = `${repo}/releases/download/openvcs-nightly/latest.json`; + + if (slug === 'beta') { + return { + mainBinaryName: 'openvcs-beta', + productName: 'OpenVCS Beta', + identifier: 'dev.jordon.openvcs.beta', + windowTitle: 'OpenVCS Beta', + updaterEndpoints: [betaEndpoint, stableEndpoint], + }; + } + + if (slug === 'nightly') { + return { + mainBinaryName: 'openvcs-nightly', + productName: 'OpenVCS Nightly', + identifier: 'dev.jordon.openvcs.nightly', + windowTitle: 'OpenVCS Nightly', + updaterEndpoints: [nightlyEndpoint, stableEndpoint], + }; + } + + return { + mainBinaryName: 'openvcs', + productName: 'OpenVCS', + identifier: 'dev.jordon.openvcs', + windowTitle: 'OpenVCS', + updaterEndpoints: [stableEndpoint], + }; +} + +/** + * Writes a Tauri merge config matching the requested channel. + * + * @param {string} outputPath + * @returns {void} + */ +function writeChannelConfig(outputPath) { + const channel = resolveChannelConfig(process.env.OPENVCS_UPDATE_CHANNEL); + const override = { + mainBinaryName: channel.mainBinaryName, + productName: channel.productName, + identifier: channel.identifier, + app: { + windows: [ + { + title: channel.windowTitle, + }, + ], + }, + plugins: { + updater: { + endpoints: channel.updaterEndpoints, + }, + }, + }; + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, JSON.stringify(override, null, 2)); +} + +/** + * CLI entry point. + * + * @returns {void} + */ +function main() { + const outputPath = process.argv[2]; + if (!outputPath) { + console.error('Usage: node scripts/write-tauri-channel-config.js '); + process.exit(2); + } + + writeChannelConfig(path.resolve(outputPath)); +} + +module.exports = { + writeChannelConfig, +}; + +if (require.main === module) { + main(); +} From 99c74f1d9e4bee1f91fb56a95165d2f55a879263 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 18:11:00 +0000 Subject: [PATCH 07/23] Update app_identity.rs --- Backend/src/app_identity.rs | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Backend/src/app_identity.rs b/Backend/src/app_identity.rs index 2f6e3446..0fa96cc1 100644 --- a/Backend/src/app_identity.rs +++ b/Backend/src/app_identity.rs @@ -33,16 +33,16 @@ impl AppChannel { } } - /// Returns the user-facing desktop product name for this channel. + /// Returns the filesystem app name used for persistence. + /// + /// All desktop channels intentionally share the historical `OpenVCS` + /// directory so builds keep using the same config and plugin roots. /// /// # Returns - /// - Product name used by bundles and channel-specific docs. - pub fn product_name(self) -> &'static str { - match self { - Self::Stable => "OpenVCS", - Self::Beta => "OpenVCS Beta", - Self::Nightly => "OpenVCS Nightly", - } + /// - Application name for `ProjectDirs`. + pub fn persistence_name(self) -> &'static str { + let _ = self; + "OpenVCS" } } @@ -56,15 +56,14 @@ pub fn current_channel() -> AppChannel { /// Returns channel-aware project directories for app config and data. /// -/// Stable builds preserve the legacy `OpenVCS` application name so existing -/// users keep the same config and data roots. Beta and nightly use their -/// distinct product names so they can coexist with stable. +/// All desktop channels preserve the legacy `OpenVCS` application name so +/// existing users keep the same config and data roots. /// /// # Returns /// - `Some(ProjectDirs)` when the platform exposes standard app directories. /// - `None` when no platform-specific directories are available. pub fn project_dirs() -> Option { - ProjectDirs::from("dev", "OpenVCS", current_channel().product_name()) + ProjectDirs::from("dev", "OpenVCS", current_channel().persistence_name()) } #[cfg(test)] @@ -92,11 +91,11 @@ mod tests { assert_eq!(AppChannel::from_build_channel("BETA"), AppChannel::Beta); } - /// Verifies each channel exposes the expected desktop product name. + /// Verifies all channels keep the shared legacy persistence root. #[test] - fn exposes_product_names() { - assert_eq!(AppChannel::Stable.product_name(), "OpenVCS"); - assert_eq!(AppChannel::Beta.product_name(), "OpenVCS Beta"); - assert_eq!(AppChannel::Nightly.product_name(), "OpenVCS Nightly"); + fn exposes_persistence_names() { + assert_eq!(AppChannel::Stable.persistence_name(), "OpenVCS"); + assert_eq!(AppChannel::Beta.persistence_name(), "OpenVCS"); + assert_eq!(AppChannel::Nightly.persistence_name(), "OpenVCS"); } } From bb7de745bce5a5ce0cc18a45f90ed9104a337dbc Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 18:11:04 +0000 Subject: [PATCH 08/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d9740a2..f3a4bae2 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ curl -fsSL https://raw.githubusercontent.com/Jordonbc/OpenVCS/stable/install.sh The script targets Linux, leaves existing configuration untouched, and can be re-run to pull the newest release. -Stable desktop builds keep the legacy `OpenVCS` app identity and reuse the existing config and plugin directories. +Desktop builds keep using the legacy shared `OpenVCS` config and plugin directories. **Install pre-release (nightly):** From 209ffe7b741be00e6819e31fc9e1ad70e16dcc9f Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 18:14:47 +0000 Subject: [PATCH 09/23] Update build.rs --- Backend/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Backend/build.rs b/Backend/build.rs index f58dc2d9..9f4f7307 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -31,14 +31,14 @@ impl ChannelConfig { "beta" => Self { slug: "beta", main_binary_name: "openvcs-beta", - product_name: "OpenVCS Beta", + product_name: "OpenVCS-Beta", identifier: "dev.jordon.openvcs.beta", window_title: "OpenVCS Beta", }, "nightly" => Self { slug: "nightly", main_binary_name: "openvcs-nightly", - product_name: "OpenVCS Nightly", + product_name: "OpenVCS-Nightly", identifier: "dev.jordon.openvcs.nightly", window_title: "OpenVCS Nightly", }, From 3b262cf6c8ad8bbf8e057f6b39a408959436872c Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 18:14:50 +0000 Subject: [PATCH 10/23] Update write-tauri-channel-config.js --- scripts/write-tauri-channel-config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/write-tauri-channel-config.js b/scripts/write-tauri-channel-config.js index 04678a31..19263929 100644 --- a/scripts/write-tauri-channel-config.js +++ b/scripts/write-tauri-channel-config.js @@ -20,7 +20,7 @@ function resolveChannelConfig(raw) { if (slug === 'beta') { return { mainBinaryName: 'openvcs-beta', - productName: 'OpenVCS Beta', + productName: 'OpenVCS-Beta', identifier: 'dev.jordon.openvcs.beta', windowTitle: 'OpenVCS Beta', updaterEndpoints: [betaEndpoint, stableEndpoint], @@ -30,7 +30,7 @@ function resolveChannelConfig(raw) { if (slug === 'nightly') { return { mainBinaryName: 'openvcs-nightly', - productName: 'OpenVCS Nightly', + productName: 'OpenVCS-Nightly', identifier: 'dev.jordon.openvcs.nightly', windowTitle: 'OpenVCS Nightly', updaterEndpoints: [nightlyEndpoint, stableEndpoint], From 845905b7289fd72ae8a2b7d9d1a71f462f9aaedf Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:51:56 +0000 Subject: [PATCH 11/23] Fix issues --- Backend/Cargo.toml | 1 + Backend/build.rs | 116 +++++++++++++++----------- Backend/src/app_identity.rs | 9 +- channel-metadata.json | 36 ++++++++ scripts/tauri-build.js | 1 + scripts/write-tauri-channel-config.js | 61 ++++---------- 6 files changed, 125 insertions(+), 99 deletions(-) create mode 100644 channel-metadata.json diff --git a/Backend/Cargo.toml b/Backend/Cargo.toml index 7ae0ff8d..abe1ff68 100644 --- a/Backend/Cargo.toml +++ b/Backend/Cargo.toml @@ -19,6 +19,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2.4", default-features = false, features = [] } +serde = { version = "1", features = ["derive"] } serde_json = "1.0" [features] diff --git a/Backend/build.rs b/Backend/build.rs index 9f4f7307..f00ebea8 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -1,7 +1,41 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later +use serde::Deserialize; use std::{env, fs, path::PathBuf, process::Command}; +fn load_channel_metadata() -> ChannelMetadata { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let config_path = manifest_dir.join("../channel-metadata.json"); + let data = fs::read_to_string(&config_path).expect("read channel-metadata.json"); + serde_json::from_str(&data).expect("parse channel-metadata.json") +} + +#[derive(Clone, Debug, Deserialize)] +struct ChannelMetadata { + channels: Channels, +} + +#[derive(Clone, Debug, Deserialize)] +struct Channels { + stable: ChannelEntry, + beta: ChannelEntry, + nightly: ChannelEntry, +} + +#[derive(Clone, Debug, Deserialize)] +struct ChannelEntry { + slug: String, + #[serde(rename = "mainBinaryName")] + main_binary_name: String, + #[serde(rename = "productName")] + product_name: String, + identifier: String, + #[serde(rename = "windowTitle")] + window_title: String, + #[serde(rename = "updaterEndpoints")] + updater_endpoints: Vec, +} + /// Channel-specific metadata used for generated desktop bundles. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct ChannelConfig { @@ -15,6 +49,8 @@ struct ChannelConfig { identifier: &'static str, /// Main window title. window_title: &'static str, + /// Updater endpoint URLs. + updater_endpoints: &'static [&'static str], } impl ChannelConfig { @@ -26,29 +62,28 @@ impl ChannelConfig { /// # Returns /// - Stable metadata when the input is missing or unknown. /// - Beta or nightly metadata for recognized channel names. - fn from_env_value(raw: &str) -> Self { - match raw.trim().to_ascii_lowercase().as_str() { - "beta" => Self { - slug: "beta", - main_binary_name: "openvcs-beta", - product_name: "OpenVCS-Beta", - identifier: "dev.jordon.openvcs.beta", - window_title: "OpenVCS Beta", - }, - "nightly" => Self { - slug: "nightly", - main_binary_name: "openvcs-nightly", - product_name: "OpenVCS-Nightly", - identifier: "dev.jordon.openvcs.nightly", - window_title: "OpenVCS Nightly", - }, - _ => Self { - slug: "stable", - main_binary_name: "openvcs", - product_name: "OpenVCS", - identifier: "dev.jordon.openvcs", - window_title: "OpenVCS", - }, + fn from_env_value(raw: &str, metadata: &ChannelMetadata) -> Self { + let slug = raw.trim().to_ascii_lowercase(); + match slug.as_str() { + "beta" => Self::from_entry(&metadata.channels.beta), + "nightly" => Self::from_entry(&metadata.channels.nightly), + _ => Self::from_entry(&metadata.channels.stable), + } + } + + fn from_entry(entry: &ChannelEntry) -> Self { + let endpoints: Vec<&'static str> = entry + .updater_endpoints + .iter() + .map(|s| Box::leak(s.to_string().into_boxed_str()) as &str) + .collect(); + Self { + slug: Box::leak(entry.slug.clone().into_boxed_str()), + main_binary_name: Box::leak(entry.main_binary_name.clone().into_boxed_str()), + product_name: Box::leak(entry.product_name.clone().into_boxed_str()), + identifier: Box::leak(entry.identifier.clone().into_boxed_str()), + window_title: Box::leak(entry.window_title.clone().into_boxed_str()), + updater_endpoints: Box::leak(endpoints.into_boxed_slice()), } } } @@ -183,38 +218,21 @@ fn main() { let mut json: serde_json::Value = serde_json::from_str(&data).expect("parse tauri.conf.json"); // Compute channel based on environment; default to stable. + let channel_metadata = load_channel_metadata(); let channel = ChannelConfig::from_env_value( &env::var("OPENVCS_UPDATE_CHANNEL").unwrap_or_else(|_| "stable".into()), + &channel_metadata, ); - // Repository URL (can be overridden via env var for forks) - let repo = - env::var("OPENVCS_REPO").unwrap_or_else(|_| "https://github.com/Jordonbc/OpenVCS".into()); - - // Build update URLs from repository - let stable = - serde_json::Value::String(format!("{}/releases/latest/download/latest.json", repo)); - let beta = serde_json::Value::String(format!( - "{}/releases/download/openvcs-beta/latest.json", - repo - )); - let nightly = serde_json::Value::String(format!( - "{}/releases/download/openvcs-nightly/latest.json", - repo - )); - // Navigate: plugins.updater.endpoints if let Some(plugins) = json.get_mut("plugins") { if let Some(updater) = plugins.get_mut("updater") { - let endpoints = match channel.slug { - // Beta: check beta first, then stable - "beta" => serde_json::Value::Array(vec![beta.clone(), stable.clone()]), - // Nightly: check nightly first, then stable - "nightly" => serde_json::Value::Array(vec![nightly.clone(), stable.clone()]), - // Stable: stable only - _ => serde_json::Value::Array(vec![stable.clone()]), - }; - updater["endpoints"] = endpoints; + let endpoints: Vec = channel + .updater_endpoints + .iter() + .map(|s| serde_json::Value::String((*s).to_string())) + .collect(); + updater["endpoints"] = serde_json::Value::Array(endpoints); } } @@ -273,6 +291,8 @@ fn main() { // Re-run if the base config changes println!("cargo:rerun-if-changed={}", base.display()); + let config_path = manifest_dir.join("../channel-metadata.json"); + println!("cargo:rerun-if-changed={}", config_path.display()); println!("cargo:rerun-if-env-changed=OPENVCS_UPDATE_CHANNEL"); println!("cargo:rerun-if-env-changed=OPENVCS_FLATPAK"); println!("cargo:rerun-if-env-changed=OPENVCS_OFFICIAL_RELEASE"); diff --git a/Backend/src/app_identity.rs b/Backend/src/app_identity.rs index 0fa96cc1..cc2aaaa5 100644 --- a/Backend/src/app_identity.rs +++ b/Backend/src/app_identity.rs @@ -40,8 +40,7 @@ impl AppChannel { /// /// # Returns /// - Application name for `ProjectDirs`. - pub fn persistence_name(self) -> &'static str { - let _ = self; + pub fn persistence_name() -> &'static str { "OpenVCS" } } @@ -63,7 +62,7 @@ pub fn current_channel() -> AppChannel { /// - `Some(ProjectDirs)` when the platform exposes standard app directories. /// - `None` when no platform-specific directories are available. pub fn project_dirs() -> Option { - ProjectDirs::from("dev", "OpenVCS", current_channel().persistence_name()) + ProjectDirs::from("dev", "OpenVCS", AppChannel::persistence_name()) } #[cfg(test)] @@ -94,8 +93,6 @@ mod tests { /// Verifies all channels keep the shared legacy persistence root. #[test] fn exposes_persistence_names() { - assert_eq!(AppChannel::Stable.persistence_name(), "OpenVCS"); - assert_eq!(AppChannel::Beta.persistence_name(), "OpenVCS"); - assert_eq!(AppChannel::Nightly.persistence_name(), "OpenVCS"); + assert_eq!(AppChannel::persistence_name(), "OpenVCS"); } } diff --git a/channel-metadata.json b/channel-metadata.json new file mode 100644 index 00000000..4e053fff --- /dev/null +++ b/channel-metadata.json @@ -0,0 +1,36 @@ +{ + "repo": "https://github.com/Jordonbc/OpenVCS", + "channels": { + "stable": { + "slug": "stable", + "mainBinaryName": "openvcs", + "productName": "OpenVCS", + "identifier": "dev.jordon.openvcs", + "windowTitle": "OpenVCS", + "updaterEndpoints": ["https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json"] + }, + "beta": { + "slug": "beta", + "mainBinaryName": "openvcs-beta", + "productName": "OpenVCS-Beta", + "identifier": "dev.jordon.openvcs.beta", + "windowTitle": "OpenVCS Beta", + "updaterEndpoints": [ + "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-beta/latest.json", + "https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json" + ] + }, + "nightly": { + "slug": "nightly", + "mainBinaryName": "openvcs-nightly", + "productName": "OpenVCS-Nightly", + "identifier": "dev.jordon.openvcs.nightly", + "windowTitle": "OpenVCS Nightly", + "updaterEndpoints": [ + "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-nightly/latest.json", + "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-beta/latest.json", + "https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json" + ] + } + } +} diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index 0ef74044..5a4edc10 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -109,6 +109,7 @@ async function main() { child.on('exit', (code, signal) => { try { + // channelConfigPath is a file inside the temp dir, so dirname removes the temp dir itself fs.rmSync(path.dirname(channelConfigPath), { recursive: true, force: true }); } catch {} if (signal) process.kill(process.pid, signal); diff --git a/scripts/write-tauri-channel-config.js b/scripts/write-tauri-channel-config.js index 19263929..dd9c5209 100644 --- a/scripts/write-tauri-channel-config.js +++ b/scripts/write-tauri-channel-config.js @@ -4,54 +4,28 @@ const fs = require('fs'); const path = require('path'); -/** - * Returns normalized desktop channel metadata for Tauri CLI config overrides. - * - * @param {string | undefined} raw - * @returns {{mainBinaryName: string, productName: string, identifier: string, windowTitle: string, updaterEndpoints: string[]}} - */ +const CHANNEL_METADATA_PATH = path.resolve(__dirname, '../channel-metadata.json'); + +function loadChannelMetadata() { + const data = fs.readFileSync(CHANNEL_METADATA_PATH, 'utf8'); + return JSON.parse(data); +} + function resolveChannelConfig(raw) { + const metadata = loadChannelMetadata(); const slug = (raw || 'stable').trim().toLowerCase(); - const repo = process.env.OPENVCS_REPO || 'https://github.com/Jordonbc/OpenVCS'; - const stableEndpoint = `${repo}/releases/latest/download/latest.json`; - const betaEndpoint = `${repo}/releases/download/openvcs-beta/latest.json`; - const nightlyEndpoint = `${repo}/releases/download/openvcs-nightly/latest.json`; - if (slug === 'beta') { - return { - mainBinaryName: 'openvcs-beta', - productName: 'OpenVCS-Beta', - identifier: 'dev.jordon.openvcs.beta', - windowTitle: 'OpenVCS Beta', - updaterEndpoints: [betaEndpoint, stableEndpoint], - }; - } - - if (slug === 'nightly') { - return { - mainBinaryName: 'openvcs-nightly', - productName: 'OpenVCS-Nightly', - identifier: 'dev.jordon.openvcs.nightly', - windowTitle: 'OpenVCS Nightly', - updaterEndpoints: [nightlyEndpoint, stableEndpoint], - }; - } + const entry = metadata.channels[slug] || metadata.channels.stable; return { - mainBinaryName: 'openvcs', - productName: 'OpenVCS', - identifier: 'dev.jordon.openvcs', - windowTitle: 'OpenVCS', - updaterEndpoints: [stableEndpoint], + mainBinaryName: entry.mainBinaryName, + productName: entry.productName, + identifier: entry.identifier, + windowTitle: entry.windowTitle, + updaterEndpoints: entry.updaterEndpoints, }; } -/** - * Writes a Tauri merge config matching the requested channel. - * - * @param {string} outputPath - * @returns {void} - */ function writeChannelConfig(outputPath) { const channel = resolveChannelConfig(process.env.OPENVCS_UPDATE_CHANNEL); const override = { @@ -76,11 +50,6 @@ function writeChannelConfig(outputPath) { fs.writeFileSync(outputPath, JSON.stringify(override, null, 2)); } -/** - * CLI entry point. - * - * @returns {void} - */ function main() { const outputPath = process.argv[2]; if (!outputPath) { @@ -93,6 +62,8 @@ function main() { module.exports = { writeChannelConfig, + resolveChannelConfig, + loadChannelMetadata, }; if (require.main === module) { From d1548b0282506138942043e327a2480b9d417794 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:52:10 +0000 Subject: [PATCH 12/23] Update app_identity.rs --- Backend/src/app_identity.rs | 79 +++++-------------------------------- 1 file changed, 10 insertions(+), 69 deletions(-) diff --git a/Backend/src/app_identity.rs b/Backend/src/app_identity.rs index cc2aaaa5..c0eb545f 100644 --- a/Backend/src/app_identity.rs +++ b/Backend/src/app_identity.rs @@ -5,52 +5,15 @@ use directories::ProjectDirs; -/// Known desktop release channels. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AppChannel { - /// Stable desktop builds that preserve the legacy app identity. - Stable, - /// Beta desktop builds that use separate branding and persistence. - Beta, - /// Nightly desktop builds that use separate branding and persistence. - Nightly, -} - -impl AppChannel { - /// Parses the compile-time channel slug emitted by `build.rs`. - /// - /// # Parameters - /// - `raw`: Raw channel string. - /// - /// # Returns - /// - [`AppChannel::Stable`] for missing or unknown values. - /// - The matching prerelease channel for known values. - pub fn from_build_channel(raw: &str) -> Self { - match raw.trim().to_ascii_lowercase().as_str() { - "beta" => Self::Beta, - "nightly" => Self::Nightly, - _ => Self::Stable, - } - } - - /// Returns the filesystem app name used for persistence. - /// - /// All desktop channels intentionally share the historical `OpenVCS` - /// directory so builds keep using the same config and plugin roots. - /// - /// # Returns - /// - Application name for `ProjectDirs`. - pub fn persistence_name() -> &'static str { - "OpenVCS" - } -} - -/// Returns the current desktop release channel compiled into the binary. +/// Returns the filesystem app name used for persistence. +/// +/// All desktop channels intentionally share the historical `OpenVCS` +/// directory so builds keep using the same config and plugin roots. /// /// # Returns -/// - Current build channel, defaulting to stable when unspecified. -pub fn current_channel() -> AppChannel { - AppChannel::from_build_channel(option_env!("OPENVCS_APP_CHANNEL").unwrap_or("stable")) +/// - Application name for `ProjectDirs`. +pub fn persistence_name() -> &'static str { + "OpenVCS" } /// Returns channel-aware project directories for app config and data. @@ -62,37 +25,15 @@ pub fn current_channel() -> AppChannel { /// - `Some(ProjectDirs)` when the platform exposes standard app directories. /// - `None` when no platform-specific directories are available. pub fn project_dirs() -> Option { - ProjectDirs::from("dev", "OpenVCS", AppChannel::persistence_name()) + ProjectDirs::from("dev", "OpenVCS", persistence_name()) } #[cfg(test)] mod tests { - use super::AppChannel; - - /// Verifies unknown channel strings fall back to stable. - #[test] - fn defaults_unknown_channels_to_stable() { - assert_eq!(AppChannel::from_build_channel(""), AppChannel::Stable); - assert_eq!( - AppChannel::from_build_channel("preview"), - AppChannel::Stable - ); - } - - /// Verifies known prerelease channels parse successfully. - #[test] - fn parses_known_prerelease_channels() { - assert_eq!(AppChannel::from_build_channel("beta"), AppChannel::Beta); - assert_eq!( - AppChannel::from_build_channel("nightly"), - AppChannel::Nightly - ); - assert_eq!(AppChannel::from_build_channel("BETA"), AppChannel::Beta); - } + use super::persistence_name; - /// Verifies all channels keep the shared legacy persistence root. #[test] fn exposes_persistence_names() { - assert_eq!(AppChannel::persistence_name(), "OpenVCS"); + assert_eq!(persistence_name(), "OpenVCS"); } } From 53c7860308a9bdca31259ac6985a0e1e67f80f0c Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:55:46 +0000 Subject: [PATCH 13/23] Update build.rs --- Backend/build.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Backend/build.rs b/Backend/build.rs index f00ebea8..efc54e4d 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -64,11 +64,17 @@ impl ChannelConfig { /// - Beta or nightly metadata for recognized channel names. fn from_env_value(raw: &str, metadata: &ChannelMetadata) -> Self { let slug = raw.trim().to_ascii_lowercase(); - match slug.as_str() { - "beta" => Self::from_entry(&metadata.channels.beta), - "nightly" => Self::from_entry(&metadata.channels.nightly), - _ => Self::from_entry(&metadata.channels.stable), - } + let entry = match slug.as_str() { + "beta" => &metadata.channels.beta, + "nightly" => &metadata.channels.nightly, + _ => { + if !raw.trim().is_empty() { + eprintln!("Warning: Unknown channel '{raw}', defaulting to stable"); + } + &metadata.channels.stable + } + }; + Self::from_entry(entry) } fn from_entry(entry: &ChannelEntry) -> Self { From 3e616d8024e691c85f3f1b4b2befad01db77670c Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:55:48 +0000 Subject: [PATCH 14/23] Update write-tauri-channel-config.js --- scripts/write-tauri-channel-config.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/write-tauri-channel-config.js b/scripts/write-tauri-channel-config.js index dd9c5209..ab676dbe 100644 --- a/scripts/write-tauri-channel-config.js +++ b/scripts/write-tauri-channel-config.js @@ -15,7 +15,13 @@ function resolveChannelConfig(raw) { const metadata = loadChannelMetadata(); const slug = (raw || 'stable').trim().toLowerCase(); - const entry = metadata.channels[slug] || metadata.channels.stable; + let entry = metadata.channels[slug]; + if (!entry) { + if (raw && raw.trim()) { + console.warn(`Warning: Unknown channel '${raw}', defaulting to stable`); + } + entry = metadata.channels.stable; + } return { mainBinaryName: entry.mainBinaryName, From 1dfaecc2e6b2c21aa707b6d8511e79be740b241d Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:57:20 +0000 Subject: [PATCH 15/23] Update install.sh --- install.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/install.sh b/install.sh index aadad564..8e28ea91 100755 --- a/install.sh +++ b/install.sh @@ -146,7 +146,6 @@ detect_release_variant() { # $1: release tag, $2: asset name case "$combined" in *nightly*) printf 'nightly' ;; *beta*) printf 'beta' ;; - *stable*|*openvcs-v*) printf 'stable' ;; *) if $INCLUDE_PRERELEASE; then printf 'prerelease' From 4823528fab02d31ae8b4c19b3f58c7955fd7a0dd Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:58:49 +0000 Subject: [PATCH 16/23] Update tauri-build.js --- scripts/tauri-build.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index 5a4edc10..cae8a4a7 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -80,7 +80,12 @@ async function main() { const channelProductName = channelNames[channelSlug] || 'OpenVCS'; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openvcs-tauri-config-')); const channelConfigPath = path.join(tmpDir, 'tauri.channel.conf.json'); - writeChannelConfig(channelConfigPath); + try { + writeChannelConfig(channelConfigPath); + } catch (err) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + throw err; + } if (process.env.TAURI_SIGNING_PRIVATE_KEY_FILE && !process.env.TAURI_SIGNING_PRIVATE_KEY) { console.log('Signing: loading key from TAURI_SIGNING_PRIVATE_KEY_FILE'); @@ -109,7 +114,8 @@ async function main() { child.on('exit', (code, signal) => { try { - // channelConfigPath is a file inside the temp dir, so dirname removes the temp dir itself + // channelConfigPath is a file inside a temp dir (e.g., /tmp/openvcs-tauri-config-XXXXXX/channel.json). + // dirname strips the filename to get the temp dir, then we remove it. fs.rmSync(path.dirname(channelConfigPath), { recursive: true, force: true }); } catch {} if (signal) process.kill(process.pid, signal); From 7ff4d49c1cbe5eea7615c9abd92ab4137dc1b2fd Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:58:54 +0000 Subject: [PATCH 17/23] Update build.rs --- Backend/build.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Backend/build.rs b/Backend/build.rs index efc54e4d..4d866231 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -84,6 +84,8 @@ impl ChannelConfig { .map(|s| Box::leak(s.to_string().into_boxed_str()) as &str) .collect(); Self { + // Box::leak is acceptable here - this is build-time config that lives + // only for the duration of the build process, after which memory is reclaimed. slug: Box::leak(entry.slug.clone().into_boxed_str()), main_binary_name: Box::leak(entry.main_binary_name.clone().into_boxed_str()), product_name: Box::leak(entry.product_name.clone().into_boxed_str()), From 5781dd018d4d65c6da3aca5e46e71cf6d565c586 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 23:59:03 +0000 Subject: [PATCH 18/23] Update workflows --- .github/workflows/beta.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/publish-stable.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 97e36ef9..0fbb878f 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -129,4 +129,4 @@ jobs: Runner: ${{ runner.os }} • Run #${{ github.run_number }} releaseDraft: true prerelease: true - args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args, runner.temp) }} + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f808cb3a..a59add18 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -245,4 +245,4 @@ jobs: ${{ steps.meta.outputs.changelog }} releaseDraft: false prerelease: true - args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args, runner.temp) }} + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }} diff --git a/.github/workflows/publish-stable.yml b/.github/workflows/publish-stable.yml index e2e19cee..b6639bcc 100644 --- a/.github/workflows/publish-stable.yml +++ b/.github/workflows/publish-stable.yml @@ -160,7 +160,7 @@ jobs: releaseBody: 'See the assets to download this version and install.' releaseDraft: true prerelease: false - args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args, runner.temp) }} + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }} includeDebug: false # default; set true if you want debug archives too # bundles: '' # e.g. 'deb,appimage,msi,nsis' if you want to restrict output From 26e234c59640842e3b3bdf53eebaa8428287a4ea Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 25 Mar 2026 00:03:29 +0000 Subject: [PATCH 19/23] Update tauri-build.js --- scripts/tauri-build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index cae8a4a7..68d0b7b7 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -74,8 +74,8 @@ async function main() { const channelSlug = (process.env.OPENVCS_UPDATE_CHANNEL || 'stable').trim().toLowerCase(); const channelNames = { stable: 'OpenVCS', - beta: 'OpenVCS Beta', - nightly: 'OpenVCS Nightly', + beta: 'OpenVCS-Beta', + nightly: 'OpenVCS-Nightly', }; const channelProductName = channelNames[channelSlug] || 'OpenVCS'; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openvcs-tauri-config-')); From 0cf1f8f8813b99711c725d8c7eb6b2272bb2b9bc Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 25 Mar 2026 00:03:32 +0000 Subject: [PATCH 20/23] Update build.rs --- Backend/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Backend/build.rs b/Backend/build.rs index 4d866231..1f1c612d 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -84,8 +84,8 @@ impl ChannelConfig { .map(|s| Box::leak(s.to_string().into_boxed_str()) as &str) .collect(); Self { - // Box::leak is acceptable here - this is build-time config that lives - // only for the duration of the build process, after which memory is reclaimed. + // Box::leak is acceptable here - this is a build script that runs once during + // compilation. The leaked memory persists for the build duration only. slug: Box::leak(entry.slug.clone().into_boxed_str()), main_binary_name: Box::leak(entry.main_binary_name.clone().into_boxed_str()), product_name: Box::leak(entry.product_name.clone().into_boxed_str()), From 354e696c7382e4c218254f5a917ba16d9bdec165 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 25 Mar 2026 00:03:57 +0000 Subject: [PATCH 21/23] Update tauri-build.js --- scripts/tauri-build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/tauri-build.js b/scripts/tauri-build.js index 68d0b7b7..0fd2f635 100644 --- a/scripts/tauri-build.js +++ b/scripts/tauri-build.js @@ -114,8 +114,8 @@ async function main() { child.on('exit', (code, signal) => { try { - // channelConfigPath is a file inside a temp dir (e.g., /tmp/openvcs-tauri-config-XXXXXX/channel.json). - // dirname strips the filename to get the temp dir, then we remove it. + // channelConfigPath is /tmp/openvcs-tauri-config-XXXXXX/tauri.channel.conf.json + // dirname gives us /tmp/openvcs-tauri-config-XXXXXX which we then remove fs.rmSync(path.dirname(channelConfigPath), { recursive: true, force: true }); } catch {} if (signal) process.kill(process.pid, signal); From 4a5373a9e1cc33ae1bce9e11fa3695972a0a3990 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 25 Mar 2026 00:09:10 +0000 Subject: [PATCH 22/23] Update build.rs --- Backend/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Backend/build.rs b/Backend/build.rs index 1f1c612d..0cf32e86 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -84,8 +84,8 @@ impl ChannelConfig { .map(|s| Box::leak(s.to_string().into_boxed_str()) as &str) .collect(); Self { - // Box::leak is acceptable here - this is a build script that runs once during - // compilation. The leaked memory persists for the build duration only. + // Box::leak is acceptable here - this is a build script that runs once. + // Memory is leaked for the program duration but that's fine for a build script. slug: Box::leak(entry.slug.clone().into_boxed_str()), main_binary_name: Box::leak(entry.main_binary_name.clone().into_boxed_str()), product_name: Box::leak(entry.product_name.clone().into_boxed_str()), From 01feac2fcb219eb13815ac18045f52ca114ec995 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 25 Mar 2026 00:10:29 +0000 Subject: [PATCH 23/23] Update install.sh --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 8e28ea91..8390496a 100755 --- a/install.sh +++ b/install.sh @@ -144,8 +144,8 @@ detect_release_variant() { # $1: release tag, $2: asset name combined="${1,,} ${2,,}" case "$combined" in - *nightly*) printf 'nightly' ;; - *beta*) printf 'beta' ;; + *nightly*|*openvcs-nightly*) printf 'nightly' ;; + *beta*|*openvcs-beta*) printf 'beta' ;; *) if $INCLUDE_PRERELEASE; then printf 'prerelease'