Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
30 changes: 29 additions & 1 deletion .github/workflows/install-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand All @@ -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
6 changes: 6 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 musl-based Linux distributions, install the `libstdc++` and `libgcc` runtime libraries with your package manager first. On Alpine, run `apk add --no-cache libstdc++ libgcc`. The installer selects the musl build automatically; glibc-based distributions use the standard Linux build.

The installer can also add the Chargebee skill for detected coding agents. To choose agents manually, decline the prompt and run:

```bash
Expand Down
55 changes: 47 additions & 8 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,37 @@ 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
local LDD_VERSION
LDD_VERSION=$(ldd --version 2>&1 || true)
if echo "$LDD_VERSION" | grep -qi musl; then
LIBC="musl"
elif echo "$LDD_VERSION" | grep -Eqi 'glibc|GNU C Library|GNU libc'; then
LIBC="glibc"
elif ls /lib/ld-musl-*.so.1 >/dev/null 2>&1; 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 musl Linux." >&2
echo "On Alpine, run: 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=""
Expand Down Expand Up @@ -254,8 +284,20 @@ main() {
verify_checksum "$TMP" "$ASSET" "$SUMS_URL"
fi

chmod 0755 "$TMP"
mv "$TMP" "$BIN_DIR/$BIN_NAME"
# Probe the candidate from the destination filesystem before replacing an
# existing installation. A downloaded binary can have a valid checksum yet
# fail to start on this host (for example, a libc mismatch).
local STAGED NEW_VERSION
STAGED=$(mktemp "$BIN_DIR/.chargebee.XXXXXX")
trap 'rm -f "$TMP" "${STAGED:-}"' EXIT
cp "$TMP" "$STAGED"
chmod 0755 "$STAGED"
if ! NEW_VERSION=$("$STAGED" --version 2>/dev/null) || [[ -z "${NEW_VERSION//[[:space:]]/}" ]]; then
echo "Error: downloaded $ASSET cannot start on this system" >&2
exit 1
fi
NEW_VERSION=${NEW_VERSION%%$'\n'*}
mv -f "$STAGED" "$BIN_DIR/$BIN_NAME"

local TARGET_HOME
TARGET_HOME=$(target_home)
Expand All @@ -273,13 +315,10 @@ main() {
(mkdir -p -m 0700 "$MARKER_DIR" && printf 'github\n' > "$MARKER_DIR/install-method") || true
own_as_target_user "$MARKER_ROOT"

local NEW_VERSION
NEW_VERSION=$("$BIN_DIR/$BIN_NAME" --version 2>/dev/null | head -1 || true)

if [[ -n "$PREV_VERSION" ]]; then
echo "Updated: ${PREV_VERSION} → ${NEW_VERSION:-done}"
echo "Updated: ${PREV_VERSION} → ${NEW_VERSION}"
else
echo "Installed: ${NEW_VERSION:-chargebee} → $BIN_DIR/$BIN_NAME"
echo "Installed: ${NEW_VERSION} → $BIN_DIR/$BIN_NAME"
fi

# Warn if BIN_DIR isn't in PATH
Expand Down
8 changes: 6 additions & 2 deletions src/lib/update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,13 @@ function reportUpdated(currentVersion: string, probe: () => ReturnType<SpawnSync
}

/** Release asset name, identical to install.sh / install.ps1. */
export function githubAssetName(os: NodeJS.Platform = platform(), arch: NodeJS.Architecture = process.arch): string {
export function githubAssetName(
os: NodeJS.Platform = platform(),
arch: NodeJS.Architecture = process.arch,
libc: string | undefined = process.env.CHARGEBEE_CLI_LIBC,
): string {
if (os === "win32") return "chargebee-cli-windows-x64.exe";
return `chargebee-cli-${os}-${arch}`;
return `chargebee-cli-${os}-${arch}${os === "linux" && libc === "musl" ? "-musl" : ""}`;
}

async function downloadAsset(url: string): Promise<Buffer> {
Expand Down
144 changes: 144 additions & 0 deletions src/tests/integration/install-sh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,34 @@ main "$@"
});

describe.skipIf(process.platform === "win32")("install.sh snapshot mode", () => {
it("rejects a candidate that cannot start and preserves the installed binary", () => {
const root = mkdtempSync(join(tmpdir(), "cb-install-sh-invalid-"));
try {
const binDir = join(root, "bin");
const configDir = join(root, "config");
mkdirSync(binDir, { recursive: true });
const installed = join(binDir, "chargebee");
writeFileSync(installed, "#!/bin/sh\necho 1.0.0\n", { mode: 0o755 });
const invalid = join(root, "invalid-binary");
writeFileSync(invalid, "not an executable binary\n");

const result = runInstall({
CHARGEBEE_CLI_INSTALL_FILE: invalid,
CHARGEBEE_CLI_BIN_DIR: binDir,
CHARGEBEE_CONFIG_DIR: configDir,
HOME: root,
PATH: `${binDir}:${SYSTEM_PATH}`,
});
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("cannot start on this system");
expect(result.stdout).not.toContain("Updated:");
expect(readFileSync(installed, "utf8")).toContain("echo 1.0.0");
expect(existsSync(join(configDir, "install-method"))).toBe(false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it("copies CHARGEBEE_CLI_INSTALL_FILE into CHARGEBEE_CLI_BIN_DIR and writes github marker", () => {
const root = mkdtempSync(join(tmpdir(), "cb-install-sh-"));
try {
Expand Down Expand Up @@ -490,6 +518,122 @@ printf '200'
});
});

describe.skipIf(process.platform === "win32")("install.sh Linux libc selection", () => {
function fakeLinux(
root: string,
withLibraries: boolean,
lddVersion = "musl libc",
hasMuslLoader = false,
): { 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 '${lddVersion}'\n`, { mode: 0o755 });
if (hasMuslLoader) {
writeFileSync(join(bin, "ls"), "#!/bin/sh\necho /lib/ld-musl-x86_64.so.1\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("selects glibc when ldd reports glibc despite an installed musl loader", () => {
const root = mkdtempSync(join(tmpdir(), "cb-install-glibc-with-musl-"));
try {
const { bin, log } = fakeLinux(root, false, "ldd (GNU libc) 2.39", 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");
expect(readFileSync(log, "utf8")).not.toContain("chargebee-cli-linux-x64-musl");
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it("uses the musl loader when ldd cannot identify the active libc", () => {
const root = mkdtempSync(join(tmpdir(), "cb-install-unknown-libc-"));
try {
const { bin, log } = fakeLinux(root, true, "unknown libc", 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("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-"));
Expand Down
22 changes: 20 additions & 2 deletions src/tests/unit/lib/update/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Expand Down Expand Up @@ -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}`)
Expand Down Expand Up @@ -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([
{
Expand Down
Loading
Loading