Skip to content

Commit e81dd32

Browse files
committed
Implement beta testing
1 parent 97f4634 commit e81dd32

5 files changed

Lines changed: 254 additions & 4 deletions

File tree

.github/workflows/beta.yml

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
name: Beta
2+
run-name: "OpenVCS Beta • Run #${{ github.run_number }} • Beta@${{ github.sha.substring(0, 7) }}"
3+
4+
on:
5+
push:
6+
branches: [ beta ]
7+
workflow_dispatch:
8+
inputs:
9+
force:
10+
description: Build even if no changes since last beta tag
11+
type: boolean
12+
required: false
13+
default: false
14+
15+
# Default to least privilege; override per-job as needed
16+
permissions:
17+
contents: read
18+
19+
env:
20+
TARGET_REF: beta
21+
22+
jobs:
23+
check-changes:
24+
name: Check if target ref changed since last beta
25+
runs-on: ubuntu-22.04
26+
permissions:
27+
contents: read
28+
outputs:
29+
changed: ${{ steps.diff.outputs.changed }}
30+
since_sha: ${{ steps.diff.outputs.since_sha }}
31+
ahead_count: ${{ steps.diff.outputs.ahead_count }}
32+
steps:
33+
- name: Checkout target ref
34+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
35+
with:
36+
ref: ${{ env.TARGET_REF }}
37+
fetch-depth: 0
38+
submodules: recursive
39+
lfs: false
40+
41+
- name: Fetch tags
42+
run: git fetch --tags --force
43+
44+
- name: Determine diff vs openvcs-beta
45+
id: diff
46+
shell: bash
47+
run: |
48+
set -euo pipefail
49+
TAG="openvcs-beta"
50+
51+
# If the tag doesn't exist, force a build.
52+
if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
53+
echo "changed=true" >> "$GITHUB_OUTPUT"
54+
echo "since_sha=(none)" >> "$GITHUB_OUTPUT"
55+
echo "ahead_count=INIT" >> "$GITHUB_OUTPUT"
56+
exit 0
57+
fi
58+
59+
SINCE_SHA="$(git rev-list -n 1 "$TAG")"
60+
echo "since_sha=$SINCE_SHA" >> "$GITHUB_OUTPUT"
61+
62+
# Check for changes in Frontend and Backend directories
63+
if git diff --quiet "$TAG"..HEAD -- Frontend Backend; then
64+
echo "changed=false" >> "$GITHUB_OUTPUT"
65+
echo "ahead_count=0" >> "$GITHUB_OUTPUT"
66+
else
67+
AHEAD="$(git rev-list --count "$TAG"..HEAD -- Frontend Backend || echo 1)"
68+
echo "changed=true" >> "$GITHUB_OUTPUT"
69+
echo "ahead_count=$AHEAD" >> "$GITHUB_OUTPUT"
70+
fi
71+
72+
beta:
73+
name: Build & publish Beta (only if changed)
74+
needs: check-changes
75+
if: needs.check-changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
76+
permissions:
77+
contents: write
78+
actions: write
79+
env:
80+
RUSTC_WRAPPER: sccache
81+
SCCACHE_GHA_ENABLED: ${{ vars.SSCCACHE_GHA_ENABLED }}
82+
SCCACHE_CACHE_SIZE: ${{ vars.SSCCACHE_SIZE }}
83+
strategy:
84+
fail-fast: false
85+
matrix:
86+
include:
87+
- platform: ubuntu-24.04
88+
args: ''
89+
- platform: windows-latest
90+
args: ''
91+
runs-on: ${{ matrix.platform }}
92+
steps:
93+
- name: Checkout target ref
94+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
95+
with:
96+
ref: ${{ env.TARGET_REF }}
97+
fetch-depth: 0
98+
submodules: recursive
99+
lfs: true
100+
101+
- name: Compute metadata (date, short SHA, compare, changelog)
102+
id: meta
103+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
104+
with:
105+
script: |
106+
const date = new Date().toISOString().slice(0, 10);
107+
const owner = context.repo.owner;
108+
const repo = context.repo.repo;
109+
const target = process.env.TARGET_REF || 'beta';
110+
let branch_sha = context.sha;
111+
try {
112+
const branch = await github.rest.repos.getBranch({ owner, repo, branch: target });
113+
branch_sha = branch?.data?.commit?.sha || context.sha;
114+
} catch (e) {
115+
branch_sha = context.sha;
116+
}
117+
const short = branch_sha.substring(0, 7);
118+
const basehead = `openvcs-beta...${target}`;
119+
let compare_url = `${context.serverUrl}/${owner}/${repo}/compare/${basehead}`;
120+
let commit_url = `${context.serverUrl}/${owner}/${repo}/commit/${branch_sha}`;
121+
let changelog = '';
122+
123+
try {
124+
const res = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead });
125+
const commits = res?.data?.commits ?? [];
126+
if (commits.length === 0) {
127+
changelog = 'No changes detected.';
128+
} else {
129+
const items = commits.map(c => {
130+
const sha = (c.sha || '').substring(0, 7);
131+
const msg = (c.commit?.message || '').split('\n')[0];
132+
const author = c.author?.login || c.commit?.author?.name || 'unknown';
133+
return `- ${sha} ${msg} (@${author})`;
134+
});
135+
changelog = items.join('\n');
136+
}
137+
} catch (e) {
138+
changelog = `First beta or previous tag unavailable; showing recent commits at ${target}.`;
139+
compare_url = `${context.serverUrl}/${owner}/${repo}/tree/${target}`;
140+
}
141+
142+
core.setOutput('short_sha', short);
143+
core.setOutput('branch_sha', branch_sha);
144+
core.setOutput('date', date);
145+
core.setOutput('compare_url', compare_url);
146+
core.setOutput('commit_url', commit_url);
147+
core.setOutput('changelog', changelog);
148+
149+
# ---------- Frontend ----------
150+
- name: Setup Node
151+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
152+
with:
153+
node-version: '24'
154+
cache: 'npm'
155+
cache-dependency-path: Frontend/package-lock.json
156+
157+
- name: Install frontend deps
158+
working-directory: Frontend
159+
run: npm ci
160+
161+
- name: Build frontend
162+
working-directory: Frontend
163+
run: npm run build
164+
165+
# ---------- Rust & platform deps ----------
166+
- name: Install Rust (stable)
167+
uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable
168+
with:
169+
components: rustfmt, clippy
170+
171+
- name: Setup sccache
172+
uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9
173+
174+
- name: Install Linux deps
175+
if: matrix.platform == 'ubuntu-24.04'
176+
run: |
177+
set -euxo pipefail
178+
sudo apt-get update
179+
sudo apt-get install -y libappindicator3-dev librsvg2-dev patchelf
180+
sudo apt-get install -y libwebkit2gtk-4.1-dev || sudo apt-get install -y libwebkit2gtk-4.0-dev
181+
182+
# ---------- Cache ----------
183+
- name: Rust cache
184+
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
185+
with:
186+
cache-on-failure: true
187+
188+
- name: Cargo fmt (check)
189+
run: cargo fmt --all -- --check
190+
191+
- name: Cargo clippy
192+
run: cargo clippy --all-targets -- -D warnings
193+
194+
# ---------- Remove existing 'openvcs-beta' release & tag (if any) ----------
195+
- name: Remove existing 'openvcs-beta' release & tag
196+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
197+
with:
198+
script: |
199+
const owner = context.repo.owner;
200+
const repo = context.repo.repo;
201+
const tag = 'openvcs-beta';
202+
const releases = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 });
203+
const rel = releases.find(r => r.tag_name === tag);
204+
if (rel) await github.rest.repos.deleteRelease({ owner, repo, release_id: rel.id });
205+
try {
206+
await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` });
207+
} catch (e) {
208+
if (e.status !== 422) throw e;
209+
}
210+
211+
# ---------- Build & publish ----------
212+
- name: Build and publish Beta prerelease
213+
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2
214+
env:
215+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
216+
FRONTEND_SKIP_BUILD: '1'
217+
OPENVCS_UPDATE_CHANNEL: beta
218+
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
219+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_PRIVATE_KEY_PASSWORD }}
220+
with:
221+
projectPath: Backend
222+
tagName: openvcs-beta
223+
releaseName: "OpenVCS Beta ${{ steps.meta.outputs.date }} (${{ env.TARGET_REF }}@${{ steps.meta.outputs.short_sha }})"
224+
releaseBody: |
225+
Beta build from `${{ env.TARGET_REF }}`.
226+
Date (UTC): ${{ steps.meta.outputs.date }}
227+
Compare: ${{ steps.meta.outputs.compare_url }}
228+
Since: ${{ needs.check-changes.outputs.since_sha }}
229+
Ahead (relevant commits): ${{ needs.check-changes.outputs.ahead_count }}
230+
Commit: ${{ steps.meta.outputs.branch_sha }} (${{ env.TARGET_REF }}@${{ steps.meta.outputs.short_sha }})
231+
Runner: ${{ runner.os }} • Run #${{ github.run_number }}
232+
233+
Changes since last beta:
234+
${{ steps.meta.outputs.changelog }}
235+
releaseDraft: true
236+
prerelease: true
237+
args: ${{ matrix.args }}

Backend/build.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ fn main() {
138138
let stable = serde_json::Value::String(
139139
"https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json".into(),
140140
);
141+
let beta = serde_json::Value::String(
142+
"https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-beta/latest.json".into(),
143+
);
141144
let nightly = serde_json::Value::String(
142145
"https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-nightly/latest.json".into(),
143146
);
@@ -146,10 +149,10 @@ fn main() {
146149
if let Some(plugins) = json.get_mut("plugins") {
147150
if let Some(updater) = plugins.get_mut("updater") {
148151
let endpoints = match chan.as_str() {
152+
// Beta: check beta first, then stable
153+
"beta" => serde_json::Value::Array(vec![beta.clone(), stable.clone()]),
149154
// Nightly: check nightly first, then stable
150-
"nightly" | "beta" => {
151-
serde_json::Value::Array(vec![nightly.clone(), stable.clone()])
152-
}
155+
"nightly" => serde_json::Value::Array(vec![nightly.clone(), stable.clone()]),
153156
// Stable: stable only
154157
_ => serde_json::Value::Array(vec![stable.clone()]),
155158
};

Frontend/src/modals/settings.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ <h3 id="settings-title" style="margin:0">Settings</h3>
6868
</label>
6969
<select id="set-update-channel">
7070
<option value="stable">Stable</option>
71+
<option value="beta">Beta</option>
7172
<option value="nightly">Nightly</option>
7273
</select>
7374
</div>

Frontend/src/scripts/features/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@ function collectSettingsFromForm(root: HTMLElement): GlobalSettings {
790790
theme_pack: themePack || DEFAULT_LIGHT_THEME_ID,
791791
language: get<HTMLSelectElement>('#set-language')?.value,
792792
default_backend: (get<HTMLSelectElement>('#set-default-backend')?.value || 'git') as any,
793-
update_channel: (() => { const v = get<HTMLSelectElement>('#set-update-channel')?.value; return v === 'beta' ? 'nightly' : v; })(),
793+
update_channel: get<HTMLSelectElement>('#set-update-channel')?.value || 'stable',
794794
reopen_last_repos: !!get<HTMLInputElement>('#set-reopen-last')?.checked,
795795
checks_on_launch: !!get<HTMLInputElement>('#set-checks-on-launch')?.checked,
796796
};

Frontend/src/scripts/features/update.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ export async function showUpdateDialog(_data: any) {
3636
};
3737

3838
const stable = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/latest');
39+
const beta = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/tags/openvcs-beta');
3940
const nightly = await fetchJson('https://api.github.com/repos/Jordonbc/OpenVCS/releases/tags/openvcs-nightly');
4041

4142
const norm = (v: string) => String(v || '').replace(/^v/i, '').trim();
4243
const stableTag = norm(stable?.tag_name || stable?.name || '');
44+
const betaTag = norm(beta?.tag_name || beta?.name || '');
4345
const nightlyTag = norm(nightly?.tag_name || nightly?.name || '');
4446

4547
const base = (v: string) => norm(v).split('+', 1)[0];
@@ -51,6 +53,13 @@ export async function showUpdateDialog(_data: any) {
5153

5254
if (channel === 'stable') {
5355
if (newerThanCurrent(stableTag)) { show = true; pick = stable; }
56+
} else if (channel === 'beta') {
57+
// Beta: pick the most recent by published_at timestamp between beta and stable
58+
const sDate = Date.parse(String(stable?.published_at || stable?.created_at || '')) || 0;
59+
const bDate = Date.parse(String(beta?.published_at || beta?.created_at || '')) || 0;
60+
pick = (bDate > sDate ? beta : stable) || beta || stable;
61+
const pickTag = norm(pick?.tag_name || pick?.name || '');
62+
show = newerThanCurrent(pickTag);
5463
} else {
5564
// Nightly: pick the most recent by published_at timestamp and ensure it's newer than current
5665
const sDate = Date.parse(String(stable?.published_at || stable?.created_at || '')) || 0;

0 commit comments

Comments
 (0)