From b8cb56d2043ad296c2298b3b32492cf9f01eb2f0 Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Sat, 28 Feb 2026 09:08:26 +0200 Subject: [PATCH] fix: use npm install for Claude Code CLI on musl/Alpine systems The native binary (install.sh) requires posix_getdents which is only available in Alpine edge (musl 1.2.5-r21+), not in any stable Alpine release. Detect musl upfront via ldd and skip straight to npm install, avoiding 15+ seconds of wasted retries on deterministic failures. On glibc systems the native install path is unchanged with npm as a fallback if it fails. --- src/entrypoints/run.ts | 92 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index 9560832..5cfcc9b 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -8,7 +8,7 @@ import * as core from "@actions/core"; import { dirname } from "path"; -import { spawn } from "child_process"; +import { spawn, spawnSync } from "child_process"; import { appendFile } from "fs/promises"; import { existsSync, readFileSync } from "fs"; import { setupGitHubToken, WorkflowValidationSkipError } from "../github/token"; @@ -57,15 +57,53 @@ async function installClaudeCode(): Promise { const claudeCodeVersion = "2.1.61"; console.log(`Installing Claude Code v${claudeCodeVersion}...`); + // On musl/Alpine systems, the native binary may require a newer musl than + // available (e.g. posix_getdents added in musl 1.2.5-r21, only in Alpine edge). + // Skip straight to npm install to avoid wasting time on deterministic failures. + if (isMusl()) { + console.log( + "Detected musl libc, using npm install (native binary may be incompatible)...", + ); + await npmInstall(claudeCodeVersion); + return; + } + + // On glibc systems, try the native binary install with retry for transient failures. + const nativeInstallSuccess = await tryNativeInstall(claudeCodeVersion); + if (nativeInstallSuccess) return; + + // Fall back to npm if native install fails on glibc too (shouldn't happen normally). + console.log("Native install failed, falling back to npm install..."); + await npmInstall(claudeCodeVersion); +} + +/** + * Detect if the system uses musl libc (Alpine, etc.). + */ +function isMusl(): boolean { + try { + const output = spawnSync("ldd", ["--version"], { encoding: "utf-8" }); + // musl's ldd prints "musl libc" to stderr + return (output.stderr + output.stdout).toLowerCase().includes("musl"); + } catch { + return false; + } +} + +/** + * Try installing Claude Code via the native install script. + * Returns true on success, false on failure. + */ +async function tryNativeInstall(version: string): Promise { for (let attempt = 1; attempt <= 3; attempt++) { - console.log(`Installation attempt ${attempt}...`); + console.log(`Native installation attempt ${attempt}...`); try { await new Promise((resolve, reject) => { const child = spawn( "bash", [ "-c", - `curl -fsSL https://claude.ai/install.sh | bash -s -- ${claudeCodeVersion}`, + `curl -fsSL https://claude.ai/install.sh | bash -s -- ${version}`, ], { stdio: "inherit" }, ); @@ -75,25 +113,59 @@ async function installClaudeCode(): Promise { }); child.on("error", reject); }); - console.log("Claude Code installed successfully"); - // Add to PATH + console.log("Claude Code installed successfully (native)"); const homeBin = `${process.env.HOME}/.local/bin`; const githubPath = process.env.GITHUB_PATH; if (githubPath) { await appendFile(githubPath, `${homeBin}\n`); } process.env.PATH = `${homeBin}:${process.env.PATH}`; - return; - } catch (error) { + return true; + } catch { if (attempt === 3) { - throw new Error( - `Failed to install Claude Code after 3 attempts: ${error}`, - ); + return false; } console.log("Installation failed, retrying..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } } + return false; +} + +/** + * Install Claude Code via npm as a fallback for musl/Alpine systems where + * the native binary may be incompatible (e.g. posix_getdents symbol missing). + */ +async function npmInstall(version: string): Promise { + try { + await new Promise((resolve, reject) => { + const child = spawn( + "npm", + ["install", "-g", `@anthropic-ai/claude-code@${version}`], + { stdio: "inherit" }, + ); + child.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`npm install failed with exit code ${code}`)); + }); + child.on("error", reject); + }); + console.log("Claude Code installed successfully (npm)"); + // Resolve the npm global bin directory and ensure it's in PATH. + // Use `npm prefix -g` + /bin (npm bin -g is deprecated since npm v9). + const npmPrefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf-8" }); + const npmBin = + npmPrefix.status === 0 ? `${npmPrefix.stdout.trim()}/bin` : ""; + if (npmBin) { + const githubPath = process.env.GITHUB_PATH; + if (githubPath) { + await appendFile(githubPath, `${npmBin}\n`); + } + process.env.PATH = `${npmBin}:${process.env.PATH}`; + } + } catch (error) { + throw new Error(`Failed to install Claude Code via npm: ${error}`); + } } /**