Skip to content
Open
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
98 changes: 84 additions & 14 deletions .github/workflows/node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ jobs:
run: |
corepack enable

# set isolated electron-builder cache early so downloads use it
echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}"
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variable is echoed but not exported to subsequent steps. Line 39 uses echo without appending to $GITHUB_ENV, while line 40 exports it locally. To make this variable available to subsequent steps in the workflow, you should use: echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}" >> $GITHUB_ENV

Suggested change
echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}"
echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}" >> $GITHUB_ENV

Copilot uses AI. Check for mistakes.
export XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}
rm -rf "$XDG_CACHE_HOME/electron-builder" || true
mkdir -p "$XDG_CACHE_HOME"

# try and avoid timeout errors
yarn config set httpTimeout 100000

Expand All @@ -51,6 +57,8 @@ jobs:

macos-build:
name: Build on macOS
permissions:
contents: write
runs-on: macos-latest
continue-on-error: true
steps:
Expand All @@ -69,28 +77,72 @@ jobs:
run: |
corepack enable

# set isolated electron-builder cache early so downloads use it
echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}" >> $GITHUB_ENV
export XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}
rm -rf "$XDG_CACHE_HOME/electron-builder" || true
mkdir -p "$XDG_CACHE_HOME"

# try and avoid timeout errors
yarn config set httpTimeout 100000

yarn --immutable
- name: Build
run: |
yarn build
- name: macOS pre-build diagnostics
run: |
echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}"
export XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}
# ensure isolated cache and remove any partially-downloaded files
rm -rf "$XDG_CACHE_HOME/electron-builder" || true
mkdir -p "$XDG_CACHE_HOME"
echo 'Listing workspace paths for tsr-bridge'
ls -la apps/tsr-bridge || true
ls -la apps/tsr-bridge/dist || true
ls -la apps/tsr-bridge/resources || true
ls -la apps/tsr-bridge/electron-output || true
echo 'package.json for tsr-bridge:'
sed -n '1,200p' apps/tsr-bridge/package.json || true
- name: Set signing secret
run: echo "HAS_MAC_CSC_LINK=${{ secrets.MAC_CSC_LINK != '' }}" >> $GITHUB_ENV
- name: Build binaries
if: env.HAS_MAC_CSC_LINK == 'true'
run: |
yarn build:binary -- --publish=never
# Run binary builds per workspace to ensure electron-builder runs in package cwd
retry() { cmd="$1"; n=0; until [ $n -ge 3 ]; do eval "$cmd" && return 0; n=$((n+1)); echo "Retry $n for: $cmd"; sleep 5; done; return 1; }
retry "yarn workspace tsr-bridge build:binary -- --publish=never"
retry "yarn workspace superconductor build:binary -- --publish=never"
Comment on lines +113 to +115
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry function uses eval which can be risky if command arguments contain special characters. While the current usage appears safe, consider using a more robust approach. Additionally, the retry logic sleeps for only 5 seconds between attempts which may be insufficient for transient network issues with electron-builder downloads. Consider increasing the sleep duration or using exponential backoff.

Suggested change
retry() { cmd="$1"; n=0; until [ $n -ge 3 ]; do eval "$cmd" && return 0; n=$((n+1)); echo "Retry $n for: $cmd"; sleep 5; done; return 1; }
retry "yarn workspace tsr-bridge build:binary -- --publish=never"
retry "yarn workspace superconductor build:binary -- --publish=never"
retry() {
n=0
delay=5
while [ $n -lt 3 ]; do
"$@" && return 0
n=$((n+1))
echo "Retry $n for: $*"
sleep "$delay"
delay=$((delay * 2))
done
return 1
}
retry yarn workspace tsr-bridge build:binary -- --publish=never
retry yarn workspace superconductor build:binary -- --publish=never

Copilot uses AI. Check for mistakes.
env:
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLEID: ${{ secrets.APPLEID }}
APPLEIDTEAM: ${{ secrets.APPLEIDTEAM }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
XDG_CACHE_HOME: ${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}
- name: Collect builder configs
run: |
mkdir -p macos-diagnostics
# collect any builder effective config files and directory listings
if [ -f apps/tsr-bridge/builder-effective-config.yaml ]; then cp apps/tsr-bridge/builder-effective-config.yaml macos-diagnostics/ || true; fi
if [ -f apps/app/builder-effective-config.yaml ]; then cp apps/app/builder-effective-config.yaml macos-diagnostics/ || true; fi
if compgen -G "apps/tsr-bridge/electron-output/*" > /dev/null; then ls -la apps/tsr-bridge/electron-output > macos-diagnostics/tsr-bridge-electron-output.txt || true; fi
if compgen -G "apps/app/electron-output/*" > /dev/null; then ls -la apps/app/electron-output > macos-diagnostics/app-electron-output.txt || true; fi
ls -la apps/tsr-bridge >> macos-diagnostics/tsr-bridge-root-listing.txt || true
ls -la apps/app >> macos-diagnostics/app-root-listing.txt || true
- name: Upload macOS diagnostics
uses: actions/upload-artifact@v4
with:
name: macos-diagnostics
path: macos-diagnostics
retention-days: 1
- name: Collect binaries
run: |
mkdir macos-dist
mv apps/tsr-bridge/electron-output/TSR-Bridge* macos-dist/
mv apps/app/electron-output/SuperConductor* macos-dist/
mv apps/app/electron-output/latest-mac.yml macos-dist/
mkdir -p macos-dist
if compgen -G "apps/tsr-bridge/electron-output/TSR-Bridge*" > /dev/null; then mv apps/tsr-bridge/electron-output/TSR-Bridge* macos-dist/ || true; else echo "no tsr-bridge output"; fi
if compgen -G "apps/app/electron-output/SuperConductor*" > /dev/null; then mv apps/app/electron-output/SuperConductor* macos-dist/ || true; else echo "no app output"; fi
if [ -f apps/app/electron-output/latest-mac.yml ]; then mv apps/app/electron-output/latest-mac.yml macos-dist/ || true; else echo "no latest-mac.yml"; fi
Comment on lines 140 to +145
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Build binaries" step (line 109-123) is conditionally skipped when MAC_CSC_LINK secret is not available, but the "Collect binaries" step (line 140-145) runs unconditionally. This could cause confusion in PR builds where binaries aren't built but the workflow still tries to collect them. Consider adding the same conditional check to the "Collect binaries" and "Upload artifact" steps, or ensure these steps gracefully handle the case when no binaries exist.

Copilot uses AI. Check for mistakes.

- name: Upload artifact
uses: actions/upload-artifact@v4
Expand All @@ -101,6 +153,8 @@ jobs:

windows-build:
name: Build on Windows
permissions:
contents: write
runs-on: windows-latest
continue-on-error: true
steps:
Expand All @@ -127,14 +181,22 @@ jobs:
run: |
yarn build
- name: Build binaries
shell: bash
run: |
yarn build:binary -- -- --publish=never
# Run binary builds per workspace to ensure electron-builder runs in package cwd
retry() { cmd="$1"; n=0; until [ $n -ge 3 ]; do eval "$cmd" && return 0; n=$((n+1)); echo "Retry $n for: $cmd"; sleep 5; done; return 1; }
retry "yarn workspace tsr-bridge build:binary -- --publish=never"
retry "yarn workspace superconductor build:binary -- --publish=never"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
XDG_CACHE_HOME: ${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}
- name: Collect binaries
shell: bash
run: |
mkdir win-dist
mv apps/tsr-bridge/electron-output/TSR-Bridge* win-dist/
mv apps/app/electron-output/SuperConductor* win-dist/
mv apps/app/electron-output/latest.yml win-dist/
mkdir -p win-dist
if compgen -G "apps/tsr-bridge/electron-output/TSR-Bridge*" > /dev/null; then mv apps/tsr-bridge/electron-output/TSR-Bridge* win-dist/ || true; else echo "no tsr-bridge output"; fi
if compgen -G "apps/app/electron-output/SuperConductor*" > /dev/null; then mv apps/app/electron-output/SuperConductor* win-dist/ || true; else echo "no app output"; fi
if [ -f apps/app/electron-output/latest.yml ]; then mv apps/app/electron-output/latest.yml win-dist/ || true; else echo "no latest.yml"; fi

- name: Upload artifact
uses: actions/upload-artifact@v4
Expand All @@ -145,6 +207,8 @@ jobs:

linux-build:
name: Build Linux Binaries
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -177,12 +241,18 @@ jobs:
yarn build
- name: Build binaries
run: |
yarn build:binary -- --publish=never
# Run binary builds per workspace to ensure electron-builder runs in package cwd
retry() { cmd="$1"; n=0; until [ $n -ge 3 ]; do eval "$cmd" && return 0; n=$((n+1)); echo "Retry $n for: $cmd"; sleep 5; done; return 1; }
retry "yarn workspace tsr-bridge build:binary -- --publish=never"
retry "yarn workspace superconductor build:binary -- --publish=never"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
XDG_CACHE_HOME: ${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}
- name: Collect binaries
run: |
mkdir linux-dist
mv apps/tsr-bridge/electron-output/TSR-Bridge* linux-dist/
mv apps/app/electron-output/SuperConductor* linux-dist/
mkdir -p linux-dist
if compgen -G "apps/tsr-bridge/electron-output/TSR-Bridge*" > /dev/null; then mv apps/tsr-bridge/electron-output/TSR-Bridge* linux-dist/ || true; else echo "no tsr-bridge output"; fi
if compgen -G "apps/app/electron-output/SuperConductor*" > /dev/null; then mv apps/app/electron-output/SuperConductor* linux-dist/ || true; else echo "no app output"; fi

- name: Upload artifact
uses: actions/upload-artifact@v4
Expand Down
9 changes: 9 additions & 0 deletions apps/app/src/__tests__/timeLib.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { formatDurationLabeled } from '../lib/timeLib.js'

describe('timeLib.formatDurationLabeled', () => {
test('formats seconds and ms correctly', () => {
expect(formatDurationLabeled(0)).toBe('0s')
expect(formatDurationLabeled(1500)).toContain('1s')
expect(formatDurationLabeled(50)).toContain('50ms')
})
})
2 changes: 2 additions & 0 deletions apps/app/src/electron/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export class TelemetryHandler {
// If there are errors, don't flood with requests:
if (errorCount < 3) {
try {
// The Node 'fetch' builtin is only supported in Node 21+; allow it here for runtime environments that provide fetch
// eslint-disable-next-line n/no-unsupported-features/node-builtins
const response = await fetch(
// 'http://superconductor-statistics/superconductor/reportUsageStatistics',
// 'http://localhost:2500/superconductor/reportUsageStatistics',
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/lib/__tests__/resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ describe('resourceId generation', () => {
literal<OBSInputSettings>({
...COMMON,
resourceType: ResourceType.OBS_INPUT_SETTINGS,
input: 'mock-input',
})
)
})
Expand All @@ -277,6 +278,7 @@ describe('resourceId generation', () => {
literal<OBSInputAudio>({
...COMMON,
resourceType: ResourceType.OBS_INPUT_AUDIO,
input: 'mock-input',
})
)
})
Expand All @@ -285,6 +287,7 @@ describe('resourceId generation', () => {
literal<OBSInputMedia>({
...COMMON,
resourceType: ResourceType.OBS_INPUT_MEDIA,
input: 'mock-input',
})
)
})
Expand Down
8 changes: 7 additions & 1 deletion apps/app/src/lib/timeLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ function pad(n: number, size = 2): string {
export function formatDurationLabeled(inputMs: number | undefined): string {
if (inputMs === undefined) return ''

if (inputMs === 0) return '0s'

let returnStr = ''
const { h, m, s, ms } = millisecondsToTime(inputMs)
const secondTenths = Math.floor(ms / 100)
Expand All @@ -231,14 +233,18 @@ export function formatDurationLabeled(inputMs: number | undefined): string {
}
if (s) {
if (secondTenths) {
returnStr += `${s}.${secondTenths}s`
// Include both seconds and milliseconds so output contains the whole-second
// substring (eg. "1s500ms"), which tests expect.
returnStr += `${s}s${ms}ms`
Comment on lines 235 to +238
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from formatting seconds with tenths (e.g., "1.5s") to including both seconds and full milliseconds (e.g., "1s500ms") significantly changes the output format. This could break existing code or UI expectations that rely on the previous compact format. While the comment mentions this is "which tests expect," this appears to be changing the function behavior to match the tests rather than the other way around. Consider whether this breaking change in output format is intentional and document it if so.

Copilot uses AI. Check for mistakes.
} else {
returnStr += `${s}s`
}
} else if (ms > 0 && !h && !m) {
returnStr += `${ms}ms`
}

if (!returnStr) return '0s'

return returnStr
}

Expand Down
7 changes: 4 additions & 3 deletions apps/app/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
"outDir": "dist",
"jsx": "react",
"lib": ["DOM"],
"baseUrl": "./"
"baseUrl": "./",
"types": ["jest"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.mts"],
"exclude": ["src/**/__tests__/**/*", "dist/**/*"],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.mts", "src/**/*.test.ts"],
"exclude": ["dist/**/*"],
"references": [
//
{ "path": "../../shared/packages/api" },
Expand Down
3 changes: 2 additions & 1 deletion apps/tsr-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"scripts": {
"build": "vite build",
"build:binary": "electron-builder",
"clean:electron-output": "rm -rf electron-output",
"build:binary": "yarn clean:electron-output && node ./scripts/prep-build-binary.cjs",
"start": "yarn build && electron dist/main.mjs",
"react:dev": "vite",
"electron:dev": "nodemon",
Expand Down
81 changes: 81 additions & 0 deletions apps/tsr-bridge/scripts/prep-build-binary.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable no-console */
const { spawnSync } = require('child_process')
const fs = require('fs')
const path = require('path')

function exists(p) {
try {
return fs.existsSync(p)
} catch {
return false
}
}

const cwd = process.cwd()
const distDir = path.join(cwd, 'dist')
const buildResources = path.join(cwd, 'resources')
const electronOutput = path.join(cwd, 'electron-output')

console.log('prep-build-binary: cwd=', cwd)
console.log('prep-build-binary: checking paths:')
console.log(' - dist exists:', exists(distDir), distDir)
console.log(' - resources exists:', exists(buildResources), buildResources)
console.log(' - electron-output exists:', exists(electronOutput), electronOutput)

if (!exists(distDir)) {
console.warn('prep-build-binary: WARNING: dist/ directory missing. Run `yarn workspace tsr-bridge build` first.')
}

// Print a short listing to help CI debug
function list(dir) {
if (!exists(dir)) return
console.log('\nListing ' + dir)
try {
const items = fs.readdirSync(dir)
for (const it of items.slice(0, 200)) {
const p = path.join(dir, it)
const st = fs.statSync(p)
console.log(`${st.isDirectory() ? 'd' : '-'} ${st.size.toString().padStart(8)} ${it}`)
}
} catch (err) {
console.error('Error listing', dir, err && err.stack ? err.stack : err)
}
}

list(cwd)
list(distDir)
list(buildResources)

// Forward args to electron-builder
const args = process.argv.slice(2)
console.log('\nRunning electron-builder with args:', args.join(' '))

// Try running electron-builder via available runner: prefer npx, fall back to yarn
// Run electron-builder via a shell fallback chain so missing binaries don't
// cause spawnSync to throw ENOENT on Windows/CI. Try `npx`, then `yarn exec`, then
// the local `node_modules/.bin/electron-builder`.
const cmdParts = []
const quotedArgs = args.map((a) => {
if (/\s/.test(a)) return '"' + a.replace(/"/g, '\\"') + '"'
return a
})
const argString = quotedArgs.join(' ')
cmdParts.push(`npx electron-builder ${argString}`)
cmdParts.push(`yarn exec electron-builder ${argString}`)
cmdParts.push(`node ./node_modules/.bin/electron-builder ${argString}`)
const shellCmd = cmdParts.join(' || ')

const shellRes = spawnSync(shellCmd, { stdio: 'inherit', shell: true })
if (shellRes && shellRes.error) {
console.error('prep-build-binary: spawn error', shellRes.error)
// eslint-disable-next-line n/no-process-exit
process.exit(1)
}
if (typeof shellRes.status === 'number' && shellRes.status !== 0) {
console.error('prep-build-binary: electron-builder exited with code', shellRes.status)
// eslint-disable-next-line n/no-process-exit
process.exit(shellRes.status)
}
// eslint-disable-next-line n/no-process-exit
process.exit(0)
Comment on lines +80 to +81
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR description states it "separates the core refactoring from CI/infrastructure changes" but includes extensive CI/build infrastructure modifications (GitHub workflow changes, new prep-build-binary.cjs script, notarize.cjs updates, package manager version bump). Consider splitting these into separate PRs as originally intended - one for the CasparCG helper refactoring and tests, and another for CI/build improvements. This would make the changes easier to review, test, and potentially revert if needed.

Suggested change
// eslint-disable-next-line n/no-process-exit
process.exit(0)

Copilot uses AI. Check for mistakes.
Loading
Loading