fix(ci): add native module rebuild for Electron #35
Workflow file for this run
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
| # ============================================== | |
| # NoteCode Release Pipeline | |
| # ============================================== | |
| # Triggered by pushing a version tag (v*) | |
| # Publishes to: npm registry + GitHub Releases (Electron binaries) | |
| # | |
| # Usage: | |
| # git tag v1.0.0 && git push origin main --tags | |
| # | |
| # Pre-release: | |
| # git tag v1.0.0-beta.1 && git push origin main --tags | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write # Required for creating GitHub Releases | |
| env: | |
| NODE_VERSION: '20' | |
| jobs: | |
| # ============================================ | |
| # Job 1: Publish to npm registry | |
| # ============================================ | |
| npm-publish: | |
| name: Publish to npm | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Cache node_modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| node_modules | |
| backend/node_modules | |
| frontend/node_modules | |
| key: npm-publish-${{ runner.os }}-${{ hashFiles('package.json', 'backend/package.json', 'frontend/package.json') }} | |
| restore-keys: npm-publish-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: npm install --ignore-scripts | |
| # --ignore-scripts: skip electron rebuild (not needed for npm publish) | |
| - name: Build backend | |
| run: npm run build --prefix backend | |
| - name: Build frontend | |
| run: npm run build --prefix frontend | |
| - name: Publish to npm | |
| run: npm publish || echo "::warning::npm publish failed (version may already exist)" | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # ============================================ | |
| # Job 2: Build Electron (matrix: win/mac/linux) | |
| # ============================================ | |
| electron-build: | |
| name: Build Electron (${{ matrix.platform }}) | |
| timeout-minutes: 90 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| platform: win | |
| - os: macos-latest | |
| platform: mac | |
| - os: ubuntu-latest | |
| platform: linux | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| # Cache node_modules and Electron binaries per OS | |
| - name: Cache node_modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| node_modules | |
| backend/node_modules | |
| frontend/node_modules | |
| electron/node_modules | |
| key: electron-deps-v2-${{ runner.os }}-${{ hashFiles('package.json', 'backend/package.json', 'frontend/package.json', 'electron/package.json') }} | |
| # NO restore-keys fallback - ensure exact match only | |
| # Cache ONLY Electron binaries (not electron-builder cache - causes stale asar issues) | |
| - name: Cache Electron binaries | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/electron | |
| ~/AppData/Local/electron/Cache | |
| ~/Library/Caches/electron | |
| key: electron-bin-v2-${{ runner.os }}-${{ hashFiles('electron/package.json') }} | |
| # NO restore-keys fallback - ensure exact match only | |
| # Disable Windows Defender real-time scanning — causes massive I/O slowdown | |
| # when 7z processes thousands of small files in node_modules | |
| - name: Disable Windows Defender (Windows only) | |
| if: runner.os == 'Windows' | |
| run: Set-MpPreference -DisableRealtimeMonitoring $true | |
| shell: powershell | |
| - name: Install all dependencies | |
| run: npm install --ignore-scripts | |
| - name: Install backend dependencies | |
| run: npm install --prefix backend --ignore-scripts | |
| - name: Install frontend dependencies | |
| run: npm install --prefix frontend | |
| - name: Install electron dependencies | |
| run: npm install --prefix electron --ignore-scripts | |
| # CRITICAL: Rebuild native modules (better-sqlite3) for Electron runtime | |
| - name: Rebuild native modules for Electron | |
| run: cd electron && npx @electron/rebuild -f -m ../backend -o better-sqlite3 | |
| - name: Build backend | |
| run: cd backend && npm run build | |
| - name: Build frontend | |
| run: cd frontend && npm run build | |
| - name: Build electron | |
| run: cd electron && npm run build | |
| # CRITICAL: Strict verification - FAIL if unexpected files or bundling failed | |
| - name: Verify electron dist (STRICT) | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const distDir = 'electron/dist'; | |
| const files = fs.readdirSync(distDir); | |
| const expected = ['main.js', 'preload.js']; | |
| console.log('Files in dist:', files); | |
| // Check for unexpected files | |
| const unexpected = files.filter(f => !expected.includes(f)); | |
| if (unexpected.length > 0) { | |
| console.error('FATAL: Unexpected files in dist:', unexpected); | |
| console.error('This indicates tsc output mixed with esbuild output!'); | |
| process.exit(1); | |
| } | |
| // Verify main.js is bundled (should be > 500KB with electron-updater inlined) | |
| const mainSize = fs.statSync(path.join(distDir, 'main.js')).size; | |
| console.log('main.js size:', mainSize, 'bytes'); | |
| if (mainSize < 500000) { | |
| console.error('FATAL: main.js too small (' + mainSize + ' bytes)'); | |
| console.error('electron-updater may not be bundled!'); | |
| process.exit(1); | |
| } | |
| console.log('✓ Verification passed: only main.js and preload.js, bundled correctly'); | |
| " | |
| # Remove workspace symlinks that cause infinite traversal in 7z/electron-builder | |
| - name: Remove workspace symlinks | |
| run: | | |
| node -e "const fs=require('fs'); ['notecode','notecode-app'].forEach(p => { ['backend/node_modules/','electron/node_modules/'].forEach(d => { const t=d+p; try { fs.rmSync(t,{recursive:true,force:true}); console.log('Removed',t); } catch(e) { console.log('Skip',t,e.message); } }); });" | |
| # Clear electron-builder cache to prevent stale asar content | |
| - name: Clear electron-builder cache | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const os = require('os'); | |
| const path = require('path'); | |
| const caches = [ | |
| path.join(os.homedir(), '.cache', 'electron-builder'), | |
| path.join(os.homedir(), 'AppData', 'Local', 'electron-builder', 'Cache'), | |
| path.join(os.homedir(), 'Library', 'Caches', 'electron-builder'), | |
| path.join('..', 'release') | |
| ]; | |
| caches.forEach(c => { | |
| try { fs.rmSync(c, {recursive:true,force:true}); console.log('Cleared:', c); } | |
| catch(e) { console.log('Skip:', c); } | |
| }); | |
| " | |
| - name: Package Electron (${{ matrix.platform }}) | |
| run: cd electron && npx --no-install electron-builder --${{ matrix.platform }} --publish never | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DEBUG: electron-builder | |
| ELECTRON_BUILDER_COMPRESSION_LEVEL: 0 | |
| # Code signing (optional - add when certificates are available) | |
| # CSC_LINK: ${{ secrets.MAC_CERT_P12 }} | |
| # CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }} | |
| # WIN_CSC_LINK: ${{ secrets.WIN_CERT_PFX }} | |
| # WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }} | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: electron-${{ matrix.platform }} | |
| path: | | |
| release/*.exe | |
| release/*.dmg | |
| release/*.zip | |
| release/*.AppImage | |
| release/*.deb | |
| release/*.rpm | |
| release/*.yml | |
| release/*.yaml | |
| if-no-files-found: warn | |
| retention-days: 7 | |
| # ============================================ | |
| # Job 3: Create GitHub Release with artifacts | |
| # ============================================ | |
| create-release: | |
| name: Create GitHub Release | |
| needs: [npm-publish, electron-build] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Full history for changelog generation | |
| - name: Download all Electron artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| pattern: electron-* | |
| merge-multiple: true | |
| - name: List artifacts | |
| run: ls -la artifacts/ || echo "No artifacts found" | |
| - name: Generate changelog from commits | |
| id: changelog | |
| run: | | |
| # Find previous tag for changelog range | |
| PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") | |
| if [ -z "$PREV_TAG" ]; then | |
| CHANGELOG=$(git log --oneline --pretty=format:"- %s" HEAD) | |
| else | |
| CHANGELOG=$(git log --oneline --pretty=format:"- %s" ${PREV_TAG}..HEAD) | |
| fi | |
| # Write to output (multiline safe) | |
| echo "changelog<<EOF" >> $GITHUB_OUTPUT | |
| echo "$CHANGELOG" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: "NoteCode ${{ github.ref_name }}" | |
| body: | | |
| ## What's Changed | |
| ${{ steps.changelog.outputs.changelog }} | |
| ## Downloads | |
| | Platform | File | | |
| |----------|------| | |
| | Windows (installer) | `NoteCode-*-win-x64.exe` | | |
| | Windows (portable) | `NoteCode-*-win-x64-portable.exe` | | |
| | macOS (Intel) | `NoteCode-*-mac-x64.dmg` | | |
| | macOS (Apple Silicon) | `NoteCode-*-mac-arm64.dmg` | | |
| | Linux (AppImage) | `NoteCode-*-linux-x64.AppImage` | | |
| | Linux (deb) | `NoteCode-*-linux-x64.deb` | | |
| | Linux (rpm) | `NoteCode-*-linux-x64.rpm` | | |
| ## CLI (npm) | |
| ```bash | |
| npx notecode-app | |
| # or install globally | |
| npm install -g notecode-app | |
| ``` | |
| files: artifacts/* | |
| draft: false | |
| prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'rc') }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |