Skip to content
Open
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
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ on:

permissions:
contents: write
id-token: write

jobs:
release:
# MUST use GitHub-hosted public runners — npm --provenance requires OIDC id-token
# from a trusted CI environment (GitHub Actions on public runners).
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand All @@ -24,3 +27,20 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"

- name: Smoke test npm packages
run: |
VERSION="${GITHUB_REF_NAME#v}"
VERSION="$VERSION" scripts/npm-smoke-test.sh

- name: Publish to npm
run: |
VERSION="${GITHUB_REF_NAME#v}"
VERSION="$VERSION" scripts/npm-publish.sh
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Binaries
cg
/cg
*.exe
*.exe~
*.dll
Expand Down Expand Up @@ -29,6 +29,10 @@ Thumbs.db
# Build
dist/

# npm platform binaries (copied during CI publish)
npm/cg-*/cg
npm/cg-*/cg.exe

# Secrets
.env
.env.*
Expand Down
13 changes: 13 additions & 0 deletions npm/cg-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@coingecko/cg-darwin-arm64",
"version": "0.0.0",
"description": "CoinGecko CLI - Real Time & Historical Crypto Data (macOS ARM64)",
"repository": {
"type": "git",
"url": "https://github.com/coingecko/coingecko-cli.git"
},
"license": "MIT",
"os": ["darwin"],
"cpu": ["arm64"],
"files": ["cg"]
}
13 changes: 13 additions & 0 deletions npm/cg-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@coingecko/cg-darwin-x64",
"version": "0.0.0",
"description": "CoinGecko CLI - Real Time & Historical Crypto Data (macOS x64)",
"repository": {
"type": "git",
"url": "https://github.com/coingecko/coingecko-cli.git"
},
"license": "MIT",
"os": ["darwin"],
"cpu": ["x64"],
"files": ["cg"]
}
13 changes: 13 additions & 0 deletions npm/cg-linux-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@coingecko/cg-linux-arm64",
"version": "0.0.0",
"description": "CoinGecko CLI - Real Time & Historical Crypto Data (Linux ARM64)",
"repository": {
"type": "git",
"url": "https://github.com/coingecko/coingecko-cli.git"
},
"license": "MIT",
"os": ["linux"],
"cpu": ["arm64"],
"files": ["cg"]
}
13 changes: 13 additions & 0 deletions npm/cg-linux-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@coingecko/cg-linux-x64",
"version": "0.0.0",
"description": "CoinGecko CLI - Real Time & Historical Crypto Data (Linux x64)",
"repository": {
"type": "git",
"url": "https://github.com/coingecko/coingecko-cli.git"
},
"license": "MIT",
"os": ["linux"],
"cpu": ["x64"],
"files": ["cg"]
}
13 changes: 13 additions & 0 deletions npm/cg-win32-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@coingecko/cg-win32-arm64",
"version": "0.0.0",
"description": "CoinGecko CLI - Real Time & Historical Crypto Data (Windows ARM64)",
"repository": {
"type": "git",
"url": "https://github.com/coingecko/coingecko-cli.git"
},
"license": "MIT",
"os": ["win32"],
"cpu": ["arm64"],
"files": ["cg.exe"]
}
13 changes: 13 additions & 0 deletions npm/cg-win32-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@coingecko/cg-win32-x64",
"version": "0.0.0",
"description": "CoinGecko CLI - Real Time & Historical Crypto Data (Windows x64)",
"repository": {
"type": "git",
"url": "https://github.com/coingecko/coingecko-cli.git"
},
"license": "MIT",
"os": ["win32"],
"cpu": ["x64"],
"files": ["cg.exe"]
}
56 changes: 56 additions & 0 deletions npm/cg/bin/cg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env node

"use strict";

const { execFileSync } = require("child_process");
const path = require("path");
const os = require("os");

// Map Node.js platform/arch to npm package names
const PLATFORMS = {
"darwin arm64": "@coingecko/cg-darwin-arm64",
"darwin x64": "@coingecko/cg-darwin-x64",
"linux arm64": "@coingecko/cg-linux-arm64",
"linux x64": "@coingecko/cg-linux-x64",
"win32 arm64": "@coingecko/cg-win32-arm64",
"win32 x64": "@coingecko/cg-win32-x64",
};

function getBinaryPath() {
const platform = os.platform();
const arch = os.arch();
const key = `${platform} ${arch}`;
const pkg = PLATFORMS[key];

if (!pkg) {
throw new Error(
`Unsupported platform: ${platform} ${arch}. ` +
`Supported: ${Object.keys(PLATFORMS).join(", ")}`
);
}

const binary = platform === "win32" ? "cg.exe" : "cg";

try {
// resolve the platform package from this package's location
const pkgDir = path.dirname(require.resolve(`${pkg}/package.json`));
return path.join(pkgDir, binary);
} catch {
throw new Error(
`The platform package ${pkg} is not installed. ` +
`This usually means your package manager excluded optional dependencies. ` +
`Try reinstalling with: npm install @coingecko/cg`
);
}
}

const binary = getBinaryPath();

try {
execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
} catch (err) {
if (err.status !== undefined) {
process.exit(err.status);
}
throw err;
}
34 changes: 34 additions & 0 deletions npm/cg/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@coingecko/cg",
"version": "0.0.0",
"description": "CoinGecko CLI - Real Time & Historical Crypto Data",
"repository": {
"type": "git",
"url": "https://github.com/coingecko/coingecko-cli.git"
},
"license": "MIT",
"bin": {
"cg": "bin/cg.js"
},
"files": [
"bin/cg.js"
],
"optionalDependencies": {
"@coingecko/cg-darwin-arm64": "0.0.0",
"@coingecko/cg-darwin-x64": "0.0.0",
"@coingecko/cg-linux-arm64": "0.0.0",
"@coingecko/cg-linux-x64": "0.0.0",
"@coingecko/cg-win32-arm64": "0.0.0",
"@coingecko/cg-win32-x64": "0.0.0"
},
"engines": {
"node": ">=20"
},
"keywords": [
"coingecko",
"crypto",
"cryptocurrency",
"bitcoin",
"cli"
]
}
162 changes: 162 additions & 0 deletions scripts/npm-publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/bin/sh
set -eu

# Publish CoinGecko CLI to npm as platform-specific packages.
# Called from CI after goreleaser has built the archives.
#
# Usage: VERSION=1.2.3 scripts/npm-publish.sh
#
# Expects:
# - goreleaser dist/ directory with archives and checksums.txt
# - NPM_TOKEN environment variable (or .npmrc already configured)
#
# Safety:
# - Verifies archive checksums against goreleaser's checksums.txt before extracting
# - Aborts if any platform archive is missing (won't publish broken umbrella)
# - Skips already-published versions for retry safety after partial failures
# - Uses node to rewrite package.json versions (works regardless of current value)
# - Publishes with --provenance for supply-chain verification (requires id-token: write)

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
NPM_DIR="$ROOT_DIR/npm"
DIST_DIR="$ROOT_DIR/dist"

if [ -z "${VERSION:-}" ]; then
echo "Error: VERSION environment variable is required"
exit 1
fi

echo "Publishing @coingecko/cg v${VERSION} to npm"

# Mapping: goreleaser os_arch -> npm package directory + binary name
# darwin_arm64 -> cg-darwin-arm64/cg
# darwin_amd64 -> cg-darwin-x64/cg
# linux_arm64 -> cg-linux-arm64/cg
# linux_amd64 -> cg-linux-x64/cg
# windows_arm64 -> cg-win32-arm64/cg.exe
# windows_amd64 -> cg-win32-x64/cg.exe

# set_version <file> <version> [dependency_version]
# Rewrites version (and optionalDependencies versions) in a package.json.
set_version() {
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('$1', 'utf8'));
pkg.version = '$2';
if ('$3' !== '' && pkg.optionalDependencies) {
for (const k of Object.keys(pkg.optionalDependencies)) {
pkg.optionalDependencies[k] = '$3';
}
}
fs.writeFileSync('$1', JSON.stringify(pkg, null, 2) + '\n');
"
}

# is_published <package_name> <version>
# Returns 0 if the exact version is already on the registry.
is_published() {
npm view "$1@$2" version >/dev/null 2>&1
}

# verify_checksum <file>
# Verifies a file against goreleaser's checksums.txt. Aborts on mismatch.
verify_checksum() {
local file="$1"
local filename
filename=$(basename "$file")
local expected
expected=$(grep " ${filename}$" "${DIST_DIR}/checksums.txt" | awk '{print $1}')
if [ -z "$expected" ]; then
echo "Error: no checksum entry for ${filename} in checksums.txt"
exit 1
fi
local actual
if ! command -v sha256sum >/dev/null 2>&1; then
echo "Error: sha256sum not found"
exit 1
fi
actual=$(sha256sum "$file" | awk '{print $1}')
if [ "$expected" != "$actual" ]; then
echo "Error: checksum mismatch for ${filename}"
echo " Expected: ${expected}"
echo " Actual: ${actual}"
exit 1
fi
}

# Phase 1: Verify all platform archives exist and match checksums
echo "Verifying platform archives..."
CHECKSUMS_FILE="${DIST_DIR}/checksums.txt"
if [ ! -f "$CHECKSUMS_FILE" ]; then
echo "Error: ${CHECKSUMS_FILE} not found. Cannot verify archive integrity."
exit 1
fi
PLATFORMS="darwin:arm64:cg-darwin-arm64:cg:tar.gz
darwin:amd64:cg-darwin-x64:cg:tar.gz
linux:arm64:cg-linux-arm64:cg:tar.gz
linux:amd64:cg-linux-x64:cg:tar.gz
windows:arm64:cg-win32-arm64:cg.exe:zip
windows:amd64:cg-win32-x64:cg.exe:zip"

MISSING=""
echo "$PLATFORMS" | while IFS=: read -r goos goarch npm_dir binary ext; do
archive="${DIST_DIR}/cg_${VERSION}_${goos}_${goarch}.${ext}"
if [ ! -f "$archive" ]; then
echo " MISSING: ${archive}"
touch "${DIST_DIR}/.npm-missing"
else
verify_checksum "$archive"
fi
done

if [ -f "${DIST_DIR}/.npm-missing" ]; then
rm -f "${DIST_DIR}/.npm-missing"
echo "Error: one or more platform archives are missing. Aborting npm publish."
exit 1
fi
echo " All archives present and checksums verified."

# Phase 2: Extract, version-stamp, and publish each platform package
echo "$PLATFORMS" | while IFS=: read -r goos goarch npm_dir binary ext; do
pkg_name="@coingecko/${npm_dir}"
pkg_dir="${NPM_DIR}/${npm_dir}"
archive="cg_${VERSION}_${goos}_${goarch}.${ext}"
archive_path="${DIST_DIR}/${archive}"

if is_published "$pkg_name" "$VERSION"; then
echo " ${pkg_name}@${VERSION} already published, skipping"
continue
fi

echo " Extracting ${archive}..."
tmpdir=$(mktemp -d)
if [ "$ext" = "tar.gz" ]; then
tar -xzf "$archive_path" -C "$tmpdir"
else
unzip -q "$archive_path" -d "$tmpdir"
fi

cp "${tmpdir}/${binary}" "${pkg_dir}/${binary}"
chmod +x "${pkg_dir}/${binary}"
rm -rf "$tmpdir"

set_version "${pkg_dir}/package.json" "$VERSION" ""

echo " Publishing ${pkg_name}@${VERSION}..."
npm publish "${pkg_dir}" --access public --provenance
done

# Phase 3: Version-stamp and publish the umbrella package
UMBRELLA_DIR="${NPM_DIR}/cg"

if is_published "@coingecko/cg" "$VERSION"; then
echo " @coingecko/cg@${VERSION} already published, skipping"
else
set_version "${UMBRELLA_DIR}/package.json" "$VERSION" "$VERSION"

echo " Publishing @coingecko/cg@${VERSION}..."
npm publish "${UMBRELLA_DIR}" --access public --provenance
fi

echo "Done! Published @coingecko/cg@${VERSION} and all platform packages."
Loading