Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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) }}
51 changes: 6 additions & 45 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -252,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
Expand All @@ -278,10 +245,4 @@ jobs:
${{ steps.meta.outputs.changelog }}
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
args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }}
7 changes: 6 additions & 1 deletion .github/workflows/publish-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions Backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
152 changes: 124 additions & 28 deletions Backend/build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,101 @@
// 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<String>,
}

/// 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,
/// Binary and bundle stem without spaces.
main_binary_name: &'static str,
/// Human-facing desktop product name.
product_name: &'static str,
/// Tauri bundle identifier.
identifier: &'static str,
/// Main window title.
window_title: &'static str,
/// Updater endpoint URLs.
updater_endpoints: &'static [&'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, metadata: &ChannelMetadata) -> Self {
let slug = raw.trim().to_ascii_lowercase();
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 {
let endpoints: Vec<&'static str> = entry
.updater_endpoints
.iter()
.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.
// 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()),
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()),
}
}
}

/// Returns whether the build is running for Flatpak packaging.
fn is_flatpak_build() -> bool {
matches!(
Expand Down Expand Up @@ -131,37 +225,36 @@ 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());

// 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
));
// 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,
);

// 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() {
// 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<serde_json::Value> = channel
.updater_endpoints
.iter()
.map(|s| serde_json::Value::String((*s).to_string()))
.collect();
updater["endpoints"] = serde_json::Value::Array(endpoints);
}
}

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") {
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());
}
}
}

Expand Down Expand Up @@ -196,6 +289,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") {
Expand All @@ -205,6 +299,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");
Expand Down Expand Up @@ -266,7 +362,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",
_ => "",
Expand Down
39 changes: 39 additions & 0 deletions Backend/src/app_identity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright © 2025-2026 OpenVCS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later

//! Channel-aware desktop identity and persistence paths.

use directories::ProjectDirs;

/// 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 channel-aware project directories for app config and data.
///
/// 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> {
ProjectDirs::from("dev", "OpenVCS", persistence_name())
}

#[cfg(test)]
mod tests {
use super::persistence_name;

#[test]
fn exposes_persistence_names() {
assert_eq!(persistence_name(), "OpenVCS");
}
}
1 change: 1 addition & 0 deletions Backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use tauri_plugin_updater::UpdaterExt;

use crate::core::BackendId;

mod app_identity;
mod core;
mod logging;
mod output_log;
Expand Down
3 changes: 1 addition & 2 deletions Backend/src/plugin_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
3 changes: 1 addition & 2 deletions Backend/src/plugin_runtime/settings_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading
Loading