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 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)
Expand Down
26 changes: 25 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
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
71 changes: 71 additions & 0 deletions src/tests/integration/install-sh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-"));
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
7 changes: 6 additions & 1 deletion src/tools/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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=<t>` builds
* cross-compiles the seven release artifacts into `dist/`; `--target=<t>` builds
* just one of them (CI fans these out across runners).
*
* The version is baked in via `process.env.VERSION` — without it the binary
Expand All @@ -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;

Expand All @@ -36,6 +38,9 @@ async function compile(outfile: string, target?: string): Promise<void> {
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" });
Expand Down
Loading