Compare commits

...

7 Commits

Author SHA1 Message Date
bigjakk 2cf5623186 chore: bump version to 0.7.9
Build and Release / build-and-release (push) Failing after 7m37s
2026-04-16 10:21:26 -07:00
bigjakk 5e7786d66e fix: resolve transitive deps in build-asar, use Move-Item in swap script 2026-04-16 10:21:14 -07:00
bigjakk 9a52d6b8fc chore: bump version to 0.7.8
Build and Release / build-and-release (push) Successful in 12m59s
2026-04-16 09:38:00 -07:00
bigjakk 255befd1b8 fix: use original-fs to bypass Electron asar interception 2026-04-16 09:38:00 -07:00
bigjakk 691b363f88 ci: remove sudo from workflow (Gitea runner is root)
Build and Release / build-and-release (push) Successful in 9m20s
2026-04-16 08:56:19 -07:00
bigjakk 8972c3e363 chore: bump version to 0.7.7
Build and Release / build-and-release (push) Has been cancelled
2026-04-16 08:31:27 -07:00
bigjakk 5a8d77c494 ci: auto-detect patch vs full, control asar upload accordingly
Build and Release / build-and-release (push) Successful in 8s
2026-04-16 08:26:26 -07:00
6 changed files with 112 additions and 68 deletions
+33 -42
View File
@@ -75,7 +75,7 @@ jobs:
CHANGED=$(git diff --name-only "$PREV_TAG"..HEAD) CHANGED=$(git diff --name-only "$PREV_TAG"..HEAD)
echo "$CHANGED" echo "$CHANGED"
# Files that require a full (installer) build # Files that require a full (installer-only) release
FULL_TRIGGERS="electron-builder.yml|scripts/download-electron.js" FULL_TRIGGERS="electron-builder.yml|scripts/download-electron.js"
if echo "$CHANGED" | grep -qE "^($FULL_TRIGGERS)$"; then if echo "$CHANGED" | grep -qE "^($FULL_TRIGGERS)$"; then
@@ -105,13 +105,12 @@ jobs:
with: with:
node-version: '22' node-version: '22'
# Full build needs wine + system deps for electron-builder
- name: Install system dependencies - name: Install system dependencies
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'full' if: steps.version-check.outputs.SKIP == 'false'
run: | run: |
sudo dpkg --add-architecture i386 dpkg --add-architecture i386
sudo apt-get update -qq apt-get update -qq
sudo apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
wine wine32 wine64 \ wine wine32 wine64 \
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2t64 \ libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2t64 \
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \ libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
@@ -123,39 +122,29 @@ jobs:
if: steps.version-check.outputs.SKIP == 'false' if: steps.version-check.outputs.SKIP == 'false'
run: npm ci --legacy-peer-deps run: npm ci --legacy-peer-deps
# ── Patch release: asar only ──
- name: Build asar
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'patch'
run: npm run build:asar
# ── Full release: installer + asar ──
- name: Build source - name: Build source
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'full' if: steps.version-check.outputs.SKIP == 'false'
run: npm run build run: npm run build
- name: Build Windows distributables - name: Build Windows distributables
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'full' if: steps.version-check.outputs.SKIP == 'false'
env: env:
WINEDEBUG: "-all" WINEDEBUG: "-all"
run: npx electron-builder --win -c.electronDist=node_modules/electron/dist-win --publish never run: npx electron-builder --win -c.electronDist=node_modules/electron/dist-win --publish never
- name: Build Linux distributables - name: Build Linux distributables
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'full' if: steps.version-check.outputs.SKIP == 'false'
run: npx electron-builder --linux -c.electronDist=node_modules/electron/dist-linux --publish never run: npx electron-builder --linux -c.electronDist=node_modules/electron/dist-linux --publish never
- name: Build asar for full release - name: Build asar for patch updates
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'full' if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'patch'
run: npm run build:asar run: npm run build:asar
- name: Report build sizes - name: Report build sizes
if: steps.version-check.outputs.SKIP == 'false' if: steps.version-check.outputs.SKIP == 'false'
run: | run: |
echo "=== Release type: ${{ steps.release-type.outputs.TYPE }} ===" echo "=== Release type: ${{ steps.release-type.outputs.TYPE }} ==="
if [ "${{ steps.release-type.outputs.TYPE }}" = "full" ]; then ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true
ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true
fi
ls -lh out/asar/* 2>/dev/null || true ls -lh out/asar/* 2>/dev/null || true
- name: Generate release notes - name: Generate release notes
@@ -215,34 +204,36 @@ jobs:
exit 1 exit 1
fi fi
# Upload asar + checksums (both patch and full releases) # Upload asar + checksums (patch releases only — signals client to use fast path)
for file in out/asar/app.asar out/asar/checksums.sha256; do if [ "$RELEASE_TYPE" = "patch" ]; then
for file in out/asar/app.asar out/asar/checksums.sha256; do
[ -f "$file" ] || continue
FILENAME=$(basename "$file")
echo "Uploading: $FILENAME ($(du -h "$file" | cut -f1))"
curl -s -X POST \
"$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$FILENAME" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file" \
| jq -r '" -> \(.name) (\(.size) bytes)"'
done
else
echo "Full release — skipping asar upload (forces installer update)"
fi
# Upload installers (always — for new users and major updates)
for file in out/*.exe out/*.AppImage out/*.deb; do
[ -f "$file" ] || continue [ -f "$file" ] || continue
FILENAME=$(basename "$file") FILENAME=$(basename "$file")
echo "Uploading: $FILENAME ($(du -h "$file" | cut -f1))" SAFE_NAME=$(echo "$FILENAME" | tr ' ' '_')
echo "Uploading: $SAFE_NAME ($(du -h "$file" | cut -f1))"
curl -s -X POST \ curl -s -X POST \
"$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$FILENAME" \ "$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$SAFE_NAME" \
-H "Authorization: token $GITEA_TOKEN" \ -H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file" \ -F "attachment=@$file" \
| jq -r '" -> \(.name) (\(.size) bytes)"' | jq -r '" -> \(.name) (\(.size) bytes)"'
done done
# Upload installer artifacts (full release only) echo "All assets uploaded"
if [ "$RELEASE_TYPE" = "full" ]; then
for file in out/*.exe out/*.AppImage out/*.deb; do
[ -f "$file" ] || continue
FILENAME=$(basename "$file")
SAFE_NAME=$(echo "$FILENAME" | tr ' ' '_')
echo "Uploading: $SAFE_NAME ($(du -h "$file" | cut -f1))"
curl -s -X POST \
"$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$SAFE_NAME" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file" \
| jq -r '" -> \(.name) (\(.size) bytes)"'
done
fi
echo "All assets uploaded (release type: $RELEASE_TYPE)"
- name: Prune old releases - name: Prune old releases
if: steps.version-check.outputs.SKIP == 'false' if: steps.version-check.outputs.SKIP == 'false'
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "krunker-civilian-client", "name": "krunker-civilian-client",
"version": "0.7.6", "version": "0.7.9",
"description": "Cross-platform Krunker game client", "description": "Cross-platform Krunker game client",
"main": "dist/main/index.js", "main": "dist/main/index.js",
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client", "homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
+65 -15
View File
@@ -4,13 +4,15 @@
* Replicates what electron-builder packs into the asar (dist/ + package.json + * Replicates what electron-builder packs into the asar (dist/ + package.json +
* required node_modules) without running the full installer build. * required node_modules) without running the full installer build.
* *
* Automatically resolves transitive dependencies so sub-deps are never missed.
*
* Usage: * Usage:
* npm run build:asar # default (github) update source * npm run build:asar # default (github) update source
* UPDATE_SOURCE=gitea npm run build:asar # gitea update source * UPDATE_SOURCE=gitea npm run build:asar # gitea update source
*/ */
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { cpSync, mkdirSync, rmSync, readFileSync, writeFileSync, createReadStream } = require('fs'); const { cpSync, mkdirSync, rmSync, readFileSync, writeFileSync, createReadStream, existsSync } = require('fs');
const { join } = require('path'); const { join } = require('path');
const { createHash } = require('crypto'); const { createHash } = require('crypto');
const asar = require('@electron/asar'); const asar = require('@electron/asar');
@@ -19,14 +21,51 @@ const ROOT = join(__dirname, '..');
const STAGING = join(ROOT, 'out', 'asar-staging'); const STAGING = join(ROOT, 'out', 'asar-staging');
const OUTPUT = join(ROOT, 'out', 'asar'); const OUTPUT = join(ROOT, 'out', 'asar');
// Same node_modules list as electron-builder.yml // Top-level packages to include (same as electron-builder.yml)
const NODE_MODULES = [ const SEED_MODULES = [
'electron-store', 'conf', 'dot-prop', 'type-fest', 'pkg-up', 'electron-store', 'conf', 'dot-prop', 'type-fest', 'pkg-up',
'find-up', 'locate-path', 'p-locate', 'p-limit', 'yocto-queue', 'find-up', 'locate-path', 'p-locate', 'p-limit', 'yocto-queue',
'path-exists', 'env-paths', 'json-schema-typed', 'ajv', 'ajv-formats', 'path-exists', 'env-paths', 'json-schema-typed', 'ajv', 'ajv-formats',
'atomically', 'debounce-fn', 'mimic-fn', 'semver', 'onetime', 'atomically', 'debounce-fn', 'mimic-fn', 'semver', 'onetime',
]; ];
/**
* Walk the dependency tree starting from seed modules and collect all
* transitive production dependencies.
*/
function resolveAllDeps(seeds) {
const resolved = new Set();
const queue = [...seeds];
while (queue.length > 0) {
const mod = queue.shift();
if (resolved.has(mod)) continue;
// Check if module exists in top-level node_modules
const modDir = join(ROOT, 'node_modules', mod);
if (!existsSync(modDir)) {
console.warn(` Warning: ${mod} not found in node_modules, skipping`);
continue;
}
resolved.add(mod);
// Read its dependencies and enqueue them
const pkgPath = join(modDir, 'package.json');
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
const deps = Object.keys(pkg.dependencies || {});
for (const dep of deps) {
if (!resolved.has(dep)) queue.push(dep);
}
} catch { /* ignore malformed package.json */ }
}
}
return resolved;
}
function shouldExclude(src) { function shouldExclude(src) {
return src.endsWith('.ts') || src.endsWith('.map'); return src.endsWith('.ts') || src.endsWith('.map');
} }
@@ -39,7 +78,12 @@ async function main() {
} }
execSync('npm run build', { cwd: ROOT, stdio: 'inherit', env }); execSync('npm run build', { cwd: ROOT, stdio: 'inherit', env });
// 2. Prepare staging directory // 2. Resolve all dependencies (including transitive)
console.log('\nResolving dependencies...');
const allModules = resolveAllDeps(SEED_MODULES);
console.log(`Found ${allModules.size} modules (${SEED_MODULES.length} seed + ${allModules.size - SEED_MODULES.length} transitive)`);
// 3. Prepare staging directory
rmSync(STAGING, { recursive: true, force: true }); rmSync(STAGING, { recursive: true, force: true });
mkdirSync(STAGING, { recursive: true }); mkdirSync(STAGING, { recursive: true });
@@ -49,35 +93,41 @@ async function main() {
// Copy package.json (Electron reads "main" from it) // Copy package.json (Electron reads "main" from it)
cpSync(join(ROOT, 'package.json'), join(STAGING, 'package.json')); cpSync(join(ROOT, 'package.json'), join(STAGING, 'package.json'));
// Copy required node_modules (excluding .ts and .map files) // Copy all resolved node_modules (excluding .ts and .map files)
for (const mod of NODE_MODULES) { for (const mod of allModules) {
const src = join(ROOT, 'node_modules', mod); const src = join(ROOT, 'node_modules', mod);
const dest = join(STAGING, 'node_modules', mod); const dest = join(STAGING, 'node_modules', mod);
cpSync(src, dest, {
recursive: true, // Some packages have nested node_modules with their own deps
filter: (s) => !shouldExclude(s), // (hoisted vs non-hoisted). Check both the top-level and nested locations.
}); if (existsSync(src)) {
cpSync(src, dest, {
recursive: true,
filter: (s) => !shouldExclude(s),
});
}
} }
// 3. Pack into asar // 4. Pack into asar
mkdirSync(OUTPUT, { recursive: true }); mkdirSync(OUTPUT, { recursive: true });
const asarPath = join(OUTPUT, 'app.asar'); const asarPath = join(OUTPUT, 'app.asar');
await asar.createPackage(STAGING, asarPath); await asar.createPackage(STAGING, asarPath);
console.log('Created:', asarPath); console.log('\nCreated:', asarPath);
// 4. Generate checksums.sha256 // 5. Generate checksums.sha256
const hex = await fileHash(asarPath); const hex = await fileHash(asarPath);
const checksumsPath = join(OUTPUT, 'checksums.sha256'); const checksumsPath = join(OUTPUT, 'checksums.sha256');
writeFileSync(checksumsPath, `${hex} app.asar\n`); writeFileSync(checksumsPath, `${hex} app.asar\n`);
console.log(`SHA-256: ${hex}`); console.log(`SHA-256: ${hex}`);
console.log('Created:', checksumsPath); console.log('Created:', checksumsPath);
// 5. Cleanup staging // 6. Cleanup staging
rmSync(STAGING, { recursive: true, force: true }); rmSync(STAGING, { recursive: true, force: true });
// Print size // Print size and module list
const size = readFileSync(asarPath).length; const size = readFileSync(asarPath).length;
console.log(`\napp.asar size: ${(size / 1024).toFixed(1)} KB`); console.log(`\napp.asar size: ${(size / 1024).toFixed(1)} KB`);
console.log('Modules:', [...allModules].sort().join(', '));
} }
function fileHash(filePath) { function fileHash(filePath) {
+6 -5
View File
@@ -1,6 +1,7 @@
import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron'; import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import { existsSync, mkdirSync, unlinkSync, promises as fsp } from 'fs'; import { existsSync, mkdirSync, promises as fsp } from 'fs';
import { existsSync as origExistsSync, unlinkSync as origUnlinkSync } from 'original-fs';
import { get as httpsGet } from 'https'; import { get as httpsGet } from 'https';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import * as os from 'os'; import * as os from 'os';
@@ -179,8 +180,8 @@ app.whenReady().then(async () => {
// Clean up stale pending asar from a previous failed swap // Clean up stale pending asar from a previous failed swap
const resourcesDir = join(dirname(app.getPath('exe')), 'resources'); const resourcesDir = join(dirname(app.getPath('exe')), 'resources');
const stalePending = join(resourcesDir, 'app-pending.asar'); const stalePending = join(resourcesDir, 'app-pending.asar');
if (existsSync(stalePending)) { if (origExistsSync(stalePending)) {
try { unlinkSync(stalePending); } catch { /* ignore */ } try { origUnlinkSync(stalePending); } catch { /* ignore */ }
} }
try { try {
@@ -213,8 +214,8 @@ app.whenReady().then(async () => {
} catch (err) { } catch (err) {
electronLog.error('[KCC] Patch download failed:', err); electronLog.error('[KCC] Patch download failed:', err);
// Clean up failed download // Clean up failed download
if (existsSync(pendingPath)) { if (origExistsSync(pendingPath)) {
try { unlinkSync(pendingPath); } catch { /* ignore */ } try { origUnlinkSync(pendingPath); } catch { /* ignore */ }
} }
if (!updateWin.isDestroyed()) updateWin.close(); if (!updateWin.isDestroyed()) updateWin.close();
} }
+6 -4
View File
@@ -1,5 +1,7 @@
import { get as httpsGet } from 'https'; import { get as httpsGet } from 'https';
import { createReadStream, createWriteStream, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs'; // Use original-fs to bypass Electron's asar interception — required for
// writing/renaming .asar files in the resources directory.
import { createReadStream, createWriteStream, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'original-fs';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
@@ -78,12 +80,12 @@ if (-not (Test-Path $pending)) { exit 1 }
try { try {
if (Test-Path $backup) { Remove-Item $backup -Force } if (Test-Path $backup) { Remove-Item $backup -Force }
Rename-Item $asar $backup -Force Move-Item $asar $backup -Force
Rename-Item $pending $asar -Force Move-Item $pending $asar -Force
if (Test-Path $backup) { Remove-Item $backup -Force } if (Test-Path $backup) { Remove-Item $backup -Force }
} catch { } catch {
if ((Test-Path $backup) -and -not (Test-Path $asar)) { if ((Test-Path $backup) -and -not (Test-Path $asar)) {
Rename-Item $backup $asar -Force Move-Item $backup $asar -Force
} }
exit 1 exit 1
} }
+1 -1
View File
@@ -20,7 +20,7 @@ export default defineConfig({
outDir: 'dist/main', outDir: 'dist/main',
emptyDirBefore: true, emptyDirBefore: true,
rollupOptions: { rollupOptions: {
external: ['electron', 'electron-store', ...nodeBuiltins], external: ['electron', 'electron-store', 'original-fs', ...nodeBuiltins],
}, },
target: 'node20', target: 'node20',
minify: isProd, minify: isProd,