Add npm publish workflow and release infrastructure (#330)

* Add npm publish workflow and release infrastructure

- Add .github/workflows/npm-publish.yml for automated npm publishing on GitHub releases
- Update CI to verify library build (npm run build:lib) and package contents
- Add CHANGELOG.md for version history tracking
- Update README.md with npm installation and usage documentation

Closes #197

Co-authored-by: openhands <openhands@all-hands.dev>

* correct package version

* chore: update npm-publish workflow for trusted publishing

- Remove NODE_AUTH_TOKEN secret dependency
- Keep id-token: write permission for OIDC
- Add provenance flag for npm attestations
- Add comment explaining trusted publisher setup on npmjs.com

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: add CLI entry point for npx execution

- Add bin/agent-canvas.mjs as executable CLI
- Add bin field to package.json for npm bin linking
- Include bin/ and build/ directories in published files
- CLI serves the built application with SPA routing support
- Supports --port, --host, and --help options

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: consolidate npm executable to use dev-docker infrastructure

- bin/agent-canvas.mjs now uses dev-with-automation.mjs main() with
  dev-docker.mjs's Docker-specific agent-server starter
- Added --static and --static-dir support to dev-with-automation.mjs
  so the npm executable serves pre-built static assets instead of Vite
- Added startStaticFrontend() function that uses static-server.mjs
- npm executable runs full stack: Docker agent-server + uvx automation
  backend + static frontend + ingress proxy

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: include scripts/ in npm package files

The bin/agent-canvas.mjs executable imports from scripts/dev-with-automation.mjs
and scripts/dev-docker.mjs, so the scripts directory must be included in the
published package.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review comments

- Fix CHANGELOG.md version mismatch: 1.6.0 -> 1.0.0-alpha.1 to match package.json
- Add NODE_AUTH_TOKEN env var to npm-publish workflow for authentication
- Add CLI entry point mention to CHANGELOG

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use OIDC trusted publishing (no NPM_TOKEN needed)

npm trusted publishing with OIDC doesn't require NODE_AUTH_TOKEN.
Instead it uses short-lived OIDC tokens generated by GitHub Actions.

Requirements:
- id-token: write permission (already set)
- npm CLI 11.5.1+ (added npm install -g npm@latest step)
- Trusted publisher configured on npmjs.com

See: https://docs.npmjs.com/trusted-publishers/

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump version to 1.0.0-alpha.2

Co-authored-by: openhands <openhands@all-hands.dev>

* Build app assets before npm publish

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use Node 24 for npm trusted publishing

Trusted publishing requires Node 22.14.0+ and npm 11.5.1+.
Node 24 ships with npm 11.x which meets the requirement.
Node 22.12.0 (previous) ships with npm 10.x which doesn't support OIDC.

Also removed the manual npm upgrade step since Node 24 includes
a compatible npm version by default.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: align all workflows to Node 24 and regenerate lockfile

- Update ci.yml to use Node 24
- Update sdk-version-sync.yml to use Node 24
- Regenerate package-lock.json with npm 11.12.1

All workflows now use Node 24 which ships with npm 11.x,
required for OIDC trusted publishing (npm 11.5.1+).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: remove incorrect LLM env vars from CLI help

LLM_MODEL and LLM_API_KEY were listed in the help text but aren't
actually used by the scripts. LLM settings are configured through
the web UI settings page instead.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address PR review feedback

Critical fixes:
- Guard prepare script to only run in dev context (check for ../.git)
- Add missing existsSync import in dev-with-automation.mjs

Workflow improvements:
- Update checkout/setup-node actions to v6 for consistency
- Add npm version validation (must be 11.5.1+ for trusted publishing)
- Add package version validation (must match release tag)

CLI improvements:
- Add try-catch for dynamic imports with helpful error message
- Use console.error directly instead of imported logError/c

Documentation:
- Fix README export names: ChatInterface→ChatPanel, Terminal→TerminalPanel
- Add dist/ to .gitignore

Co-authored-by: openhands <openhands@all-hands.dev>

* ci: trigger npm publish on tag push instead of release

Simpler workflow - just push a tag like v1.0.0-alpha.2 to publish.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: remove tarball and add *.tgz to gitignore

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: npm publish errors

1. Fix bin path - remove './' prefix (npm pkg fix)
2. Add --tag for prerelease versions (alpha/beta/rc)

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add repository field for npm provenance verification

npm provenance requires repository.url to match the GitHub Actions
source. Also added description, homepage, and bugs fields.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra
2026-05-12 01:52:32 -04:00
committed by GitHub
co-authored by openhands
parent 860309f457
commit 4738f5b7c6
10 changed files with 362 additions and 12 deletions
+8 -2
View File
@@ -31,7 +31,7 @@ jobs:
- name: Set up Node.js with npm cache
uses: actions/setup-node@v6
with:
node-version: 22.12.0
node-version: '24'
cache: npm
- name: Install dependencies
@@ -43,5 +43,11 @@ jobs:
- name: Test
run: npm test
- name: Build
- name: Build app
run: npm run build
- name: Build library
run: npm run build:lib
- name: Verify package contents
run: npm pack --dry-run
+94
View File
@@ -0,0 +1,94 @@
name: Publish to npm
on:
push:
tags:
- 'v*'
concurrency:
group: npm-publish-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
id-token: write
jobs:
publish:
name: Publish to npm
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v6
# Trusted publishing requires Node 22.14.0+ and npm 11.5.1+
# See: https://docs.npmjs.com/trusted-publishers/
- name: Set up Node.js for npm trusted publishing
uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
registry-url: https://registry.npmjs.org
- name: Verify npm version supports trusted publishing
run: |
echo "Node version: $(node --version)"
echo "npm version: $(npm --version)"
NPM_VERSION=$(npm --version)
NPM_MAJOR=$(echo $NPM_VERSION | cut -d. -f1)
NPM_MINOR=$(echo $NPM_VERSION | cut -d. -f2)
if [ "$NPM_MAJOR" -lt 11 ] || ([ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -lt 5 ]); then
echo "Error: npm 11.5.1+ required for trusted publishing, got $NPM_VERSION"
exit 1
fi
echo "✓ npm $NPM_VERSION meets trusted publishing requirements"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build app
run: npm run build
- name: Build library
run: npm run build:lib
- name: Verify package contents
run: npm pack --dry-run
- name: Validate package version matches release tag
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
TAG_VERSION=${GITHUB_REF#refs/tags/v}
echo "Package version: $PACKAGE_VERSION"
echo "Release tag version: $TAG_VERSION"
if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
echo "Error: package.json version ($PACKAGE_VERSION) doesn't match release tag ($TAG_VERSION)"
exit 1
fi
echo "✓ Version $PACKAGE_VERSION matches release tag"
- name: Determine npm tag
id: npm-tag
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
if [[ "$TAG_VERSION" == *"alpha"* ]]; then
echo "tag=alpha" >> $GITHUB_OUTPUT
echo "Publishing with tag: alpha"
elif [[ "$TAG_VERSION" == *"beta"* ]]; then
echo "tag=beta" >> $GITHUB_OUTPUT
echo "Publishing with tag: beta"
elif [[ "$TAG_VERSION" == *"rc"* ]]; then
echo "tag=rc" >> $GITHUB_OUTPUT
echo "Publishing with tag: rc"
else
echo "tag=latest" >> $GITHUB_OUTPUT
echo "Publishing with tag: latest"
fi
- name: Publish to npm with provenance
run: npm publish --access public --provenance --tag ${{ steps.npm-tag.outputs.tag }}
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22.12.0
node-version: '24'
- name: Log trigger info
run: |
+2
View File
@@ -5,6 +5,7 @@ src/i18n/declaration.ts
.envrc
node_modules/
build/
dist/
/test-results/
@@ -14,3 +15,4 @@ build/
.react-router/
ralph/
.vercel
*.tgz
+29
View File
@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.0.0-alpha.2] - 2025-05-11
### Added
- Initial npm package release of `@openhands/agent-canvas`
- CLI entry point (`npx @openhands/agent-canvas`) to run full stack with Docker
- Library build mode with component barrel exports
- Subpath exports for modular imports:
- `@openhands/agent-canvas/browser`
- `@openhands/agent-canvas/conversation`
- `@openhands/agent-canvas/files`
- `@openhands/agent-canvas/settings`
- `@openhands/agent-canvas/sidebar`
- `@openhands/agent-canvas/terminal`
- `@openhands/agent-canvas/i18n`
- TypeScript type declarations
- GitHub Actions workflow for automated npm publishing (OIDC trusted publishing)
[Unreleased]: https://github.com/OpenHands/agent-canvas/compare/v1.0.0-alpha.2...HEAD
[1.0.0-alpha.2]: https://github.com/OpenHands/agent-canvas/releases/tag/v1.0.0-alpha.2
+36
View File
@@ -87,6 +87,42 @@ The Agent Server is often paired with an [Automation Server](https://github.com/
<img width="1456" height="1258" alt="image" src="https://github.com/user-attachments/assets/cb6de6f5-ac30-4d04-a76a-b5c259f0c163" />
## npm Package
Agent Canvas is also available as an npm package for embedding in your own applications:
```bash
npm install @openhands/agent-canvas
```
### Usage
Import the full package or specific components:
```typescript
// Full package
import { AgentServerUIProviders } from '@openhands/agent-canvas';
// Individual component packages
import { BrowserPanel } from '@openhands/agent-canvas/browser';
import { ChatPanel } from '@openhands/agent-canvas/conversation';
import { FileExplorer } from '@openhands/agent-canvas/files';
import { TerminalPanel } from '@openhands/agent-canvas/terminal';
```
### Available Subpath Exports
| Subpath | Description |
|---------|-------------|
| `@openhands/agent-canvas` | Main entry with providers and core components |
| `@openhands/agent-canvas/browser` | Browser/preview panel components |
| `@openhands/agent-canvas/conversation` | Chat interface and message components |
| `@openhands/agent-canvas/files` | File explorer and editor components |
| `@openhands/agent-canvas/settings` | Settings screens and forms |
| `@openhands/agent-canvas/sidebar` | Sidebar navigation components |
| `@openhands/agent-canvas/terminal` | Terminal emulator component |
| `@openhands/agent-canvas/i18n` | Internationalization resources |
## More documentation
For contributor and developer workflows, including frontend-only mode, mock mode, environment variables, and build/test commands, see [DEVELOPMENT.md](./DEVELOPMENT.md).
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* CLI entry point for @openhands/agent-canvas
*
* Runs the full Agent Canvas stack using Docker for the agent-server:
* - Agent-server runs in Docker container
* - Automation backend via uvx
* - Pre-built static frontend (not Vite dev server)
*
* This is the production equivalent of `npm run dev` - it runs the full stack
* but serves pre-built static assets instead of the Vite dev server.
*/
import { existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const BUILD_DIR = join(__dirname, "..", "build", "client");
// Check for help flag first
const args = process.argv.slice(2);
if (args.includes("-h") || args.includes("--help")) {
console.log(`
@openhands/agent-canvas - Run the Agent Canvas UI with agent-server (Docker)
Runs the full stack with agent-server in Docker, automation backend via uvx,
and serves pre-built static frontend assets.
USAGE:
npx @openhands/agent-canvas [options]
REQUIRED:
PROJECT_PATH Path to your projects directory (mounted into container)
OPTIONS:
-p, --port <port> Ingress port (default: 8000)
-h, --help Show this help message
ENVIRONMENT VARIABLES:
PROJECT_PATH Required: path to your projects directory
OH_SECRET_KEY Secret key for encrypting settings
OH_AGENT_SERVER_GIT_REF Git ref for agent-server Docker image tag
OH_AGENT_SERVER_LOCAL_PATH Path to local SDK checkout (for development)
OH_MOUNT_HOST_HOME Set to "1" to mount entire home directory
Note: LLM settings are configured through the web UI settings page,
not environment variables.
EXAMPLES:
# Start full stack (requires PROJECT_PATH)
PROJECT_PATH=/path/to/projects npx @openhands/agent-canvas
# Use a specific port
PROJECT_PATH=/path/to/projects npx @openhands/agent-canvas --port 3000
# Use local SDK checkout for development
PROJECT_PATH=/path/to/projects OH_AGENT_SERVER_LOCAL_PATH=/path/to/sdk npx @openhands/agent-canvas
For more options, see: node scripts/dev-docker.mjs --help
`);
process.exit(0);
}
// Check build exists before doing anything else
if (!existsSync(BUILD_DIR)) {
console.error(`
Error: No build found at ${BUILD_DIR}
This package needs to be built first. If you installed from npm,
this is a packaging error. If running from source:
npm install
npm run build
`);
process.exit(1);
}
// Import dev-docker's dependencies and run with static mode
let main, checkDockerPrereqs, startAgentServerDocker, CONTAINER_WORKSPACES_DIR;
try {
({ main } = await import("../scripts/dev-with-automation.mjs"));
({
checkDockerPrereqs,
startAgentServerDocker,
CONTAINER_WORKSPACES_DIR,
} = await import("../scripts/dev-docker.mjs"));
} catch (err) {
console.error("Failed to load required scripts. Try reinstalling:");
console.error(" npm install -g @openhands/agent-canvas@latest");
console.error(`\nError: ${err.message}`);
process.exit(1);
}
main({
bannerTitle: "Agent Canvas",
extraPrereqs: checkDockerPrereqs,
startAgentServer: startAgentServerDocker,
viteWorkingDir: CONTAINER_WORKSPACES_DIR,
staticMode: true,
staticDir: BUILD_DIR,
}).catch((err) => {
console.error(`Fatal error: ${err.message}`);
if (err.stack) {
console.error(err.stack);
}
process.exit(1);
});
+5 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@openhands/agent-canvas",
"version": "1.6.0",
"version": "1.0.0-alpha.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openhands/agent-canvas",
"version": "1.6.0",
"version": "1.0.0-alpha.2",
"license": "MIT",
"dependencies": {
"@heroui/react": "2.8.10",
@@ -53,6 +53,9 @@
"vite": "8.0.10",
"zustand": "5.0.12"
},
"bin": {
"agent-canvas": "bin/agent-canvas.mjs"
},
"devDependencies": {
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.39.4",
+18 -3
View File
@@ -1,9 +1,21 @@
{
"name": "@openhands/agent-canvas",
"version": "1.6.0",
"version": "1.0.0-alpha.2",
"description": "Agent Canvas UI for OpenHands - run AI coding agents with a visual interface",
"license": "MIT",
"private": false,
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/OpenHands/agent-canvas"
},
"homepage": "https://github.com/OpenHands/agent-canvas#readme",
"bugs": {
"url": "https://github.com/OpenHands/agent-canvas/issues"
},
"bin": {
"agent-canvas": "bin/agent-canvas.mjs"
},
"engines": {
"node": ">=22.12.0"
},
@@ -75,7 +87,7 @@
"prelint": "npm run make-i18n",
"lint": "npm run typecheck && eslint src && prettier --check src/**/*.{ts,tsx}",
"lint:fix": "eslint src --fix && prettier --write src/**/*.{ts,tsx}",
"prepare": "cd .. && husky frontend/.husky",
"prepare": "[ -d '../.git' ] && cd .. && husky frontend/.husky || true",
"typecheck": "react-router typegen && tsc",
"typecheck:staged": "react-router typegen && npx tsc --noEmit --skipLibCheck",
"check-translation-completeness": "node scripts/check-translation-completeness.cjs",
@@ -154,7 +166,10 @@
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
"dist",
"bin",
"build",
"scripts"
],
"exports": {
".": {
+61 -4
View File
@@ -38,7 +38,7 @@
*/
import { spawn, spawnSync } from "node:child_process";
import { mkdirSync } from "node:fs";
import { mkdirSync, existsSync } from "node:fs";
import { join, resolve, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { homedir } from "node:os";
@@ -116,6 +116,8 @@ function parseArgs() {
automationGitRef: null,
automationRepo: null,
verbose: false,
static: false,
staticDir: null,
};
for (let i = 0; i < args.length; i++) {
@@ -134,6 +136,12 @@ function parseArgs() {
case "--verbose":
config.verbose = true;
break;
case "--static":
config.static = true;
break;
case "--static-dir":
config.staticDir = args[++i];
break;
case "-h":
case "--help":
showHelp();
@@ -689,12 +697,21 @@ async function main(options = {}) {
startAgentServer: startAgentServerOverride,
extraPrereqs,
viteWorkingDir,
staticMode: staticModeOverride,
staticDir: staticDirOverride,
} = options;
const args = parseArgs();
// Allow options to override CLI args (for bin/agent-canvas.mjs)
const useStaticMode = staticModeOverride ?? args.static;
const staticDir = staticDirOverride ?? args.staticDir ?? join(projectRoot, "build", "client");
const modeLabel = useStaticMode ? "(Static)" : "";
const titleWithMode = modeLabel ? `${bannerTitle} ${modeLabel}` : bannerTitle;
console.log("");
console.log(`${c.cyan}${c.bold}${bannerTitle}${c.reset}`);
console.log(`${c.cyan}${c.bold}${titleWithMode}${c.reset}`);
console.log("");
// Setup phase
@@ -702,6 +719,13 @@ async function main(options = {}) {
// backend runs via uvx; only the agent-server is dockerized.)
checkPrerequisites();
// In static mode, verify build exists
if (useStaticMode && !existsSync(staticDir)) {
logError(`Static directory not found: ${staticDir}`);
logError(`Run 'npm run build' first to create the static files.`);
process.exit(1);
}
// Build config with dynamic port allocation
const config = await buildConfig(args);
if (viteWorkingDir) config.viteWorkingDir = viteWorkingDir;
@@ -736,8 +760,12 @@ async function main(options = {}) {
// 3. Start automation backend
startAutomationBackend(config);
// 4. Start Vite dev server (no proxy config needed - ingress handles routing)
startVite(config);
// 4. Start frontend server (Vite dev server OR static server)
if (useStaticMode) {
startStaticFrontend(config, staticDir);
} else {
startVite(config);
}
// 5. Wait for services to be ready
await delay(2000);
@@ -751,6 +779,35 @@ async function main(options = {}) {
printBanner(config);
}
function startStaticFrontend(config, staticDir) {
logService("static", `Starting on port ${config.vitePort}...`, c.magenta);
logService("static", `Serving from: ${staticDir}`, c.dim);
const staticServerScript = join(projectRoot, "scripts", "static-server.mjs");
spawnService(
"static",
"node",
[
staticServerScript,
"--dir", staticDir,
"--host", "0.0.0.0",
"--port", String(config.vitePort),
// Proxy routes to backends (same as ingress but for direct access to vitePort)
"--route", `/api/automation=http://localhost:${config.autoBackendPort}`,
"--route", `/api=http://localhost:${config.agentServerPort}`,
"--route", `/sockets=http://localhost:${config.agentServerPort}`,
"--route", `/server_info=http://localhost:${config.agentServerPort}`,
"--route", `/health=http://localhost:${config.agentServerPort}`,
"--route", `/ready=http://localhost:${config.agentServerPort}`,
"--route", `/alive=http://localhost:${config.agentServerPort}`,
],
{
cwd: config.canvasPath,
color: c.magenta,
}
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Exports for testing
// ═══════════════════════════════════════════════════════════════════════════