.github/workflows/ci.yml

name: CI

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:
  workflow_call:
    inputs:
      ref:
        description: Commit or tag to check out and build.
        required: false
        default: ""
        type: string
      release_version:
        description: Next stable patch version for the automatic release lane.
        required: false
        default: ""
        type: string

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  workflows:
    name: Validate workflows
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
          ref: ${{ inputs.ref || github.sha }}
      - name: Run actionlint
        shell: bash
        run: |
          directory="$(mktemp -d)"
          trap 'rm -rf "$directory"' EXIT
          curl --fail --silent --show-error --location \
            https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz \
            --output "$directory/actionlint.tar.gz"
          echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8  $directory/actionlint.tar.gz" | sha256sum --check --strict
          tar -xzf "$directory/actionlint.tar.gz" -C "$directory" actionlint
          "$directory/actionlint" -color

  build:
    needs: workflows
    name: Test and package (${{ matrix.platform }})
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 20
    defaults:
      run:
        shell: ${{ matrix.shell }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: windows-x64
            runner: windows-latest
            shell: pwsh
            args: --win nsis --x64
            artifact: |
              release/*.exe
              release/latest.yml
          - platform: macos-arm64
            runner: macos-15
            shell: zsh -e {0}
            args: --mac dmg --arm64
            artifact: release/*.dmg
          - platform: macos-x64
            runner: macos-15-intel
            shell: zsh -e {0}
            args: --mac dmg --x64
            artifact: release/*.dmg
          - platform: linux-x64
            runner: ubuntu-24.04
            shell: bash
            args: --linux AppImage --x64
            artifact: |
              release/*.AppImage
              release/latest-linux.yml
    env:
      CSC_IDENTITY_AUTO_DISCOVERY: "false"
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
          ref: ${{ inputs.ref || github.sha }}
          fetch-depth: 0
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version-file: .nvmrc
          cache: npm
      - name: Set build version
        env:
          EMC2_BUILD_REF: ${{ inputs.ref || github.ref }}
          EMC2_RELEASE_VERSION: ${{ inputs.release_version }}
        run: node scripts/set-build-version.mjs
      - run: npm ci --no-audit --no-fund
      - run: npm test
        # The serial suite takes about seven minutes on hosted Intel macOS runners.
        timeout-minutes: 12
      - run: npm run build
      - name: Check desktop startup
        if: runner.os != 'Linux'
        timeout-minutes: 1
        run: npm run check:desktop -- --user-data-dir="${{ runner.temp }}/emc2-check"
      - name: Create installer
        if: runner.os != 'macOS'
        run: npx --no-install electron-builder ${{ matrix.args }} --publish never
      # hdiutil intermittently fails ("Resource busy" / create failed) on hosted macOS runners.
      # Retry the DMG build up to 3 times, force-detaching stale volumes between attempts.
      - name: Create installer (macOS, with hdiutil retry)
        if: runner.os == 'macOS'
        run: |
          for attempt in 1 2 3; do
            if npx --no-install electron-builder ${{ matrix.args }} --publish never; then
              exit 0
            fi
            echo "::warning::electron-builder DMG attempt $attempt failed"
            for volume in /Volumes/Emc*(N); do
              hdiutil detach -force "$volume" || true
            done
            rm -f release/*.dmg(N) release/*.blockmap(N)
            sleep $((attempt * 10))
          done
          echo "::error::electron-builder DMG failed after 3 attempts"
          exit 1
      - name: Verify updater metadata and installer integrity
        if: runner.os != 'macOS'
        run: node --import tsx scripts/check-release-assets.ts ${{ matrix.platform }}
      - name: Upload installer
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: installer-${{ matrix.platform }}
          path: ${{ matrix.artifact }}
          if-no-files-found: error
          retention-days: 1
          compression-level: 0
          overwrite: true