diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index c4347717..9a778ba0 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -37,6 +37,11 @@ jobs: key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-linux-amd64- + - name: Install dependencies + run: npm install --legacy-peer-deps + - name: Update version from Git + run: node scripts/update-version.mjs + - run: cargo generate-lockfile --manifest-path src-tauri/Cargo.toml - run: cargo fmt --manifest-path src-tauri/Cargo.toml --check rust-clippy: @@ -72,7 +77,7 @@ jobs: key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-linux-amd64- - - run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml -- -D warnings + - run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings rust-tests: runs-on: ubuntu-latest @@ -107,7 +112,7 @@ jobs: key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-linux-amd64- - - run: cargo test --locked --manifest-path src-tauri/Cargo.toml -- --test-threads=1 + - run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1 frontend-typecheck: runs-on: ubuntu-latest diff --git a/package.json b/package.json index 63d46fe2..20f212f2 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "tftsr", "private": true, - "version": "0.2.50", + "version": "0.2.62", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", + "version:update": "node scripts/update-version.mjs", "preview": "vite preview", "tauri": "tauri", "test": "vitest", diff --git a/scripts/update-version.mjs b/scripts/update-version.mjs new file mode 100644 index 00000000..08583761 --- /dev/null +++ b/scripts/update-version.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import { execSync } from 'child_process'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectRoot = resolve(__dirname, '..'); + +/** + * Validate version is semver-compliant (X.Y.Z) + */ +function isValidSemver(version) { + return /^[0-9]+\.[0-9]+\.[0-9]+$/.test(version); +} + +function validateGitRepo(root) { + if (!existsSync(resolve(root, '.git'))) { + throw new Error(`Not a Git repository: ${root}`); + } +} + +function getVersionFromGit() { + validateGitRepo(projectRoot); + try { + const output = execSync('git describe --tags --abbrev=0', { + encoding: 'utf-8', + cwd: projectRoot, + shell: false + }); + let version = output.trim(); + + // Remove v prefix + version = version.replace(/^v/, ''); + + // Validate it's a valid semver + if (!isValidSemver(version)) { + const pkgJsonVersion = getFallbackVersion(); + console.warn(`Invalid version format "${version}" from git describe, using package.json fallback: ${pkgJsonVersion}`); + return pkgJsonVersion; + } + + return version; + } catch (e) { + const pkgJsonVersion = getFallbackVersion(); + console.warn(`Failed to get version from Git tags, using package.json fallback: ${pkgJsonVersion}`); + return pkgJsonVersion; + } +} + +function getFallbackVersion() { + const pkgPath = resolve(projectRoot, 'package.json'); + if (!existsSync(pkgPath)) { + return '0.2.50'; + } + try { + const content = readFileSync(pkgPath, 'utf-8'); + const json = JSON.parse(content); + return json.version || '0.2.50'; + } catch { + return '0.2.50'; + } +} + +function updatePackageJson(version) { + const fullPath = resolve(projectRoot, 'package.json'); + if (!existsSync(fullPath)) { + throw new Error(`File not found: ${fullPath}`); + } + + const content = readFileSync(fullPath, 'utf-8'); + const json = JSON.parse(content); + json.version = version; + + // Write with 2-space indentation + writeFileSync(fullPath, JSON.stringify(json, null, 2) + '\n', 'utf-8'); + console.log(`✓ Updated package.json to ${version}`); +} + +function updateTOML(path, version) { + const fullPath = resolve(projectRoot, path); + if (!existsSync(fullPath)) { + throw new Error(`File not found: ${fullPath}`); + } + + const content = readFileSync(fullPath, 'utf-8'); + const lines = content.split('\n'); + const output = []; + + for (const line of lines) { + if (line.match(/^\s*version\s*=\s*"/)) { + output.push(`version = "${version}"`); + } else { + output.push(line); + } + } + + writeFileSync(fullPath, output.join('\n') + '\n', 'utf-8'); + console.log(`✓ Updated ${path} to ${version}`); +} + +const version = getVersionFromGit(); +console.log(`Setting version to: ${version}`); + +updatePackageJson(version); +updateTOML('src-tauri/Cargo.toml', version); +updateTOML('src-tauri/tauri.conf.json', version); + +console.log(`✓ All version fields updated to ${version}`); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4788dd5f..073c25d4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6139,7 +6139,7 @@ dependencies = [ [[package]] name = "trcaa" -version = "0.2.50" +version = "0.2.62" dependencies = [ "aes-gcm", "aho-corasick", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5fb2e7ae..a3457ee7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trcaa" -version = "0.2.50" +version = "0.2.62" edition = "2021" [lib] @@ -53,3 +53,7 @@ mockito = "1.2" [profile.release] opt-level = "s" strip = true + + + + diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e6..55db2c01 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,30 @@ fn main() { + let version = get_version_from_git(); + + println!("cargo:rustc-env=APP_VERSION={version}"); + println!("cargo:rerun-if-changed=.git/refs/heads/master"); + println!("cargo:rerun-if-changed=.git/refs/tags"); + tauri_build::build() } + +fn get_version_from_git() -> String { + if let Ok(output) = std::process::Command::new("git") + .arg("describe") + .arg("--tags") + .arg("--abbrev=0") + .output() + { + if output.status.success() { + let version = String::from_utf8_lossy(&output.stdout) + .trim() + .trim_start_matches('v') + .to_string(); + if !version.is_empty() { + return version; + } + } + } + + "0.2.50".to_string() +} diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 1263a8d8..7ab59e73 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -4,6 +4,7 @@ use crate::ollama::{ OllamaStatus, }; use crate::state::{AppSettings, AppState, ProviderConfig}; +use std::env; // --- Ollama commands --- @@ -275,3 +276,11 @@ pub async fn delete_ai_provider( Ok(()) } + +/// Get the application version from build-time environment +#[tauri::command] +pub async fn get_app_version() -> Result { + env::var("APP_VERSION") + .or_else(|_| env::var("CARGO_PKG_VERSION")) + .map_err(|e| format!("Failed to get version: {e}")) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a4780614..cdf319ba 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -120,6 +120,7 @@ pub fn run() { commands::system::get_settings, commands::system::update_settings, commands::system::get_audit_log, + commands::system::get_app_version, ]) .run(tauri::generate_context!()) .expect("Error running Troubleshooting and RCA Assistant application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9baf1d1f..84be9cc8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "frontendDist": "../dist", "devUrl": "http://localhost:1420", "beforeDevCommand": "npm run dev", - "beforeBuildCommand": "npm run build" + "beforeBuildCommand": "npm run version:update && npm run build" }, "app": { "security": { @@ -41,4 +41,7 @@ "shortDescription": "Troubleshooting and RCA Assistant", "longDescription": "Structured AI-backed assistant for IT troubleshooting, 5-whys root cause analysis, and post-mortem documentation with offline Ollama support." } -} \ No newline at end of file +} + + + diff --git a/src/App.tsx b/src/App.tsx index 076a53fe..ab38ef17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from "react"; -import { getVersion } from "@tauri-apps/api/app"; import { Routes, Route, NavLink, useLocation } from "react-router-dom"; import { Home, @@ -15,7 +14,7 @@ import { Moon, } from "lucide-react"; import { useSettingsStore } from "@/stores/settingsStore"; -import { loadAiProvidersCmd, testProviderConnectionCmd } from "@/lib/tauriCommands"; +import { getAppVersionCmd, loadAiProvidersCmd, testProviderConnectionCmd } from "@/lib/tauriCommands"; import Dashboard from "@/pages/Dashboard"; import NewIssue from "@/pages/NewIssue"; @@ -50,7 +49,7 @@ export default function App() { void useLocation(); useEffect(() => { - getVersion().then(setAppVersion).catch(() => {}); + getAppVersionCmd().then(setAppVersion).catch(() => {}); }, []); // Load providers and auto-test active provider on startup diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index ef46f452..78ae9962 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -486,3 +486,8 @@ export const loadAiProvidersCmd = () => export const deleteAiProviderCmd = (name: string) => invoke("delete_ai_provider", { name }); + +// ─── System / Version ───────────────────────────────────────────────────────── + +export const getAppVersionCmd = () => + invoke("get_app_version");