From e81dd320c6a500333111267cfddcb62392220838 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 02:47:30 +0000 Subject: [PATCH 01/13] Implement beta testing --- .github/workflows/beta.yml | 237 ++++++++++++++++++++++ Backend/build.rs | 9 +- Frontend/src/modals/settings.html | 1 + Frontend/src/scripts/features/settings.ts | 2 +- Frontend/src/scripts/features/update.ts | 9 + 5 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/beta.yml diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml new file mode 100644 index 0000000..2905ab8 --- /dev/null +++ b/.github/workflows/beta.yml @@ -0,0 +1,237 @@ +name: Beta +run-name: "OpenVCS Beta • Run #${{ github.run_number }} • Beta@${{ github.sha.substring(0, 7) }}" + +on: + push: + branches: [ beta ] + workflow_dispatch: + inputs: + force: + description: Build even if no changes since last beta tag + type: boolean + required: false + default: false + +# Default to least privilege; override per-job as needed +permissions: + contents: read + +env: + TARGET_REF: beta + +jobs: + check-changes: + name: Check if target ref changed since last beta + runs-on: ubuntu-22.04 + permissions: + contents: read + outputs: + changed: ${{ steps.diff.outputs.changed }} + since_sha: ${{ steps.diff.outputs.since_sha }} + ahead_count: ${{ steps.diff.outputs.ahead_count }} + steps: + - name: Checkout target ref + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.TARGET_REF }} + fetch-depth: 0 + submodules: recursive + lfs: false + + - name: Fetch tags + run: git fetch --tags --force + + - name: Determine diff vs openvcs-beta + id: diff + shell: bash + run: | + set -euo pipefail + TAG="openvcs-beta" + + # If the tag doesn't exist, force a build. + if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "since_sha=(none)" >> "$GITHUB_OUTPUT" + echo "ahead_count=INIT" >> "$GITHUB_OUTPUT" + exit 0 + fi + + SINCE_SHA="$(git rev-list -n 1 "$TAG")" + echo "since_sha=$SINCE_SHA" >> "$GITHUB_OUTPUT" + + # Check for changes in Frontend and Backend directories + if git diff --quiet "$TAG"..HEAD -- Frontend Backend; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "ahead_count=0" >> "$GITHUB_OUTPUT" + else + AHEAD="$(git rev-list --count "$TAG"..HEAD -- Frontend Backend || echo 1)" + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "ahead_count=$AHEAD" >> "$GITHUB_OUTPUT" + fi + + beta: + name: Build & publish Beta (only if changed) + needs: check-changes + if: needs.check-changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch' + permissions: + contents: write + actions: write + env: + RUSTC_WRAPPER: sccache + SCCACHE_GHA_ENABLED: ${{ vars.SSCCACHE_GHA_ENABLED }} + SCCACHE_CACHE_SIZE: ${{ vars.SSCCACHE_SIZE }} + strategy: + fail-fast: false + matrix: + include: + - platform: ubuntu-24.04 + args: '' + - platform: windows-latest + args: '' + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout target ref + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.TARGET_REF }} + fetch-depth: 0 + submodules: recursive + lfs: true + + - name: Compute metadata (date, short SHA, compare, changelog) + id: meta + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const date = new Date().toISOString().slice(0, 10); + const owner = context.repo.owner; + const repo = context.repo.repo; + const target = process.env.TARGET_REF || 'beta'; + let branch_sha = context.sha; + try { + const branch = await github.rest.repos.getBranch({ owner, repo, branch: target }); + branch_sha = branch?.data?.commit?.sha || context.sha; + } catch (e) { + branch_sha = context.sha; + } + const short = branch_sha.substring(0, 7); + const basehead = `openvcs-beta...${target}`; + let compare_url = `${context.serverUrl}/${owner}/${repo}/compare/${basehead}`; + let commit_url = `${context.serverUrl}/${owner}/${repo}/commit/${branch_sha}`; + let changelog = ''; + + try { + const res = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead }); + const commits = res?.data?.commits ?? []; + if (commits.length === 0) { + changelog = 'No changes detected.'; + } else { + const items = commits.map(c => { + const sha = (c.sha || '').substring(0, 7); + const msg = (c.commit?.message || '').split('\n')[0]; + const author = c.author?.login || c.commit?.author?.name || 'unknown'; + return `- ${sha} ${msg} (@${author})`; + }); + changelog = items.join('\n'); + } + } catch (e) { + changelog = `First beta or previous tag unavailable; showing recent commits at ${target}.`; + compare_url = `${context.serverUrl}/${owner}/${repo}/tree/${target}`; + } + + core.setOutput('short_sha', short); + core.setOutput('branch_sha', branch_sha); + core.setOutput('date', date); + core.setOutput('compare_url', compare_url); + core.setOutput('commit_url', commit_url); + core.setOutput('changelog', changelog); + + # ---------- Frontend ---------- + - name: Setup Node + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: Frontend/package-lock.json + + - name: Install frontend deps + working-directory: Frontend + run: npm ci + + - name: Build frontend + working-directory: Frontend + run: npm run build + + # ---------- Rust & platform deps ---------- + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable + with: + components: rustfmt, clippy + + - name: Setup sccache + uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 + + - name: Install Linux deps + if: matrix.platform == 'ubuntu-24.04' + run: | + set -euxo pipefail + sudo apt-get update + 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 + + # ---------- Cache ---------- + - name: Rust cache + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + cache-on-failure: true + + - name: Cargo fmt (check) + run: cargo fmt --all -- --check + + - name: Cargo clippy + run: cargo clippy --all-targets -- -D warnings + + # ---------- Remove existing 'openvcs-beta' release & tag (if any) ---------- + - name: Remove existing 'openvcs-beta' release & tag + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const tag = 'openvcs-beta'; + const releases = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 }); + const rel = releases.find(r => r.tag_name === tag); + if (rel) await github.rest.repos.deleteRelease({ owner, repo, release_id: rel.id }); + try { + await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` }); + } catch (e) { + if (e.status !== 422) throw e; + } + + # ---------- Build & publish ---------- + - name: Build and publish Beta prerelease + uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FRONTEND_SKIP_BUILD: '1' + OPENVCS_UPDATE_CHANNEL: beta + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_PRIVATE_KEY_PASSWORD }} + with: + projectPath: Backend + tagName: openvcs-beta + releaseName: "OpenVCS Beta ${{ steps.meta.outputs.date }} (${{ env.TARGET_REF }}@${{ steps.meta.outputs.short_sha }})" + releaseBody: | + Beta build from `${{ env.TARGET_REF }}`. + Date (UTC): ${{ steps.meta.outputs.date }} + Compare: ${{ steps.meta.outputs.compare_url }} + Since: ${{ needs.check-changes.outputs.since_sha }} + Ahead (relevant commits): ${{ needs.check-changes.outputs.ahead_count }} + Commit: ${{ steps.meta.outputs.branch_sha }} (${{ env.TARGET_REF }}@${{ steps.meta.outputs.short_sha }}) + Runner: ${{ runner.os }} • Run #${{ github.run_number }} + + Changes since last beta: + ${{ steps.meta.outputs.changelog }} + releaseDraft: true + prerelease: true + args: ${{ matrix.args }} diff --git a/Backend/build.rs b/Backend/build.rs index 2281c8d..ead6258 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -138,6 +138,9 @@ fn main() { let stable = serde_json::Value::String( "https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json".into(), ); + let beta = serde_json::Value::String( + "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-beta/latest.json".into(), + ); let nightly = serde_json::Value::String( "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-nightly/latest.json".into(), ); @@ -146,10 +149,10 @@ fn main() { 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" | "beta" => { - serde_json::Value::Array(vec![nightly.clone(), stable.clone()]) - } + "nightly" => serde_json::Value::Array(vec![nightly.clone(), stable.clone()]), // Stable: stable only _ => serde_json::Value::Array(vec![stable.clone()]), }; diff --git a/Frontend/src/modals/settings.html b/Frontend/src/modals/settings.html index 8584563..7c2e8f3 100644 --- a/Frontend/src/modals/settings.html +++ b/Frontend/src/modals/settings.html @@ -68,6 +68,7 @@

Settings

diff --git a/Frontend/src/scripts/features/settings.ts b/Frontend/src/scripts/features/settings.ts index 5e94508..93bcba5 100644 --- a/Frontend/src/scripts/features/settings.ts +++ b/Frontend/src/scripts/features/settings.ts @@ -790,7 +790,7 @@ function collectSettingsFromForm(root: HTMLElement): GlobalSettings { theme_pack: themePack || DEFAULT_LIGHT_THEME_ID, language: get('#set-language')?.value, default_backend: (get('#set-default-backend')?.value || 'git') as any, - update_channel: (() => { const v = get('#set-update-channel')?.value; return v === 'beta' ? 'nightly' : v; })(), + update_channel: get('#set-update-channel')?.value || 'stable', reopen_last_repos: !!get('#set-reopen-last')?.checked, checks_on_launch: !!get('#set-checks-on-launch')?.checked, }; diff --git a/Frontend/src/scripts/features/update.ts b/Frontend/src/scripts/features/update.ts index 9a1f69b..79b6bb7 100644 --- a/Frontend/src/scripts/features/update.ts +++ b/Frontend/src/scripts/features/update.ts @@ -36,10 +36,12 @@ export async function showUpdateDialog(_data: any) { }; const stable = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/latest'); + const beta = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/tags/openvcs-beta'); const nightly = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/tags/openvcs-nightly'); const norm = (v: string) => String(v || '').replace(/^v/i, '').trim(); const stableTag = norm(stable?.tag_name || stable?.name || ''); + const betaTag = norm(beta?.tag_name || beta?.name || ''); const nightlyTag = norm(nightly?.tag_name || nightly?.name || ''); const base = (v: string) => norm(v).split('+', 1)[0]; @@ -51,6 +53,13 @@ export async function showUpdateDialog(_data: any) { if (channel === 'stable') { if (newerThanCurrent(stableTag)) { show = true; pick = stable; } + } else if (channel === 'beta') { + // Beta: pick the most recent by published_at timestamp between beta and stable + const sDate = Date.parse(String(stable?.published_at || stable?.created_at || '')) || 0; + const bDate = Date.parse(String(beta?.published_at || beta?.created_at || '')) || 0; + pick = (bDate > sDate ? beta : stable) || beta || stable; + const pickTag = norm(pick?.tag_name || pick?.name || ''); + show = newerThanCurrent(pickTag); } else { // Nightly: pick the most recent by published_at timestamp and ensure it's newer than current const sDate = Date.parse(String(stable?.published_at || stable?.created_at || '')) || 0; From 6d7efaab2cec41b46fc829f6e1e8c917b6f2b272 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 02:48:53 +0000 Subject: [PATCH 02/13] Update beta.yml --- .github/workflows/beta.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 2905ab8..e48f033 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -3,7 +3,7 @@ run-name: "OpenVCS Beta • Run #${{ github.run_number }} • Beta@${{ github.sh on: push: - branches: [ beta ] + branches: [ Beta ] workflow_dispatch: inputs: force: @@ -17,7 +17,7 @@ permissions: contents: read env: - TARGET_REF: beta + TARGET_REF: Beta jobs: check-changes: From 6cce50569ab1753ac3bc402cca71568ef5f8e6c1 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 02:53:10 +0000 Subject: [PATCH 03/13] Update beta.yml --- .github/workflows/beta.yml | 132 ++++--------------------------------- 1 file changed, 11 insertions(+), 121 deletions(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index e48f033..52001d5 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -5,81 +5,20 @@ on: push: branches: [ Beta ] workflow_dispatch: - inputs: - force: - description: Build even if no changes since last beta tag - type: boolean - required: false - default: false - -# Default to least privilege; override per-job as needed + permissions: - contents: read + contents: write + actions: write env: TARGET_REF: Beta + RUSTC_WRAPPER: sccache + SCCACHE_GHA_ENABLED: ${{ vars.SSCCACHE_GHA_ENABLED }} + SCCACHE_CACHE_SIZE: ${{ vars.SSCCACHE_SIZE }} jobs: - check-changes: - name: Check if target ref changed since last beta - runs-on: ubuntu-22.04 - permissions: - contents: read - outputs: - changed: ${{ steps.diff.outputs.changed }} - since_sha: ${{ steps.diff.outputs.since_sha }} - ahead_count: ${{ steps.diff.outputs.ahead_count }} - steps: - - name: Checkout target ref - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ env.TARGET_REF }} - fetch-depth: 0 - submodules: recursive - lfs: false - - - name: Fetch tags - run: git fetch --tags --force - - - name: Determine diff vs openvcs-beta - id: diff - shell: bash - run: | - set -euo pipefail - TAG="openvcs-beta" - - # If the tag doesn't exist, force a build. - if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "since_sha=(none)" >> "$GITHUB_OUTPUT" - echo "ahead_count=INIT" >> "$GITHUB_OUTPUT" - exit 0 - fi - - SINCE_SHA="$(git rev-list -n 1 "$TAG")" - echo "since_sha=$SINCE_SHA" >> "$GITHUB_OUTPUT" - - # Check for changes in Frontend and Backend directories - if git diff --quiet "$TAG"..HEAD -- Frontend Backend; then - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "ahead_count=0" >> "$GITHUB_OUTPUT" - else - AHEAD="$(git rev-list --count "$TAG"..HEAD -- Frontend Backend || echo 1)" - echo "changed=true" >> "$GITHUB_OUTPUT" - echo "ahead_count=$AHEAD" >> "$GITHUB_OUTPUT" - fi - beta: - name: Build & publish Beta (only if changed) - needs: check-changes - if: needs.check-changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch' - permissions: - contents: write - actions: write - env: - RUSTC_WRAPPER: sccache - SCCACHE_GHA_ENABLED: ${{ vars.SSCCACHE_GHA_ENABLED }} - SCCACHE_CACHE_SIZE: ${{ vars.SSCCACHE_SIZE }} + name: Build & publish Beta strategy: fail-fast: false matrix: @@ -98,55 +37,16 @@ jobs: submodules: recursive lfs: true - - name: Compute metadata (date, short SHA, compare, changelog) + - name: Compute metadata (date, short SHA) id: meta uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const date = new Date().toISOString().slice(0, 10); - const owner = context.repo.owner; - const repo = context.repo.repo; - const target = process.env.TARGET_REF || 'beta'; - let branch_sha = context.sha; - try { - const branch = await github.rest.repos.getBranch({ owner, repo, branch: target }); - branch_sha = branch?.data?.commit?.sha || context.sha; - } catch (e) { - branch_sha = context.sha; - } - const short = branch_sha.substring(0, 7); - const basehead = `openvcs-beta...${target}`; - let compare_url = `${context.serverUrl}/${owner}/${repo}/compare/${basehead}`; - let commit_url = `${context.serverUrl}/${owner}/${repo}/commit/${branch_sha}`; - let changelog = ''; - - try { - const res = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead }); - const commits = res?.data?.commits ?? []; - if (commits.length === 0) { - changelog = 'No changes detected.'; - } else { - const items = commits.map(c => { - const sha = (c.sha || '').substring(0, 7); - const msg = (c.commit?.message || '').split('\n')[0]; - const author = c.author?.login || c.commit?.author?.name || 'unknown'; - return `- ${sha} ${msg} (@${author})`; - }); - changelog = items.join('\n'); - } - } catch (e) { - changelog = `First beta or previous tag unavailable; showing recent commits at ${target}.`; - compare_url = `${context.serverUrl}/${owner}/${repo}/tree/${target}`; - } - + const short = context.sha.substring(0, 7); core.setOutput('short_sha', short); - core.setOutput('branch_sha', branch_sha); core.setOutput('date', date); - core.setOutput('compare_url', compare_url); - core.setOutput('commit_url', commit_url); - core.setOutput('changelog', changelog); - # ---------- Frontend ---------- - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -162,7 +62,6 @@ jobs: working-directory: Frontend run: npm run build - # ---------- Rust & platform deps ---------- - name: Install Rust (stable) uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable with: @@ -179,7 +78,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 - # ---------- Cache ---------- - name: Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: @@ -191,7 +89,6 @@ jobs: - name: Cargo clippy run: cargo clippy --all-targets -- -D warnings - # ---------- Remove existing 'openvcs-beta' release & tag (if any) ---------- - name: Remove existing 'openvcs-beta' release & tag uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: @@ -208,7 +105,6 @@ jobs: if (e.status !== 422) throw e; } - # ---------- Build & publish ---------- - name: Build and publish Beta prerelease uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 env: @@ -224,14 +120,8 @@ jobs: releaseBody: | Beta build from `${{ env.TARGET_REF }}`. Date (UTC): ${{ steps.meta.outputs.date }} - Compare: ${{ steps.meta.outputs.compare_url }} - Since: ${{ needs.check-changes.outputs.since_sha }} - Ahead (relevant commits): ${{ needs.check-changes.outputs.ahead_count }} - Commit: ${{ steps.meta.outputs.branch_sha }} (${{ env.TARGET_REF }}@${{ steps.meta.outputs.short_sha }}) + Commit: ${{ github.sha }} (${{ env.TARGET_REF }}@${{ steps.meta.outputs.short_sha }}) Runner: ${{ runner.os }} • Run #${{ github.run_number }} - - Changes since last beta: - ${{ steps.meta.outputs.changelog }} releaseDraft: true prerelease: true - args: ${{ matrix.args }} + args: ${{ matrix.args }} \ No newline at end of file From 36a81ec2a8db39e6fe2387c83f677614bd9f40ef Mon Sep 17 00:00:00 2001 From: Jordon <16258926+Jordonbc@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:56:09 +0000 Subject: [PATCH 04/13] Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- Frontend/src/scripts/features/update.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Frontend/src/scripts/features/update.ts b/Frontend/src/scripts/features/update.ts index 79b6bb7..6ecc58e 100644 --- a/Frontend/src/scripts/features/update.ts +++ b/Frontend/src/scripts/features/update.ts @@ -41,7 +41,6 @@ export async function showUpdateDialog(_data: any) { const norm = (v: string) => String(v || '').replace(/^v/i, '').trim(); const stableTag = norm(stable?.tag_name || stable?.name || ''); - const betaTag = norm(beta?.tag_name || beta?.name || ''); const nightlyTag = norm(nightly?.tag_name || nightly?.name || ''); const base = (v: string) => norm(v).split('+', 1)[0]; From d87602d3026d31552b91edc369196dbb40edc832 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 02:58:50 +0000 Subject: [PATCH 05/13] Update build.rs --- Backend/build.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Backend/build.rs b/Backend/build.rs index ead6258..aa80514 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -260,10 +260,18 @@ fn main() { pkg_version.clone() } else { let branch_ident = sanitize_semver_ident(&branch); - format!( - "{pkg_version}+git.{branch_ident}.{hash}{}", + let channel_suffix = match chan.as_str() { + "beta" => "-beta", + "nightly" => "-nightly", + _ => "", + }; + let suffix = format!( + "+git.{}{}{}", + branch_ident, + hash, if dirty { ".dirty" } else { "" } - ) + ); + format!("{}{}{}", pkg_version, channel_suffix, suffix) }; println!("cargo:rustc-env=OPENVCS_VERSION={}", version); From 17e9a9e3a708182850c571483fe217b3b4726ecf Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:03:17 +0000 Subject: [PATCH 06/13] Update update.ts --- Frontend/src/scripts/features/update.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Frontend/src/scripts/features/update.ts b/Frontend/src/scripts/features/update.ts index 79b6bb7..ddd28f2 100644 --- a/Frontend/src/scripts/features/update.ts +++ b/Frontend/src/scripts/features/update.ts @@ -4,6 +4,14 @@ import { TAURI } from '../lib/tauri'; import { openModal, closeModal } from '../ui/modals'; import { notify } from '../lib/notify'; +const GITHUB_OWNER = 'Jordonbc'; +const GITHUB_REPO = 'OpenVCS'; + +const API_BASE = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`; +const URL_STABLE = `${API_BASE}/releases/latest`; +const URL_BETA = `${API_BASE}/repos/tags/openvcs-beta`; +const URL_NIGHTLY = `${API_BASE}/repos/tags/openvcs-nightly`; + export function wireUpdate() { const modal = document.getElementById('update-modal') as HTMLElement | null; if (!modal || (modal as any).__wired) return; @@ -32,16 +40,22 @@ export async function showUpdateDialog(_data: any) { const current = String(about?.version || '').trim(); const fetchJson = async (url: string) => { - const r = await fetch(url, { cache: 'no-store' }); return r.ok ? r.json() : null; + try { + const r = await fetch(url, { cache: 'no-store' }); + return r.ok ? r.json() : null; + } catch { + return null; + } }; - const stable = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/latest'); - const beta = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/tags/openvcs-beta'); - const nightly = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/tags/openvcs-nightly'); + const [stable, beta, nightly] = await Promise.all([ + fetchJson(URL_STABLE), + fetchJson(URL_BETA), + fetchJson(URL_NIGHTLY), + ]); const norm = (v: string) => String(v || '').replace(/^v/i, '').trim(); const stableTag = norm(stable?.tag_name || stable?.name || ''); - const betaTag = norm(beta?.tag_name || beta?.name || ''); const nightlyTag = norm(nightly?.tag_name || nightly?.name || ''); const base = (v: string) => norm(v).split('+', 1)[0]; From 0c731af435c97794e84d6faec83dc4cc2aa5494a Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:03:20 +0000 Subject: [PATCH 07/13] Update settings.ts --- Frontend/src/scripts/features/settings.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Frontend/src/scripts/features/settings.ts b/Frontend/src/scripts/features/settings.ts index 93bcba5..ac8b5e8 100644 --- a/Frontend/src/scripts/features/settings.ts +++ b/Frontend/src/scripts/features/settings.ts @@ -927,8 +927,7 @@ export async function loadSettingsIntoForm(root?: HTMLElement) { const elLang = get('#set-language'); if (elLang) elLang.value = toKebab(cfg.general?.language); await refreshDefaultBackendOptions(m, cfg); const elChan = get('#set-update-channel'); if (elChan) { - const v = toKebab(cfg.general?.update_channel); - elChan.value = (v === 'beta') ? 'nightly' : v; + elChan.value = toKebab(cfg.general?.update_channel); } const elReo = get('#set-reopen-last'); if (elReo) elReo.checked = !!cfg.general?.reopen_last_repos; const elChk = get('#set-checks-on-launch'); if (elChk) elChk.checked = !!cfg.general?.checks_on_launch; From 84e50e430b91b30313217eb52ecdcb4a109735a5 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:03:22 +0000 Subject: [PATCH 08/13] Update beta.yml --- .github/workflows/beta.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 52001d5..46ee7a2 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -124,4 +124,4 @@ jobs: Runner: ${{ runner.os }} • Run #${{ github.run_number }} releaseDraft: true prerelease: true - args: ${{ matrix.args }} \ No newline at end of file + args: ${{ matrix.args }} From 60bb340687d4771be20dad5e4f7259b791c57c73 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:13:01 +0000 Subject: [PATCH 09/13] Moved update over to rust. --- Backend/src/lib.rs | 1 + Backend/src/tauri_commands/updater.rs | 56 ++++++++++++++++ Frontend/src/scripts/features/update.ts | 85 ++++--------------------- 3 files changed, 70 insertions(+), 72 deletions(-) diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 694f9b4..475422d 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -327,6 +327,7 @@ fn build_invoke_handler( tauri_commands::ssh_key_candidates, tauri_commands::ssh_add_key, tauri_commands::updater_install_now, + tauri_commands::get_update_status, tauri_commands::open_repo_dotfile, tauri_commands::open_docs, tauri_commands::open_output_log_window, diff --git a/Backend/src/tauri_commands/updater.rs b/Backend/src/tauri_commands/updater.rs index 1b5dfaa..021058c 100644 --- a/Backend/src/tauri_commands/updater.rs +++ b/Backend/src/tauri_commands/updater.rs @@ -1,10 +1,66 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later use log::{debug, error, info, trace}; +use serde::Serialize; use tauri::{Emitter, Manager, Runtime, Window}; use tauri_plugin_updater::UpdaterExt; +/// Response payload for update status check. +#[derive(Serialize)] +pub struct UpdateStatus { + pub available: bool, + pub version: Option, + pub current_version: Option, + pub body: Option, + pub date: Option, +} + +#[tauri::command] +/// Checks for available updates and returns detailed status. +/// +/// # Parameters +/// - `window`: Calling window handle. +/// +/// # Returns +/// - [`UpdateStatus`] with version info if update available, or {available: false}. +pub async fn get_update_status(window: Window) -> Result { + let app = window.app_handle(); + let updater = app.updater().map_err(|e| { + error!("get_update_status: failed to get updater: {}", e); + e.to_string() + })?; + + match updater.check().await { + Ok(Some(update)) => { + let date_str = update.date.map(|d| d.to_string()); + let status = UpdateStatus { + available: true, + version: Some(update.version.clone()), + current_version: Some(update.current_version.clone()), + body: update.body.clone(), + date: date_str, + }; + debug!( + "get_update_status: update available: {} -> {}", + update.current_version, update.version + ); + Ok(status) + } + Ok(None) => Ok(UpdateStatus { + available: false, + version: None, + current_version: None, + body: None, + date: None, + }), + Err(e) => { + error!("get_update_status: check failed: {}", e); + Err(e.to_string()) + } + } +} + #[tauri::command] /// Downloads and installs an available application update. /// diff --git a/Frontend/src/scripts/features/update.ts b/Frontend/src/scripts/features/update.ts index ddd28f2..cf99e72 100644 --- a/Frontend/src/scripts/features/update.ts +++ b/Frontend/src/scripts/features/update.ts @@ -4,13 +4,13 @@ import { TAURI } from '../lib/tauri'; import { openModal, closeModal } from '../ui/modals'; import { notify } from '../lib/notify'; -const GITHUB_OWNER = 'Jordonbc'; -const GITHUB_REPO = 'OpenVCS'; - -const API_BASE = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`; -const URL_STABLE = `${API_BASE}/releases/latest`; -const URL_BETA = `${API_BASE}/repos/tags/openvcs-beta`; -const URL_NIGHTLY = `${API_BASE}/repos/tags/openvcs-nightly`; +interface UpdateStatus { + available: boolean; + version: string | null; + current_version: string | null; + body: string | null; + date: string | null; +} export function wireUpdate() { const modal = document.getElementById('update-modal') as HTMLElement | null; @@ -34,83 +34,24 @@ export function wireUpdate() { export async function showUpdateDialog(_data: any) { try { if (!TAURI.has) return; - const cfg = await TAURI.invoke('get_global_settings'); - const about = await TAURI.invoke('about_info'); - const channel = String(cfg?.general?.update_channel || 'stable'); - const current = String(about?.version || '').trim(); - - const fetchJson = async (url: string) => { - try { - const r = await fetch(url, { cache: 'no-store' }); - return r.ok ? r.json() : null; - } catch { - return null; - } - }; - - const [stable, beta, nightly] = await Promise.all([ - fetchJson(URL_STABLE), - fetchJson(URL_BETA), - fetchJson(URL_NIGHTLY), - ]); - - const norm = (v: string) => String(v || '').replace(/^v/i, '').trim(); - const stableTag = norm(stable?.tag_name || stable?.name || ''); - const nightlyTag = norm(nightly?.tag_name || nightly?.name || ''); - - const base = (v: string) => norm(v).split('+', 1)[0]; - const currentBase = base(current); - const newerThanCurrent = (v: string) => Boolean(v) && v !== '' && currentBase !== base(v); - let show = false; - let pick = null as any; + const status = await TAURI.invoke('get_update_status'); - if (channel === 'stable') { - if (newerThanCurrent(stableTag)) { show = true; pick = stable; } - } else if (channel === 'beta') { - // Beta: pick the most recent by published_at timestamp between beta and stable - const sDate = Date.parse(String(stable?.published_at || stable?.created_at || '')) || 0; - const bDate = Date.parse(String(beta?.published_at || beta?.created_at || '')) || 0; - pick = (bDate > sDate ? beta : stable) || beta || stable; - const pickTag = norm(pick?.tag_name || pick?.name || ''); - show = newerThanCurrent(pickTag); - } else { - // Nightly: pick the most recent by published_at timestamp and ensure it's newer than current - const sDate = Date.parse(String(stable?.published_at || stable?.created_at || '')) || 0; - const nDate = Date.parse(String(nightly?.published_at || nightly?.created_at || '')) || 0; - pick = (nDate > sDate ? nightly : stable) || nightly || stable; - const pickTag = norm(pick?.tag_name || pick?.name || ''); - show = newerThanCurrent(pickTag); + if (!status.available) { + notify('Already up to date'); + return; } - if (!show || !pick) { notify('Already up to date'); return; } - openModal('update-modal'); const modal = document.getElementById('update-modal') as HTMLElement | null; if (!modal) return; const verEl = modal.querySelector('#update-version'); const notesEl = modal.querySelector('#update-notes'); - const v = pick?.tag_name || pick?.name || ''; - const body = String(pick?.body || '').trim(); + const v = status.version || ''; + const body = String(status.body || '').trim(); if (verEl) verEl.textContent = v ? `Version ${v}` : 'Update available'; if (notesEl) (notesEl as HTMLElement).textContent = body || '(No changelog provided)'; } catch { notify('Update check failed'); } } - -function inferString(obj: any, keys: string[]): string | undefined { - if (!obj || typeof obj !== 'object') return undefined; - for (const k of keys) { - const v = obj[k as any]; - if (typeof v === 'string' && v.trim()) return v; - } - // nested common containers - if (obj.update && typeof obj.update === 'object') { - const v = inferString(obj.update, keys); if (v) return v; - } - if (obj.manifest && typeof obj.manifest === 'object') { - const v = inferString(obj.manifest, keys); if (v) return v; - } - return undefined; -} From 23df40556e0b1fbd49fbaeb0de342f51c9a0eb65 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:13:57 +0000 Subject: [PATCH 10/13] Update beta.yml --- .github/workflows/beta.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 46ee7a2..2f09a40 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -1,5 +1,5 @@ name: Beta -run-name: "OpenVCS Beta • Run #${{ github.run_number }} • Beta@${{ github.sha.substring(0, 7) }}" +run-name: "OpenVCS Beta • Run #${{ github.run_number }} • Beta@${{ github.sha }}" on: push: From 5c4b2ee47ac510bbc95699dda78afaa910d1d931 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:22:16 +0000 Subject: [PATCH 11/13] Update beta.yml --- .github/workflows/beta.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 2f09a40..6dcaf9c 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -1,5 +1,5 @@ name: Beta -run-name: "OpenVCS Beta • Run #${{ github.run_number }} • Beta@${{ github.sha }}" +run-name: "OpenVCS Beta • Run #${{ github.run_number }} • Beta@${{ steps.meta.outputs.short_sha }}" on: push: From a9957e8cd664507dafe9501f53110cebb27a5a9e Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:32:38 +0000 Subject: [PATCH 12/13] Update build.rs --- Backend/build.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Backend/build.rs b/Backend/build.rs index aa80514..21372f0 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -134,16 +134,21 @@ fn main() { // Compute channel based on environment; default to stable let chan = env::var("OPENVCS_UPDATE_CHANNEL").unwrap_or_else(|_| "stable".into()); - // Locations - let stable = serde_json::Value::String( - "https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json".into(), - ); - let beta = serde_json::Value::String( - "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-beta/latest.json".into(), - ); - let nightly = serde_json::Value::String( - "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-nightly/latest.json".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 + )); // Navigate: plugins.updater.endpoints if let Some(plugins) = json.get_mut("plugins") { @@ -203,6 +208,7 @@ fn main() { 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"); + println!("cargo:rerun-if-env-changed=OPENVCS_REPO"); // Export a GIT_DESCRIBE string for About dialog and diagnostics let describe = Command::new("git") From cf9fe20efd24c3df8d303e805886ff0f6bd9c6d5 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 24 Mar 2026 03:32:40 +0000 Subject: [PATCH 13/13] Update beta.yml --- .github/workflows/beta.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 6dcaf9c..2a588fd 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -125,3 +125,4 @@ jobs: releaseDraft: true prerelease: true args: ${{ matrix.args }} +