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
30 changes: 30 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,33 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
npm:
needs: binaries
runs-on: ubuntu-latest
permissions:
contents: read
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Download release binaries
run: gh release download "$RELEASE_TAG" --pattern 'gorse-cli_*' --dir dist/npm-binaries
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Stage npm packages
run: node npm/scripts/build-packages.js "$RELEASE_TAG" dist/npm-binaries dist/npm
- name: Publish platform packages before the launcher
run: |
set -e
tag=latest
if [[ "$RELEASE_TAG" == *-* ]]; then tag=next; fi
for package in dist/npm/cli-*; do
npm publish "./$package" --access public --tag "$tag"
done
npm publish ./dist/npm/cli --access public --tag "$tag"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ profile.cov
go.work
go.work.sum

# npm and release artifacts
node_modules/
dist/
*.tgz
/gorse-cli

# env file
.env

Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ Command-line tools for managing Gorse clusters. Migrated from

## Install

With npm (Node.js 18 or later):

```sh
npm install -g @gorse/cli
gorse-cli --help
```

Or run without a global installation:

```sh
npx @gorse/cli --help
```

The npm package selects a prebuilt native binary through platform-specific optional
dependencies; no Go compiler or install scripts are required. Keep optional
dependencies enabled. Supported platforms match the standalone releases: Linux
(x64, arm64, loong64, riscv64), Windows (x64, arm64), and macOS (arm64).

Build from source:

```sh
Expand Down Expand Up @@ -47,6 +65,23 @@ go vet ./...
go build -o gorse-cli .
```

### Publishing npm packages

The release workflow packages the same binaries uploaded by GoReleaser and publishes
platform packages first, followed by `@gorse/cli` with exact-version optional
dependencies. Stable releases use npm's `latest` tag; prereleases use `next`.
Configure the repository's `NPM_TOKEN` secret with permission to publish all
`@gorse/cli*` packages in the npm organization before publishing a release.
The GitHub release tag must be a canonical npm SemVer version, optionally prefixed
with `v` (for example, `v1.2.3` or `v1.2.3-rc.1`).

To inspect packages locally after downloading all release binaries:

```sh
node npm/scripts/build-packages.js v1.2.3 dist/npm-binaries dist/npm
npm pack ./dist/npm/cli
```

Tests start a real local Gorse Master with temporary SQLite stores. The Gorse
module is pinned in `go.mod`; its SQLite replacement must also be declared here
because Go does not inherit dependency replacement directives.
31 changes: 31 additions & 0 deletions npm/bin/gorse-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { createRequire } from "node:module";
import path from "node:path";

const require = createRequire(import.meta.url);
const packageName = `@gorse/cli-${process.platform}-${process.arch}`;
let binary;
try {
binary = path.join(path.dirname(require.resolve(`${packageName}/package.json`)), "bin", process.platform === "win32" ? "gorse-cli.exe" : "gorse-cli");
} catch {
console.error(`Missing optional dependency ${packageName}. Check that your platform is supported and reinstall with optional dependencies enabled: npm install -g @gorse/cli`);
process.exit(1);
}

const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
const handlers = new Map();
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
const handler = () => child.kill(signal);
handlers.set(signal, handler);
process.on(signal, handler);
}
child.on("error", (error) => {
console.error(`Unable to start gorse-cli: ${error.message}`);
process.exit(1);
});
child.on("exit", (code, signal) => {
for (const [name, handler] of handlers) process.removeListener(name, handler);
if (signal) process.kill(process.pid, signal);
else process.exit(code ?? 1);
});
16 changes: 16 additions & 0 deletions npm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@gorse/cli",
"version": "0.0.0-dev",
"description": "Command-line tools for managing Gorse clusters",
"license": "Apache-2.0",
"type": "module",
"bin": { "gorse-cli": "bin/gorse-cli.js" },
"engines": { "node": ">=18" },
"files": ["bin/gorse-cli.js"],
"repository": {
"type": "git",
"url": "git+https://github.com/gorse-io/cli.git",
"directory": "npm"
},
"publishConfig": { "access": "public" }
}
10 changes: 10 additions & 0 deletions npm/platforms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Keep aligned with the targets in .goreleaser.yaml.
export const platforms = [
{ os: "linux", cpu: "x64", goos: "linux", goarch: "amd64" },
{ os: "linux", cpu: "arm64", goos: "linux", goarch: "arm64" },
{ os: "linux", cpu: "loong64", goos: "linux", goarch: "loong64" },
{ os: "linux", cpu: "riscv64", goos: "linux", goarch: "riscv64" },
{ os: "win32", cpu: "x64", goos: "windows", goarch: "amd64" },
{ os: "win32", cpu: "arm64", goos: "windows", goarch: "arm64" },
{ os: "darwin", cpu: "arm64", goos: "darwin", goarch: "arm64" },
];
46 changes: 46 additions & 0 deletions npm/scripts/build-packages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env node
import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { platforms } from "../platforms.js";

const [tag, binaries, output] = process.argv.slice(2);
const version = tag?.replace(/^v/, "");
// npm versions must be canonical SemVer; reject build metadata (npm strips it).
const number = "(?:0|[1-9][0-9]*)";
const identifier = `(?:${number}|[0-9]*[A-Za-z-][0-9A-Za-z-]*)`;
if (!version || !new RegExp(`^${number}\\.${number}\\.${number}(?:-${identifier}(?:\\.${identifier})*)?$`).test(version) || !binaries || !output) {
throw new Error("Usage: node npm/scripts/build-packages.js <version> <binaries-dir> <output-dir> (version must be canonical SemVer)");
}
const npmRoot = fileURLToPath(new URL("../", import.meta.url));
const repoRoot = path.resolve(npmRoot, "..");
const main = JSON.parse(readFileSync(path.join(npmRoot, "package.json"), "utf8"));
main.version = version;
main.optionalDependencies = {};

function stage(directory, manifest) {
mkdirSync(directory, { recursive: true });
writeFileSync(path.join(directory, "package.json"), JSON.stringify(manifest, null, 2) + "\n");
for (const file of ["README.md", "LICENSE"]) copyFileSync(path.join(repoRoot, file), path.join(directory, file));
}

for (const target of platforms) {
const name = `cli-${target.os}-${target.cpu}`;
const packageName = `@gorse/${name}`;
const directory = path.join(output, name);
const executable = target.os === "win32" ? "gorse-cli.exe" : "gorse-cli";
mkdirSync(path.join(directory, "bin"), { recursive: true });
copyFileSync(path.join(binaries, `gorse-cli_${target.goos}_${target.goarch}${target.os === "win32" ? ".exe" : ""}`), path.join(directory, "bin", executable));
chmodSync(path.join(directory, "bin", executable), 0o755);
stage(directory, {
name: packageName, version, description: main.description, license: main.license,
repository: main.repository, publishConfig: main.publishConfig,
os: [target.os], cpu: [target.cpu], files: ["bin"],
});
main.optionalDependencies[packageName] = version;
}
const directory = path.join(output, "cli");
mkdirSync(path.join(directory, "bin"), { recursive: true });
copyFileSync(path.join(npmRoot, "bin/gorse-cli.js"), path.join(directory, "bin/gorse-cli.js"));
chmodSync(path.join(directory, "bin/gorse-cli.js"), 0o755);
stage(directory, main);
Loading