Files
GithubStarsManager/.github/workflows/build-desktop.yml
AmintaCCCP 29e39265e7 update icon
2025-07-28 21:37:21 +08:00

417 lines
14 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
- 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
shell: bash
run: |
node -e "
const fs = require('fs');
const sharp = require('sharp');
const path = require('path');
async function generateIcons() {
const buildDir = 'build';
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}
// Look for source icon in common locations
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('No source icon found, creating default icon...');
// Create a simple colored square as default
await sharp({
create: {
width: 512,
height: 512,
channels: 4,
background: { r: 59, g: 130, b: 246, alpha: 1 }
}
})
.png()
.toFile('build/icon-512x512.png');
// Copy for other formats
fs.copyFileSync('build/icon-512x512.png', 'build/icon.png');
} else {
console.log('Using source icon:', sourceIcon);
// Generate PNG icons for Linux
await sharp(sourceIcon)
.resize(512, 512)
.png()
.toFile('build/icon-512x512.png');
await sharp(sourceIcon)
.resize(256, 256)
.png()
.toFile('build/icon-256x256.png');
// Copy original for general use
fs.copyFileSync(sourceIcon, 'build/icon.png');
}
console.log('Icon files generated successfully');
}
generateIcons().catch(console.error);
"
- name: Generate Windows ICO file
if: matrix.os == 'windows-latest'
shell: bash
run: |
# Install imagemagick for ICO conversion
if command -v magick >/dev/null 2>&1; then
magick build/icon.png -define icon:auto-resize=256,128,64,48,32,16 build/icon.ico
elif command -v convert >/dev/null 2>&1; then
convert build/icon.png -define icon:auto-resize=256,128,64,48,32,16 build/icon.ico
else
echo "ImageMagick not available, using PNG as fallback"
cp build/icon-512x512.png build/icon.ico
fi
- name: Generate macOS ICNS file
if: matrix.os == 'macos-latest'
shell: bash
run: |
# Create iconset directory
mkdir -p build/icon.iconset
# Generate different sizes for ICNS
node -e "
const sharp = require('sharp');
const fs = require('fs');
async function generateIconSet() {
const sizes = [16, 32, 64, 128, 256, 512, 1024];
const sourceIcon = fs.existsSync('build/icon.png') ? 'build/icon.png' : 'build/icon-512x512.png';
for (const size of sizes) {
await sharp(sourceIcon)
.resize(size, size)
.png()
.toFile(\`build/icon.iconset/icon_\${size}x\${size}.png\`);
if (size <= 512) {
await sharp(sourceIcon)
.resize(size * 2, size * 2)
.png()
.toFile(\`build/icon.iconset/icon_\${size}x\${size}@2x.png\`);
}
}
console.log('IconSet generated');
}
generateIconSet().catch(console.error);
"
# Convert to ICNS using iconutil (macOS only)
if command -v iconutil >/dev/null 2>&1; then
iconutil -c icns build/icon.iconset -o build/icon.icns
else
echo "iconutil not available, using PNG as fallback"
cp build/icon-512x512.png build/icon.icns
fi
- 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 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: true\\n' +
' },\\n' +
' icon: path.join(__dirname, \\'../public/icon.svg\\'),\\n' +
' titleBarStyle: process.platform === \\'darwin\\' ? \\'hiddenInset\\' : \\'default\\',\\n' +
' show: false\\n' +
' });\\n\\n' +
' if (isDev) {\\n' +
' mainWindow.loadURL(\\'http://localhost:5173\\');\\n' +
' mainWindow.webContents.openDevTools();\\n' +
' } else {\\n' +
' mainWindow.loadFile(path.join(__dirname, \\'../dist/index.html\\'));\\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.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'
},
files: [
'dist/**/*',
'electron/**/*',
'node_modules/**/*',
'package.json'
]
};
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: 'nsis',
icon: 'build/icon.ico'
};
packageJson.build.nsis = {
oneClick: false,
allowToChangeInstallationDirectory: true
};
} else if ('${{ matrix.os }}' === 'macos-latest') {
packageJson.build.mac = {
target: 'dmg',
icon: 'build/icon.icns',
category: 'public.app-category.productivity'
};
} 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: Build Electron app
run: npm run dist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CI: true
- name: List build output
run: |
echo "Build output directory contents:"
ls -la release/ || echo "Release directory not found"
find . -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.AppImage" || echo "No build artifacts found"
- 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
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
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 }}