fix(web): complete dark mode support for logs #267
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: Release Electron App | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Tag to release (e.g. v1.0.57)" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| jobs: | |
| # Runs FIRST so the GitHub Release exists with real notes before any | |
| # artifact upload. Previously notes were generated in the last job: upload | |
| # steps created the release with `--notes ""`, and a notes-job failure left | |
| # the body empty forever (exactly what beta users see in the update popup). | |
| notes: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.tag || github.ref }} | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Fetch dev for stable release notes | |
| run: git fetch origin dev:refs/remotes/origin/dev || true | |
| - name: Generate release notes and create release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ inputs.tag || github.ref_name }} | |
| RELEASE_NOTES_BASE_URL: ${{ secrets.RELEASE_NOTES_BASE_URL }} | |
| RELEASE_NOTES_API_KEY: ${{ secrets.RELEASE_NOTES_API_KEY }} | |
| RELEASE_NOTES_MODEL: ${{ secrets.RELEASE_NOTES_MODEL }} | |
| run: | | |
| bash .github/scripts/generate-release-notes.sh "$TAG" > /tmp/release-notes.md | |
| # Tag like v1.2.3-beta.SHA → mark as prerelease (semver convention). | |
| # Stable tags stay --latest=false until assets are uploaded: this | |
| # release has zero assets for the whole build window, and stable | |
| # update isolation relies on GitHub /releases/latest — clients must | |
| # keep resolving the previous good release until the notify job | |
| # flips --latest after all platforms pass smoke. | |
| PRERELEASE_FLAG="" | |
| LATEST_FLAG="" | |
| case "$TAG" in | |
| *-*) PRERELEASE_FLAG="--prerelease" ;; | |
| *) LATEST_FLAG="--latest=false" ;; | |
| esac | |
| PUBLISHED="" | |
| for i in 1 2 3; do | |
| if gh release edit "$TAG" --draft=false $PRERELEASE_FLAG $LATEST_FLAG --notes-file /tmp/release-notes.md; then | |
| PUBLISHED=1; break | |
| fi | |
| if gh release create "$TAG" --title "$TAG" $PRERELEASE_FLAG $LATEST_FLAG --notes-file /tmp/release-notes.md; then | |
| PUBLISHED=1; break | |
| fi | |
| echo "release edit/create failed (attempt $i/3), retrying..." | |
| sleep $(( i * 15 )) | |
| done | |
| if [ -z "$PUBLISHED" ]; then | |
| echo "::error::could not create or update release $TAG after 3 attempts" | |
| exit 1 | |
| fi | |
| build: | |
| needs: notes | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| platform: win | |
| artifact: windows | |
| - os: macos-latest | |
| platform: mac | |
| arch: arm64 | |
| artifact: macos-arm64 | |
| - os: ubuntu-latest | |
| platform: linux | |
| artifact: linux | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.tag || github.ref }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: | | |
| package-lock.json | |
| web/package-lock.json | |
| native/package-lock.json | |
| - name: Install all workspace dependencies | |
| run: npm ci --ignore-scripts | |
| - name: Install web frontend dependencies | |
| run: cd web && npm ci | |
| - name: Build core (web + tsc) | |
| run: npm run build | |
| - name: Setup Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust build | |
| uses: actions/cache@v4 | |
| with: | |
| path: native/target | |
| key: native-${{ runner.os }}-${{ matrix.arch || 'x64' }}-${{ hashFiles('native/Cargo.lock') }} | |
| restore-keys: native-${{ runner.os }}-${{ matrix.arch || 'x64' }}- | |
| - name: Install native addon dependencies | |
| working-directory: native | |
| run: npm ci | |
| - name: Build native addon | |
| working-directory: native | |
| run: npm run build | |
| - name: Bundle Electron (esbuild) | |
| working-directory: packages/electron | |
| run: node electron/build.mjs | |
| - name: Prepare pack (copy root resources) | |
| working-directory: packages/electron | |
| run: node electron/prepare-pack.mjs | |
| - name: Clean stale release assets | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| ASSETS=$(gh release view "$TAG" --json assets -q '.assets[].name' 2>/dev/null || echo "") | |
| [ -z "$ASSETS" ] && exit 0 | |
| case "${{ matrix.platform }}" in | |
| mac) PATTERN="mac-${{ matrix.arch }}" ;; | |
| win) PATTERN="win-" ;; | |
| linux) PATTERN="linux-" ;; | |
| esac | |
| echo "$ASSETS" | grep -iE "$PATTERN" | while read -r name; do | |
| echo "Removing stale asset: $name" | |
| gh release delete-asset "$TAG" "$name" -y 2>/dev/null || true | |
| done | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Pack (${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }}) | |
| working-directory: packages/electron | |
| # shell: bash needed so the Windows runner uses Git Bash instead of pwsh | |
| # (pwsh doesn't handle backslash line continuations the same way). | |
| shell: bash | |
| run: | | |
| # Drive electron-builder version from the tag, not package.json. Required | |
| # so beta tags (vX.Y.Z-beta.SHA, where bump-electron-beta.yml deliberately | |
| # does NOT touch package.json) publish to the correct GitHub release and | |
| # are detected as prerelease by electron-builder's semver suffix check. | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| VERSION="${TAG#v}" | |
| # --publish never: build artifacts to release/ but don't upload yet. | |
| # The Smoke step below gates the upload — better to ship nothing than | |
| # ship a binary that crashes on startup (cf. v2.0.71/72 events bug). | |
| npx electron-builder --config electron-builder.yml \ | |
| --${{ matrix.platform }} ${{ matrix.arch && format('--{0}', matrix.arch) || '' }} \ | |
| --config.extraMetadata.version="$VERSION" \ | |
| --publish never | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Install AppImage runtime (Linux only) | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update -qq | |
| # libfuse2 = AppImage runtime; xvfb = virtual display so the | |
| # Electron renderer can spawn a window in a headless runner. | |
| sudo apt-get install -y libfuse2 xvfb | |
| - name: Smoke test packaged binary (win) | |
| if: matrix.platform == 'win' | |
| shell: pwsh | |
| env: | |
| RELEASE_DIR: packages/electron/release | |
| SMOKE_LOG: ${{ runner.temp }}/electron-smoke.log | |
| run: ./.github/scripts/electron-smoke.ps1 | |
| - name: Smoke test packaged binary (${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }}) | |
| if: matrix.platform != 'win' | |
| shell: bash | |
| env: | |
| RELEASE_DIR: packages/electron/release | |
| MAC_ARCH: ${{ matrix.arch }} | |
| SMOKE_LOG: ${{ runner.temp }}/electron-smoke.log | |
| run: bash .github/scripts/electron-smoke.sh | |
| - name: Upload smoke log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: electron-smoke-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }}-log | |
| path: ${{ runner.temp }}/electron-smoke.log | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Upload artifacts to release (${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }}) | |
| # Only runs when smoke succeeded. Mirrors the build-mac-x64 | |
| # job's manual-upload pattern; replaces what `--publish always` | |
| # used to do, but conditional on artifact actually working. | |
| shell: bash | |
| run: | | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| # Ensure the GitHub Release exists. With --publish never above, | |
| # electron-builder no longer creates it, so the first uploader | |
| # to arrive needs to. `|| true` swallows the 422 that the | |
| # concurrent matrix jobs hit when a peer wins the race. | |
| IS_PRERELEASE="" | |
| LATEST_FLAG="" | |
| if [[ "$TAG" == *-* ]]; then IS_PRERELEASE="--prerelease"; else LATEST_FLAG="--latest=false"; fi | |
| gh release create "$TAG" --title "$TAG" --notes "" --draft=false $IS_PRERELEASE $LATEST_FLAG 2>/dev/null || true | |
| cd packages/electron/release | |
| case "${{ matrix.platform }}" in | |
| mac) | |
| # arm64 only — x64 handled by build-mac-x64 job. | |
| for f in *arm64* latest-mac.yml; do | |
| [ -f "$f" ] || continue | |
| echo "Uploading: $f" | |
| gh release upload "$TAG" "$f" --clobber | |
| done | |
| ;; | |
| linux) | |
| for f in Codex-Proxy-*-linux* latest-linux.yml; do | |
| [ -f "$f" ] || continue | |
| echo "Uploading: $f" | |
| gh release upload "$TAG" "$f" --clobber | |
| done | |
| ;; | |
| win) | |
| for f in Codex-Proxy-*-win* latest.yml; do | |
| [ -f "$f" ] || continue | |
| echo "Uploading: $f" | |
| gh release upload "$TAG" "$f" --clobber | |
| done | |
| ;; | |
| esac | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # macOS x64 runs AFTER arm64 to avoid release asset upload collisions | |
| # (shared files like latest-mac.yml don't include arch in the name) | |
| build-mac-x64: | |
| needs: build | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.tag || github.ref }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: | | |
| package-lock.json | |
| web/package-lock.json | |
| native/package-lock.json | |
| - name: Install all workspace dependencies | |
| run: npm ci --ignore-scripts | |
| - name: Install web frontend dependencies | |
| run: cd web && npm ci | |
| - name: Build core (web + tsc) | |
| run: npm run build | |
| - name: Setup Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: x86_64-apple-darwin | |
| - name: Cache Rust build | |
| uses: actions/cache@v4 | |
| with: | |
| path: native/target | |
| key: native-macos-x64-${{ hashFiles('native/Cargo.lock') }} | |
| restore-keys: native-macos-x64- | |
| - name: Install native addon dependencies | |
| working-directory: native | |
| run: npm ci | |
| - name: Build native addon (x64) | |
| working-directory: native | |
| run: npx napi build --platform --release --target x86_64-apple-darwin | |
| - name: Bundle Electron (esbuild) | |
| working-directory: packages/electron | |
| run: node electron/build.mjs | |
| - name: Prepare pack (copy root resources) | |
| working-directory: packages/electron | |
| run: node electron/prepare-pack.mjs | |
| - name: Clean stale x64 release assets | |
| continue-on-error: true | |
| run: | | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| ASSETS=$(gh release view "$TAG" --json assets -q '.assets[].name' 2>/dev/null || echo "") | |
| [ -z "$ASSETS" ] && exit 0 | |
| echo "$ASSETS" | grep -iE "mac-x64" | while read -r name; do | |
| echo "Removing stale asset: $name" | |
| gh release delete-asset "$TAG" "$name" -y 2>/dev/null || true | |
| done | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Pack (mac-x64) | |
| working-directory: packages/electron | |
| shell: bash | |
| run: | | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| VERSION="${TAG#v}" | |
| npx electron-builder --config electron-builder.yml \ | |
| --mac --x64 \ | |
| --config.extraMetadata.version="$VERSION" \ | |
| --publish never | |
| - name: Smoke test packaged binary (mac-x64) | |
| shell: bash | |
| env: | |
| RELEASE_DIR: packages/electron/release | |
| MAC_ARCH: x64 | |
| # x64 app startup runs through Rosetta on macOS runners and can | |
| # legitimately take just over the default 90s smoke timeout. | |
| SMOKE_TIMEOUT: 180 | |
| SMOKE_LOG: ${{ runner.temp }}/electron-smoke.log | |
| run: bash .github/scripts/electron-smoke.sh | |
| - name: Upload smoke log on failure (mac-x64) | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: electron-smoke-mac-x64-log | |
| path: ${{ runner.temp }}/electron-smoke.log | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Upload x64 artifacts to release | |
| run: | | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| # Cheap backstop only: the `notes` job (which every build job | |
| # depends on) already created the release with real notes, so | |
| # this create normally no-ops with "already exists". | |
| IS_PRERELEASE="" | |
| LATEST_FLAG="" | |
| if [[ "$TAG" == *-* ]]; then IS_PRERELEASE="--prerelease"; else LATEST_FLAG="--latest=false"; fi | |
| gh release create "$TAG" --title "$TAG" --notes "" --draft=false $IS_PRERELEASE $LATEST_FLAG 2>/dev/null || true | |
| cd packages/electron/release | |
| for f in *; do | |
| [ -f "$f" ] || continue | |
| # Only upload x64 files; skip arm64 and yml manifests | |
| echo "$f" | grep -qi "arm64" && continue | |
| echo "$f" | grep -qiE "\.yml$" && continue | |
| echo "Uploading: $f" | |
| gh release upload "$TAG" "$f" --clobber 2>/dev/null || true | |
| done | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Merge x64 entries into latest-mac.yml | |
| run: | | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| gh release download "$TAG" --pattern "latest-mac.yml" --output /tmp/arm64-mac.yml --clobber 2>/dev/null || exit 0 | |
| LOCAL_YML="packages/electron/release/latest-mac.yml" | |
| [ -f "$LOCAL_YML" ] || exit 0 | |
| node -e " | |
| const fs = require('fs'); | |
| const yaml = require('js-yaml'); | |
| const arm64Yml = yaml.load(fs.readFileSync('/tmp/arm64-mac.yml', 'utf8')); | |
| const x64Yml = yaml.load(fs.readFileSync('$LOCAL_YML', 'utf8')); | |
| const arm64Files = (arm64Yml.files || []).filter(f => f.url.includes('arm64')); | |
| const x64Files = (x64Yml.files || []).filter(f => !f.url.includes('arm64')); | |
| arm64Yml.files = [...arm64Files, ...x64Files]; | |
| fs.writeFileSync('/tmp/merged-mac.yml', yaml.dump(arm64Yml, { lineWidth: -1, quotingType: '\"' })); | |
| " | |
| mv /tmp/merged-mac.yml /tmp/latest-mac.yml | |
| gh release upload "$TAG" /tmp/latest-mac.yml --clobber | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| notify: | |
| needs: [notes, build, build-mac-x64] | |
| runs-on: ubuntu-latest | |
| if: always() && (startsWith(github.ref, 'refs/tags/v') || inputs.tag) | |
| steps: | |
| - name: Checkout (scripts only) | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.tag || github.ref }} | |
| - name: Mark stable release as latest (assets are complete now) | |
| id: flip | |
| if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ inputs.tag || github.ref_name }} | |
| run: | | |
| case "$TAG" in *-*) echo "prerelease, never latest"; exit 0 ;; esac | |
| # A flip failure makes a fully-built stable release invisible to the | |
| # auto-updater (/releases/latest keeps pointing at the previous | |
| # one) — retry, and let the webhook below report the failure. | |
| for i in 1 2 3; do | |
| if gh release edit "$TAG" --latest; then exit 0; fi | |
| echo "marking latest failed (attempt $i/3), retrying..." | |
| sleep 10 | |
| done | |
| echo "::error::failed to mark $TAG as latest after 3 attempts" | |
| exit 1 | |
| - name: Delete asset-less release on build failure | |
| if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ inputs.tag || github.ref_name }} | |
| run: | | |
| # The notes job publishes the release before any artifact exists. | |
| # If every platform failed, a published zero-asset release would | |
| # break beta clients (allow_prerelease resolves the NEWEST release | |
| # and 404s on latest*.yml). Delete it — the tag survives, so a | |
| # manual re-dispatch of release.yml can retry the same version. | |
| COUNT=$(gh release view "$TAG" --json assets -q '.assets | length' 2>/dev/null || echo "") | |
| if [ "$COUNT" = "0" ]; then | |
| echo "deleting zero-asset release $TAG (tag kept)" | |
| gh release delete "$TAG" -y || true | |
| else | |
| echo "release $TAG has ${COUNT:-unknown} asset(s), leaving it in place" | |
| fi | |
| - name: Send webhook notification | |
| if: always() | |
| env: | |
| NOTIFY_WEBHOOK_URL: ${{ secrets.NOTIFY_WEBHOOK_URL }} | |
| TAG: ${{ inputs.tag || github.ref_name }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| RELEASE_URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }} | |
| run: | | |
| if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || steps.flip.outcome == 'failure' }}" = "true" ]; then | |
| MSG="❌ Release $TAG failed: $RUN_URL" | |
| else | |
| MSG="✅ Release $TAG published: $RELEASE_URL" | |
| fi | |
| bash .github/scripts/notify-webhook.sh "$MSG" |