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
222 changes: 222 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
name: Publish npm Packages

on:
push:
tags:
- "*"
workflow_dispatch:
inputs:
tag:
description: "Tag to publish from (for example: v1.7.2)"
required: true
type: string

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a separate npm package for each platform? We could also avoid rebuilding the binaries since we already have a GitHub Action that handles that. Instead, once that workflow completes, we can package all platform-specific binaries into a single npm package and just a have a small logic to resolve the binary path relative to the current js module

Suggested change
workflow_run:
workflows: ["Native Binary Release"]
types:
- completed

permissions:
contents: read

jobs:
prepare:
runs-on: ubuntu-latest
outputs:
release_ref: ${{ steps.resolve.outputs.release_ref }}
tag_name: ${{ steps.resolve.outputs.tag_name }}
version: ${{ steps.resolve.outputs.version }}
steps:
- name: Resolve Tag and Version
id: resolve
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG_NAME="${{ inputs.tag }}"
RELEASE_REF="refs/tags/${{ inputs.tag }}"
else
TAG_NAME="${{ github.ref_name }}"
RELEASE_REF="${{ github.ref }}"
fi

VERSION="${TAG_NAME#v}"
echo "release_ref=${RELEASE_REF}" >> "${GITHUB_OUTPUT}"
echo "tag_name=${TAG_NAME}" >> "${GITHUB_OUTPUT}"
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"

publish-platform-packages:
name: Publish ${{ matrix.package_name }}
needs:
- prepare
- validate-release
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
package_name: corgea-cli-linux-x64
package_dir: packages/corgea-cli-linux-x64
target: x86_64-unknown-linux-gnu
binary_name: corgea
- runner: ubuntu-24.04-arm
package_name: corgea-cli-linux-arm64
package_dir: packages/corgea-cli-linux-arm64
target: aarch64-unknown-linux-gnu
binary_name: corgea
- runner: macos-13
package_name: corgea-cli-darwin-x64
package_dir: packages/corgea-cli-darwin-x64
target: x86_64-apple-darwin
binary_name: corgea
- runner: macos-14
package_name: corgea-cli-darwin-arm64
package_dir: packages/corgea-cli-darwin-arm64
target: aarch64-apple-darwin
binary_name: corgea
- runner: windows-latest
package_name: corgea-cli-win32-x64
package_dir: packages/corgea-cli-win32-x64
target: x86_64-pc-windows-msvc
binary_name: corgea.exe
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.release_ref }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Build Binary
run: cargo build --release --target "${{ matrix.target }}"

- name: Stage Platform Package
run: |
node scripts/npm/prepare-platform-package.js \
"${{ matrix.package_dir }}" \
"${{ matrix.target }}" \
"${{ matrix.binary_name }}" \
"${{ needs.prepare.outputs.version }}"

- name: Publish Platform Package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_NAME: ${{ matrix.package_name }}
PACKAGE_DIR: ${{ matrix.package_dir }}
PACKAGE_VERSION: ${{ needs.prepare.outputs.version }}
run: |
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm, skipping."
exit 0
fi

npm publish "${PACKAGE_DIR}" --access public

publish-main-package:
name: Publish corgea-cli
needs:
- prepare
- validate-release
- publish-platform-packages
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.release_ref }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org

- name: Validate npm Package Contents
run: npm pack --dry-run

- name: Publish corgea-cli
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_VERSION: ${{ needs.prepare.outputs.version }}
run: |
if npm view "corgea-cli@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
echo "corgea-cli@${PACKAGE_VERSION} already exists on npm, skipping."
exit 0
fi

npm publish --access public

validate-release:
name: Validate Release Metadata
needs: prepare
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.release_ref }}

- name: Validate package versions
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
run: |
node - <<'NODE'
const fs = require("node:fs");
const path = require("node:path");

const releaseVersion = process.env.RELEASE_VERSION;
const rootPackage = JSON.parse(fs.readFileSync("package.json", "utf8"));
const optionalDependencies = rootPackage.optionalDependencies || {};

const errors = [];
const platformPackageNames = new Set();

if (rootPackage.version !== releaseVersion) {
errors.push(`package.json version ${rootPackage.version} does not match release version ${releaseVersion}`);
}

const packageRoot = "packages";
const platformDirs = fs.readdirSync(packageRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(packageRoot, entry.name));

for (const packageDir of platformDirs) {
const packageJsonPath = path.join(packageDir, "package.json");
const platformPackage = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const expectedVersion = optionalDependencies[platformPackage.name];
platformPackageNames.add(platformPackage.name);

if (!expectedVersion) {
errors.push(`missing optional dependency entry for ${platformPackage.name}`);
continue;
}

if (expectedVersion !== releaseVersion) {
errors.push(`optional dependency ${platformPackage.name} has version ${expectedVersion}, expected ${releaseVersion}`);
}
}

for (const dependencyName of Object.keys(optionalDependencies)) {
if (!platformPackageNames.has(dependencyName)) {
errors.push(`optional dependency ${dependencyName} has no matching package under packages/`);
}
}

if (errors.length > 0) {
console.error(errors.join("\n"));
process.exit(1);
}
NODE
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.DS_Store
.dccache
*.zip
/packages/*/vendor/
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ For full documentation, visit https://docs.corgea.app/cli

## Installation

### Using npm
```
npm install -g corgea-cli
```
The npm package resolves a platform-specific optional dependency (for example, `corgea-cli-linux-x64`) that contains the native `corgea` binary.

### Using pip
```
pip install corgea-cli
Expand Down Expand Up @@ -63,4 +69,3 @@ corgea login <token>
```

Note: After making changes to Rust code, you'll need to run `maturin develop` again to rebuild the package.

135 changes: 135 additions & 0 deletions bin/corgea.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env node
"use strict";

const { spawn } = require("node:child_process");
const { existsSync } = require("node:fs");
const path = require("node:path");

const PLATFORM_PACKAGE_BY_TARGET = {
"x86_64-unknown-linux-gnu": "corgea-cli-linux-x64",
"aarch64-unknown-linux-gnu": "corgea-cli-linux-arm64",
"x86_64-apple-darwin": "corgea-cli-darwin-x64",
"aarch64-apple-darwin": "corgea-cli-darwin-arm64",
"x86_64-pc-windows-msvc": "corgea-cli-win32-x64"
};

function resolveTargetTriple() {
switch (process.platform) {
case "linux":
case "android":
if (process.arch === "x64") return "x86_64-unknown-linux-gnu";
if (process.arch === "arm64") return "aarch64-unknown-linux-gnu";
return null;
case "darwin":
if (process.arch === "x64") return "x86_64-apple-darwin";
if (process.arch === "arm64") return "aarch64-apple-darwin";
return null;
case "win32":
if (process.arch === "x64") return "x86_64-pc-windows-msvc";
return null;
default:
return null;
}
}

function detectPackageManager() {
const userAgent = process.env.npm_config_user_agent || "";
if (/\bbun\//.test(userAgent)) return "bun";

const execPath = process.env.npm_execpath || "";
if (execPath.includes("bun")) return "bun";

if (__dirname.includes(".bun/install/global") || __dirname.includes(".bun\\install\\global")) {
return "bun";
}

if (/\bpnpm\//.test(userAgent) || execPath.includes("pnpm")) return "pnpm";
if (/\byarn\//.test(userAgent) || execPath.includes("yarn")) return "yarn";
return userAgent ? "npm" : null;
}

function getReinstallCommand() {
const manager = detectPackageManager();
if (manager === "bun") return "bun install -g corgea-cli@latest";
if (manager === "pnpm") return "pnpm add -g corgea-cli@latest";
if (manager === "yarn") return "yarn global add corgea-cli@latest";
return "npm install -g corgea-cli@latest";
}

function getUpdatedPath(newDirs) {
const pathSep = process.platform === "win32" ? ";" : ":";
const existingPath = process.env.PATH || "";
return [...newDirs, ...existingPath.split(pathSep).filter(Boolean)].join(pathSep);
}

const targetTriple = resolveTargetTriple();
if (!targetTriple) {
throw new Error(`Unsupported platform: ${process.platform} (${process.arch})`);
}

const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
if (!platformPackage) {
throw new Error(`Unsupported target triple: ${targetTriple}`);
}

const binaryName = process.platform === "win32" ? "corgea.exe" : "corgea";
const localVendorRoot = path.join(__dirname, "..", "vendor");
const localBinaryPath = path.join(localVendorRoot, targetTriple, "corgea", binaryName);

let vendorRoot = null;
try {
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
} catch (error) {
if (existsSync(localBinaryPath)) {
vendorRoot = localVendorRoot;
} else {
throw new Error(`Missing optional dependency ${platformPackage}. Reinstall Corgea CLI: ${getReinstallCommand()}`);
}
}

const archRoot = path.join(vendorRoot, targetTriple);
const binaryPath = path.join(archRoot, "corgea", binaryName);

if (!existsSync(binaryPath)) {
throw new Error(`Corgea binary not found at ${binaryPath}`);
}

const additionalDirs = [];
const pathDir = path.join(archRoot, "path");
if (existsSync(pathDir)) {
additionalDirs.push(pathDir);
}

const env = { ...process.env, PATH: getUpdatedPath(additionalDirs) };
const packageManagerEnvVar = detectPackageManager() === "bun" ? "CORGEA_MANAGED_BY_BUN" : "CORGEA_MANAGED_BY_NPM";
env[packageManagerEnvVar] = "1";

const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", env });

child.on("error", (error) => {
console.error(error);
process.exit(1);
});

const forwardSignal = (signal) => {
if (child.killed) return;
try {
child.kill(signal);
} catch (error) {
// Ignore signal forwarding errors.
}
};

["SIGINT", "SIGTERM", "SIGHUP"].forEach((signal) => {
process.on(signal, () => forwardSignal(signal));
});

child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}

process.exit(code === null ? 1 : code);
});
Loading