From 49290feecd173075fea4e3aa3b4df80a8f062c16 Mon Sep 17 00:00:00 2001 From: Muthiah Muralidharan Date: Mon, 12 Oct 2026 12:03:20 +0530 Subject: [PATCH] fix(install): ship and select Linux musl binaries --- .github/workflows/ci.yml | 6 ++ .github/workflows/install-smoke.yml | 30 +++++++++- .github/workflows/release-please.yml | 6 ++ README.md | 2 + install.sh | 26 ++++++++- src/lib/update/index.ts | 8 ++- src/tests/integration/install-sh.test.ts | 71 ++++++++++++++++++++++++ src/tests/unit/lib/update/index.test.ts | 22 +++++++- src/tools/build.ts | 7 ++- 9 files changed, 171 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e8c3db..3f13e42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,6 +105,8 @@ jobs: - bun-darwin-x64 - bun-linux-arm64 - bun-linux-x64 + - bun-linux-arm64-musl + - bun-linux-x64-musl - bun-windows-x64 include: - target: bun-darwin-arm64 @@ -115,6 +117,10 @@ jobs: outfile: chargebee-cli-linux-arm64 - target: bun-linux-x64 outfile: chargebee-cli-linux-x64 + - target: bun-linux-arm64-musl + outfile: chargebee-cli-linux-arm64-musl + - target: bun-linux-x64-musl + outfile: chargebee-cli-linux-x64-musl - target: bun-windows-x64 outfile: chargebee-cli-windows-x64.exe diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 8dee5e9..a415e46 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -24,6 +24,32 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + alpine-install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: "1.4.2" + + - run: bun install --frozen-lockfile + + - name: Build and install the musl binary on Alpine + run: | + bun run build --target=bun-linux-x64-musl + docker run --rm -v "$PWD:/work" -w /work alpine:3.20 sh -c ' + set -eu + apk add --no-cache bash libstdc++ libgcc + export CI=1 CHARGEBEE_CLI_NO_ONBOARDING=1 + export CHARGEBEE_CLI_INSTALL_FILE=/work/dist/chargebee-cli-linux-x64-musl + export CHARGEBEE_CLI_BIN_DIR=/tmp/cb-bin CHARGEBEE_CONFIG_DIR=/tmp/cb-config + bash install.sh + export PATH=/tmp/cb-bin:$PATH + bash scripts/install-smoke-assert.sh + test "$(cat /tmp/cb-config/install-method)" = github + ' + github-install: strategy: fail-fast: false @@ -148,12 +174,13 @@ jobs: required-checks: name: Install required checks if: ${{ always() }} - needs: [github-install, npm-install, windows-install] + needs: [alpine-install, github-install, npm-install, windows-install] runs-on: ubuntu-latest steps: - name: Verify required job results env: GITHUB_INSTALL_RESULT: ${{ needs.github-install.result }} + ALPINE_INSTALL_RESULT: ${{ needs.alpine-install.result }} NPM_INSTALL_RESULT: ${{ needs.npm-install.result }} WINDOWS_INSTALL_RESULT: ${{ needs.windows-install.result }} run: | @@ -165,5 +192,6 @@ jobs: fi } require_result github-install "$GITHUB_INSTALL_RESULT" success + require_result alpine-install "$ALPINE_INSTALL_RESULT" success require_result npm-install "$NPM_INSTALL_RESULT" success require_result windows-install "$WINDOWS_INSTALL_RESULT" success diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 2f08549..58f8938 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -75,6 +75,8 @@ jobs: - bun-darwin-x64 - bun-linux-arm64 - bun-linux-x64 + - bun-linux-arm64-musl + - bun-linux-x64-musl - bun-windows-x64 include: - target: bun-darwin-arm64 @@ -85,6 +87,10 @@ jobs: outfile: chargebee-cli-linux-arm64 - target: bun-linux-x64 outfile: chargebee-cli-linux-x64 + - target: bun-linux-arm64-musl + outfile: chargebee-cli-linux-arm64-musl + - target: bun-linux-x64-musl + outfile: chargebee-cli-linux-x64-musl - target: bun-windows-x64 outfile: chargebee-cli-windows-x64.exe steps: diff --git a/README.md b/README.md index 33cf9e6..86fa3c9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ The Chargebee CLI is available for macOS, Linux, and Windows. curl -fsSL https://raw.githubusercontent.com/chargebee/cli/main/install.sh | bash ``` +On Alpine Linux, install the runtime libraries first: `apk add --no-cache libstdc++ libgcc`. The installer then selects the musl build automatically. Other Linux distributions use the standard Linux build. + If you accept the skill installation prompt, the installer adds the skill for every detected coding agent. To choose agents yourself, answer No and then run `chargebee skills add --global --agent cursor` (repeat `--agent` for other agents). **Windows** (PowerShell) diff --git a/install.sh b/install.sh index c2080bb..5e799e8 100755 --- a/install.sh +++ b/install.sh @@ -186,7 +186,31 @@ main() { mkdir -p "$BIN_DIR" fi - local ASSET="chargebee-cli-${OS}-${ARCH}" + local LIBC_SUFFIX="" + if [[ "$OS" == "linux" ]]; then + local LIBC="${CHARGEBEE_CLI_LIBC:-}" + if [[ -z "$LIBC" ]]; then + if ls /lib/ld-musl-*.so.1 >/dev/null 2>&1 || (ldd --version 2>&1 | grep -qi musl); then + LIBC="musl" + else + LIBC="glibc" + fi + fi + case "$LIBC" in + musl) + LIBC_SUFFIX="-musl" + if [[ -z "$(find /lib /usr/lib -name 'libstdc++.so.6' -print -quit 2>/dev/null)" ]] || + [[ -z "$(find /lib /usr/lib -name 'libgcc_s.so.1' -print -quit 2>/dev/null)" ]]; then + echo "The Chargebee CLI needs libstdc++ and libgcc on Alpine:" >&2 + echo " apk add --no-cache libstdc++ libgcc" >&2 + exit 1 + fi + ;; + glibc) ;; + *) echo "Error: CHARGEBEE_CLI_LIBC must be musl or glibc" >&2; exit 1 ;; + esac + fi + local ASSET="chargebee-cli-${OS}-${ARCH}${LIBC_SUFFIX}" # Show current version if already installed local PREV_VERSION="" diff --git a/src/lib/update/index.ts b/src/lib/update/index.ts index c4d15ae..dfe6e76 100644 --- a/src/lib/update/index.ts +++ b/src/lib/update/index.ts @@ -340,9 +340,13 @@ function reportUpdated(currentVersion: string, probe: () => ReturnType { diff --git a/src/tests/integration/install-sh.test.ts b/src/tests/integration/install-sh.test.ts index dcc4b75..b08878a 100644 --- a/src/tests/integration/install-sh.test.ts +++ b/src/tests/integration/install-sh.test.ts @@ -518,6 +518,77 @@ printf '200' }); }); +describe.skipIf(process.platform === "win32")("install.sh Linux libc selection", () => { + function fakeLinux(root: string, withLibraries: boolean): { bin: string; log: string } { + const bin = join(root, "fake-bin"); + const log = join(root, "curl.log"); + mkdirSync(bin); + writeFakeCurl(bin, log, '{"tag_name":"v9.9.9"}'); + writeFileSync(join(bin, "uname"), "#!/bin/sh\nif [ \"$1\" = -s ]; then echo Linux; else echo x86_64; fi\n", { mode: 0o755 }); + writeFileSync(join(bin, "ldd"), "#!/bin/sh\necho musl libc\n", { mode: 0o755 }); + writeFileSync(join(bin, "find"), withLibraries + ? "#!/bin/sh\necho /usr/lib/$4\n" + : "#!/bin/sh\nexit 0\n", { mode: 0o755 }); + return { bin, log }; + } + + it("downloads the musl asset when Linux uses musl", () => { + const root = mkdtempSync(join(tmpdir(), "cb-install-musl-")); + try { + const { bin, log } = fakeLinux(root, true); + const result = runInstall({ + PATH: `${bin}:${SYSTEM_PATH}`, + HOME: root, + CHARGEBEE_CLI_BIN_DIR: join(root, "bin"), + CHARGEBEE_CONFIG_DIR: join(root, "config"), + CHARGEBEE_CLI_SKIP_CHECKSUM: "1", + }); + expect(result.exitCode).toBe(0); + expect(readFileSync(log, "utf8")).toContain("chargebee-cli-linux-x64-musl"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("honors the glibc override", () => { + const root = mkdtempSync(join(tmpdir(), "cb-install-glibc-")); + try { + const { bin, log } = fakeLinux(root, false); + const result = runInstall({ + PATH: `${bin}:${SYSTEM_PATH}`, + HOME: root, + CHARGEBEE_CLI_BIN_DIR: join(root, "bin"), + CHARGEBEE_CONFIG_DIR: join(root, "config"), + CHARGEBEE_CLI_SKIP_CHECKSUM: "1", + CHARGEBEE_CLI_LIBC: "glibc", + }); + expect(result.exitCode).toBe(0); + expect(readFileSync(log, "utf8")).toContain("chargebee-cli-linux-x64"); + expect(readFileSync(log, "utf8")).not.toContain("chargebee-cli-linux-x64-musl"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("explains missing Alpine runtime libraries before downloading", () => { + const root = mkdtempSync(join(tmpdir(), "cb-install-musl-missing-")); + try { + const { bin, log } = fakeLinux(root, false); + const result = runInstall({ + PATH: `${bin}:${SYSTEM_PATH}`, + HOME: root, + CHARGEBEE_CLI_BIN_DIR: join(root, "bin"), + CHARGEBEE_CONFIG_DIR: join(root, "config"), + }); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("apk add --no-cache libstdc++ libgcc"); + expect(existsSync(log)).toBe(false); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); + describe.skipIf(process.platform === "win32")("install.sh checksum verification", () => { function setup(): { root: string; fakeBin: string; binDir: string; configDir: string; log: string } { const root = mkdtempSync(join(tmpdir(), "cb-install-sh-checksum-")); diff --git a/src/tests/unit/lib/update/index.test.ts b/src/tests/unit/lib/update/index.test.ts index c989961..115a526 100644 --- a/src/tests/unit/lib/update/index.test.ts +++ b/src/tests/unit/lib/update/index.test.ts @@ -107,8 +107,10 @@ describe("githubAssetName", () => { it("matches the release asset naming used by install.sh / install.ps1", () => { expect(githubAssetName("darwin", "arm64")).toBe("chargebee-cli-darwin-arm64"); expect(githubAssetName("darwin", "x64")).toBe("chargebee-cli-darwin-x64"); - expect(githubAssetName("linux", "x64")).toBe("chargebee-cli-linux-x64"); - expect(githubAssetName("linux", "arm64")).toBe("chargebee-cli-linux-arm64"); + expect(githubAssetName("linux", "x64", "glibc")).toBe("chargebee-cli-linux-x64"); + expect(githubAssetName("linux", "arm64", "glibc")).toBe("chargebee-cli-linux-arm64"); + expect(githubAssetName("linux", "x64", "musl")).toBe("chargebee-cli-linux-x64-musl"); + expect(githubAssetName("linux", "arm64", "musl")).toBe("chargebee-cli-linux-arm64-musl"); expect(githubAssetName("win32", "x64")).toBe("chargebee-cli-windows-x64.exe"); }); }); @@ -469,6 +471,8 @@ describe("update action (mocked spawn + fetch)", () => { "chargebee-cli-darwin-x64", "chargebee-cli-linux-arm64", "chargebee-cli-linux-x64", + "chargebee-cli-linux-arm64-musl", + "chargebee-cli-linux-x64-musl", "chargebee-cli-windows-x64.exe", ] .map((name) => `${ASSET_HASH} ${name}`) @@ -805,6 +809,20 @@ describe("update action (mocked spawn + fetch)", () => { expect(readInstallMethod()).toBe("github"); }); + it("keeps a musl installation on the musl release asset", async () => { + const previousLibc = process.env.CHARGEBEE_CLI_LIBC; + process.env.CHARGEBEE_CLI_LIBC = "musl"; + try { + await runUpdate(); + const assetReq = fetched.find((f) => f.url.includes("/releases/download/")); + expect(assetReq?.url).toContain(`chargebee-cli-linux-${process.arch}-musl`); + expect(readFileSync(execPath, "utf8")).toBe(ASSET_BYTES); + } finally { + if (previousLibc === undefined) delete process.env.CHARGEBEE_CLI_LIBC; + else process.env.CHARGEBEE_CLI_LIBC = previousLibc; + } + }); + it("prefers the release's API asset URL (works for private repos) with an octet-stream Accept", async () => { stubFetch([ { diff --git a/src/tools/build.ts b/src/tools/build.ts index f346ef9..6e85369 100644 --- a/src/tools/build.ts +++ b/src/tools/build.ts @@ -3,7 +3,7 @@ * Compiles the standalone binary. * * Default: one binary for the host platform at `./chargebee-cli`. `--all` - * cross-compiles the five release artifacts into `dist/`; `--target=` builds + * cross-compiles the seven release artifacts into `dist/`; `--target=` builds * just one of them (CI fans these out across runners). * * The version is baked in via `process.env.VERSION` — without it the binary @@ -20,6 +20,8 @@ const TARGETS = [ { target: "bun-darwin-x64", outfile: `dist/${BINARY}-darwin-x64` }, { target: "bun-linux-arm64", outfile: `dist/${BINARY}-linux-arm64` }, { target: "bun-linux-x64", outfile: `dist/${BINARY}-linux-x64` }, + { target: "bun-linux-arm64-musl", outfile: `dist/${BINARY}-linux-arm64-musl` }, + { target: "bun-linux-x64-musl", outfile: `dist/${BINARY}-linux-x64-musl` }, { target: "bun-windows-x64", outfile: `dist/${BINARY}-windows-x64.exe` }, ] as const; @@ -36,6 +38,9 @@ async function compile(outfile: string, target?: string): Promise { outfile, "--define", `process.env.VERSION='${version}'`, + ...(target?.startsWith("bun-linux-") + ? ["--define", `process.env.CHARGEBEE_CLI_LIBC='${target.endsWith("-musl") ? "musl" : "glibc"}'`] + : []), ...(target ? [`--target=${target}`] : []), ]; const proc = Bun.spawn(["bun", ...args], { stdout: "inherit", stderr: "inherit" });