mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 13:18:19 +08:00
* feat: add Docker CI to build all-in-one image with agent-server + automation + frontend
Adds a GitHub Actions workflow (.github/workflows/docker.yml) that builds and
publishes ghcr.io/openhands/agent-canvas — a single Docker image combining:
1. Agent Server (ghcr.io/openhands/agent-server base image from SDK repo)
2. Automation server (pip-installed from openhands-automation)
3. agent-canvas frontend (static build from this repo)
The automation server is pip-installed rather than copied from its Docker image
because both services share openhands-sdk, fastapi, uvicorn, pydantic, httpx
etc. — installing into the agent-server's Python 3.13 deduplicates all shared
packages. Only automation-specific deps (asyncpg, sqlalchemy, boto3, …) are
added on top.
An entrypoint script starts all three services and a static-server proxy that
unifies them behind a single port (default 8000):
/api/automation/* → automation backend (:18001)
/api/* → agent-server (:18000)
/* → static frontend + SPA fallback
Workflow triggers:
- Push to main: builds and pushes with branch + SHA tags
- v* tags (releases): also pushes semver tags (1.2.3, 1.2, 1, latest)
- PRs: builds, pushes SHA-tagged image, updates PR description with
pull/run instructions (same pattern as the SDK repo)
- workflow_dispatch: supports overriding base image and automation version
Files added:
- docker/Dockerfile (multi-stage: frontend build + agent-server base)
- docker/entrypoint.sh (process manager for all three services)
- .dockerignore
- .github/workflows/docker.yml
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: build multi-arch Docker images (amd64 + arm64)
Adds QEMU setup for cross-compilation and defaults the platform matrix
to linux/amd64,linux/arm64 so the image works on both Intel and Apple
Silicon machines.
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor: rewrite Docker workflow to match SDK repo structure
Replace the single-job QEMU approach with the same architecture-matrix
pattern used by the SDK repo's server.yml:
1. build-and-push-image — matrix over {amd64, arm64} with native runners
(ubuntu-24.04 for amd64, ubuntu-24.04-arm for arm64). Each job pushes
arch-suffixed tags (e.g. sha-abc1234-amd64) and uploads build-info
artifacts.
2. merge-manifests — downloads both arch build-infos, strips the -amd64
suffix from amd64 tags to derive manifest tags, and creates multi-arch
manifests via `docker buildx imagetools create`.
3. consolidate-build-info — aggregates all build-info and manifest-info
artifacts into a single JSON summary (PR-only).
4. update-pr-description — renders the summary into the PR body between
AGENT_CANVAS_DOCKER_START/END markers.
Native runners avoid the 3-5× slowdown of QEMU emulation for arm64
builds.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: sanitize branch names in Docker tags (/ is not allowed)
Branch names like 'feat/docker-ci' produce invalid Docker tags because
'/' is forbidden in tag names. Replace '/' with '-' so the tag becomes
'feat-docker-ci-amd64'.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: default automation to SQLite and fix wait blocking proxy startup
Two bugs:
1. The automation server defaults to PostgreSQL on localhost, which
doesn't exist in the all-in-one container. Default AUTOMATION_DB_URL
to sqlite+aiosqlite:// so it works out of the box. Users can override
with a real Postgres URL for production.
2. The bare 'wait' command waited for ALL background children — including
the long-running agent-server and automation processes — so the
static-server/proxy on port 8000 never started. Fix by waiting only
for the wait_for_port subshell PIDs.
Verified locally: all three services start, endpoints respond correctly,
no more scheduler ConnectionRefusedError.
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: add VOLUME directives for persistence and project mounts
Declare /home/openhands/.openhands (settings, secrets, conversations,
automation SQLite DB) and /projects (user code) as Docker volumes so
data survives container restarts by default. Users should bind-mount
these for durable persistence:
docker run -v ~/.openhands:/home/openhands/.openhands \
-v ~/projects:/projects \
-p 8000:8000 ghcr.io/openhands/agent-canvas
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: set OH_SECRET_KEY default and pre-create persistence dirs
Three issues fixed:
1. OH_SECRET_KEY was not set → agent-server refused to return encrypted
secrets → conversation creation failed with 503. Set the same static
default used by dev-safe.mjs / dev-docker.mjs.
2. Persistence dirs (conversations, bash_events, automation DB) were not
pre-created → the openhands user got PermissionError when the VOLUME
directive created them as root. Pre-create with correct ownership
before the USER switch in the Dockerfile.
3. Set OH_PERSISTENCE_DIR, OH_CONVERSATIONS_PATH, OH_BASH_EVENTS_DIR
defaults in the entrypoint (matching dev-docker.mjs) so data lands
under the well-known ~/.openhands tree.
Verified locally: all three services start clean, no warnings about
OH_SECRET_KEY, SQLite migrations apply successfully.
Co-authored-by: openhands <openhands@all-hands.dev>
* chore: merge main and remove stale dev-docker.mjs references
Main removed scripts/dev-docker.mjs (Docker is no longer a dependency of
the npm package flow). Update comments in docker.yml, entrypoint.sh, and
AGENTS.md that referenced the deleted file.
Co-authored-by: openhands <openhands@all-hands.dev>
* feat: centralize config into config/defaults.json (single source of truth)
All version pins, port defaults, persistence paths, package names, and
the dev secret key now live in config/defaults.json. Consumers read from
it instead of hardcoding values:
- scripts/dev-safe.mjs: reads via JSON.parse(readFileSync(...))
- scripts/dev-with-automation.mjs: same
- scripts/check-sdk-version-sync.mjs: same (no longer regex-parses JS)
- docker/Dockerfile: config-gen build stage converts JSON to
/opt/agent-canvas/defaults.env (shell-sourceable)
- docker/entrypoint.sh: sources defaults.env at startup; also adds
session API key auto-generation so the image doesn't run wide-open
- .github/workflows/docker.yml: reads versions from JSON in a setup
step (no more hardcoded env vars)
To bump a version, edit config/defaults.json only.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: address PR review feedback (#634)
- Fix PID tracking bug: move PIDS+=($!) inside if/elif branches so the
else (automation-not-found) path doesn't add a stale PID
- chmod 600 session API key file to prevent credential leak
- Warn when using insecure default OH_SECRET_KEY in Docker entrypoint
- Add try/catch + field validation for config/defaults.json loading in
check-sdk-version-sync.mjs
- Fix semver tag parsing: strip pre-release/build metadata, only create
abbreviated tags (major.minor, major, latest) for stable releases
- Sanitize branch names for Docker tags (tr invalid chars, strip leading
dot/dash) to handle branches with #, @, spaces, etc.
- Add arch validation before manifest merge (assert both amd64.json and
arm64.json exist)
- Remove $schema reference to non-existent defaults.schema.json
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: remove hardcoded version defaults from Dockerfile
Replace hardcoded ARG defaults (AGENT_SERVER_IMAGE, AUTOMATION_VERSION)
with empty ARGs. Values are always derived from config/defaults.json:
- CI: reads JSON in the workflow config step, passes --build-arg
- Local: new scripts/docker-build.mjs helper reads JSON and invokes
docker build with the correct --build-arg values
Added npm run build:docker convenience script.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: stabilize snapshot tests and auto-generate Docker secret key
Two fixes:
1. **Flaky snapshot tests**: The 'Local pagination fixture' mock conversation
used a fixed absolute timestamp (PAGINATION_BASE_TIME = May 13, 2026) for
its created_at/updated_at, while 'Errored Project' used a relative
timestamp (now - 7d). As real time progressed past the crossover point,
their sort order in the sidebar flipped, causing 30/73 snapshot diffs on
every PR. Fix: use relative timestamps (now - 6d) for the pagination
fixture's conversation listing fields. The internal event timestamps
(used by pagination tests) still use PAGINATION_BASE_TIME — only the
sidebar ordering is affected.
2. **Docker OH_SECRET_KEY**: The entrypoint used a static insecure default
for OH_SECRET_KEY and warned about it. Now mirrors the session API key
pattern: auto-generate a cryptographic random key on first run, persist
it to ~/.openhands/agent-canvas/secret-key.txt, and reuse on restart.
Users can still override via the OH_SECRET_KEY env var. Removed the
now-unused CONFIG_SECRET_KEY from the Docker defaults.env generation.
Also deduped STATE_DIR computation (was repeated for session key path).
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: update AGENTS.md with mock timestamp and Docker secret key notes
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: include canvas_ui tool in Docker image
The Docker image was missing the tools/ directory and OH_EXTRA_PYTHON_PATH,
so the agent-server couldn't import canvas_ui_tool.py when the frontend
sent canvas_ui in the conversation tools list. This caused:
HTTP 500: ToolDefinition 'canvas_ui' is not registered
Fix: COPY tools/ into the image and set OH_EXTRA_PYTHON_PATH in the
entrypoint, matching what scripts/dev-safe.mjs already does for local dev.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
448 lines
14 KiB
JavaScript
448 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Check SDK Version Sync
|
|
*
|
|
* Verifies that the released automation package (openhands-automation on PyPI)
|
|
* uses the SDK version expected for that automation release for all agent SDK libraries:
|
|
* - openhands-sdk
|
|
* - openhands-tools
|
|
* - openhands-workspace
|
|
* - openhands-agent-server
|
|
*
|
|
* This script checks the RELEASED PyPI version of openhands-automation (as specified
|
|
* by versions.automation in config/defaults.json), not the main branch.
|
|
* versions.automationSdk records the SDK dependency version for that
|
|
* released automation package and may intentionally lag versions.agentServer.
|
|
*
|
|
* This script is run in CI to catch version drift between projects.
|
|
*
|
|
* Usage:
|
|
* node scripts/check-sdk-version-sync.mjs
|
|
* EXPECTED_SDK_VERSION=1.22.1 node scripts/check-sdk-version-sync.mjs
|
|
* node scripts/check-sdk-version-sync.mjs --check-pypi
|
|
*
|
|
* Environment variables:
|
|
* EXPECTED_SDK_VERSION - Override the expected version (instead of reading from config/defaults.json)
|
|
* AUTOMATION_PACKAGE_NAME - Override the automation package name (default: openhands-automation)
|
|
* AUTOMATION_PACKAGE_VERSION - Override the automation package version (instead of reading from config/defaults.json)
|
|
*
|
|
* Options:
|
|
* --check-pypi Also check the latest SDK version on PyPI
|
|
* --help Show help
|
|
*
|
|
* Exit codes:
|
|
* 0 - All SDK versions match
|
|
* 1 - Version mismatch detected or error occurred
|
|
*/
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import process from "node:process";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const projectRoot = join(__dirname, "..");
|
|
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const checkPyPI = args.includes("--check-pypi");
|
|
const showHelp = args.includes("--help") || args.includes("-h");
|
|
|
|
if (showHelp) {
|
|
console.log(`
|
|
SDK Version Sync Check
|
|
|
|
Verifies that the released openhands-automation package on PyPI uses the
|
|
SDK version expected for that automation release.
|
|
|
|
The automation version is read from config/defaults.json (versions.automation).
|
|
The expected SDK dependency version is read from versions.automationSdk,
|
|
falling back to versions.agentServer for older configs.
|
|
|
|
Usage:
|
|
node scripts/check-sdk-version-sync.mjs [options]
|
|
|
|
Options:
|
|
--check-pypi Also check the latest SDK version on PyPI
|
|
--help, -h Show this help
|
|
|
|
Environment variables:
|
|
EXPECTED_SDK_VERSION Override the expected SDK version (instead of reading from config/defaults.json)
|
|
AUTOMATION_PACKAGE_NAME Override the automation package name (default: openhands-automation)
|
|
AUTOMATION_PACKAGE_VERSION Override the automation package version (instead of reading from config/defaults.json)
|
|
|
|
Triggering from other repos:
|
|
The automation repo or SDK repo can trigger this check via GitHub repository_dispatch:
|
|
|
|
curl -X POST \\
|
|
-H "Authorization: token \$GITHUB_TOKEN" \\
|
|
-H "Accept: application/vnd.github.v3+json" \\
|
|
https://api.github.com/repos/OpenHands/agent-canvas/dispatches \\
|
|
-d '{"event_type": "sdk-version-check", "client_payload": {"version": "1.22.1"}}'
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// ANSI color codes for terminal output
|
|
const colors = {
|
|
reset: "\x1b[0m",
|
|
red: "\x1b[31m",
|
|
green: "\x1b[32m",
|
|
yellow: "\x1b[33m",
|
|
cyan: "\x1b[36m",
|
|
dim: "\x1b[2m",
|
|
};
|
|
|
|
// SDK packages that must have matching versions
|
|
const SDK_PACKAGES = [
|
|
"openhands-sdk",
|
|
"openhands-tools",
|
|
"openhands-workspace",
|
|
"openhands-agent-server",
|
|
];
|
|
|
|
// Configurable automation package (can be overridden via env)
|
|
const AUTOMATION_PACKAGE_NAME = process.env.AUTOMATION_PACKAGE_NAME || "openhands-automation";
|
|
|
|
// Default retry configuration
|
|
const RETRY_COUNT = 3;
|
|
const RETRY_DELAY_MS = 1000;
|
|
|
|
/**
|
|
* Normalize a version string for comparison.
|
|
* Handles variations like "1.22" vs "1.22.0" by ensuring consistent format.
|
|
*/
|
|
function normalizeVersion(version) {
|
|
if (!version) return null;
|
|
|
|
// Remove any pre-release or build metadata for base comparison
|
|
const baseVersion = version.split(/[-+]/)[0];
|
|
|
|
// Split into parts and pad to 3 parts (major.minor.patch)
|
|
const parts = baseVersion.split(".").map((p) => parseInt(p, 10) || 0);
|
|
while (parts.length < 3) {
|
|
parts.push(0);
|
|
}
|
|
|
|
return parts.slice(0, 3).join(".");
|
|
}
|
|
|
|
/**
|
|
* Compare two versions for equality (handles semantic equivalence)
|
|
*/
|
|
function versionsEqual(v1, v2) {
|
|
return normalizeVersion(v1) === normalizeVersion(v2);
|
|
}
|
|
|
|
/**
|
|
* Sleep for a given number of milliseconds
|
|
*/
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// ── Centralized config ──────────────────────────────────────────────────────
|
|
let SHARED_DEFAULTS;
|
|
try {
|
|
SHARED_DEFAULTS = JSON.parse(
|
|
readFileSync(join(projectRoot, "config", "defaults.json"), "utf-8"),
|
|
);
|
|
if (!SHARED_DEFAULTS.versions?.agentServer || !SHARED_DEFAULTS.versions?.automationSdk) {
|
|
throw new Error("missing required fields: versions.agentServer, versions.automationSdk");
|
|
}
|
|
} catch (err) {
|
|
console.error(`${colors.red}Failed to load config/defaults.json: ${err.message}${colors.reset}`);
|
|
console.error("Ensure the file exists and contains valid JSON with required fields.");
|
|
process.exit(1);
|
|
}
|
|
|
|
/**
|
|
* Read the default agent-server SDK version from config/defaults.json.
|
|
*/
|
|
function getDefaultAgentServerVersion() {
|
|
return { version: SHARED_DEFAULTS.versions.agentServer, source: "config/defaults.json" };
|
|
}
|
|
|
|
/**
|
|
* Read the expected automation SDK dependency version from environment
|
|
* or config/defaults.json.
|
|
*/
|
|
function getExpectedVersion() {
|
|
// Allow override via environment variable (useful for CI triggers).
|
|
const envVersion = process.env.EXPECTED_SDK_VERSION;
|
|
if (envVersion && envVersion.trim()) {
|
|
return { version: envVersion.trim(), source: "EXPECTED_SDK_VERSION env var" };
|
|
}
|
|
|
|
return {
|
|
version: SHARED_DEFAULTS.versions.automationSdk,
|
|
source: "config/defaults.json (versions.automationSdk)",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch the latest version of a package from PyPI
|
|
*/
|
|
async function fetchPyPIVersion(packageName) {
|
|
const url = `https://pypi.org/pypi/${packageName}/json`;
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
const data = await response.json();
|
|
return data.info?.version || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the automation version from env var or config/defaults.json
|
|
*/
|
|
function getAutomationVersion() {
|
|
// Allow override via environment variable
|
|
const envVersion = process.env.AUTOMATION_PACKAGE_VERSION;
|
|
if (envVersion && envVersion.trim()) {
|
|
return { version: envVersion.trim(), source: "AUTOMATION_PACKAGE_VERSION env var" };
|
|
}
|
|
|
|
return {
|
|
version: SHARED_DEFAULTS.versions.automation,
|
|
source: "config/defaults.json (versions.automation)",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch package metadata from PyPI and extract dependencies (with retry)
|
|
*/
|
|
async function fetchPyPIDependencies(packageName, version) {
|
|
const url = `https://pypi.org/pypi/${packageName}/${version}/json`;
|
|
|
|
console.log(`${colors.dim}Fetching ${url}${colors.reset}`);
|
|
|
|
let lastError;
|
|
for (let attempt = 0; attempt < RETRY_COUNT; attempt++) {
|
|
try {
|
|
const response = await fetch(url);
|
|
|
|
// 404 is a config issue, don't retry
|
|
if (response.status === 404) {
|
|
throw new Error(
|
|
`Package ${packageName}==${version} not found on PyPI (404). Check the package name and version.`,
|
|
);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Failed to fetch ${packageName}==${version} from PyPI: ${response.status} ${response.statusText}`,
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.info?.requires_dist || [];
|
|
} catch (err) {
|
|
lastError = err;
|
|
|
|
// Don't retry on 404 (config issue)
|
|
if (err.message.includes("not found on PyPI (404)")) {
|
|
throw err;
|
|
}
|
|
|
|
// Retry on other errors (network issues, 5xx, etc.)
|
|
if (attempt < RETRY_COUNT - 1) {
|
|
const delay = RETRY_DELAY_MS * (attempt + 1);
|
|
console.log(
|
|
`${colors.yellow}Retry ${attempt + 1}/${RETRY_COUNT - 1} after ${delay}ms...${colors.reset}`,
|
|
);
|
|
await sleep(delay);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
/**
|
|
* Parse PyPI requires_dist array and extract SDK package versions
|
|
*
|
|
* PyPI returns dependencies in PEP 508 format like:
|
|
* "openhands-sdk>=1.22.1,<2.0.0"
|
|
* "openhands-tools==1.22.1"
|
|
* "openhands-workspace (>=1.22.1)"
|
|
*/
|
|
function parseSdkVersionsFromRequiresDist(requiresDist) {
|
|
const versions = {};
|
|
|
|
for (const pkg of SDK_PACKAGES) {
|
|
for (const dep of requiresDist) {
|
|
// Check if the dependency starts with our package name
|
|
// The package name may be followed by whitespace, operators, or parentheses
|
|
if (!dep.toLowerCase().startsWith(pkg.toLowerCase())) {
|
|
continue;
|
|
}
|
|
|
|
// Extract the version number - look for patterns like:
|
|
// ">=1.22.1", "==1.22.1", "(>=1.22.1)", "~=1.22.1"
|
|
// After the package name and before any comma or closing paren
|
|
const versionPattern = /[><=~!]+\s*([0-9]+(?:\.[0-9]+)*)/;
|
|
const match = dep.match(versionPattern);
|
|
if (match) {
|
|
versions[pkg] = match[1];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return versions;
|
|
}
|
|
|
|
/**
|
|
* Main entry point
|
|
*/
|
|
async function main() {
|
|
console.log("");
|
|
console.log(
|
|
`${colors.cyan}SDK Version Sync Check${colors.reset}`,
|
|
);
|
|
console.log("─".repeat(50));
|
|
console.log("");
|
|
|
|
try {
|
|
// Get expected version from env var or config/defaults.json
|
|
const { version: expectedVersion, source: versionSource } = getExpectedVersion();
|
|
console.log(
|
|
`Expected automation SDK version: ${colors.green}${expectedVersion}${colors.reset} (from ${versionSource})`,
|
|
);
|
|
|
|
const { version: agentServerVersion } = getDefaultAgentServerVersion();
|
|
if (!versionsEqual(agentServerVersion, expectedVersion)) {
|
|
console.log(
|
|
`${colors.yellow}Note:${colors.reset} DEFAULT_AGENT_SERVER_VERSION is ${agentServerVersion}; automation release dependencies may lag while a compatible automation package is pending.`,
|
|
);
|
|
}
|
|
|
|
// Get automation version from env var or config/defaults.json
|
|
const { version: automationVersion, source: automationSource } = getAutomationVersion();
|
|
console.log(
|
|
`Automation package: ${colors.cyan}${AUTOMATION_PACKAGE_NAME}==${automationVersion}${colors.reset} (from ${automationSource})`,
|
|
);
|
|
|
|
// Optionally check PyPI for the latest SDK version
|
|
if (checkPyPI) {
|
|
console.log("");
|
|
console.log("Checking latest SDK versions on PyPI:");
|
|
for (const pkg of SDK_PACKAGES) {
|
|
const pypiVersion = await fetchPyPIVersion(pkg);
|
|
if (pypiVersion) {
|
|
const status = versionsEqual(pypiVersion, expectedVersion)
|
|
? colors.green
|
|
: colors.yellow;
|
|
console.log(` ${pkg.padEnd(25)} ${status}${pypiVersion}${colors.reset}`);
|
|
} else {
|
|
console.log(` ${pkg.padEnd(25)} ${colors.dim}(not found on PyPI)${colors.reset}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log("");
|
|
|
|
// Fetch automation package dependencies from PyPI
|
|
const requiresDist = await fetchPyPIDependencies(AUTOMATION_PACKAGE_NAME, automationVersion);
|
|
const automationVersions = parseSdkVersionsFromRequiresDist(requiresDist);
|
|
|
|
// Check each SDK package
|
|
let hasErrors = false;
|
|
let foundAny = false;
|
|
const mismatches = [];
|
|
|
|
console.log(`Checking ${AUTOMATION_PACKAGE_NAME}==${automationVersion} SDK dependencies:`);
|
|
console.log("");
|
|
|
|
for (const pkg of SDK_PACKAGES) {
|
|
const actualVersion = automationVersions[pkg];
|
|
|
|
if (actualVersion) {
|
|
foundAny = true;
|
|
if (versionsEqual(actualVersion, expectedVersion)) {
|
|
console.log(
|
|
` ${pkg.padEnd(25)} ${colors.green}✓ ${actualVersion}${colors.reset}`,
|
|
);
|
|
} else {
|
|
hasErrors = true;
|
|
console.log(
|
|
` ${pkg.padEnd(25)} ${colors.red}✗ ${actualVersion} (expected ${expectedVersion})${colors.reset}`,
|
|
);
|
|
mismatches.push({
|
|
package: pkg,
|
|
expected: expectedVersion,
|
|
actual: actualVersion,
|
|
});
|
|
}
|
|
} else {
|
|
// Package not found - might be a transitive dependency, not an error
|
|
console.log(
|
|
` ${pkg.padEnd(25)} ${colors.dim}- not a direct dependency${colors.reset}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log("");
|
|
|
|
if (!foundAny) {
|
|
console.log(
|
|
`${colors.yellow}Warning: No SDK packages found in ${AUTOMATION_PACKAGE_NAME}==${automationVersion} dependencies${colors.reset}`,
|
|
);
|
|
console.log("This might indicate a parsing issue or the package is not yet published.");
|
|
console.log("");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (hasErrors) {
|
|
console.log(
|
|
`${colors.red}Version mismatch detected!${colors.reset}`,
|
|
);
|
|
console.log("");
|
|
console.log(`The released ${AUTOMATION_PACKAGE_NAME}==${automationVersion} uses different SDK versions than expected for that automation release.`);
|
|
console.log("");
|
|
console.log("Mismatched packages:");
|
|
for (const m of mismatches) {
|
|
console.log(` - ${m.package}: ${m.actual} (expected ${m.expected})`);
|
|
}
|
|
console.log("");
|
|
console.log("To fix, update one of the following:");
|
|
console.log(
|
|
` 1. Update versions.automationSdk in config/defaults.json to match the automation release`,
|
|
);
|
|
console.log(
|
|
` 2. Release a new version of ${AUTOMATION_PACKAGE_NAME} with SDK dependencies pinned to ${expectedVersion}`,
|
|
);
|
|
console.log(
|
|
` 3. Update versions.automation in config/defaults.json to a newer release`,
|
|
);
|
|
console.log("");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`${colors.green}All SDK versions are in sync!${colors.reset}`,
|
|
);
|
|
console.log("");
|
|
} catch (error) {
|
|
console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Export for testing
|
|
export {
|
|
normalizeVersion,
|
|
versionsEqual,
|
|
parseSdkVersionsFromRequiresDist,
|
|
SDK_PACKAGES,
|
|
AUTOMATION_PACKAGE_NAME,
|
|
};
|
|
|
|
main();
|