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.
This commit is contained in:
Vlad Mocanu committed 2026-02-28 09:08:26 +02:00
1 parent f9c9754a16
commit b8cb56d204
1 file changed
+82 -10
+82 -10
View File
@@ -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<void> {
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<boolean> {
for (let attempt = 1; attempt <= 3; attempt++) {
console.log(`Installation attempt ${attempt}...`);
console.log(`Native installation attempt ${attempt}...`);
try {
await new Promise<void>((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<void> {
});
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<void> {
try {
await new Promise<void>((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}`);
}
}
/**