Files
OpenNoodl/dev-docs/tasks/phase-8-distribution/TASK-7.5-github-actions.md

15 KiB

Task 7.5: GitHub Actions CI/CD Pipeline

Problem Statement

Currently, building Nodegex for distribution requires:

  • Manual builds on each platform (macOS, Windows, Linux)
  • Access to a macOS machine for Apple Silicon builds
  • Manual code signing
  • Manual upload to distribution channels
  • No automated testing before release

Goals

  1. Automated Builds: Push tag → builds start automatically
  2. All Platforms: macOS (x64 + arm64), Windows (x64), Linux (x64)
  3. Code Signing: Automatic for all platforms
  4. GitHub Releases: Automatic creation with all artifacts
  5. Update Manifests: latest.yml, latest-mac.yml, latest-linux.yml generated

Architecture

┌──────────────────────────────────────────────────────────────────────────┐
│                        GitHub Actions Workflow                            │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  Tag Push (v1.2.0)                                                       │
│       │                                                                  │
│       ▼                                                                  │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                    Build Matrix                                  │    │
│  ├─────────────────┬─────────────────┬─────────────────────────────┤    │
│  │  macOS x64      │  macOS arm64    │  Windows x64  │  Linux x64  │    │
│  │  (macos-13)     │  (macos-14)     │  (windows)    │  (ubuntu)   │    │
│  │                 │                 │               │             │    │
│  │  • Build        │  • Build        │  • Build      │  • Build    │    │
│  │  • Sign         │  • Sign         │  • Sign*      │  • No sign  │    │
│  │  • Notarize     │  • Notarize     │               │             │    │
│  └─────────────────┴─────────────────┴───────────────┴─────────────┘    │
│       │                   │                 │               │            │
│       └───────────────────┴─────────────────┴───────────────┘            │
│                                    │                                      │
│                                    ▼                                      │
│                          GitHub Release                                   │
│                    • DMG (x64, arm64)                                    │
│                    • ZIP (x64, arm64)                                    │
│                    • EXE installer                                        │
│                    • AppImage                                             │
│                    • DEB                                                  │
│                    • latest*.yml manifests                               │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

Implementation

Phase 1: Create Workflow File

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  release:
    runs-on: ${{ matrix.os }}
    
    strategy:
      fail-fast: false
      matrix:
        include:
          # macOS Intel
          - os: macos-13
            platform: darwin
            arch: x64
            
          # macOS Apple Silicon
          - os: macos-14
            platform: darwin
            arch: arm64
            
          # Windows
          - os: windows-latest
            platform: win32
            arch: x64
            
          # Linux
          - os: ubuntu-22.04
            platform: linux
            arch: x64

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build packages
        run: npm run build --workspaces --if-present

      # macOS: Import certificate and configure signing
      - name: Import macOS signing certificate
        if: matrix.platform == 'darwin'
        env:
          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
        run: |
          # Create temporary keychain
          KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
          KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
          
          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          
          # Import certificate
          echo "$MACOS_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
          security import $RUNNER_TEMP/certificate.p12 \
            -k "$KEYCHAIN_PATH" \
            -P "$MACOS_CERTIFICATE_PASSWORD" \
            -T /usr/bin/codesign \
            -T /usr/bin/security
          
          # Allow codesign to access keychain
          security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
          security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain
          
          # Verify certificate
          security find-identity -v -p codesigning "$KEYCHAIN_PATH"

      # Windows: Import certificate (if signing enabled)
      - name: Import Windows signing certificate
        if: matrix.platform == 'win32' && secrets.WINDOWS_CERTIFICATE != ''
        env:
          WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
          WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
        run: |
          echo "$env:WINDOWS_CERTIFICATE" | Out-File -FilePath certificate.b64
          certutil -decode certificate.b64 certificate.pfx
          # Certificate will be used by electron-builder automatically
        shell: pwsh

      # Build Electron app
      - name: Build Electron app
        env:
          # macOS signing
          CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.MACOS_CERTIFICATE || '' }}
          CSC_KEY_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.MACOS_CERTIFICATE_PASSWORD || '' }}
          
          # macOS notarization
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          
          # Windows signing (optional)
          WIN_CSC_LINK: ${{ matrix.platform == 'win32' && secrets.WINDOWS_CERTIFICATE || '' }}
          WIN_CSC_KEY_PASSWORD: ${{ matrix.platform == 'win32' && secrets.WINDOWS_CERTIFICATE_PASSWORD || '' }}
          
        run: |
          cd packages/noodl-editor
          npx electron-builder --${{ matrix.platform == 'darwin' && 'mac' || matrix.platform == 'win32' && 'win' || 'linux' }} --${{ matrix.arch }} --publish always

      # Upload artifacts to GitHub Release
      - name: Upload artifacts
        uses: softprops/action-gh-release@v1
        with:
          files: |
            packages/noodl-editor/dist/*.dmg
            packages/noodl-editor/dist/*.zip
            packages/noodl-editor/dist/*.exe
            packages/noodl-editor/dist/*.AppImage
            packages/noodl-editor/dist/*.deb
            packages/noodl-editor/dist/*.yml
            packages/noodl-editor/dist/*.yaml
          draft: false
          prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}

Phase 2: Configure GitHub Secrets

Navigate to: Repository → Settings → Secrets and variables → Actions

Required Secrets:

Secret Description How to Get
MACOS_CERTIFICATE Base64-encoded .p12 file base64 -i certificate.p12
MACOS_CERTIFICATE_PASSWORD Password for .p12 Your certificate password
APPLE_ID Apple Developer email Your Apple ID
APPLE_APP_SPECIFIC_PASSWORD App-specific password appleid.apple.com → Security
APPLE_TEAM_ID Team ID Y35J975HXR (from certificate)

Optional Secrets (for Windows signing):

Secret Description
WINDOWS_CERTIFICATE Base64-encoded .pfx file
WINDOWS_CERTIFICATE_PASSWORD Certificate password

Phase 3: Export macOS Certificate for CI

# 1. Export from Keychain Access
#    - Open Keychain Access
#    - Find "Developer ID Application: Osborne Solutions"
#    - Right-click → Export
#    - Save as .p12 with strong password

# 2. Base64 encode
base64 -i "Developer ID Application.p12" | pbcopy
# Paste into MACOS_CERTIFICATE secret

# 3. Generate app-specific password
#    - Go to appleid.apple.com
#    - Sign In & Security → App-Specific Passwords
#    - Generate new password labeled "nodegex-ci"
#    - Copy to APPLE_APP_SPECIFIC_PASSWORD secret

Phase 4: Update package.json for CI

{
  "build": {
    "publish": {
      "provider": "github",
      "owner": "the-low-code-foundation",
      "repo": "opennoodl",
      "releaseType": "release"
    },
    
    "mac": {
      "target": [
        { "target": "dmg", "arch": ["x64", "arm64"] },
        { "target": "zip", "arch": ["x64", "arm64"] }
      ]
    },
    
    "win": {
      "target": [
        { "target": "nsis", "arch": ["x64"] }
      ]
    },
    
    "linux": {
      "target": [
        { "target": "AppImage", "arch": ["x64"] },
        { "target": "deb", "arch": ["x64"] }
      ]
    }
  }
}

Phase 5: Release Process

Step 5.1: Create release

# Update version in package.json
npm version patch  # or minor, major

# Push with tags
git push && git push --tags

Step 5.2: Monitor workflow

Go to: Repository → Actions → Release workflow

Each platform builds in parallel (~15-30 minutes total).

Step 5.3: Verify release

  1. Check GitHub Releases for all artifacts
  2. Download and test on each platform
  3. Verify auto-update works from previous version

Phase 6: Version Management

Semantic Versioning:

  • v1.0.0 → Stable release
  • v1.1.0-beta.1 → Beta release
  • v1.1.0-alpha.1 → Alpha release

Pre-release handling:

Tags containing beta or alpha automatically create pre-releases that don't trigger auto-update for stable users.

Phase 7: Update Manifests

electron-builder automatically generates:

# latest-mac.yml
version: 1.2.0
files:
  - url: Nodegex-1.2.0-arm64.dmg
    sha512: abc123...
    size: 150000000
  - url: Nodegex-1.2.0-x64.dmg
    sha512: def456...
    size: 155000000
path: Nodegex-1.2.0-arm64.dmg
sha512: abc123...
releaseDate: '2024-01-15T10:30:00.000Z'

These files tell electron-updater which version is latest and where to download.

Workflow Customizations

Manual Trigger

Add workflow_dispatch for manual runs:

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to build'
        required: true

Build on PR (Testing)

Create separate workflow for PR testing:

# .github/workflows/build-test.yml
name: Build Test

on:
  pull_request:
    paths:
      - 'packages/noodl-editor/**'
      - '.github/workflows/**'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build --workspaces
      - run: cd packages/noodl-editor && npx electron-builder --linux --dir

Caching

Speed up builds with dependency caching:

- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ~/.cache/electron
      ~/.cache/electron-builder
    key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}

Troubleshooting

macOS: "No identity found"

Certificate not imported correctly:

# Debug step
- name: Debug certificates
  if: matrix.platform == 'darwin'
  run: |
    security list-keychains
    security find-identity -v -p codesigning

macOS: Notarization timeout

Apple's servers can be slow. Increase timeout or retry:

// macos-notarize.js
await notarize({
  // ...
  timeout: 1800000 // 30 minutes
});

Windows: SmartScreen warning

Without EV certificate, SmartScreen shows warning for first ~1000 downloads. Solutions:

  1. Purchase EV code signing certificate (~$400/year)
  2. Accept warnings initially (reputation builds over time)

Linux: AppImage won't run

Missing FUSE. Document in release notes:

## Linux Users
AppImage requires FUSE:
\`\`\`bash
sudo apt install libfuse2
\`\`\`

Files to Create/Modify

File Action
.github/workflows/release.yml Create
.github/workflows/build-test.yml Create (optional)
packages/noodl-editor/package.json Add publish config
packages/noodl-editor/build/macos-notarize.js Update for CI

Success Criteria

  1. git tag v1.2.0 && git push --tags triggers workflow
  2. All 4 build targets complete successfully
  3. GitHub Release created with all artifacts
  4. Update manifests (latest*.yml) present
  5. Existing users see "Update Available" within 1 minute
  6. Build time < 30 minutes total

Cost Considerations

GitHub Actions is free for public repositories.

For private repos:

  • 2,000 minutes/month free
  • Each release uses ~60-90 minutes (all platforms combined)
  • ~20-30 releases/month possible on free tier

Security Considerations

  1. Secrets Protection: Never log secrets or expose in artifacts
  2. Certificate Security: Rotate certificates before expiration
  3. Tag Protection: Consider requiring reviews for tags
  4. Artifact Integrity: SHA512 checksums in update manifests

Future Enhancements

  1. Changelog Generation: Auto-generate from merged PRs
  2. Slack/Discord Notifications: Post release announcements
  3. Download Statistics: Track per-platform adoption
  4. Rollback Mechanism: Quick revert to previous version