feat: platform-specific npm packages (esbuild-style binary distribution)

This commit is contained in:
kite 2026-06-17 14:28:04 +08:00
commit 11b42c3826
16 changed files with 418 additions and 8 deletions

View file

@ -93,14 +93,56 @@ jobs:
- name: Install jq
run: apt-get update && apt-get install -y jq
- name: Inject version from tag
run: jq --arg v "${GITHUB_REF_NAME#v}" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json
- uses: actions/download-artifact@v4
with:
pattern: binary-*
merge-multiple: true
- name: Configure npm registry
run: echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish platform packages
run: |
VERSION="${GITHUB_REF_NAME#v}"
PLATFORMS="darwin-arm64:opencodereview-darwin-arm64:opencodereview
darwin-x64:opencodereview-darwin-amd64:opencodereview
linux-arm64:opencodereview-linux-arm64:opencodereview
linux-x64:opencodereview-linux-amd64:opencodereview
win32-arm64:opencodereview-windows-arm64.exe:opencodereview.exe
win32-x64:opencodereview-windows-amd64.exe:opencodereview.exe"
for entry in $PLATFORMS; do
IFS=':' read -r platform_dir dist_binary dest_name <<< "$entry"
pkg_dir="npm/$platform_dir"
mkdir -p "$pkg_dir/bin"
cp "$dist_binary" "$pkg_dir/bin/$dest_name"
chmod 755 "$pkg_dir/bin/$dest_name" 2>/dev/null || true
jq --arg v "$VERSION" '.version = $v' "$pkg_dir/package.json" > "$pkg_dir/package.json.tmp"
mv "$pkg_dir/package.json.tmp" "$pkg_dir/package.json"
pkg_name=$(jq -r '.name' "$pkg_dir/package.json")
already=$(npm view "${pkg_name}@${VERSION}" version 2>/dev/null || true)
if [ "$already" = "$VERSION" ]; then
echo "Skip: ${pkg_name}@${VERSION} already published"
else
npm publish "$pkg_dir" --access public
echo "Published: ${pkg_name}@${VERSION}"
fi
done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Inject version and optionalDependencies
run: |
VERSION="${GITHUB_REF_NAME#v}"
jq --arg v "$VERSION" '
.version = $v |
.optionalDependencies |= with_entries(.value = $v)
' package.json > package.json.tmp && mv package.json.tmp package.json
- name: Publish to npm
run: npm publish --access public
env:

1
.gitignore vendored
View file

@ -5,6 +5,7 @@
*.iws
dist/
npm/*/bin/
.claude/plans
.claude/settings.local.json

View file

@ -1,6 +1,7 @@
cmd/
internal/
dist/
npm/
pages/
Makefile
go.mod

View file

@ -6,8 +6,16 @@ const path = require("path");
const fs = require("fs");
const os = require("os");
const IS_WINDOWS = process.platform === "win32";
const binaryPath = path.join(__dirname, IS_WINDOWS ? "opencodereview.exe" : "opencodereview");
const { resolveNativeBinary } = require("../scripts/platform");
const resolved = resolveNativeBinary();
if (!resolved) {
console.error(
"[ERROR] OpenCodeReview binary not found. Run: npm install -g @alibaba-group/open-code-review"
);
process.exit(1);
}
const binaryPath = resolved.path;
if (!process.env.OCR_NO_UPDATE) {
const stateDir = path.join(os.homedir(), ".opencodereview");

View file

@ -0,0 +1,23 @@
{
"name": "@alibaba-group/ocr-darwin-arm64",
"version": "0.0.0",
"description": "Platform binary for open-code-review (darwin arm64)",
"os": [
"darwin"
],
"cpu": [
"arm64"
],
"files": [
"bin/"
],
"preferUnplugged": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/open-code-review.git"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,23 @@
{
"name": "@alibaba-group/ocr-darwin-x64",
"version": "0.0.0",
"description": "Platform binary for open-code-review (darwin x64)",
"os": [
"darwin"
],
"cpu": [
"x64"
],
"files": [
"bin/"
],
"preferUnplugged": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/open-code-review.git"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,23 @@
{
"name": "@alibaba-group/ocr-linux-arm64",
"version": "0.0.0",
"description": "Platform binary for open-code-review (linux arm64)",
"os": [
"linux"
],
"cpu": [
"arm64"
],
"files": [
"bin/"
],
"preferUnplugged": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/open-code-review.git"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,23 @@
{
"name": "@alibaba-group/ocr-linux-x64",
"version": "0.0.0",
"description": "Platform binary for open-code-review (linux x64)",
"os": [
"linux"
],
"cpu": [
"x64"
],
"files": [
"bin/"
],
"preferUnplugged": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/open-code-review.git"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,23 @@
{
"name": "@alibaba-group/ocr-win32-arm64",
"version": "0.0.0",
"description": "Platform binary for open-code-review (win32 arm64)",
"os": [
"win32"
],
"cpu": [
"arm64"
],
"files": [
"bin/"
],
"preferUnplugged": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/open-code-review.git"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,23 @@
{
"name": "@alibaba-group/ocr-win32-x64",
"version": "0.0.0",
"description": "Platform binary for open-code-review (win32 x64)",
"os": [
"win32"
],
"cpu": [
"x64"
],
"files": [
"bin/"
],
"preferUnplugged": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/open-code-review.git"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -20,6 +20,14 @@
"urlPattern": "https://github.com/alibaba/open-code-review/releases/download/v{version}/opencodereview-{os}-{arch}",
"checksumPattern": "https://github.com/alibaba/open-code-review/releases/download/v{version}/sha256sum.txt"
},
"optionalDependencies": {
"@alibaba-group/ocr-darwin-arm64": "0.0.0",
"@alibaba-group/ocr-darwin-x64": "0.0.0",
"@alibaba-group/ocr-linux-arm64": "0.0.0",
"@alibaba-group/ocr-linux-x64": "0.0.0",
"@alibaba-group/ocr-win32-arm64": "0.0.0",
"@alibaba-group/ocr-win32-x64": "0.0.0"
},
"engines": {
"node": ">=14"
},

View file

@ -6,8 +6,7 @@ const path = require("path");
const https = require("https");
const crypto = require("crypto");
const IS_WINDOWS = process.platform === "win32";
const BINARY_NAME = IS_WINDOWS ? "opencodereview.exe" : "opencodereview";
const { IS_WINDOWS, BINARY_FILENAME: BINARY_NAME, resolveNativeBinary } = require("./platform");
const packageRoot = path.join(__dirname, "..");
const binDir = path.join(packageRoot, "bin");
@ -154,6 +153,13 @@ async function main() {
info("OpenCodeReview Installer");
info("=========================");
const existing = resolveNativeBinary();
if (existing && existing.fromPlatformPkg) {
info("Binary provided by platform package, skipping download.");
info(` ${existing.path}`);
return;
}
const { os, arch } = detectPlatform();
info(`Detected platform: ${os}/${arch}`);

66
scripts/platform.js Normal file
View file

@ -0,0 +1,66 @@
"use strict";
const path = require("path");
const fs = require("fs");
const IS_WINDOWS = process.platform === "win32";
const BINARY_FILENAME = IS_WINDOWS ? "opencodereview.exe" : "opencodereview";
const PLATFORM_PKG = {
"darwin-arm64": "@alibaba-group/ocr-darwin-arm64",
"darwin-x64": "@alibaba-group/ocr-darwin-x64",
"linux-arm64": "@alibaba-group/ocr-linux-arm64",
"linux-x64": "@alibaba-group/ocr-linux-x64",
"win32-arm64": "@alibaba-group/ocr-win32-arm64",
"win32-x64": "@alibaba-group/ocr-win32-x64",
};
function getPlatformPackageName() {
const key = `${process.platform}-${process.arch}`;
try {
const parentPkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
);
const optDeps = parentPkg.optionalDependencies || {};
for (const name of Object.keys(optDeps)) {
if (name.endsWith(`-${key}`)) {
return name;
}
}
} catch (_) {}
return PLATFORM_PKG[key] || null;
}
function resolveNativeBinary() {
const pkgName = getPlatformPackageName();
if (pkgName) {
try {
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
const binPath = path.join(pkgDir, "bin", BINARY_FILENAME);
if (fs.existsSync(binPath)) {
return { path: binPath, fromPlatformPkg: true };
}
} catch (err) {
if (err.code !== "MODULE_NOT_FOUND") {
console.warn(`[WARN] Unexpected error resolving ${pkgName}: ${err.message}`);
}
}
}
const legacyPath = path.join(__dirname, "..", "bin", BINARY_FILENAME);
if (fs.existsSync(legacyPath)) {
return { path: legacyPath, fromPlatformPkg: false };
}
return null;
}
module.exports = {
IS_WINDOWS,
BINARY_FILENAME,
PLATFORM_PKG,
getPlatformPackageName,
resolveNativeBinary,
};

View file

@ -0,0 +1,115 @@
#!/usr/bin/env bash
#
# publish-platform.sh — Publish platform-specific npm packages.
#
# Expects NPM_VERSION to be exported by the caller (publish.sh).
# Each platform package ships the Go binary for a single OS/arch combination.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_common.sh"
PROJECT_ROOT="$(resolve_project_root)"
[ -z "${NPM_VERSION:-}" ] && die "NPM_VERSION is not set. This script should be called from publish.sh."
# Go dist/ binary name → npm platform directory
# Format: "npm_dir:dist_binary:dest_filename"
PLATFORMS=(
"darwin-arm64:opencodereview-darwin-arm64:opencodereview"
"darwin-x64:opencodereview-darwin-amd64:opencodereview"
"linux-arm64:opencodereview-linux-arm64:opencodereview"
"linux-x64:opencodereview-linux-amd64:opencodereview"
"win32-arm64:opencodereview-windows-arm64.exe:opencodereview.exe"
"win32-x64:opencodereview-windows-amd64.exe:opencodereview.exe"
)
REGISTRY_ARGS=()
if [ -n "${OCR_PUBLISH_REGISTRY:-}" ]; then
REGISTRY_ARGS=(--registry "$OCR_PUBLISH_REGISTRY")
fi
# Derive scope override from OCR_PKG_NAME (e.g. @ali/open-code-review → @ali)
SCOPE_OVERRIDE=""
if [ -n "${OCR_PKG_NAME:-}" ]; then
SCOPE_OVERRIDE="${OCR_PKG_NAME%%/*}"
fi
# Pre-check all binaries exist before publishing anything
for entry in "${PLATFORMS[@]}"; do
IFS=':' read -r _ dist_binary _ <<< "$entry"
[ -f "$PROJECT_ROOT/dist/$dist_binary" ] || die "Binary not found: $PROJECT_ROOT/dist/$dist_binary"
done
FAILED_PLATFORMS=()
CURRENT_BACKUP=""
CURRENT_PKG_JSON=""
CURRENT_BIN_DIR=""
cleanup_current() {
if [ -n "$CURRENT_BACKUP" ] && [ -n "$CURRENT_PKG_JSON" ]; then
cp "$CURRENT_BACKUP" "$CURRENT_PKG_JSON" 2>/dev/null || true
rm -f "$CURRENT_BACKUP"
fi
if [ -n "$CURRENT_BIN_DIR" ]; then
rm -rf "$CURRENT_BIN_DIR"
fi
CURRENT_BACKUP=""
CURRENT_PKG_JSON=""
CURRENT_BIN_DIR=""
}
trap cleanup_current EXIT
for entry in "${PLATFORMS[@]}"; do
IFS=':' read -r platform_dir dist_binary dest_name <<< "$entry"
pkg_dir="$PROJECT_ROOT/npm/$platform_dir"
bin_dir="$pkg_dir/bin"
dist_path="$PROJECT_ROOT/dist/$dist_binary"
pkg_json="$pkg_dir/package.json"
info "Preparing $platform_dir..."
mkdir -p "$bin_dir"
cp "$dist_path" "$bin_dir/$dest_name"
chmod 755 "$bin_dir/$dest_name" 2>/dev/null || true
CURRENT_BACKUP=$(mktemp)
CURRENT_PKG_JSON="$pkg_json"
CURRENT_BIN_DIR="$bin_dir"
cp "$pkg_json" "$CURRENT_BACKUP"
if [ -n "$SCOPE_OVERRIDE" ]; then
jq --arg v "$NPM_VERSION" --arg s "$SCOPE_OVERRIDE" \
'.version = $v | .name = (.name | sub("^@[^/]+"; $s))' \
"$pkg_json" > "${pkg_json}.tmp" && mv "${pkg_json}.tmp" "$pkg_json" || { rm -f "${pkg_json}.tmp"; false; }
else
jq --arg v "$NPM_VERSION" '.version = $v' "$pkg_json" > "${pkg_json}.tmp" && mv "${pkg_json}.tmp" "$pkg_json" || { rm -f "${pkg_json}.tmp"; false; }
fi
pkg_name=$(jq -r '.name' "$pkg_json")
already=$(npm view "${pkg_name}@${NPM_VERSION}" version ${REGISTRY_ARGS[@]+"${REGISTRY_ARGS[@]}"} 2>/dev/null || true)
if [ "$already" = "$NPM_VERSION" ]; then
warn " ${pkg_name}@${NPM_VERSION} already published, skipping."
else
info " Publishing ${pkg_name}@${NPM_VERSION}..."
if ! npm publish "$pkg_dir" --access public ${REGISTRY_ARGS[@]+"${REGISTRY_ARGS[@]}"}; then
warn " Failed to publish ${pkg_name}@${NPM_VERSION}"
FAILED_PLATFORMS+=("$platform_dir")
else
success " Published ${pkg_name}@${NPM_VERSION}"
fi
fi
cleanup_current
done
trap - EXIT
if [ ${#FAILED_PLATFORMS[@]} -gt 0 ]; then
die "Failed to publish platforms: ${FAILED_PLATFORMS[*]}"
fi
success "All platform packages processed."

View file

@ -100,7 +100,13 @@ else
"make -C \"$PROJECT_ROOT\" dist"
fi
# ── (version is injected temporarily during patch_package_json below) ────────
# ── Publish platform-specific packages ────────────────────────────────────────
if [ "$SKIP_PUBLISH" = "1" ]; then
warn "Skipping platform packages (OCR_SKIP_PUBLISH=1)"
else
run_step "Publishing platform packages" \
"bash \"$SCRIPT_DIR/publish-platform.sh\""
fi
# ── Upload to git-based release repo (optional) ──────────────────────────────
upload_to_git_repo() {
@ -188,9 +194,22 @@ patch_package_json() {
jq --arg v "$NPM_VERSION" '.version = $v' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
info " version → ${NPM_VERSION}"
# Pin optionalDependencies (platform packages) to the same version
if jq -e '.optionalDependencies' "$tmp" >/dev/null 2>&1; then
jq --arg v "$NPM_VERSION" '.optionalDependencies |= with_entries(.value = $v)' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
info " optionalDependencies → all pinned to ${NPM_VERSION}"
fi
if [ -n "${OCR_PKG_NAME:-}" ]; then
jq --arg n "$OCR_PKG_NAME" '.name = $n' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
local new_scope="${OCR_PKG_NAME%%/*}"
jq --arg n "$OCR_PKG_NAME" --arg s "$new_scope" '
.name = $n |
if .optionalDependencies then
.optionalDependencies |= with_entries(.key |= sub("^@[^/]+"; $s))
else . end
' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
info " name → ${OCR_PKG_NAME}"
info " optionalDependencies scope → ${new_scope}"
fi
if [ -n "${OCR_PUBLISH_REGISTRY:-}" ]; then

View file

@ -147,6 +147,12 @@ async function main() {
cleanupTemp();
try {
const { resolveNativeBinary } = require("./platform");
const resolved = resolveNativeBinary();
if (resolved && resolved.fromPlatformPkg) {
info("Binary managed by platform package, skipping auto-update.");
return;
}
const installedVersion = getInstalledVersion();
if (!installedVersion) return;