This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: Verify 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 | ||
| else | ||
| echo "Creating fallback dist directory and index.html" | ||
| mkdir -p dist | ||
| cat > dist/index.html << 'HTML_END' | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>GitHub Stars Manager</title> | ||
| <style> | ||
| body { | ||
| font-family: Arial, sans-serif; | ||
| margin: 0; | ||
| padding: 20px; | ||
| background: #f5f5f5; | ||
| } | ||
| .container { | ||
| max-width: 800px; | ||
| margin: 0 auto; | ||
| background: white; | ||
| padding: 20px; | ||
| border-radius: 8px; | ||
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | ||
| } | ||
| h1 { color: #333; } | ||
| .status { color: #666; margin-top: 20px; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="container"> | ||
| <h1>GitHub Stars Manager</h1> | ||
| <p>Welcome to GitHub Stars Manager Desktop Application!</p> | ||
| <div class="status"> | ||
| <p>This is a fallback page. The main application should load here.</p> | ||
| <p>Application built successfully with GitHub Actions.</p> | ||
| </div> | ||
| </div> | ||
| <script> | ||
| console.log('GitHub Stars Manager loaded successfully'); | ||
| document.addEventListener('DOMContentLoaded', function() { | ||
| console.log('DOM ready'); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| HTML_END | ||
| echo "✓ Fallback index.html created" | ||
| fi | ||
| - 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 path = require('path'); | ||
| 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('Using source icon:', sourceIcon); | ||
| fs.copyFileSync(sourceIcon, 'build/icon.png'); | ||
| fs.copyFileSync(sourceIcon, 'build/icon-512x512.png'); | ||
| } else { | ||
| console.log('No source icon found, will use electron-builder default'); | ||
| // Create a simple placeholder | ||
| const placeholderSvg = '<svg width=\"512\" height=\"512\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"512\" height=\"512\" fill=\"#3b82f6\"/><text x=\"256\" y=\"256\" text-anchor=\"middle\" dy=\".3em\" fill=\"white\" font-size=\"48\" font-family=\"Arial\">APP</text></svg>'; | ||
| fs.writeFileSync('build/icon.svg', placeholderSvg); | ||
| } | ||
| 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 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' + | ||
| ' },\\n' + | ||
| ' icon: path.join(__dirname, \\'../build/icon.png\\'),\\n' + | ||
| ' titleBarStyle: process.platform === \\'darwin\\' ? \\'hiddenInset\\' : \\'default\\',\\n' + | ||
| ' show: false\\n' + | ||
| ' });\\n\\n' + | ||
| ' // Add error handling\\n' + | ||
| ' mainWindow.webContents.on(\\'did-fail-load\\', (event, errorCode, errorDescription) => {\\n' + | ||
| ' console.error(\\'Failed to load:\\', errorCode, errorDescription);\\n' + | ||
| ' });\\n\\n' + | ||
| ' mainWindow.webContents.on(\\'dom-ready\\', () => {\\n' + | ||
| ' console.log(\\'DOM ready\\');\\n' + | ||
| ' });\\n\\n' + | ||
| ' if (isDev) {\\n' + | ||
| ' mainWindow.loadURL(\\'http://localhost:5173\\');\\n' + | ||
| ' mainWindow.webContents.openDevTools();\\n' + | ||
| ' } else {\\n' + | ||
| ' // Try multiple possible paths for the built app\\n' + | ||
| ' const possiblePaths = [\\n' + | ||
| ' path.join(__dirname, \\'../dist/index.html\\'),\\n' + | ||
| ' path.join(__dirname, \\'../build/index.html\\'),\\n' + | ||
| ' path.join(process.resourcesPath, \\'app/dist/index.html\\'),\\n' + | ||
| ' path.join(process.resourcesPath, \\'dist/index.html\\')\\n' + | ||
| ' ];\\n\\n' + | ||
| ' let indexPath = null;\\n' + | ||
| ' for (const testPath of possiblePaths) {\\n' + | ||
| ' if (fs.existsSync(testPath)) {\\n' + | ||
| ' indexPath = testPath;\\n' + | ||
| ' break;\\n' + | ||
| ' }\\n' + | ||
| ' }\\n\\n' + | ||
| ' if (indexPath) {\\n' + | ||
| ' console.log(\\'Loading from:\\', indexPath);\\n' + | ||
| ' mainWindow.loadFile(indexPath);\\n' + | ||
| ' } else {\\n' + | ||
| ' console.error(\\'Could not find index.html in any expected location\\');\\n' + | ||
| ' console.log(\\'Checked paths:\\', possiblePaths);\\n' + | ||
| ' // Load a simple error page\\n' + | ||
| ' mainWindow.loadURL(\\'data:text/html,<h1>Error: Could not load application</h1><p>Please check the console for details.</p>\\');\\n' + | ||
| ' }\\n' + | ||
| ' // Open DevTools in production for debugging\\n' + | ||
| ' mainWindow.webContents.openDevTools();\\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 || {}; | ||
| // 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' | ||
| ], | ||
| extraResources: [ | ||
| { | ||
| from: 'dist', | ||
| to: 'dist', | ||
| filter: ['**/*'] | ||
| } | ||
| ] | ||
| }; | ||
| 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.png' | ||
| }; | ||
| packageJson.build.nsis = { | ||
| oneClick: false, | ||
| allowToChangeInstallationDirectory: true | ||
| }; | ||
| } else if ('${{ matrix.os }}' === 'macos-latest') { | ||
| packageJson.build.mac = { | ||
| target: 'dmg', | ||
| icon: 'build/icon.png', | ||
| 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: Debug before build | ||
| shell: bash | ||
| run: | | ||
| echo "=== Debug Information ===" | ||
| echo "Current directory contents:" | ||
| ls -la | ||
| echo "Dist directory contents:" | ||
| ls -la dist/ || echo "No dist directory" | ||
| echo "Electron directory contents:" | ||
| ls -la electron/ || echo "No electron directory" | ||
| echo "Package.json build config:" | ||
| node -e "console.log(JSON.stringify(require('./package.json').build, null, 2))" | ||
| - name: Build Electron app | ||
| run: npm run dist | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| CI: true | ||
| DEBUG: electron-builder | ||
| - name: List build output | ||
| shell: bash | ||
| 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: 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 }} | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||