Skip to content

ci: speed up linux and windows builds#71

Merged
akemmanuel merged 2 commits into
masterfrom
ci/speed-linux-windows
May 26, 2026
Merged

ci: speed up linux and windows builds#71
akemmanuel merged 2 commits into
masterfrom
ci/speed-linux-windows

Conversation

@akemmanuel
Copy link
Copy Markdown
Owner

What

Linux and Windows builds were ~4 min each. This brings them to ~1.5-2 min.

Changes

  • node_modules caching via actions/cache@v5 -- skip pnpm install on cache hit
  • Electron binary caching -- avoid re-downloading 80-120MB per build
  • Dist artifact sharing -- preflight builds once, upload@v7 + download@v8
  • Docker layer caching -- docker/build-push-action with cache-from: type=gha
  • Bumped actions -- checkout@v6, setup-node@v6, cache@v5

Impact

Job Before After
Linux ~4 min ~1.5-2 min
Windows ~4 min ~1.5-2 min
macOS ~8-12 min unchanged

- Cache node_modules via actions/cache@v5, skip pnpm install on hit
- Cache Electron binaries (~/.cache/electron, ~/.cache/electron-builder)
- Use upload-artifact@v7 + download-artifact@v8 for dist transfer across jobs
- archive: false for faster non-zipped artifact transfer
- Replace raw docker build with docker/build-push-action + GHA layer cache
- Bump actions/checkout@v6, actions/setup-node@v6, actions/cache@v5
@github-actions
Copy link
Copy Markdown

Confidence Score: 4/5

Issues Detected

1. No restore-keys on cache entries — full cache invalidation on lockfile changes

All three actions/cache@v5 steps (node_modules, Electron binaries) specify only a key with no restore-keys. If pnpm-lock.yaml or package.json changes even slightly, the entire cache is invalidated with zero fallback. For example, a dependency version bump in a transitive dep that updates the lockfile hash would force a complete pnpm install --frozen-lockfile with no partial reuse from a previous key.

Consider adding restore-keys like:

key: ${{ runner.os }}-nm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
  ${{ runner.os }}-nm-

2. Frontend build artifact has no path specified on download

In build-linux (#77-80), build-mac (#141-144), and build-windows (#195-198), the Restore frontend build step uses actions/download-artifact@v8 without a path:

- name: Restore frontend build
  uses: actions/download-artifact@v8
  with:
    name: frontend-build

The default path for download-artifact@v8 is ${{ github.workspace }} (the runner's working directory). The artifact was uploaded with archive: false — while this should work, it's fragile. If the working directory changes or the runner shifts where ${{ github.workspace }} resolves to, electron-builder won't find dist/ and dist-electron/. Being explicit about the path would eliminate ambiguity:

- name: Restore frontend build
  uses: actions/download-artifact@v8
  with:
    name: frontend-build
    path: .

3. macOS Electron cache key will miss on the first build after merge

The Electron cache key changed from electron-cache-mac-${{ matrix.arch }}-... (old) to electron-cache-${{ runner.os }}-${{ matrix.arch }}-... (new). On macOS, runner.os evaluates to macOS (capitalized), so the new key becomes electron-cache-macOS-arm64-... vs the old electron-cache-mac-arm64-.... This is a full cache miss, meaning the first macOS build after merge will re-download Electron binaries (80–120 MB). Not a bug, but worth noting for the first post-merge pipeline run.

4. build-linux and build-windows cache keys use hardcoded x64; macOS uses matrix.arch

Linux (#63) and Windows (#181):

key: electron-cache-${{ runner.os }}-x64-${{ hashFiles('package.json') }}

macOS (#139):

key: electron-cache-${{ runner.os }}-${{ matrix.arch }}-${{ hashFiles('package.json') }}

This is correct today (GitHub's ubuntu-latest and windows-latest are x64), but it's a subtle trap if the org ever switches to ARM runners. Consider using ${{ runner.arch }} or a runtime-detected architecture variable.

Improvements (non-blocking)

  • Add restore-keys to all cache steps for partial cache reuse
  • Explicitly specify path: . on frontend build downloads for clarity
  • Consider swapping the order: run vp build before vp lint/vp check in preflight to parallelize the build upload with the longer lint/check steps (shaves ~30-60s off critical path)
  • Docker cache cache-to: type=gha,mode=max sets mode=max on every run, which can be expensive for layer uploads; consider mode=min for CI-only builds and mode=max only on the publish-docker job

New%20session%20-%202026-05-25T20%3A05%3A38.957Z
opencode session  |  github run

electron-builder generates arch-specific update manifests (latest-mac-arm64.yml)
when building with --arm64 flag. The merge script expected latest-mac.yml in both
artifacts, causing the release job to fail.

- Add normalization step: copy latest-mac-*.yml to latest-mac.yml after packaging
- Switch upload path to release/* to ensure all files are included
- Improve merge script: fall back to arch-specific filenames, better debug output
- Add artifact listing in release job for future debugging
@akemmanuel
Copy link
Copy Markdown
Owner Author

Added a fix for the release job that was failing on master (electron-builder generates latest-mac-arm64.yml instead of latest-mac.yml when building with --arm64, so the merge script couldn't find the manifest in the arm64 artifact).

Changes in 2def9c6:

  • Normalize step copies latest-mac-*.yml to latest-mac.yml after each mac build
  • Upload path changed from selective list to release/* for clarity
  • Merge script now searches multiple candidate filenames as fallback
  • Debug listing added in release job to show downloaded artifact structure

@github-actions
Copy link
Copy Markdown

PR Review

Confidence Score: 4/5

Issues Detected

1. actions/cache@v5 lacks restore-keys — full cache miss on any lockfile change

All three Cache node_modules steps (lines 63, 127, 184) and both Cache Electron binaries steps (lines 72–75, 133–139, 190–196) specify only key with no restore-keys. If pnpm-lock.yaml or package.json changes even slightly, every job hits a cold cache with zero fallback. Adding restore-keys would allow partial reuse:

key: ${{ runner.os }}-nm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
  ${{ runner.os }}-nm-

2. Electron cache path on Windows is likely incorrect

# build.yml:193-195
path: |
  ~/.cache/electron
  ~/.cache/electron-builder

On windows-latest, ~ resolves to C:\Users\runneradmin. ~/.cache/electron-builder is not where electron-builder stores its cache on Windows — it uses %LOCALAPPDATA%\electron-builder\cache. This means the Electron binary cache on Windows never actually hits, forcing a re-download of ~80-120 MB on every Windows build. The correct Windows path would be ~\AppData\Local\electron-builder\cache.

3. vp check / vp lint run before vp build in preflight

# build.yml:23-30
- name: Lint
  run: pnpm exec vp lint
- name: Check
  run: pnpm exec vp check
- name: Build frontend
  run: pnpm exec vp build

vp check typically includes type checking. If the project generates .d.ts or other type declarations during vp build, vp check will fail or produce false positives. Consider running vp build before vp check, or verify that vp check does not depend on build output.

4. Redundant dual caching layer (setup-node cache: pnpm + manual node_modules cache)

actions/setup-node@v6 with cache: pnpm (lines 18, 56, 120, 177) caches the pnpm store. Separate actions/cache@v5 for node_modules adds a second layer. If the node_modules cache hits, pnpm install is skipped, but the setup-node cache step still saves its store every run — doubling cache maintenance. Recommend picking one approach.

5. No validation that download artifact succeeded before proceeding

# build.yml:77-80, 141-144, 198-201
- name: Restore frontend build
  uses: actions/download-artifact@v8
  with:
    name: frontend-build

If download-artifact fails (e.g., preflight job was skipped or the artifact expired), the subsequent electron-builder step will fail with a confusing "dist/ not found" error. Adding a verification step after download would produce clearer failures.

6. merge-latest-mac.mjs — merged version silently picks x64 when mismatched

// merge-latest-mac.mjs:58
const merged = {
  version: x64.version ?? arm64.version,  // BUG: version mismatch is silently hidden
  releaseDate,
  files: [...(x64.files ?? []), ...(arm64.files ?? [])],
};

If x64.version differs from arm64.version (indicating a real build configuration error), the script silently picks the x64 version. A mismatch should throw or at least warn.

7. merge-latest-mac.mjs — epoch date when both manifests lack releaseDate

// merge-latest-mac.mjs:50-55
const releaseDate = new Date(
  Math.max(
    new Date(x64.releaseDate ?? 0).getTime(),
    new Date(arm64.releaseDate ?? 0).getTime(),
  ),
).toISOString();

If neither manifest contains a releaseDate, this produces 1970-01-01T00:00:00.000Z in the merged output. Should validate that at least one releaseDate exists.

8. build-linux and build-windows hardcode x64 — no ${{ matrix.arch }} or ${{ runner.arch }}

# build.yml:75
key: electron-cache-${{ runner.os }}-x64-${{ hashFiles('package.json') }}
# build.yml:196
key: electron-cache-${{ runner.os }}-x64-${{ hashFiles('package.json') }}

While ubuntu-latest and windows-latest are x64 today, switching to ARM runners would silently break Electron caching. Use ${{ runner.arch }} or a matrix parameter.

9. macOS runner.os evaluates to macOS (capital M), breaking Electron cache compatibility

build-mac uses ${{ runner.os }}-${{ matrix.arch }}macOS-arm64/macOS-x64. If a prior cache existed with a different key scheme, this is a guaranteed cold start on the first post-merge macOS build.

10. setup-node cache: pnpm on Windows may conflict with the manual cache restore order

On windows-latest, actions/setup-node@v6 with cache: pnpm restores the pnpm store before the manual Cache node_modules step. If both are active, the store restoration + node_modules restoration could be inconsistent (store has packages, node_modules doesn't, or vice versa). pnpm install --frozen-lockfile handles this, but it adds unnecessary complexity.

Improvements (non-blocking)

  • Use path: . explicitly on download-artifact steps for clarity
  • Consider mode=min for Docker cache in build-linux (only load: true, not pushing multi-arch)
  • Add fail-fast: false to Linux/Windows if multiple archs are added later (macOS already has it)
  • The Normalize update manifest filename step uses cp latest-mac-*.yml latest-mac.yml — if multiple glob matches, cp errors. Consider for f in release/latest-mac-*.yml; do cp "$f" release/latest-mac.yml; break; done for safety
  • Remove release/* from mac-release-${{ matrix.arch }} upload — it uploads everything including latest-mac.yml twice (once as latest-mac.yml, once as the arch-specific name after normalization), which works but wastes storage

New%20session%20-%202026-05-25T20%3A15%3A28.812Z
opencode session  |  github run

@akemmanuel akemmanuel merged commit 3b466f2 into master May 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant