Skip to content

Update package.json #726

Update package.json

Update package.json #726

Workflow file for this run

name: Build Desktop App
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
continue-on-error: false
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build web app
run: npm run build
# env:
# VITE_BASE_PATH: './'
- name: Verify and fix web build
shell: bash
run: |
echo "Checking if dist directory exists and contains files:"
if [ -d "dist" ] && [ -f "dist/index.html" ]; then
echo "✓ dist directory and index.html found"
echo "Contents of dist directory:"
ls -la dist/ | head -10
# 检查 index.html 中的路径并修复
echo "Checking and fixing asset paths in index.html..."
if [ -f "dist/index.html" ]; then
# 确保所有资源路径都是相对路径
sed -i.bak 's|href="/|href="./|g' dist/index.html
sed -i.bak 's|src="/|src="./|g' dist/index.html
sed -i.bak 's|href="\([^"]*\)"|href="./\1"|g' dist/index.html
sed -i.bak 's|src="\([^"]*\)"|src="./\1"|g' dist/index.html
# 移除备份文件
rm -f dist/index.html.bak
echo "✓ Asset paths fixed in index.html"
fi
else
echo "Creating fallback dist directory and index.html"
mkdir -p dist
cp templates/fallback-index.html dist/index.html
echo "✓ Fallback index.html created from template"
fi
# 显示最终的 index.html 内容(前几行)
echo "Final index.html content (first 10 lines):"
head -10 dist/index.html || echo "Could not read index.html"
- name: Install sharp for icon generation
run: npm install sharp --save-dev
- name: Create build directory
shell: bash
run: |
node -e "
const fs = require('fs');
if (!fs.existsSync('build')) {
fs.mkdirSync('build', { recursive: true });
}
console.log('Build directory created');
"
- name: Generate icons and app resources
shell: bash
run: |
node -e "
const fs = require('fs');
const path = require('path');
function generateIcons() {
const buildDir = 'build';
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}
// Look for source icon in common locations (优先使用 assets/icon.png)
let sourceIcon = null;
const possiblePaths = [
'assets/icon.png',
'public/icon.png',
'src/assets/icon.png',
'icon.png'
];
for (const iconPath of possiblePaths) {
if (fs.existsSync(iconPath)) {
sourceIcon = iconPath;
break;
}
}
if (sourceIcon) {
console.log('Using source icon:', sourceIcon);
fs.copyFileSync(sourceIcon, 'build/icon.png');
fs.copyFileSync(sourceIcon, 'build/icon-512x512.png');
} else {
console.log('Creating default application icon');
// Create a better default icon
const iconSvg = \`<svg width=\"512\" height=\"512\" xmlns=\"http://www.w3.org/2000/svg\">
<defs>
<linearGradient id=\"grad1\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">
<stop offset=\"0%\" style=\"stop-color:#667eea;stop-opacity:1\" />
<stop offset=\"100%\" style=\"stop-color:#764ba2;stop-opacity:1\" />
</linearGradient>
</defs>
<rect width=\"512\" height=\"512\" fill=\"url(#grad1)\" rx=\"64\"/>
<text x=\"256\" y=\"280\" text-anchor=\"middle\" fill=\"white\" font-size=\"120\" font-family=\"Arial, sans-serif\" font-weight=\"bold\">⭐</text>
<text x=\"256\" y=\"380\" text-anchor=\"middle\" fill=\"white\" font-size=\"32\" font-family=\"Arial, sans-serif\">GitHub</text>
<text x=\"256\" y=\"420\" text-anchor=\"middle\" fill=\"white\" font-size=\"32\" font-family=\"Arial, sans-serif\">Stars</text>
</svg>\`;
fs.writeFileSync('build/icon.svg', iconSvg);
}
console.log('Icon files prepared successfully');
}
generateIcons();
"
- name: Generate Windows ICO file
if: matrix.os == 'windows-latest'
shell: bash
run: |
# For Windows, electron-builder can handle PNG to ICO conversion
if [ -f "build/icon.png" ]; then
cp build/icon.png build/icon.ico
else
echo "No icon file found, electron-builder will use default"
fi
- name: Generate macOS ICNS file
if: matrix.os == 'macos-latest'
shell: bash
run: |
# For macOS, electron-builder can handle PNG to ICNS conversion
if [ -f "build/icon.png" ]; then
cp build/icon.png build/icon.icns
else
echo "No icon file found, electron-builder will use default"
fi
- name: Install system dependencies (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libnss3-dev libatk-bridge2.0-dev libdrm2 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libxss1 libasound2-dev
- name: Install Electron dependencies
run: npm install --save-dev electron electron-builder
- name: Setup Windows build environment
if: matrix.os == 'windows-latest'
run: |
# Install Windows SDK components if needed
echo "Setting up Windows build environment"
- name: Create Electron main process
shell: bash
run: |
node -e "
const fs = require('fs');
const path = require('path');
if (!fs.existsSync('electron')) {
fs.mkdirSync('electron', { recursive: true });
}
const mainJsContent = 'const { app, BrowserWindow, Menu, shell, globalShortcut, ipcMain } = require(\\'electron\\');\\n' +
'const path = require(\\'path\\');\\n' +
'const fs = require(\\'fs\\');\\n' +
'const isDev = process.env.NODE_ENV === \\'development\\';\\n\\n' +
'let mainWindow;\\n\\n' +
'function createWindow() {\\n' +
' mainWindow = new BrowserWindow({\\n' +
' width: 1200,\\n' +
' height: 800,\\n' +
' minWidth: 800,\\n' +
' minHeight: 600,\\n' +
' webPreferences: {\\n' +
' nodeIntegration: false,\\n' +
' contextIsolation: true,\\n' +
' enableRemoteModule: false,\\n' +
' webSecurity: false,\\n' +
' allowRunningInsecureContent: true,\\n' +
' devTools: true, // 生产环境也允许 DevTools 便于排障\\n' +
' preload: path.join(__dirname, \\'preload.js\\')\\n' +
' },\\n' +
' icon: path.join(__dirname, \\'../build/icon.png\\'),\\n' +
' titleBarStyle: \\'default\\', // 使用默认标题栏,避免重叠问题\\n' +
' show: false,\\n' +
' autoHideMenuBar: false, // 显示菜单栏,确保编辑快捷键行为一致\\n' +
' frame: true, // 保持窗口框架\\n' +
' backgroundColor: \\'#ffffff\\', // 设置背景色,避免白屏闪烁\\n' +
' titleBarOverlay: false, // 禁用标题栏覆盖\\n' +
' trafficLightPosition: { x: 20, y: 20 } // macOS 交通灯按钮位置\\n' +
' });\\n\\n' +
' // 添加错误处理和加载事件\\n' +
' mainWindow.webContents.on(\\'did-fail-load\\', (event, errorCode, errorDescription, validatedURL) => {\\n' +
' console.error(\\'Failed to load:\\', errorCode, errorDescription, validatedURL);\\n' +
' // 如果主页面加载失败,尝试加载 fallback 页面\\n' +
' const fallbackPath = path.join(__dirname, \\'../dist/index.html\\');\\n' +
' if (fs.existsSync(fallbackPath)) {\\n' +
' console.log(\\'Loading fallback page:\\', fallbackPath);\\n' +
' mainWindow.loadFile(fallbackPath);\\n' +
' }\\n' +
' });\\n\\n' +
' mainWindow.webContents.on(\\'dom-ready\\', () => {\\n' +
' if (isDev) console.log(\\'DOM ready\\');\\n' +
' // 注入一些基础样式,防止白屏\\n' +
' mainWindow.webContents.insertCSS(\\'body { background-color: #ffffff; }\\');\\n' +
' });\\n\\n' +
' mainWindow.webContents.on(\\'did-finish-load\\', () => {\\n' +
' if (isDev) console.log(\\'Page finished loading\\');\\n' +
' // 页面加载完成后显示窗口\\n' +
' if (!mainWindow.isVisible()) {\\n' +
' mainWindow.show();\\n' +
' }\\n' +
' });\\n\\n' +
' if (isDev) {\\n' +
' mainWindow.loadURL(\\'http://localhost:5173\\');\\n' +
' mainWindow.webContents.openDevTools();\\n' +
' } else {\\n' +
' // 生产环境:尝试多个可能的路径\\n' +
' const possiblePaths = [\\n' +
' path.join(__dirname, \\'../dist/index.html\\'),\\n' +
' path.join(process.resourcesPath, \\'app.asar/dist/index.html\\'),\\n' +
' path.join(process.resourcesPath, \\'app/dist/index.html\\'),\\n' +
' path.join(process.resourcesPath, \\'dist/index.html\\'),\\n' +
' path.join(__dirname, \\'../build/index.html\\')\\n' +
' ];\\n\\n' +
' let indexPath = null;\\n' +
' for (const testPath of possiblePaths) {\\n' +
' try {\\n' +
' if (fs.existsSync(testPath)) {\\n' +
' indexPath = testPath;\\n' +
' break;\\n' +
' }\\n' +
' } catch (error) {\\n' +
' // 忽略文件系统错误,继续尝试下一个路径\\n' +
' continue;\\n' +
' }\\n' +
' }\\n\\n' +
' if (indexPath) {\\n' +
' console.log(\\'Loading application from:\\', indexPath);\\n' +
' mainWindow.loadFile(indexPath).catch(error => {\\n' +
' console.error(\\'Failed to load file:\\', error);\\n' +
' // 加载失败时显示错误页面\\n' +
' mainWindow.loadURL(\\'data:text/html,<h1>Application Load Error</h1><p>Could not load the main application. Please restart the app.</p>\\');\\n' +
' });\\n' +
' } else {\\n' +
' console.error(\\'Could not find index.html in any expected location\\');\\n' +
' console.log(\\'Checked paths:\\', possiblePaths);\\n' +
' console.log(\\'Current directory:\\', __dirname);\\n' +
' console.log(\\'Process resources path:\\', process.resourcesPath);\\n' +
' // 显示详细的错误信息\\n' +
' const errorHtml = \\'<h1>Application Not Found</h1><p>Could not locate the application files.</p><p>Please reinstall the application.</p>\\';\\n' +
' mainWindow.loadURL(\\'data:text/html,\\' + encodeURIComponent(errorHtml));\\n' +
' }\\n' +
' }\\n\\n' +
' mainWindow.once(\\'ready-to-show\\', () => {\\n' +
' mainWindow.show();\\n' +
' });\\n\\n' +
' // 提供稳定的菜单与编辑快捷键(生产环境)\n' +
' const menuTemplate = process.platform === \'darwin\' ? [\n' +
' {\n' +
' label: app.name,\n' +
' submenu: [\n' +
' { role: \'about\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'services\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'hide\' },\n' +
' { role: \'hideOthers\' },\n' +
' { role: \'unhide\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'quit\' }\n' +
' ]\n' +
' },\n' +
' {\n' +
' label: \'Edit\',\n' +
' submenu: [\n' +
' { role: \'undo\' },\n' +
' { role: \'redo\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'cut\' },\n' +
' { role: \'copy\' },\n' +
' { role: \'paste\' },\n' +
' { role: \'selectAll\' }\n' +
' ]\n' +
' },\n' +
' {\n' +
' label: \'View\',\n' +
' submenu: [\n' +
' { role: \'reload\' },\n' +
' { role: \'forceReload\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'toggleDevTools\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'resetZoom\' },\n' +
' { role: \'zoomIn\' },\n' +
' { role: \'zoomOut\' },\n' +
' { role: \'togglefullscreen\' }\n' +
' ]\n' +
' },\n' +
' {\n' +
' label: \'Window\',\n' +
' submenu: [\n' +
' { role: \'minimize\' },\n' +
' { role: \'close\' }\n' +
' ]\n' +
' }\n' +
' ] : [\n' +
' {\n' +
' label: \'Edit\',\n' +
' submenu: [\n' +
' { role: \'undo\' },\n' +
' { role: \'redo\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'cut\' },\n' +
' { role: \'copy\' },\n' +
' { role: \'paste\' },\n' +
' { role: \'selectAll\' }\n' +
' ]\n' +
' },\n' +
' {\n' +
' label: \'View\',\n' +
' submenu: [\n' +
' { role: \'reload\' },\n' +
' { role: \'forceReload\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'toggleDevTools\' },\n' +
' { type: \'separator\' },\n' +
' { role: \'resetZoom\' },\n' +
' { role: \'zoomIn\' },\n' +
' { role: \'zoomOut\' },\n' +
' { role: \'togglefullscreen\' }\n' +
' ]\n' +
' },\n' +
' {\n' +
' label: \'Window\',\n' +
' submenu: [\n' +
' { role: \'minimize\' },\n' +
' { role: \'close\' }\n' +
' ]\n' +
' }\n' +
' ];\n' +
' Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));\n' +
'\n' +
' mainWindow.webContents.setWindowOpenHandler(({ url }) => {\\n' +
' shell.openExternal(url);\\n' +
' return { action: \\'deny\\' };\\n' +
' });\\n\\n' +
' mainWindow.on(\\'closed\\', () => {\\n' +
' mainWindow = null;\\n' +
' });\\n' +
'}\\n\\n' +
'const PROXY_CONFIG_PATH = path.join(app.getPath(\\'userData\\'), \\'proxy-config.json\\');\\n\\n' +
'function loadProxyConfig() {\\n' +
' try {\\n' +
' if (fs.existsSync(PROXY_CONFIG_PATH)) {\\n' +
' return JSON.parse(fs.readFileSync(PROXY_CONFIG_PATH, \\'utf-8\\'));\\n' +
' }\\n' +
' } catch (e) { console.error(\\'Failed to load proxy config:\\', e); }\\n' +
' return { enabled: false, type: \\'http\\', host: \\'\\', port: 7890 };\\n' +
'}\\n\\n' +
'function saveProxyConfig(config) {\\n' +
' fs.writeFileSync(PROXY_CONFIG_PATH, JSON.stringify(config, null, 2));\\n' +
'}\\n\\n' +
'async function applyProxy(config) {\\n' +
' if (!mainWindow || mainWindow.isDestroyed()) return;\\n' +
' if (config.enabled && config.host && config.port) {\\n' +
' let auth = \\'\\';\\n' +
' if (config.username) {\\n' +
' auth = config.password\\n' +
' ? encodeURIComponent(config.username) + \\':\\' + encodeURIComponent(config.password) + \\'@\\'\\n' +
' : encodeURIComponent(config.username) + \\'@\\';\\n' +
' }\\n' +
' const proxyUrl = config.type === \\'socks5\\'\\n' +
' ? \\'socks5://\\' + auth + config.host + \\':\\' + config.port\\n' +
' : \\'http://\\' + auth + config.host + \\':\\' + config.port;\\n' +
' await mainWindow.webContents.session.setProxy({\\n' +
' proxyRules: proxyUrl,\\n' +
' proxyBypassRules: \\'<local>;localhost;127.0.0.1\\'\\n' +
' });\\n' +
' console.log(\\'[Proxy] Applied:\\', proxyUrl);\\n' +
' } else {\\n' +
' await mainWindow.webContents.session.setProxy({ proxyRules: \\'direct://\\' });\\n' +
' console.log(\\'[Proxy] Disabled, using direct connection\\');\\n' +
' }\\n' +
'}\\n\\n' +
'ipcMain.handle(\\'set-proxy\\', async (event, config) => {\\n' +
' saveProxyConfig(config);\\n' +
' await applyProxy(config);\\n' +
' return { success: true };\\n' +
'});\\n\\n' +
'ipcMain.handle(\\'get-proxy\\', () => {\\n' +
' return loadProxyConfig();\\n' +
'});\\n\\n' +
'ipcMain.handle(\\'test-proxy\\', async (event, config) => {\\n' +
' const net = require(\\'net\\');\\n' +
' const connectToProxy = () => new Promise((resolve, reject) => {\\n' +
' const socket = new net.Socket();\\n' +
' socket.setTimeout(5000);\\n' +
' socket.on(\\'connect\\', () => resolve(socket));\\n' +
' socket.on(\\'timeout\\', () => { socket.destroy(); reject(new Error(\\'Connection timeout\\')); });\\n' +
' socket.on(\\'error\\', (err) => reject(err));\\n' +
' socket.connect(config.port, config.host);\\n' +
' });\\n' +
' try {\\n' +
' if (config.type === \\'socks5\\') {\\n' +
' const socket = await connectToProxy();\\n' +
' return await new Promise((resolve) => {\\n' +
' const greeting = config.username\\n' +
' ? Buffer.from([0x05, 0x02, 0x00, 0x02])\\n' +
' : Buffer.from([0x05, 0x01, 0x00]);\\n' +
' socket.setTimeout(5000);\\n' +
' socket.write(greeting);\\n' +
' let step = 0;\\n' +
' socket.on(\\'data\\', (data) => {\\n' +
' if (step === 0) {\\n' +
' if (data[0] !== 0x05) { socket.destroy(); resolve({ success: false, error: \\'Invalid SOCKS5 version\\' }); return; }\\n' +
' if (data[1] === 0xFF) { socket.destroy(); resolve({ success: false, error: \\'No acceptable auth method\\' }); return; }\\n' +
' if (data[1] === 0x02 && config.username && config.password) {\\n' +
' step = 1;\\n' +
' const userBuf = Buffer.from(config.username, \\'utf8\\');\\n' +
' const passBuf = Buffer.from(config.password, \\'utf8\\');\\n' +
' const authReq = Buffer.alloc(3 + userBuf.length + passBuf.length);\\n' +
' authReq[0] = 0x01; authReq[1] = userBuf.length;\\n' +
' userBuf.copy(authReq, 2);\\n' +
' authReq[2 + userBuf.length] = passBuf.length;\\n' +
' passBuf.copy(authReq, 3 + userBuf.length);\\n' +
' socket.write(authReq);\\n' +
' } else { socket.destroy(); resolve({ success: true }); }\\n' +
' } else if (step === 1) {\\n' +
' socket.destroy();\\n' +
' resolve(data[0] === 0x01 && data[1] === 0x00\\n' +
' ? { success: true }\\n' +
' : { success: false, error: \\'SOCKS5 authentication failed\\' });\\n' +
' }\\n' +
' });\\n' +
' socket.on(\\'timeout\\', () => { socket.destroy(); resolve({ success: false, error: \\'SOCKS5 handshake timeout\\' }); });\\n' +
' socket.on(\\'error\\', (err) => resolve({ success: false, error: err.message }));\\n' +
' });\\n' +
' } else {\\n' +
' const socket = await connectToProxy();\\n' +
' return await new Promise((resolve) => {\\n' +
' socket.setTimeout(5000);\\n' +
' const authHeader = config.username && config.password\\n' +
' ? \\'Proxy-Authorization: Basic \\' + Buffer.from(config.username + \\':\\' + config.password).toString(\\'base64\\') + \\'\\\\r\\\\n\\'\\n' +
' : \\'\\';\\n' +
' socket.write(\\'CONNECT httpbin.org:443 HTTP/1.1\\\\r\\\\nHost: httpbin.org:443\\\\r\\\\n\\' + authHeader + \\'\\\\r\\\\n\\');\\n' +
' let responseData = \\'\\';\\n' +
' socket.on(\\'data\\', (data) => {\\n' +
' responseData += data.toString();\\n' +
' if (responseData.includes(\\'\\\\r\\\\n\\\\r\\\\n\\')) {\\n' +
' socket.destroy();\\n' +
' if (responseData.includes(\\'200\\')) resolve({ success: true });\\n' +
' else if (responseData.includes(\\'407\\')) resolve({ success: false, error: \\'Proxy authentication required\\' });\\n' +
' else resolve({ success: false, error: \\'Proxy rejected: \\' + (responseData.split(\\'\\\\r\\\\n\\')[0] || \\'Unknown\\') });\\n' +
' }\\n' +
' });\\n' +
' socket.on(\\'timeout\\', () => { socket.destroy(); resolve({ success: false, error: \\'HTTP proxy handshake timeout\\' }); });\\n' +
' socket.on(\\'error\\', (err) => resolve({ success: false, error: err.message }));\\n' +
' });\\n' +
' }\\n' +
' } catch (e) { return { success: false, error: e.message }; }\\n' +
'});\\n\\n' +
'app.whenReady().then(() => {\\n' +
' createWindow();\\n' +
' const savedProxy = loadProxyConfig();\\n' +
' if (savedProxy.enabled && savedProxy.host && savedProxy.port) {\\n' +
' applyProxy(savedProxy);\\n' +
' }\\n' +
' globalShortcut.register(\\'CommandOrControl+Shift+I\\', () => {\\n' +
' const focused = BrowserWindow.getFocusedWindow();\\n' +
' if (focused && !focused.isDestroyed()) {\\n' +
' focused.webContents.toggleDevTools();\\n' +
' }\\n' +
' });\\n' +
'});\\n\\n' +
'app.on(\\'window-all-closed\\', () => {\\n' +
' if (process.platform !== \\'darwin\\') {\\n' +
' app.quit();\\n' +
' }\\n' +
'});\\n\\n' +
'app.on(\\'will-quit\\', () => {\\n' +
' globalShortcut.unregisterAll();\\n' +
'});\\n\\n' +
'app.on(\\'activate\\', () => {\\n' +
' if (BrowserWindow.getAllWindows().length === 0) {\\n' +
' createWindow();\\n' +
' }\\n' +
'});';
fs.writeFileSync('electron/main.js', mainJsContent);
const preloadJsContent = 'const { contextBridge, ipcRenderer } = require(\\'electron\\');\\n' +
'\\n' +
'contextBridge.exposeInMainWorld(\\'electronAPI\\', {\\n' +
' setProxy: (config) => ipcRenderer.invoke(\\'set-proxy\\', config),\\n' +
' getProxy: () => ipcRenderer.invoke(\\'get-proxy\\'),\\n' +
' testProxy: (config) => ipcRenderer.invoke(\\'test-proxy\\', config),\\n' +
'});\\n';
fs.writeFileSync('electron/preload.js', preloadJsContent);
const electronPackageJson = {
name: 'github-stars-manager-desktop',
version: '1.0.0',
description: 'GitHub Stars Manager Desktop App',
main: 'main.js',
author: 'GitHub Stars Manager',
license: 'MIT'
};
fs.writeFileSync('electron/package.json', JSON.stringify(electronPackageJson, null, 2));
console.log('Electron files created successfully');
"
- name: Update main package.json for Electron
shell: bash
run: |
node -e "
const fs = require('fs');
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
packageJson.main = 'electron/main.js';
packageJson.homepage = './';
packageJson.scripts = packageJson.scripts || {};
// 确保构建脚本使用正确的基础路径
packageJson.scripts.build = 'vite build --base=./';
packageJson.scripts['build:electron'] = 'vite build --base=./ && electron-builder';
packageJson.scripts.dist = 'electron-builder --publish=never';
// Ensure proper base path for Electron
if (!packageJson.build) packageJson.build = {};
packageJson.build.extraMetadata = {
main: 'electron/main.js'
};
packageJson.scripts.electron = 'electron .';
packageJson.scripts['electron-dev'] = 'NODE_ENV=development electron .';
packageJson.scripts.dist = 'electron-builder';
packageJson.build = {
appId: 'com.github-stars-manager.app',
productName: 'GitHub Stars Manager',
directories: {
output: 'release',
buildResources: 'build'
},
files: [
'dist/**/*',
'build/**/*',
'electron/**/*',
'node_modules/**/*',
'package.json',
'!node_modules/.cache/**/*',
'!**/*.map'
],
extraResources: [
{
from: 'dist',
to: 'dist',
filter: ['**/*']
}
],
compression: 'normal',
asar: false, // 暂时禁用 ASAR 以简化调试
publish: null // 确保不会尝试发布
};
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
console.log('Package.json updated successfully');
"
- name: Configure platform-specific build settings
shell: bash
run: |
node -e "
const fs = require('fs');
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
if ('${{ matrix.os }}' === 'windows-latest') {
packageJson.build.win = {
target: [
{
target: 'nsis',
arch: ['x64']
}
],
icon: 'build/icon.png',
requestedExecutionLevel: 'asInvoker'
};
packageJson.build.nsis = {
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: 'GitHub Stars Manager'
};
} else if ('${{ matrix.os }}' === 'macos-latest') {
packageJson.build.mac = {
target: [
{
target: 'dmg',
arch: ['x64', 'arm64']
}
],
icon: 'build/icon.png',
category: 'public.app-category.productivity',
gatekeeperAssess: false,
};
packageJson.build.dmg = {
title: 'GitHub Stars Manager',
icon: 'build/icon.png'
};
} else {
packageJson.build.linux = {
target: 'AppImage',
icon: 'build/icon-512x512.png',
category: 'Office'
};
}
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
console.log('Platform-specific settings configured');
"
- name: Debug before build
shell: bash
run: |
echo "=== Pre-Build Debug Information ==="
echo "Current directory contents:"
ls -la
echo ""
echo "Dist directory contents:"
ls -la dist/ || echo "No dist directory"
echo ""
echo "Electron directory contents:"
ls -la electron/ || echo "No electron directory"
echo ""
echo "Build directory contents:"
ls -la build/ || echo "No build directory"
echo ""
echo "Package.json build config:"
node -e "console.log(JSON.stringify(require('./package.json').build, null, 2))"
echo ""
echo "Checking dist/index.html content:"
if [ -f "dist/index.html" ]; then
echo "First 20 lines of dist/index.html:"
head -20 dist/index.html
else
echo "dist/index.html not found"
fi
echo ""
echo "Setting proper permissions:"
chmod -R 755 dist/ || echo "Could not set dist permissions"
chmod -R 755 electron/ || echo "Could not set electron permissions"
chmod -R 755 build/ || echo "Could not set build permissions"
- name: Configure macOS signing
if: matrix.os == 'macos-latest'
shell: bash
run: |
# Only set CSC_LINK when a signing certificate is actually configured.
# An empty CSC_LINK causes electron-builder to treat CWD as a cert
# file and fail with "not a file".
if [ -n "$CSC_LINK_SECRET" ]; then
{
echo "CSC_LINK<<__EOF__"
echo "$CSC_LINK_SECRET"
echo "__EOF__"
} >> "$GITHUB_ENV"
if [ -n "$CSC_KEY_PASSWORD_SECRET" ]; then
{
echo "CSC_KEY_PASSWORD<<__EOF__"
echo "$CSC_KEY_PASSWORD_SECRET"
echo "__EOF__"
} >> "$GITHUB_ENV"
fi
echo "Signing certificate configured"
else
echo "No signing certificate — electron-builder will ad-hoc sign"
fi
env:
CSC_LINK_SECRET: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD_SECRET: ${{ secrets.CSC_KEY_PASSWORD }}
- name: Build Electron app
run: npm run dist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CI: true
DEBUG: electron-builder
# Linux 特定环境变量
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
- name: List build output
shell: bash
run: |
echo "Build output directory contents:"
ls -la release/ || echo "Release directory not found"
echo ""
echo "Looking for build artifacts:"
find . -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.AppImage" || echo "No build artifacts found"
echo ""
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
echo "Linux specific checks:"
echo "AppImage files:"
find . -name "*.AppImage" -exec ls -la {} \; || echo "No AppImage files found"
echo "Checking AppImage permissions:"
find . -name "*.AppImage" -exec file {} \; || echo "No AppImage files to check"
fi
- name: Test Electron app (Linux only)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
# Install xvfb for headless testing
sudo apt-get update
sudo apt-get install -y xvfb
# Test if the app can start (will exit quickly but should not crash)
echo "Testing Electron app startup..."
timeout 10s xvfb-run -a npm run electron || echo "App test completed (timeout expected)"
- name: Upload artifacts (Windows)
if: matrix.os == 'windows-latest' && success()
uses: actions/upload-artifact@v7
with:
name: windows-app
path: |
release/*.exe
release/*.msi
if-no-files-found: ignore
- name: Upload artifacts (macOS)
if: matrix.os == 'macos-latest' && success()
uses: actions/upload-artifact@v7
with:
name: macos-app
path: release/*.dmg
if-no-files-found: ignore
- name: Upload artifacts (Linux)
if: matrix.os == 'ubuntu-latest' && success()
uses: actions/upload-artifact@v7
with:
name: linux-app
path: release/*.AppImage
if-no-files-found: ignore
release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') && always()
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v8
continue-on-error: true
- name: List downloaded files
shell: bash
run: |
echo "Downloaded files structure:"
find . -type f | head -20
echo "Looking for build artifacts:"
find . -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.AppImage" | head -20
- name: Prepare release files
shell: bash
run: |
mkdir -p release-files
# Copy all found artifacts to a single directory
find . -name "*.exe" -exec cp {} release-files/ \; 2>/dev/null || true
find . -name "*.msi" -exec cp {} release-files/ \; 2>/dev/null || true
find . -name "*.dmg" -exec cp {} release-files/ \; 2>/dev/null || true
find . -name "*.AppImage" -exec cp {} release-files/ \; 2>/dev/null || true
echo "Files prepared for release:"
ls -la release-files/ || echo "No files found"
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: release-files/*
draft: false
prerelease: false
generate_release_notes: true
fail_on_unmatched_files: false
body: |
## Desktop Application Release
This release includes desktop applications for multiple platforms.
### Available Downloads:
- Windows: `.exe` installer
- macOS: `.dmg` installer
- Linux: `.AppImage` portable executable
Note: Some platform builds may not be available if they failed during the build process.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}