fix(webui): handle EADDRINUSE when starting server in WebUI mode #1649
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
| name: '✅ PR Checks' | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to run checks on' | |
| required: true | |
| type: string | |
| skip_build_test: | |
| description: 'Skip the build test job (saves ~45 min)' | |
| type: boolean | |
| required: false | |
| default: false | |
| pull_request: | |
| types: [opened, synchronize, edited, closed] | |
| branches: [main, dev] | |
| paths-ignore: | |
| - '**/*.md' | |
| - 'docs/**' | |
| - '.vscode/**' | |
| - '.github/ISSUE_TEMPLATE/**' | |
| concurrency: | |
| group: pr-checks-${{ github.event.pull_request.number || inputs.pr_number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: read | |
| checks: write | |
| id-token: write | |
| env: | |
| BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/' | |
| jobs: | |
| # Cancel in-progress runs when PR is closed (merged or manually closed) | |
| cancel-if-closed: | |
| name: Cancel if PR closed | |
| if: github.event.action == 'closed' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: echo "PR closed, cancelling in-progress runs via concurrency." | |
| # Job 1: Code quality checks (TypeScript, Oxlint, Oxfmt) | |
| code-quality: | |
| name: Code Quality | |
| if: github.event_name == 'workflow_dispatch' || (github.event.action != 'edited' && github.event.action != 'closed') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve PR context | |
| uses: ./.github/actions/checkout-pr | |
| with: | |
| pr_number: ${{ inputs.pr_number }} | |
| github_token: ${{ github.token }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Setup bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install --no-frozen-lockfile | |
| - name: Run postinstall | |
| run: npm run postinstall || true | |
| - name: Install prek | |
| run: npm install -g @j178/prek | |
| - name: Run prek checks | |
| run: prek run --from-ref origin/${{ env.PR_BASE_REF }} --to-ref HEAD | |
| # Job 2: Unit tests across all platforms | |
| unit-tests: | |
| name: Unit Tests (${{ matrix.os }}) | |
| if: github.event_name == 'workflow_dispatch' || (github.event.action != 'edited' && github.event.action != 'closed') | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-14, windows-2022] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Resolve PR context | |
| uses: ./.github/actions/checkout-pr | |
| with: | |
| pr_number: ${{ inputs.pr_number }} | |
| github_token: ${{ github.token }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Setup bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install --no-frozen-lockfile | |
| - name: Run postinstall | |
| run: npm run postinstall || true | |
| - name: Run extension system tests | |
| run: bunx vitest run | |
| # Job 3: Coverage test (Linux only) | |
| coverage-tests: | |
| name: Coverage Test | |
| if: github.event_name == 'workflow_dispatch' || (github.event.action != 'edited' && github.event.action != 'closed') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Resolve PR context | |
| uses: ./.github/actions/checkout-pr | |
| with: | |
| pr_number: ${{ inputs.pr_number }} | |
| github_token: ${{ github.token }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Setup bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install --no-frozen-lockfile | |
| - name: Run postinstall | |
| run: npm run postinstall || true | |
| - name: Run coverage tests | |
| id: coverage | |
| continue-on-error: true | |
| run: bun run test:coverage | |
| - name: Upload coverage to Codecov (Linux coverage only) | |
| if: always() && hashFiles('coverage/lcov.info') != '' && github.repository == 'iOfficeAI/AionUi' | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| use_oidc: ${{ secrets.CODECOV_TOKEN == '' }} | |
| files: coverage/lcov.info | |
| fail_ci_if_error: false | |
| verbose: true | |
| - name: Skip Codecov upload when preconditions are not met | |
| if: always() && hashFiles('coverage/lcov.info') != '' && github.repository != 'iOfficeAI/AionUi' | |
| shell: bash | |
| run: | | |
| echo 'Skipping Codecov upload due to unmet preconditions.' | |
| echo "Repository: ${{ github.repository }}" | |
| - name: Coverage result summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| if [ "${{ steps.coverage.outcome }}" = "failure" ]; then | |
| echo "::warning::Coverage tests failed (non-blocking). Check logs for failed test details." | |
| echo "## Coverage check (non-blocking warning)" >> $GITHUB_STEP_SUMMARY | |
| echo "Coverage command failed in this run. Please review test failures in logs." >> $GITHUB_STEP_SUMMARY | |
| if [ -f coverage/lcov.info ]; then | |
| echo "lcov.info exists, Codecov upload can still run." >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "lcov.info not found, Codecov upload was skipped." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "## Coverage check" >> $GITHUB_STEP_SUMMARY | |
| echo "Coverage command passed." >> $GITHUB_STEP_SUMMARY | |
| if [ -f coverage/lcov.info ]; then | |
| echo "Uploaded Linux (ubuntu-latest) coverage to Codecov." >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "Coverage passed but lcov.info is missing; Codecov upload was skipped." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| fi | |
| - name: Upload coverage artifacts | |
| if: always() && hashFiles('coverage/lcov.info') != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: coverage/ | |
| if-no-files-found: warn | |
| i18n-check: | |
| name: I18n Check | |
| if: github.event_name == 'workflow_dispatch' || (github.event.action != 'edited' && github.event.action != 'closed') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Setup bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install --no-frozen-lockfile | |
| - name: Run postinstall | |
| run: npm run postinstall || true | |
| - name: Run i18n validation (warning-only) | |
| id: i18n | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| node scripts/check-i18n.js 2>&1 | tee i18n-check.log | |
| - name: Publish i18n summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| echo "## i18n validation" >> $GITHUB_STEP_SUMMARY | |
| if grep -q "⚠️" i18n-check.log; then | |
| echo "Missing/incomplete translations found. Please review warnings below." >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "No i18n warnings detected." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "<details><summary>i18n log</summary>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo '```text' >> $GITHUB_STEP_SUMMARY | |
| cat i18n-check.log >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "</details>" >> $GITHUB_STEP_SUMMARY | |
| # Job 4: Build test across all platforms (parallel with code-quality and unit-tests) | |
| build-test: | |
| name: Build Test (${{ matrix.platform }}) | |
| if: github.event.action != 'edited' && github.event.action != 'closed' && inputs.skip_build_test != true | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: 'macos-arm64' | |
| os: 'macos-14' | |
| arch: 'arm64' | |
| target: '--mac' | |
| build_args: '--mac --arm64' | |
| unpacked_dir: 'mac-arm64' | |
| - platform: 'macos-x64' | |
| os: 'macos-14' | |
| arch: 'x64' | |
| target: '--mac' | |
| build_args: '--mac --x64' | |
| unpacked_dir: 'mac' | |
| - platform: 'windows-x64' | |
| os: 'windows-2022' | |
| arch: 'x64' | |
| target: '--win' | |
| build_args: '--win --x64' | |
| unpacked_dir: 'win-unpacked' | |
| - platform: 'windows-arm64' | |
| os: 'windows-2022' | |
| arch: 'arm64' | |
| target: '--win' | |
| build_args: '--win --arm64' | |
| unpacked_dir: 'win-arm64-unpacked' | |
| - platform: 'linux' | |
| os: 'ubuntu-latest' | |
| arch: 'x64' | |
| target: '--linux' | |
| build_args: '--linux --x64' | |
| unpacked_dir: 'linux-unpacked' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Setup bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Setup just | |
| uses: extractions/setup-just@v2 | |
| - name: Install dependencies | |
| run: bun install --no-frozen-lockfile | |
| - name: Run postinstall | |
| run: npm run postinstall || true | |
| - name: Install Linux dependencies | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential python3 python3-pip pkg-config libsqlite3-dev fakeroot dpkg-dev rpm libnss3-dev libatk-bridge2.0-dev libdrm2 libxkbcommon-dev libxss1 libatspi2.0-dev libgtk-3-dev libxrandr2 libasound2-dev | |
| - name: Get Electron version | |
| id: electron-version | |
| shell: bash | |
| run: | | |
| ELECTRON_VERSION=$(node -p "require('./package.json').devDependencies.electron.replace(/[\^~]/g, '')") | |
| echo "version=$ELECTRON_VERSION" >> $GITHUB_OUTPUT | |
| echo "Electron version: $ELECTRON_VERSION" | |
| # Restore Electron/Electron-Builder caches before install-app-deps | |
| - name: Cache Electron artifacts | |
| id: electron-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ${{ runner.temp }}/.cache/electron | |
| ${{ runner.temp }}/.cache/electron-builder | |
| ~/.cache/electron | |
| ~/.cache/electron-builder | |
| ${{ env.LOCALAPPDATA }}\electron-builder\Cache | |
| key: electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('package.json', 'bun.lock') }} | |
| restore-keys: | | |
| electron-cache-${{ matrix.platform }}-${{ matrix.arch }}- | |
| electron-cache-${{ matrix.platform }}- | |
| - name: Cache status | |
| run: echo "electron-cache-hit=${{ steps.electron-cache.outputs.cache-hit }}" | |
| - name: Rebuild native modules for Electron (non-Windows) | |
| if: "!startsWith(matrix.platform, 'windows')" | |
| run: bunx electron-builder install-app-deps | |
| env: | |
| npm_config_runtime: electron | |
| npm_config_disturl: https://electronjs.org/headers | |
| ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron | |
| ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder | |
| - name: Setup MSBuild (Windows only) | |
| if: startsWith(matrix.platform, 'windows') | |
| uses: microsoft/setup-msbuild@v2 | |
| with: | |
| vs-version: '17.0' | |
| - name: Build test (Windows x64) | |
| if: matrix.platform == 'windows-x64' | |
| shell: pwsh | |
| run: | | |
| Write-Host "==========================================" | |
| Write-Host "BUILD TEST: ${{ matrix.platform }}" | |
| Write-Host "==========================================" | |
| just build-win-x64 | |
| Write-Host "✓Build test passed for ${{ matrix.platform }}" | |
| env: | |
| NODE_OPTIONS: '--max-old-space-size=8192' | |
| MSVS_VERSION: 2022 | |
| GYP_MSVS_VERSION: 2022 | |
| WindowsTargetPlatformVersion: 10.0.19041.0 | |
| CI: true | |
| - name: Build test (Windows arm64) | |
| if: matrix.platform == 'windows-arm64' | |
| shell: pwsh | |
| run: | | |
| Write-Host "==========================================" | |
| Write-Host "BUILD TEST: ${{ matrix.platform }}" | |
| Write-Host "==========================================" | |
| just build-win-arm64 | |
| Write-Host "✓Build test passed for ${{ matrix.platform }}" | |
| env: | |
| NODE_OPTIONS: '--max-old-space-size=8192' | |
| MSVS_VERSION: 2022 | |
| GYP_MSVS_VERSION: 2022 | |
| WindowsTargetPlatformVersion: 10.0.19041.0 | |
| CI: true | |
| - name: Build test (non-Windows) | |
| if: "!startsWith(matrix.platform, 'windows')" | |
| shell: bash | |
| run: | | |
| echo "==========================================" | |
| echo "BUILD TEST: ${{ matrix.platform }}" | |
| echo "==========================================" | |
| node scripts/build-with-builder.js auto ${{ matrix.build_args }} | |
| echo "✓Build test passed for ${{ matrix.platform }}" | |
| env: | |
| NODE_OPTIONS: '--max-old-space-size=8192' | |
| npm_config_arch: ${{ matrix.arch }} | |
| npm_config_target_arch: ${{ matrix.arch }} | |
| npm_config_runtime: electron | |
| npm_config_target: ${{ steps.electron-version.outputs.version }} | |
| npm_config_disturl: https://electronjs.org/headers | |
| CI: true | |
| - name: Verify packaged bundled bun assets | |
| run: bun run test:packaged:bun | |
| - name: Verify build artifacts exist | |
| shell: bash | |
| run: | | |
| echo "==========================================" | |
| echo "VERIFY ARTIFACTS: ${{ matrix.platform }}" | |
| echo "==========================================" | |
| ls -lah out || true | |
| case "${{ matrix.platform }}" in | |
| windows-*) | |
| ls out/*.exe out/*latest*.yml >/dev/null | |
| ;; | |
| macos-*) | |
| ls out/*.dmg out/*.zip out/*latest*.yml >/dev/null | |
| ;; | |
| linux) | |
| ls out/*.deb out/*latest*.yml >/dev/null | |
| ;; | |
| esac | |
| - name: Silent install smoke test (Windows x64) | |
| if: matrix.platform == 'windows-x64' | |
| shell: pwsh | |
| run: | | |
| Write-Host "==========================================" | |
| Write-Host "SMOKE INSTALL: windows-x64" | |
| Write-Host "==========================================" | |
| $installer = Get-ChildItem -Path out -Filter "AionUi-*-win-*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | |
| if (-not $installer) { | |
| throw "No Windows installer found in out/" | |
| } | |
| Write-Host "Using installer: $($installer.FullName)" | |
| Start-Process -FilePath $installer.FullName -ArgumentList '/S' -Wait -NoNewWindow | |
| $candidates = @( | |
| "$env:LOCALAPPDATA\\Programs\\AionUi\\AionUi.exe", | |
| "$env:ProgramFiles\\AionUi\\AionUi.exe", | |
| "$env:ProgramFiles(x86)\\AionUi\\AionUi.exe" | |
| ) | |
| $installedExe = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1 | |
| if (-not $installedExe) { | |
| throw "Silent install finished but app executable not found in expected locations" | |
| } | |
| Write-Host "Installed executable: $installedExe" | |
| - name: Skip executable smoke for Windows arm64 cross build | |
| if: matrix.platform == 'windows-arm64' | |
| shell: pwsh | |
| run: | | |
| Write-Host "Skipping runtime install smoke for windows-arm64: runner is windows-x64, cannot reliably execute arm64 installer." | |
| Get-ChildItem -Path out -Filter "AionUi-*-win-*.exe" | Format-Table Name, Length | |
| - name: Install smoke test (macOS arm64) | |
| if: matrix.platform == 'macos-arm64' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "==========================================" | |
| echo "SMOKE INSTALL: macos-arm64" | |
| echo "==========================================" | |
| DMG_FILE=$(ls out/*.dmg | head -n 1) | |
| MOUNT_POINT="/tmp/aionui-smoke-mount" | |
| APP_DIR="/tmp/aionui-smoke-app" | |
| rm -rf "$MOUNT_POINT" "$APP_DIR" | |
| mkdir -p "$MOUNT_POINT" "$APP_DIR" | |
| hdiutil attach "$DMG_FILE" -nobrowse -mountpoint "$MOUNT_POINT" | |
| cp -R "$MOUNT_POINT"/*.app "$APP_DIR"/ | |
| hdiutil detach "$MOUNT_POINT" | |
| APP_PATH=$(ls -d "$APP_DIR"/*.app | head -n 1) | |
| APP_BIN="$APP_PATH/Contents/MacOS/AionUi" | |
| test -x "$APP_BIN" | |
| "$APP_BIN" --version || true | |
| - name: Skip executable smoke for macOS x64 cross build | |
| if: matrix.platform == 'macos-x64' | |
| shell: bash | |
| run: | | |
| echo "Skipping runtime launch smoke for macos-x64 cross build on arm64 runner." | |
| ls -lah out/*.dmg out/*.zip | |
| - name: Install smoke test (Linux) | |
| if: matrix.platform == 'linux' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "==========================================" | |
| echo "SMOKE INSTALL: linux" | |
| echo "==========================================" | |
| DEB_FILE=$(ls out/*.deb | head -n 1) | |
| PKG_NAME=$(dpkg-deb -f "$DEB_FILE" Package) | |
| sudo dpkg -i "$DEB_FILE" || sudo apt-get install -f -y | |
| INSTALLED_BIN=$(dpkg -L "$PKG_NAME" | grep -Ei '/(bin|opt)/.*(aionui)$' | head -n 1 || true) | |
| if [ -z "$INSTALLED_BIN" ]; then | |
| echo "Package files:" | |
| dpkg -L "$PKG_NAME" | head -n 50 | |
| echo "No installed executable path matched expected pattern" | |
| exit 1 | |
| fi | |
| test -x "$INSTALLED_BIN" | |
| # Job 5: Test release scripts (fast, no build required) | |
| release-script-test: | |
| name: Release Script Test | |
| if: github.event.action != 'edited' && github.event.action != 'closed' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Create mock build artifacts | |
| shell: bash | |
| run: bash scripts/create-mock-release-artifacts.sh build-artifacts | |
| - name: Run prepare-release-assets script | |
| shell: bash | |
| run: bash scripts/prepare-release-assets.sh build-artifacts release-assets | |
| - name: Validate script output | |
| shell: bash | |
| run: bash scripts/verify-release-assets.sh release-assets |