Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 60 additions & 6 deletions .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ jobs:
timeout-minutes: 120
permissions:
contents: write
env:
P12_CHECK: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
API_KEY_CHECK: ${{ secrets.APPLE_API_KEY_P8 }}

steps:
- name: Checkout
Expand All @@ -68,7 +71,26 @@ jobs:
with:
node-version: '20.18.2'
cache: 'npm'
cache-dependency-path: apps/editor/package-lock.json
cache-dependency-path: |
apps/editor/package-lock.json
apps/editor/build/package-lock.json

- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v4
with:
path: |
apps/editor/node_modules
apps/editor/build/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/editor/package-lock.json', 'apps/editor/build/package-lock.json') }}
restore-keys: |
node-modules-${{ runner.os }}-${{ runner.arch }}-

- name: Cache Electron gyp headers
uses: actions/cache@v4
with:
path: ~/.electron-gyp
key: electron-gyp-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/editor/package.json') }}

- name: Stamp release version in product.json
if: startsWith(github.ref, 'refs/tags/')
Expand All @@ -92,8 +114,10 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=7168
CI_DEPS_READY: ${{ steps.cache-node-modules.outputs.cache-hit }}

- name: Import certificate to keychain
if: ${{ env.P12_CHECK != '' }}
env:
P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
Expand All @@ -112,6 +136,7 @@ jobs:
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "keychain-password" "$KEYCHAIN_PATH"

- name: Sign app
if: ${{ env.P12_CHECK != '' }}
env:
CODESIGN_IDENTITY: "Developer ID Application: HITL, Inc (SQZ9VHYXJ3)"
AGENT_BUILDDIRECTORY: ${{ github.workspace }}/apps
Expand All @@ -126,6 +151,7 @@ jobs:
zip -Xry "$RUNNER_TEMP/OCcode-${{ matrix.artifact_name }}-${GITHUB_REF_NAME}.zip" "OCcode.app"

- name: Notarize
if: ${{ env.P12_CHECK != '' && env.API_KEY_CHECK != '' }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
Expand All @@ -144,32 +170,40 @@ jobs:
--timeout 30m

- name: Staple notarization ticket
if: ${{ env.P12_CHECK != '' && env.API_KEY_CHECK != '' }}
run: |
xcrun stapler staple "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}/OCcode.app"

- name: Re-zip stapled app
if: ${{ env.P12_CHECK != '' && env.API_KEY_CHECK != '' }}
run: |
cd "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}"
zip -Xry "$RUNNER_TEMP/OCcode-${{ matrix.artifact_name }}-${GITHUB_REF_NAME}-signed.zip" "OCcode.app"

- name: Verify signature
if: ${{ env.P12_CHECK != '' }}
run: |
codesign -dv --deep --verbose=4 "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}/OCcode.app" 2>&1
spctl -a -vvv -t install "$GITHUB_WORKSPACE/apps/${{ matrix.output_dir }}/OCcode.app" 2>&1

- name: Upload signed app artifact
- name: Upload app artifact (signed or unsigned)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
continue-on-error: true
with:
name: OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed
path: ${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed.zip
name: OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}
path: |
${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed.zip
${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}.zip
retention-days: 30

- name: Create GitHub Release (on tag)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b
continue-on-error: true
with:
files: ${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed.zip
files: |
${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}-signed.zip
${{ runner.temp }}/OCcode-${{ matrix.artifact_name }}-${{ github.ref_name }}.zip
name: OCcode ${{ github.ref_name }}
body_path: CHANGELOG.md
draft: false
Expand Down Expand Up @@ -197,7 +231,26 @@ jobs:
with:
node-version: '20.18.2'
cache: 'npm'
cache-dependency-path: apps/editor/package-lock.json
cache-dependency-path: |
apps/editor/package-lock.json
apps/editor/build/package-lock.json

- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v4
with:
path: |
apps/editor/node_modules
apps/editor/build/node_modules
key: node-modules-${{ runner.os }}-x64-${{ hashFiles('apps/editor/package-lock.json', 'apps/editor/build/package-lock.json') }}
restore-keys: |
node-modules-${{ runner.os }}-x64-

- name: Cache Electron gyp headers
uses: actions/cache@v4
with:
path: ~/AppData/Roaming/electron-gyp
key: electron-gyp-${{ runner.os }}-x64-${{ hashFiles('apps/editor/package.json') }}

- name: Stamp release version in product.json
if: startsWith(github.ref, 'refs/tags/')
Expand All @@ -220,6 +273,7 @@ jobs:
run: make build-windows
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CI_DEPS_READY: ${{ steps.cache-node-modules.outputs.cache-hit }}

- name: Sign Windows installers (Azure Trusted Signing)
if: ${{ env.AZURE_CLIENT_ID_CHECK != '' }}
Expand Down
169 changes: 169 additions & 0 deletions .tickets/ticket-050-ci-build-caching/prd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Ticket 050 — CI build caching for Windows & macOS

## 2.1 Problem Statement

GitHub Actions builds for Windows and macOS take significantly longer than necessary. Each workflow run performs identical work:
- Re-downloads all npm tarballs (even when `package-lock.json` hasn't changed)
- Re-downloads Electron gyp headers (~100 MB per architecture per run)
- Recompiles all native modules from source (Electron 34.3.2, `build_from_source=true` in `.npmrc`)

The `setup-node` action currently caches only `~/.npm` for `apps/editor/package-lock.json`, missing `apps/editor/build/package-lock.json` entirely and providing no cache for compiled `node_modules/` or Electron headers.

## 2.2 Proposed Solution

Implement per-platform caching across the CI workflow:

1. **Extend npm tarball cache** — Fix `cache-dependency-path` in both `build-macos` and `build-windows` jobs to cover both `apps/editor/package-lock.json` AND `apps/editor/build/package-lock.json`. This caches tarballs for the build toolchain, eliminating re-downloads.

2. **Cache compiled node_modules** — Add `actions/cache` step to cache `apps/editor/node_modules/` and `apps/editor/build/node_modules/` keyed on OS + architecture + combined lock file hash. Pass cache-hit status (`CI_DEPS_READY` env var) to the build target.

3. **Cache Electron gyp headers** — Add `actions/cache` step for `~/.electron-gyp` (macOS) and `~/AppData/Roaming/electron-gyp` (Windows), keyed on OS + architecture + Electron version hash.

4. **Conditional npm ci** — Modify Makefile `build-core` to check `CI_DEPS_READY=true` and skip `npm ci` when `node_modules/` are already cached, using a shell conditional:
```makefile
if [ "$$CI_DEPS_READY" = "true" ]; then \
echo "==> Skipping npm ci (node_modules cache hit)"; \
else \
( npm ci --ignore-scripts & (cd build && npm ci --ignore-scripts) & wait ); \
fi && \
```

### Architecture

Cache keys guarantee platform isolation:
- `node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles(...) }}`
- macOS arm64: `node-modules-macOS-arm64-<hash>`
- macOS x64: `node-modules-macOS-x64-<hash>`
- Windows x64: `node-modules-Windows-x64-<hash>`

- `electron-gyp-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/editor/package.json') }}`
- Ensures Electron version changes invalidate the cache

Fallback via `restore-keys` allows partial hits on `runner.os-runner.arch-` when lock files change slightly.

## 2.3 Acceptance Criteria

- [ ] Makefile `build-core` respects `CI_DEPS_READY` env var and skips `npm ci` when set to `true`
- [ ] `build-macos` job caches both `package-lock.json` files in npm tarball cache
- [ ] `build-macos` job caches `node_modules/` directories with per-arch key
- [ ] `build-macos` job caches `~/.electron-gyp` with Electron version key
- [ ] `build-windows` job has identical cache setup (different header path)
- [ ] First run on `ci-test` branch: creates cache entries
- [ ] Second run on `ci-test` branch: cache hits and `npm ci` is skipped (confirmed in logs)
- [ ] Windows build total time is measurably lower on warm cache (target: >30% reduction)
- [ ] No regression: build succeeds identically on cold cache (no cache entry)

## 2.4 Technical Considerations

- **Actions/cache v4**: Uses GitHub-provided cache storage (5 GB per repo, LRU eviction). Paths and keys must exactly match across runs for hits.
- **Shell variable escaping**: Makefile uses `$$CI_DEPS_READY` to ensure shell-level variable expansion, not Make-level.
- **node_modules deletion by npm ci**: The conditional skip is necessary because `npm ci` always deletes `node_modules/` before installing. Caching `node_modules/` only helps if `npm ci` is skipped.
- **Extension node_modules**: Not cached (too granular, installed per-extension via `xargs -P8` in `postinstall.js`). Cache focuses on critical path: main editor + build toolchain.
- **Cross-platform header paths**:
- macOS: `~/.electron-gyp`
- Windows (bash shell): `~/AppData/Roaming/electron-gyp`

## 2.5 Dependencies

- None. All changes are in CI/CD configuration and Makefile (non-blocking for code changes).

## 2.6 Constraints & Non-Goals

- **Constraint**: Cache storage is limited (5 GB per repo). If cache grows beyond this, GitHub Actions LRU eviction takes effect. Monitor cache size via GHA Settings > Actions > General > Caches.
- **Non-goal**: Do not cache `~/.npm` (setup-node does this already); do not cache extension-specific node_modules (too granular).
- **Non-goal**: Do not modify package.json or Makefile scripts; only add conditional logic.

## 2.7 Success Metrics

- **Cache hit rate**: On repeated runs of the same branch, >90% cache hit rate for `node_modules` (confirmed via GHA cache analytics).
- **Build time savings**:
- Windows: baseline ~20-25 min (cold), target ~15-18 min (warm, -25% to -35%)
- macOS: baseline ~15-20 min per arch (cold), target ~12-16 min (warm)
- **No build failures**: All platforms continue to build successfully on cold cache (first run, no entries).

---

## Tasks

### Task 1: Modify Makefile build-core

**Status**: TODO
**Depends on**: None

- [ ] Subtask 1.1: Add conditional check for `CI_DEPS_READY` in `build-core` target
- **Objective**: Wrap the parallel `npm ci` block with a shell `if` statement that skips when `CI_DEPS_READY=true`
- **Test**: Run `make build-core` locally with `CI_DEPS_READY=true` and confirm `npm ci` message does not appear
- **Depends on**: None

### Task 2: Update build-macos workflow job

**Status**: TODO
**Depends on**: Task 1

- [ ] Subtask 2.1: Fix `cache-dependency-path` in Setup Node.js
- **Objective**: Add `apps/editor/build/package-lock.json` to the multi-line path list
- **Test**: Validate YAML syntax
- **Depends on**: None

- [ ] Subtask 2.2: Add Cache node_modules step
- **Objective**: Insert after Setup Node.js, before Stamp release version. Use OS+arch-specific key with hashFiles.
- **Test**: Run on ci-test branch, confirm GHA shows cache step
- **Depends on**: Subtask 2.1

- [ ] Subtask 2.3: Add Cache Electron gyp headers step
- **Objective**: Insert after Cache node_modules. Cache `~/.electron-gyp` with Electron version key.
- **Test**: First run creates cache entry; inspect GHA cache storage
- **Depends on**: Subtask 2.2

- [ ] Subtask 2.4: Pass CI_DEPS_READY to Build macOS step
- **Objective**: Add `CI_DEPS_READY: ${{ steps.cache-node-modules.outputs.cache-hit }}` to env
- **Test**: On second run, confirm logs show "Skipping npm ci" message
- **Depends on**: Subtask 2.3

### Task 3: Update build-windows workflow job

**Status**: TODO
**Depends on**: Task 2

- [ ] Subtask 3.1–3.4: Repeat identical changes to build-windows job
- **Objective**: Mirror build-macos changes (different Electron header path for Windows)
- **Test**: Validate YAML; run on ci-test, confirm cache behavior
- **Depends on**: Subtask 2.4

### Task 4: Test on ci-test branch

**Status**: TODO
**Depends on**: Task 3

- [ ] Subtask 4.1: Push to ci-test and run first build
- **Objective**: Populate cache (no hits expected)
- **Test**: Workflow completes; GHA shows cache created entries
- **Depends on**: Task 3

- [ ] Subtask 4.2: Re-run same branch
- **Objective**: Trigger cache hits
- **Test**: Logs show "Skipping npm ci" message in build-core step; cache-hit: true for both cache steps
- **Depends on**: Subtask 4.1

- [ ] Subtask 4.3: Measure build time before/after
- **Objective**: Document wall-clock time for Windows job (cold vs warm cache)
- **Test**: Compare job duration from first vs second run
- **Depends on**: Subtask 4.2

## Notes

- Cache keys use `hashFiles()` to detect lock file changes. If lock files change, cache is automatically invalidated.
- Extensions node_modules (installed via `postinstall.js`) are NOT cached to keep cache size manageable. These install quickly in parallel (xargs -P8).
- The `restore-keys` fallback allows partial cache hits when lock files change slightly (e.g., one dependency version bump).

## Root Cause & Fix

**Root Cause**:
- npm tarball cache only covered main `package-lock.json`, not the separate `build/` subdirectory.
- `node_modules/` was never cached, requiring full recompilation of native modules (Electron headers + `build_from_source=true`).
- Electron gyp headers were re-downloaded on every run (~100 MB waste).

**Fix**:
- Extended caching to all lock files and compiled artifacts.
- Added conditional `npm ci` skip to avoid re-deleting cached `node_modules/`.
- Ensured cache keys are platform-specific (OS + arch) to prevent cross-platform binary incompatibility.
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,61 @@

All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.

## [3.6.1](https://github.com/asieduernest12/occ/compare/v3.6.0...v3.6.1) (2026-04-17)


### Bug Fixes

* **macos-ci:** publish unsigned artifacts when signing secrets missing ([1d5c5b9](https://github.com/asieduernest12/occ/commit/1d5c5b9205c06639bb01a0f26ec1df739c2042a8))

## [3.6.0](https://github.com/asieduernest12/occ/compare/v3.5.4...v3.6.0) (2026-04-17)


### Features

* **ci:** per-platform npm and electron caching for Windows & macOS ([797f11f](https://github.com/asieduernest12/occ/commit/797f11f314b54950583545c0bc4cac34b8f41a04))
* **ci:** skip macOS signing/notarization when secrets missing ([45afba1](https://github.com/asieduernest12/occ/commit/45afba1b282e0e2b421c5966ae83a77ae3f9989e))


### Bug Fixes

* **native-modules:** add explicit npm rebuild on Windows for vscode-policy-watcher.node ([cb79286](https://github.com/asieduernest12/occ/commit/cb792862fa5df6671973ee0f1c396437dc9e4fa7))
* **ticket-039:** apply branded icons across all platforms ([77d7676](https://github.com/asieduernest12/occ/commit/77d7676e7549ccb3ff61fe97a75444108bdb052c))
* **ticket-039:** remove stale branding across all platform manifests ([e0d8f06](https://github.com/asieduernest12/occ/commit/e0d8f063804a2000b0e7e1552dba71d868039edd))
* **ticket-039:** replace web server icons and fix manifest branding ([152433e](https://github.com/asieduernest12/occ/commit/152433eac56eeb1493376ee53d8a924422124f5c))
* **windows-build:** ensure native modules rebuild even on npm cache hit ([46f894e](https://github.com/asieduernest12/occ/commit/46f894e40bee8396b21cd8e7698a2085a9aa05fb))

## [3.6.0](https://github.com/asieduernest12/occ/compare/v3.5.4...v3.6.0) (2026-04-17)


### Features

* **ci:** per-platform npm and electron caching for Windows & macOS ([797f11f](https://github.com/asieduernest12/occ/commit/797f11f314b54950583545c0bc4cac34b8f41a04))
* **ci:** skip macOS signing/notarization when secrets missing ([45afba1](https://github.com/asieduernest12/occ/commit/45afba1b282e0e2b421c5966ae83a77ae3f9989e))


### Bug Fixes

* **native-modules:** add explicit npm rebuild on Windows for vscode-policy-watcher.node ([cb79286](https://github.com/asieduernest12/occ/commit/cb792862fa5df6671973ee0f1c396437dc9e4fa7))
* **ticket-039:** apply branded icons across all platforms ([77d7676](https://github.com/asieduernest12/occ/commit/77d7676e7549ccb3ff61fe97a75444108bdb052c))
* **ticket-039:** remove stale branding across all platform manifests ([e0d8f06](https://github.com/asieduernest12/occ/commit/e0d8f063804a2000b0e7e1552dba71d868039edd))
* **ticket-039:** replace web server icons and fix manifest branding ([152433e](https://github.com/asieduernest12/occ/commit/152433eac56eeb1493376ee53d8a924422124f5c))
* **windows-build:** ensure native modules rebuild even on npm cache hit ([46f894e](https://github.com/asieduernest12/occ/commit/46f894e40bee8396b21cd8e7698a2085a9aa05fb))

## [3.5.4](https://github.com/asieduernest12/occ/compare/v3.5.3...v3.5.4) (2026-04-16)

## [3.5.5](https://github.com/asieduernest12/occ/compare/v3.5.4...v3.5.5) (2026-04-17)


### Bug Fixes

* **native-modules:** add explicit npm rebuild on Windows for vscode-policy-watcher.node ([cb79286](https://github.com/asieduernest12/occ/commit/cb792862fa5df6671973ee0f1c396437dc9e4fa7))
* **ticket-039:** apply branded icons across all platforms ([77d7676](https://github.com/asieduernest12/occ/commit/77d7676e7549ccb3ff61fe97a75444108bdb052c))
* **ticket-039:** remove stale branding across all platform manifests ([e0d8f06](https://github.com/asieduernest12/occ/commit/e0d8f063804a2000b0e7e1552dba71d868039edd))
* **ticket-039:** replace web server icons and fix manifest branding ([152433e](https://github.com/asieduernest12/occ/commit/152433eac56eeb1493376ee53d8a924422124f5c))

## [3.5.4](https://github.com/asieduernest12/occ/compare/v3.5.3...v3.5.4) (2026-04-16)

## [3.5.4](https://github.com/asieduernest12/occ/compare/v3.5.3...v3.5.4) (2026-04-16)

## [3.5.3](https://github.com/asieduernest12/occ/compare/v3.4.3...v3.5.3) (2026-04-16)
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ build-core:
cd $(PROJECT_ROOT)/apps/editor && \
export NODE_OPTIONS="--max-old-space-size=7168" && \
echo "==> Install editor + build dependencies (parallel)" && \
( npm ci --ignore-scripts & (cd build && npm ci --ignore-scripts) & wait ) && \
if [ "$$CI_DEPS_READY" != "true" ]; then \
( npm ci --ignore-scripts & (cd build && npm ci --ignore-scripts) & wait ); \
else \
echo "==> Skipping npm ci (node_modules cache hit)"; \
fi && \
echo "==> Rebuild native modules for Electron ($(ELECTRON_ARCH))" && \
npx --yes @electron/rebuild -v 34.3.2 -a $(ELECTRON_ARCH) && \
echo "==> Patch compilation.js" && \
node -e " \
const fs = require('fs'); \
Expand Down
Loading
Loading