Files
GithubStarsManager/.github/workflows/build-desktop.yml
AmintaCCCP e0af19dd2e Revert "fix workflow"
This reverts commit 0678fe9b04.
2025-09-23 16:37:52 +08:00

584 lines
22 KiB
YAML

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@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
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 } = 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: isDev // 只在开发模式下启用 DevTools\\n' +
' },\\n' +
' icon: path.join(__dirname, \\'../build/icon.png\\'),\\n' +
' titleBarStyle: \\'default\\', // 使用默认标题栏,避免重叠问题\\n' +
' show: false,\\n' +
' autoHideMenuBar: true, // 隐藏菜单栏\\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' +
' 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' +
'app.whenReady().then(createWindow);\\n\\n' +
'app.on(\\'window-all-closed\\', () => {\\n' +
' if (process.platform !== \\'darwin\\') {\\n' +
' app.quit();\\n' +
' }\\n' +
'});\\n\\n' +
'app.on(\\'activate\\', () => {\\n' +
' if (BrowserWindow.getAllWindows().length === 0) {\\n' +
' createWindow();\\n' +
' }\\n' +
'});';
fs.writeFileSync('electron/main.js', mainJsContent);
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',
hardenedRuntime: true,
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: 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
# macOS signing and notarization
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- 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@v4
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@v4
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@v4
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@v4
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 }}