Skip to content

Commit a68c44e

Browse files
committed
Add signing pipeline scoreboard backend and runner profiles
1 parent 03e2898 commit a68c44e

12 files changed

Lines changed: 548 additions & 70 deletions

File tree

.github/workflows/release.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ jobs:
4848
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4949
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
5050
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
51+
MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
52+
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
53+
APPLE_ID: ${{ secrets.APPLE_ID }}
54+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
55+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
5156

5257
steps:
5358
- name: Checkout
@@ -98,6 +103,25 @@ jobs:
98103
"CSC_LINK=$env:WIN_CSC_LINK" >> $env:GITHUB_ENV
99104
"CSC_KEY_PASSWORD=$env:WIN_CSC_KEY_PASSWORD" >> $env:GITHUB_ENV
100105
106+
- name: Decode macOS signing certificate
107+
if: runner.os == 'macOS' && env.MACOS_CERTIFICATE_BASE64 != ''
108+
shell: bash
109+
run: |
110+
CERT_PATH="$RUNNER_TEMP/macos-signing-cert.p12"
111+
echo "$MACOS_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH"
112+
echo "CSC_LINK=$CERT_PATH" >> "$GITHUB_ENV"
113+
echo "CSC_KEY_PASSWORD=$MACOS_CERTIFICATE_PASSWORD" >> "$GITHUB_ENV"
114+
echo "CSC_IDENTITY_AUTO_DISCOVERY=true" >> "$GITHUB_ENV"
115+
116+
- name: Disable signing when no certificate is configured
117+
shell: pwsh
118+
run: |
119+
if ($env:CSC_LINK) {
120+
"CSC_IDENTITY_AUTO_DISCOVERY=true" >> $env:GITHUB_ENV
121+
} else {
122+
"CSC_IDENTITY_AUTO_DISCOVERY=false" >> $env:GITHUB_ENV
123+
}
124+
101125
- name: Build packages
102126
shell: pwsh
103127
run: |
@@ -114,7 +138,6 @@ jobs:
114138
exit $exitCode
115139
env:
116140
BUILD_SCRIPT: ${{ matrix.script }}
117-
CSC_IDENTITY_AUTO_DISCOVERY: false
118141
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
119142

120143
- name: Upload build artifacts

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,16 @@ npm start
161161

162162
Tokens stay on the backend. The browser UI never asks for or stores a GitHub token.
163163

164+
For a proper leaderboard service, run the bundled scoreboard backend:
165+
166+
```powershell
167+
npm run scoreboard
168+
$env:RIGSCOPE_SCOREBOARD_URL="http://127.0.0.1:8797"
169+
npm start
170+
```
171+
172+
The scoreboard backend adds challenge nonces, rate limiting, server-side profile normalization, score bounds, and setup lookup endpoints. See [docs/SCOREBOARD.md](docs/SCOREBOARD.md).
173+
164174
## Security Model
165175

166176
- Local server binds to `127.0.0.1`.
@@ -202,6 +212,7 @@ npm start # local server only
202212
npm run open # server + default browser
203213
npm run app # browser app mode
204214
npm run desktop # Electron shell
215+
npm run scoreboard # local leaderboard backend
205216
npm run pack # unpacked desktop build
206217
```
207218

build/entitlements.mac.plist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.cs.allow-jit</key>
6+
<true/>
7+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8+
<true/>
9+
</dict>
10+
</plist>

docs/RELEASE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ Windows builds still run without these secrets, but the generated installer and
8383

8484
For signing, configure:
8585

86-
- `CSC_LINK`: Developer ID Application certificate, usually a base64 certificate or secure URL supported by `electron-builder`
87-
- `CSC_KEY_PASSWORD`: certificate password
86+
- `MACOS_CERTIFICATE_BASE64`: base64-encoded Developer ID Application `.p12`
87+
- `MACOS_CERTIFICATE_PASSWORD`: certificate password
88+
89+
The workflow decodes the certificate and exposes it to `electron-builder` as `CSC_LINK` and `CSC_KEY_PASSWORD`. Local builds may still use `CSC_LINK` and `CSC_KEY_PASSWORD` directly.
8890

8991
For notarization, configure placeholders expected by `electron-builder` tooling:
9092

@@ -94,6 +96,8 @@ For notarization, configure placeholders expected by `electron-builder` tooling:
9496

9597
The project currently defines macOS DMG and ZIP targets in `package.json`. Signing and notarization are active only when the certificate and Apple credentials are present in GitHub Secrets.
9698

99+
The macOS build uses hardened runtime and `build/entitlements.mac.plist`. The notarization hook is `scripts/notarize.js`; it no-ops when Apple credentials are missing, so unsigned preview builds still work.
100+
97101
### Linux packages
98102

99103
Linux AppImage, `.deb`, and `.tar.gz` packages are built unsigned by default. Add package signing later only if the distribution channel requires it.

docs/SCOREBOARD.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# RigScope Scoreboard Backend
2+
3+
RigScope can use a real scoreboard service instead of the GitHub gist MVP.
4+
5+
## Start Locally
6+
7+
```powershell
8+
npm run scoreboard
9+
```
10+
11+
Default URL:
12+
13+
```text
14+
http://127.0.0.1:8797
15+
```
16+
17+
Point the app server at it:
18+
19+
```powershell
20+
$env:RIGSCOPE_SCOREBOARD_URL="http://127.0.0.1:8797"
21+
npm start
22+
```
23+
24+
## API
25+
26+
- `POST /api/v1/challenge` returns a short-lived nonce.
27+
- `POST /api/v1/submissions` accepts `{ nonce, profile }`, validates the nonce, normalizes the public score card, calculates the server-side public record, and stores it.
28+
- `GET /api/v1/leaderboard?limit=100` returns ranked public profiles.
29+
- `GET /api/v1/setups/:id` returns one public setup profile.
30+
- `GET /api/v1/health` returns service health.
31+
32+
## Current Anti-Abuse Layer
33+
34+
- short-lived challenge nonce
35+
- per-IP rate limit
36+
- server-side normalization and score bounds
37+
- public reduced profile only
38+
- raw IP is not stored; submissions store an IP hash
39+
- bounded JSON body size
40+
41+
This is stronger than GitHub/gist sync, but it is not full anti-cheat. A production leaderboard should add signed benchmark attestations, account identity, moderation, replay detection, and server-side anomaly scoring.
42+
43+
## Data
44+
45+
By default data is stored in:
46+
47+
```text
48+
~/.rigscope-scoreboard/scoreboard.json
49+
```
50+
51+
Override:
52+
53+
```powershell
54+
$env:RIGSCOPE_SCOREBOARD_DATA="D:\rigscope-scoreboard"
55+
```

native-runners.js

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,116 @@ const MAX_DURATION_SEC = 900;
99

1010
const PROFILE_DEFINITIONS = [
1111
{
12-
id: "prime95-torture",
12+
id: "prime95-small-fft",
1313
toolId: "prime95",
14-
label: "Prime95 / mprime Torture",
14+
label: "Prime95 / mprime Small FFT",
15+
target: "cpu",
16+
risk: "high",
17+
durationDefaultSec: 180,
18+
durationMaxSec: 600,
19+
args: () => ["-t"],
20+
safety: {
21+
maxDurationSec: 600,
22+
recommendedMonitor: "Watch CPU package temperature and VRM temperature. Stop above your platform limit.",
23+
stopBehavior: "RigScope terminates the process tree at the duration cap or when Stop Native is pressed."
24+
},
25+
notes: "Starts Prime95 torture mode. Prime95 chooses the exact torture preset from its local configuration, so treat it as a high-heat CPU test."
26+
},
27+
{
28+
id: "prime95-blend",
29+
toolId: "prime95",
30+
label: "Prime95 / mprime Blend",
1531
target: "cpu-memory",
1632
risk: "high",
1733
durationDefaultSec: 300,
1834
durationMaxSec: 900,
1935
args: () => ["-t"],
20-
notes: "Starts the built-in torture mode. Stop from RigScope or close Prime95/mprime."
36+
safety: {
37+
maxDurationSec: 900,
38+
recommendedMonitor: "Watch CPU temperature, memory temperature where available, and system responsiveness.",
39+
stopBehavior: "RigScope terminates the process tree at the duration cap or when Stop Native is pressed."
40+
},
41+
notes: "Launches Prime95/mprime torture mode for a longer CPU/RAM stability pass. Configure blend mode inside Prime95 if prompted."
2142
},
2243
{
2344
id: "furmark-smoke",
2445
toolId: "furmark",
25-
label: "FurMark GPU Smoke",
46+
label: "FurMark GPU Smoke 720p",
2647
target: "gpu",
2748
risk: "high",
2849
durationDefaultSec: 120,
50+
durationMaxSec: 300,
51+
args: (tool, durationSec) => {
52+
const exe = path.basename(tool.executable?.path || "").toLowerCase();
53+
if (process.platform === "win32") {
54+
return exe.includes("furmark2")
55+
? ["--demo", "--fullscreen=0"]
56+
: ["/nogui", "/width=1280", "/height=720", `/max_time=${durationSec * 1000}`];
57+
}
58+
return [];
59+
},
60+
safety: {
61+
maxDurationSec: 300,
62+
recommendedMonitor: "Watch GPU hotspot, memory junction where available, fan speed, and power draw.",
63+
stopBehavior: "RigScope terminates the process tree at the duration cap or when Stop Native is pressed."
64+
},
65+
notes: "Launches FurMark with a conservative 720p windowed smoke profile when supported by the installed build."
66+
},
67+
{
68+
id: "furmark-burn-in-1080p",
69+
toolId: "furmark",
70+
label: "FurMark Burn-in 1080p",
71+
target: "gpu",
72+
risk: "extreme",
73+
durationDefaultSec: 180,
2974
durationMaxSec: 600,
30-
args: (tool) => {
75+
args: (tool, durationSec) => {
3176
const exe = path.basename(tool.executable?.path || "").toLowerCase();
3277
if (process.platform === "win32") {
3378
return exe.includes("furmark2")
3479
? ["--demo", "--fullscreen=0"]
35-
: ["/nogui", "/width=1280", "/height=720", "/max_time=120000"];
80+
: ["/nogui", "/width=1920", "/height=1080", `/max_time=${durationSec * 1000}`];
3681
}
3782
return [];
3883
},
39-
notes: "Launches FurMark with a conservative windowed/smoke profile when supported by the installed build."
84+
safety: {
85+
maxDurationSec: 600,
86+
recommendedMonitor: "Use only with active temperature monitoring. Stop immediately on artifacts, throttling, or unstable power.",
87+
stopBehavior: "RigScope terminates the process tree at the duration cap or when Stop Native is pressed."
88+
},
89+
notes: "High-load FurMark profile intended for short validation runs, not unattended overnight testing."
4090
},
4191
{
4292
id: "occt-manual",
4393
toolId: "occt",
44-
label: "OCCT Manual Session",
94+
label: "OCCT Manual Stability Session",
4595
target: "stability-suite",
4696
risk: "high",
4797
durationDefaultSec: 300,
4898
durationMaxSec: 900,
4999
args: () => [],
50-
notes: "OCCT CLI differs by release, so RigScope launches OCCT and tracks the process; choose the exact test in OCCT."
100+
safety: {
101+
maxDurationSec: 900,
102+
recommendedMonitor: "Use OCCT's own telemetry and stop on errors, thermal throttling, or PSU instability.",
103+
stopBehavior: "RigScope tracks and terminates the launched OCCT process when requested."
104+
},
105+
notes: "OCCT CLI differs by release, so RigScope launches OCCT and tracks the process; choose CPU, memory, GPU, or PSU inside OCCT."
106+
},
107+
{
108+
id: "occt-psu-manual",
109+
toolId: "occt",
110+
label: "OCCT PSU Manual Session",
111+
target: "psu-system",
112+
risk: "extreme",
113+
durationDefaultSec: 120,
114+
durationMaxSec: 300,
115+
args: () => [],
116+
safety: {
117+
maxDurationSec: 300,
118+
recommendedMonitor: "PSU tests can load CPU and GPU together. Do not run unattended.",
119+
stopBehavior: "RigScope tracks and terminates the launched OCCT process when requested."
120+
},
121+
notes: "Opens OCCT for a short manual PSU validation session. The exact test must be started inside OCCT."
51122
},
52123
{
53124
id: "y-cruncher-manual",
@@ -58,7 +129,12 @@ const PROFILE_DEFINITIONS = [
58129
durationDefaultSec: 300,
59130
durationMaxSec: 900,
60131
args: () => [],
61-
notes: "y-cruncher automation is intentionally not scripted yet; this launches the native tool and tracks it."
132+
safety: {
133+
maxDurationSec: 900,
134+
recommendedMonitor: "Watch CPU temperature and memory stability. y-cruncher can expose marginal RAM/IMC instability quickly.",
135+
stopBehavior: "RigScope tracks and terminates the launched y-cruncher process when requested."
136+
},
137+
notes: "Launches y-cruncher for manual stress/benchmark selection and tracks the process."
62138
}
63139
];
64140

@@ -75,7 +151,8 @@ const nativeRunner = {
75151
child: null,
76152
exitCode: null,
77153
signal: null,
78-
output: []
154+
output: [],
155+
report: null
79156
};
80157

81158
function getProfiles() {
@@ -95,6 +172,7 @@ function getProfiles() {
95172
durationDefaultSec: profile.durationDefaultSec,
96173
durationMaxSec: profile.durationMaxSec,
97174
acknowledgement: ACK,
175+
safety: profile.safety,
98176
notes: profile.notes
99177
};
100178
});
@@ -115,7 +193,31 @@ function getStatus(reason = "status") {
115193
durationMs: nativeRunner.durationMs,
116194
exitCode: nativeRunner.exitCode,
117195
signal: nativeRunner.signal,
118-
output: nativeRunner.output.slice(-12)
196+
output: nativeRunner.output.slice(-12),
197+
report: nativeRunner.report
198+
};
199+
}
200+
201+
function buildReport(reason = "status") {
202+
const elapsedMs = nativeRunner.startedAt ? Date.now() - nativeRunner.startedAt : 0;
203+
const completedRatio = nativeRunner.durationMs ? Math.min(1, elapsedMs / nativeRunner.durationMs) : 0;
204+
return {
205+
generatedAt: new Date().toISOString(),
206+
reason,
207+
id: nativeRunner.id,
208+
profileId: nativeRunner.profileId,
209+
label: nativeRunner.label,
210+
target: nativeRunner.target,
211+
risk: nativeRunner.risk,
212+
pid: nativeRunner.pid,
213+
elapsedMs,
214+
durationMs: nativeRunner.durationMs,
215+
completedRatio: Math.round(completedRatio * 100) / 100,
216+
exitCode: nativeRunner.exitCode,
217+
signal: nativeRunner.signal,
218+
verdict: nativeRunner.exitCode === 0 ? "completed" : nativeRunner.signal ? "stopped" : reason,
219+
outputTail: nativeRunner.output.slice(-20),
220+
safety: nativeRunner.safety
119221
};
120222
}
121223

@@ -151,7 +253,7 @@ function startProfile(options = {}) {
151253

152254
const requested = Number(options.durationSec) || profile.durationDefaultSec;
153255
const durationSec = Math.max(10, Math.min(requested, profile.durationMaxSec, MAX_DURATION_SEC));
154-
const args = profile.args(tool);
256+
const args = profile.args(tool, durationSec);
155257
const child = spawn(tool.executable.path, args, {
156258
cwd: path.dirname(tool.executable.path),
157259
windowsHide: false,
@@ -164,13 +266,17 @@ function startProfile(options = {}) {
164266
profileId: profile.id,
165267
toolId: profile.toolId,
166268
label: profile.label,
269+
target: profile.target,
270+
risk: profile.risk,
271+
safety: profile.safety,
167272
pid: child.pid,
168273
startedAt: Date.now(),
169274
durationMs: durationSec * 1000,
170275
child,
171276
exitCode: null,
172277
signal: null,
173-
output: []
278+
output: [],
279+
report: null
174280
});
175281

176282
const capture = (source, chunk) => {
@@ -185,6 +291,7 @@ function startProfile(options = {}) {
185291
nativeRunner.active = false;
186292
nativeRunner.exitCode = code;
187293
nativeRunner.signal = signal;
294+
nativeRunner.report = buildReport(signal ? "stopped" : "exited");
188295
nativeRunner.child = null;
189296
clearTimeout(nativeRunner.timer);
190297
});
@@ -200,9 +307,11 @@ function stopProfile(reason = "stopped") {
200307
if (!nativeRunner.active || !child) {
201308
nativeRunner.active = false;
202309
nativeRunner.child = null;
310+
nativeRunner.report = buildReport(reason);
203311
return getStatus(reason);
204312
}
205313
nativeRunner.active = false;
314+
nativeRunner.report = buildReport(reason);
206315
if (process.platform === "win32" && child.pid) {
207316
execFile("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true }, () => {});
208317
} else {

0 commit comments

Comments
 (0)