Skip to content

Fix GitHub Actions#86

Open
JustLikeCheese wants to merge 6 commits into
Haleclipse:masterfrom
JustLikeCheese:fix/ci-build-resilience
Open

Fix GitHub Actions#86
JustLikeCheese wants to merge 6 commits into
Haleclipse:masterfrom
JustLikeCheese:fix/ci-build-resilience

Conversation

@JustLikeCheese

@JustLikeCheese JustLikeCheese commented Jun 16, 2026

Copy link
Copy Markdown

修复 Linux 平台的 GitHub Actions
修复 Windows MSIX %-encoded 路径解码问题
增强 getWindowsVersion(),加了 4 次重试
Ciallo~(∠・ω< )⌒★!

Linux 的平台喔还没测试,Linux 佬可以试试
https://github.com/JustLikeCheese/CodexDesktop-Rebuild/actions/runs/27545680480

Summary by Sourcery

Fix CI build failures on Linux and Windows by replacing the Linux packaging pipeline with a custom Electron-based builder and improving Windows packaging and version checks.

New Features:

  • Introduce a dedicated Linux build script that downloads Electron templates, packs the app into app.asar, copies Linux resources, and invokes Electron Forge makers for deb, rpm, and zip artifacts.

Bug Fixes:

  • Decode percent-encoded paths in Windows MSIX extractions to align the filesystem with asar headers and prevent unpacked binary resolution failures.
  • Make Linux builds fail fast when required @cometix/codex or Linux ripgrep vendor binaries are missing instead of silently shipping macOS binaries in Linux packages.
  • Avoid treating missing main bundles as hard errors in patch scripts when sync-upstream has not populated src/.

Enhancements:

  • Add retry logic to getWindowsVersion() in the sync script to improve resilience of Windows version detection in CI.
  • Adjust Linux build npm scripts to use architecture-specific native rebuilds and the new Linux build pipeline.
  • Update better-sqlite3 to a newer version.

CI:

  • Tighten GitHub Actions build workflows by installing all required Linux build dependencies, enforcing artifact presence with if-no-files-found=error, and wiring the workflow to the new Linux build process.

The Windows MSIX is extracted via 7z, which preserves URL-encoded names
like %40worklouder/ for the @worklouder scoped npm package. The asar
header still references the originals, so the subsequent asar extract
crashes with ENOENT when resolving unpacked .node binaries.

Walk app.asar.unpacked/ after extraction and decodeURIComponent every
%XX-encoded entry name. Logs how many it renamed.

Also pulls package-lock.json up to the current package.json version so
the lockfile doesn't churn on every npm install.
Electron 42 ships a v8 that changed v8::External::Value() to require an
ExternalPointerTypeTag argument. better-sqlite3 <= 12.9.0 calls it
0-arg and fails to compile during electron-rebuild on Linux:

  ../src/util/macros.cpp:30:76: error: no matching function for call
  to 'v8::External::Value()'

12.10.1 fixes it (release notes: "Fix V8 external API usage for
Electron 42 by @tstone-1 in #1475"). No code changes here — just a
caret bump, lockfile follows.
Linux build was silently passing with zero artifacts: electron-forge make
exits 0 after the Packaging stage's "Finalizing package" line without ever
reaching the Make stage, and upload-artifact@v7 defaults if-no-files-found
to warn so the job stays green. We can't reproduce the forge silent-exit
locally on Windows, so this commit ships the minimum changes needed to
make the next CI run produce actionable evidence and refuse to ship broken
packages:

- DEBUG=electron-forge:*,electron-packager,@electron/packager* on the Linux
  build step so the next run logs what forge is actually doing between
  package and make.
- Post-build sanity step that lists out/ and out/make/ contents and fails
  the job hard if zero .deb/.rpm/.zip were produced (replacing the silent
  upload warning with a loud, structured ::error).
- if-no-files-found: error on every upload-artifact step (Linux, macOS,
  Windows) so any future zero-files scenario fails red.
- Expand Linux apt install with build-essential, python3, libudev-dev so
  the better-sqlite3 / node-pty source-compile fallback works if
  prebuild-install ever misses.
- Split rebuild:native into per-arch scripts (rebuild:native:x64 /
  :arm64) and wire build:linux-{x64,arm64} to the matching one. Defensive
  on x64; correctness fix for the arm64 leg, which previously ran
  electron-rebuild without --arch on an x64 host and produced wrong-arch
  native .node files inside the arm64 package.
- Harden prepare-src.js: on Linux, refuse to fall through when the
  @cometix/codex or rg vendor binary can't be resolved. Previously we
  logged "[!] keeping upstream" and shipped the macOS Mach-O binary
  inside a Linux .deb/.rpm.
…cript

electron-forge make exits cleanly with status 0 in CI between the Packaging
stage's "Finalizing package" listr line and ever reaching the Make stage —
no error, no unhandledRejection, no stack trace. The previous commit's
DEBUG output confirmed @electron/packager logs "Extracting electron.zip"
then the process exits 40ms later without writing anything to out/. Two
runs reproduced this deterministically. Rather than continue chasing a
silent listr2/packager interaction, switch Linux to the same pattern
mac/win already use: a custom build script with explicit promises and
loud error handling.

- New scripts/build-from-upstream-linux.js calls @electron/packager
  directly (no listr2 wrapper, no hidden lifecycle), then invokes
  MakerDeb / MakerRpm / MakerZip via their programmatic API. Per-maker
  errors are isolated and reported; partial-success still exits 1.
- The afterCopy hook inlines forge.config.js's packageAfterCopy logic so
  the same Linux-only file/dir skip rules apply (no Mach-O native dir,
  no app.asar, no .lproj).
- Wires build:linux-{x64,arm64} npm scripts to call the new script
  instead of electron-forge make.
- Drops the post-make diagnostic step from build.yml now that the new
  script's own output is structured and loud. if-no-files-found:error
  on upload-artifact is still the final safety net.

forge.config.js is left untouched — mac/win still go through their
existing build-from-upstream.js path and don't touch forge make either,
so the file only matters for `npm run forge:package` / `forge:make`
direct invocations which we don't use in CI.
… + asar

The previous attempt's @electron/packager call exits the Node process
silently with code 0 in the middle of extractElectronZip() — extract-zip
2.0.1 (the unmaintained upstream archived in 2021) is the culprit. CI
log shows packager logs "Packaging app for platform linux x64 using
electron v42.0.1" then the process ends 37 ms later with no error, no
unhandled rejection, no maker output. Reproduced on three runs.

Rather than fight extract-zip on Node 24, bypass it: download the
Electron Linux template zip via @electron/get directly, extract with
the system unzip CLI (preinstalled on ubuntu-latest), repack src/ to
app.asar via @electron/asar, and copy Linux resources from src/mac-{arch}/
inline. Then invoke maker-deb/rpm/zip programmatically as before. Each
phase logs its progress so any future regression is loud, and `unzip` is
a battle-tested POSIX tool with no Node interop surprises.

No changes to forge.config.js — it's unused on the Linux path now.

apt deps added in the previous commit (build-essential, python3,
libudev-dev) stay for the maker-deb/rpm peer-package compile path. The
`unzip` CLI is preinstalled on ubuntu-latest.
Last run: arm64 RPM is green (3/3), x64 RPM fails on rpmbuild's brp-strip
pass at:
  resources/cua_node/lib/node_modules/@oai/sky/bin/linux/sky_linux_arm64
  /usr/bin/strip: Unable to recognise the format of the input file ...

Upstream Codex ships both sky_linux_x64 and sky_linux_arm64 side-by-side.
The host's x86_64 strip can parse x64 ELF (so arm64 RPMs build fine on
the x64 runner — confirmed in the last run), but not arm64 ELF — so
the x64 RPM blows up. deb and zip don't run a strip pass so they were
fine all along.

Before any maker runs, delete the cross-arch sky binary from
<packageDir>/resources/. The runtime selector only ever invokes
sky_linux_${process.arch}, so the other-arch copy is dead weight on the
end-user machine; removing it also slims the .deb and .zip slightly.
Symmetric: arm64 build drops sky_linux_x64, x64 build drops sky_linux_arm64.
@sourcery-ai

sourcery-ai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors Linux build to bypass @electron/packager on CI, tightens Linux packaging safety, fixes Windows MSIX percent-encoded paths, hardens Windows version detection, and makes the GitHub Actions workflows fail-fast when artifacts or dependencies are missing.

Sequence diagram for Windows syncWin MSIX path decoding

sequenceDiagram
  participant syncWin
  participant decodePercentNames
  participant assembleOutput

  syncWin->>decodePercentNames: decodePercentNames(app.asar.unpacked)
  decodePercentNames-->>syncWin: [percent-encoded paths decoded]
  syncWin->>assembleOutput: assembleOutput(resourcesDir, destDir, Windows)
  assembleOutput-->>syncWin: info
  syncWin-->>syncWin: return info
Loading

File-Level Changes

Change Details Files
Fix Windows MSIX extraction by normalizing percent-encoded file and directory names before assembling output.
  • Introduce a recursive decodePercentNames() helper that scans app.asar.unpacked and renames entries whose names contain %XX sequences using decodeURIComponent.
  • Integrate decodePercentNames() into the Windows sync pipeline so unpacked .node binaries match asar header paths and extraction no longer fails on scoped package directories.
scripts/sync-upstream.js
Improve robustness of Windows version detection in the sync script.
  • Wrap getWindowsVersion() in a retry loop with up to 4 attempts, logging attempt counts and errors.
  • Add exponential backoff-style delays between attempts to mitigate transient failures while preserving existing result structure.
scripts/sync-upstream.js
Rework Linux build pipeline to avoid extract-zip issues on CI and to explicitly rebuild native modules per-arch.
  • Add a new build-from-upstream-linux.js script that downloads Electron using @electron/get, extracts it via the system unzip, packs src/ into app.asar with @electron/asar, copies Linux resources from src/mac-{arch}, strips cross-arch helper binaries, and runs @electron-forge makers for deb, rpm, and zip.
  • Update package.json Linux build scripts to call rebuild:native:x64 / rebuild:native:arm64 and the new build-from-upstream-linux.js instead of electron-forge make directly.
  • Introduce arch-specific electron-rebuild scripts (rebuild:native:x64 and rebuild:native:arm64) while keeping the old rebuild:native as a generic entry point.
scripts/build-from-upstream-linux.js
package.json
Harden Linux packaging so we never ship upstream macOS binaries inside Linux artifacts.
  • In prepare-src.js, when running on Linux, fail the build (process.exit(1)) if the @cometix/codex vendor binary is missing instead of leaving the upstream macOS Mach-O binary in place.
  • Similarly fail when a Linux ripgrep (rg) binary is not found in the vendor directory, instead of silently keeping upstream macOS Mach-O rg.
scripts/prepare-src.js
Make patch scripts no-op instead of hard-failing when src/ hasn’t been populated by sync-upstream.
  • Change patch-copyright.js to log a [skip] message and return when no main bundle is found, instead of treating it as an error and exiting.
  • Apply the same behavior to patch-devtools.js so it also skips gracefully when bundles are absent.
scripts/patch-copyright.js
scripts/patch-devtools.js
Tighten GitHub Actions behavior and Linux CI environment for build jobs.
  • Add if-no-files-found: error to all artifact upload steps for macOS, Windows, and Linux to turn missing artifacts into explicit CI failures.
  • Expand the Linux build job’s apt dependencies to include build-essential, python3, and libudev-dev to support native builds and Electron tooling.
  • Ensure Linux build artifacts (deb/rpm/zip) uploads also fail the job if no files are found.
.github/workflows/build.yml
Align dependency versions with the new Linux build flow.
  • Bump better-sqlite3 to ^12.10.1 to stay current and compatible with the updated native build setup.
  • Regenerate package-lock.json to capture updated dependencies and ensure reproducible installs.
package.json
package-lock.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In decodePercentNames, consider handling the case where the decoded name already exists in the target directory before calling fs.renameSync, otherwise a collision between encoded/decoded names could throw and abort processing of the rest of the tree.
  • In build-from-upstream-linux.js, it might be safer to explicitly check for the presence of the unzip binary (or wrap execFileSync('unzip', ...) with a clearer error) so that CI or local environments without unzip fail with a more actionable message.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `decodePercentNames`, consider handling the case where the decoded name already exists in the target directory before calling `fs.renameSync`, otherwise a collision between encoded/decoded names could throw and abort processing of the rest of the tree.
- In `build-from-upstream-linux.js`, it might be safer to explicitly check for the presence of the `unzip` binary (or wrap `execFileSync('unzip', ...)` with a clearer error) so that CI or local environments without `unzip` fail with a more actionable message.

## Individual Comments

### Comment 1
<location path="scripts/sync-upstream.js" line_range="220-229" />
<code_context>
   return info;
 }

+function decodePercentNames(root) {
+  if (!fs.existsSync(root)) return;
+  let renamed = 0;
+  const walk = (dir) => {
+    for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+      const cur = path.join(dir, e.name);
+      let next = cur;
+      if (/%[0-9A-Fa-f]{2}/.test(e.name)) {
+        try {
+          const decoded = decodeURIComponent(e.name);
+          if (decoded !== e.name) {
+            next = path.join(dir, decoded);
+            fs.renameSync(cur, next);
+            renamed++;
+          }
+        } catch {
+          /* keep as-is on malformed sequences */
+        }
+      }
+      if (e.isDirectory()) walk(next);
+    }
+  };
+  walk(root);
+  if (renamed) console.log(`   [decode] renamed ${renamed} percent-encoded path(s)`);
+}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle potential name collisions when decoding percent-encoded filenames.

On Windows, `fs.renameSync` will throw if the decoded target already exists, which will abort `syncWin`. Consider checking `fs.existsSync(next)` before renaming and either skipping, logging, or resolving the conflict (e.g., by appending a suffix) so a single collision doesn’t stop the whole sync.
</issue_to_address>

### Comment 2
<location path="scripts/build-from-upstream-linux.js" line_range="50-59" />
<code_context>
+function copyLinuxResources(srcDir, destDir) {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Preserve original file permissions instead of chmod 755 for every copied file.

`chmodSync(..., 0o755)` is being applied to all copied files, which incorrectly makes non-executables (e.g., images, text files) executable and may conflict with upstream packaging expectations. Please preserve the source file mode (e.g., via `fs.statSync(srcPath).mode`) and only enforce 755 for known executables, or at least restrict chmod to files that were already executable.

Suggested implementation:

```javascript
      const srcStat = fs.statSync(srcPath);
      let mode = srcStat.mode;

      // If the source file was executable, normalize to 755.
      // Otherwise, preserve the original (non-executable) mode.
      if ((mode & 0o111) !== 0) {
        mode = 0o755;
      }

      fs.chmodSync(destPath, mode);

```

If there are other places in this file (or nearby scripts) that use `fs.chmodSync(..., 0o755)` for copies performed by `copyLinuxResources` or similar helpers, you should apply the same pattern there: compute `srcStat = fs.statSync(srcPath)`, derive `mode` from `srcStat.mode`, and only enforce `0o755` when the source file was already executable.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/sync-upstream.js
Comment on lines +220 to +229
function decodePercentNames(root) {
if (!fs.existsSync(root)) return;
let renamed = 0;
const walk = (dir) => {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const cur = path.join(dir, e.name);
let next = cur;
if (/%[0-9A-Fa-f]{2}/.test(e.name)) {
try {
const decoded = decodeURIComponent(e.name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Handle potential name collisions when decoding percent-encoded filenames.

On Windows, fs.renameSync will throw if the decoded target already exists, which will abort syncWin. Consider checking fs.existsSync(next) before renaming and either skipping, logging, or resolving the conflict (e.g., by appending a suffix) so a single collision doesn’t stop the whole sync.

Comment on lines +50 to +59
function copyLinuxResources(srcDir, destDir) {
if (!fs.existsSync(srcDir)) {
throw new Error(`Source platform dir not found: ${srcDir}`);
}
const skip = new Set(["_asar"]);
for (const f of MACOS_ONLY_FILES) skip.add(f);
for (const d of MACOS_ONLY_DIRS) skip.add(d);

let copied = 0;
const copyDir = (s, d) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Preserve original file permissions instead of chmod 755 for every copied file.

chmodSync(..., 0o755) is being applied to all copied files, which incorrectly makes non-executables (e.g., images, text files) executable and may conflict with upstream packaging expectations. Please preserve the source file mode (e.g., via fs.statSync(srcPath).mode) and only enforce 755 for known executables, or at least restrict chmod to files that were already executable.

Suggested implementation:

      const srcStat = fs.statSync(srcPath);
      let mode = srcStat.mode;

      // If the source file was executable, normalize to 755.
      // Otherwise, preserve the original (non-executable) mode.
      if ((mode & 0o111) !== 0) {
        mode = 0o755;
      }

      fs.chmodSync(destPath, mode);

If there are other places in this file (or nearby scripts) that use fs.chmodSync(..., 0o755) for copies performed by copyLinuxResources or similar helpers, you should apply the same pattern there: compute srcStat = fs.statSync(srcPath), derive mode from srcStat.mode, and only enforce 0o755 when the source file was already executable.

@MAE5blog

Copy link
Copy Markdown

I tested a very similar fix set on a native Ubuntu 24.04 arm64 host and wanted to confirm that the overall direction here is correct.

What matched the root causes we reproduced locally:

  • better-sqlite3@12.4.6 + Electron 42 / V8 14.x failed during electron-rebuild with the SetNativeDataProperty / v8::External API breakage.
  • Linux packaging also needed to avoid carrying the wrong @oai/sky/bin/linux/sky_linux_* binary into the final artifact, otherwise RPM strip would fail on arm64.

What we validated locally:

  • Built 26.609.71450 successfully on native arm64 and produced .deb, .rpm, and .zip artifacts.
  • Switched the machine to the produced build and it is currently running successfully, so this was not just a compile-only check.

Two follow-ups I would still recommend before merge:

  1. package.json still keeps @electron/rebuild at ^3.7.1. The exact pair we validated end-to-end was @electron/rebuild ^4.0.4 + better-sqlite3 ^12.11.1. 12.10.1 may already be enough, but if CI / arm64 still flakes, that version pair is worth aligning to.
  2. Please add at least one post-build runtime smoke for Linux arm64 artifacts. In our local run, build artifacts were green, but Browser / Chrome functionality still depended on shipping Linux-arm64 runtime assets such as resources/node_repl, node_repl-obu, and plugins/openai-bundled/plugins/chrome/extension-host/linux/arm64/extension-host. The packaging fix and the runtime completeness check ended up being two separate steps for us.

Overall: this PR is moving in the right direction and it matches the failures we reproduced locally on arm64.

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.

2 participants