diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 1fd90c71..d26aa024 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -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 @@ -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/') @@ -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 }} @@ -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 @@ -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 }} @@ -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 @@ -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/') @@ -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 != '' }} diff --git a/.tickets/ticket-050-ci-build-caching/prd.md b/.tickets/ticket-050-ci-build-caching/prd.md new file mode 100644 index 00000000..6d548d83 --- /dev/null +++ b/.tickets/ticket-050-ci-build-caching/prd.md @@ -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-` + - macOS x64: `node-modules-macOS-x64-` + - Windows x64: `node-modules-Windows-x64-` + +- `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. diff --git a/CHANGELOG.md b/CHANGELOG.md index eed10173..66cf512f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Makefile b/Makefile index 2a26c5a5..479518b4 100644 --- a/Makefile +++ b/Makefile @@ -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'); \ diff --git a/apps/editor/build/npm/postinstall.js b/apps/editor/build/npm/postinstall.js index 7a72694d..96dd751e 100644 --- a/apps/editor/build/npm/postinstall.js +++ b/apps/editor/build/npm/postinstall.js @@ -175,6 +175,27 @@ function removeParcelWatcherPrebuild(dir) { } } +/** + * Rebuild native modules explicitly on Windows. + * @param {string} dir + * @param {*} [opts] + */ +function npmRebuild(dir, opts) { + opts = { + env: { ...process.env }, + ...(opts ?? {}), + cwd: dir, + stdio: 'inherit', + shell: true + }; + + // Only rebuild on Windows where build_from_source might need explicit rebuild + if (process.platform === 'win32') { + log(dir, 'Rebuilding native modules...'); + run(npm, ['rebuild'], opts); + } +} + // ── Main ──────────────────────────────────────────────────────────────────── async function main() { @@ -253,6 +274,13 @@ async function main() { const elapsed = ((Date.now() - t0) / 1000).toFixed(1); log('.', `Parallel install done — ${plainDirs.length} dirs in ${elapsed}s`); + // ── Native module rebuild (Windows) ───────────────────────────────────── + // On Windows, explicitly rebuild native modules after npm install + // to ensure modules like vscode-policy-watcher.node are compiled + if (process.platform === 'win32') { + npmRebuild('', { env: { ...process.env } }); + } + // ── Git config ────────────────────────────────────────────────────────── try { cp.execSync('git config pull.rebase merges', { cwd: root }); diff --git a/apps/editor/package-lock.json b/apps/editor/package-lock.json index 99bea4dc..d19edf01 100644 --- a/apps/editor/package-lock.json +++ b/apps/editor/package-lock.json @@ -1,11 +1,11 @@ { - "name": "code-oss-dev", + "name": "occode-dev", "version": "1.99.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "code-oss-dev", + "name": "occode-dev", "version": "1.99.3", "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index 6f7e3133..79310507 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "occode", - "version": "3.5.4", + "version": "3.6.1", "private": true, "description": "OCcode — branded cross-platform IDE wrapper with OpenClaw extension", "workspaces": [ diff --git a/version.txt b/version.txt index e5b8a844..d1428a7e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.5.4 \ No newline at end of file +3.6.1 \ No newline at end of file