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:
1 parent
f9c9754a16
commit
b8cb56d204
1 file changed
+82
-10
+82
-10
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user