Skip to content

Commit 3039ed2

Browse files
authored
Merge branch 'main' into fix/sandbox-credentials-dir
2 parents 075a5fc + 39e9b1f commit 3039ed2

7 files changed

Lines changed: 219 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Build artifacts and caches
2+
.version
23
*.pyc
34
*.tsbuildinfo
45
.pytest_cache/

.pre-commit-config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,15 @@ repos:
213213
stages: [pre-push]
214214
priority: 10
215215

216+
- id: version-tag-sync
217+
name: package.json ↔ git tag version sync
218+
entry: bash scripts/check-version-tag-sync.sh
219+
language: system
220+
always_run: true
221+
pass_filenames: false
222+
stages: [pre-push]
223+
priority: 10
224+
216225
# ── Priority 20: project-level checks (coverage + ratchet) ─────────────────
217226
- repo: local
218227
hooks:

bin/lib/version.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/**
5+
* Resolve the NemoClaw version from (in order):
6+
* 1. `git describe --tags --match "v*"` — works in dev / source checkouts
7+
* 2. `.version` file at repo root — stamped at publish time
8+
* 3. `package.json` version — hard-coded fallback
9+
*/
10+
11+
const { execFileSync } = require("child_process");
12+
const path = require("path");
13+
const fs = require("fs");
14+
15+
const ROOT = path.resolve(__dirname, "..", "..");
16+
17+
function getVersion() {
18+
// 1. Try git (available in dev clones and CI)
19+
try {
20+
const raw = execFileSync("git", ["describe", "--tags", "--match", "v*"], {
21+
cwd: ROOT,
22+
encoding: "utf-8",
23+
stdio: ["ignore", "pipe", "ignore"],
24+
}).trim();
25+
// raw looks like "v0.3.0" or "v0.3.0-4-gabcdef1"
26+
if (raw) return raw.replace(/^v/, "");
27+
} catch {
28+
// no git, or no matching tags — fall through
29+
}
30+
31+
// 2. Try .version file (stamped by prepublishOnly)
32+
try {
33+
const ver = fs.readFileSync(path.join(ROOT, ".version"), "utf-8").trim();
34+
if (ver) return ver;
35+
} catch {
36+
// not present — fall through
37+
}
38+
39+
// 3. Fallback to package.json
40+
return require(path.join(ROOT, "package.json")).version;
41+
}
42+
43+
module.exports = { getVersion };

bin/nemoclaw.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const registry = require("./lib/registry");
4242
const nim = require("./lib/nim");
4343
const policies = require("./lib/policies");
4444
const { parseGatewayInference } = require("./lib/inference-config");
45+
const { getVersion } = require("./lib/version");
4546
const onboardSession = require("./lib/onboard-session");
4647
const { parseLiveSandboxNames } = require("./lib/runtime-recovery");
4748

@@ -1123,9 +1124,8 @@ async function sandboxDestroy(sandboxName, args = []) {
11231124
// ── Help ─────────────────────────────────────────────────────────
11241125

11251126
function help() {
1126-
const pkg = require(path.join(__dirname, "..", "package.json"));
11271127
console.log(`
1128-
${B}${G}NemoClaw${R} ${D}v${pkg.version}${R}
1128+
${B}${G}NemoClaw${R} ${D}v${getVersion()}${R}
11291129
${D}Deploy more secure, always-on AI assistants with a single command.${R}
11301130
11311131
${G}Getting Started:${R}
@@ -1216,8 +1216,7 @@ const [cmd, ...args] = process.argv.slice(2);
12161216
break;
12171217
case "--version":
12181218
case "-v": {
1219-
const pkg = require(path.join(__dirname, "..", "package.json"));
1220-
console.log(`nemoclaw v${pkg.version}`);
1219+
console.log(`nemoclaw v${getVersion()}`);
12211220
break;
12221221
}
12231222
default:

install.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,27 @@ DEFAULT_NEMOCLAW_VERSION="0.1.0"
2525
TOTAL_STEPS=3
2626

2727
resolve_installer_version() {
28+
# Prefer git tags (works in dev clones and CI)
29+
if command -v git &>/dev/null && [[ -d "${SCRIPT_DIR}/.git" ]]; then
30+
local git_ver=""
31+
if git_ver="$(git -C "$SCRIPT_DIR" describe --tags --match 'v*' 2>/dev/null)"; then
32+
git_ver="${git_ver#v}"
33+
if [[ -n "$git_ver" ]]; then
34+
printf "%s" "$git_ver"
35+
return
36+
fi
37+
fi
38+
fi
39+
# Fall back to .version file (stamped during install)
40+
if [[ -f "${SCRIPT_DIR}/.version" ]]; then
41+
local file_ver
42+
file_ver="$(cat "${SCRIPT_DIR}/.version")"
43+
if [[ -n "$file_ver" ]]; then
44+
printf "%s" "$file_ver"
45+
return
46+
fi
47+
fi
48+
# Last resort: package.json
2849
local package_json="${SCRIPT_DIR}/package.json"
2950
local version=""
3051
if [[ -f "$package_json" ]]; then
@@ -693,6 +714,14 @@ install_nemoclaw() {
693714
rm -rf "$nemoclaw_src"
694715
mkdir -p "$(dirname "$nemoclaw_src")"
695716
spin "Cloning NemoClaw source" git clone --depth 1 --branch "$release_ref" https://github.com/NVIDIA/NemoClaw.git "$nemoclaw_src"
717+
# Fetch version tags into the shallow clone so `git describe --tags
718+
# --match "v*"` works at runtime (the shallow clone only has the
719+
# single ref we asked for).
720+
git -C "$nemoclaw_src" fetch --depth=1 origin 'refs/tags/v*:refs/tags/v*' 2>/dev/null || true
721+
# Also stamp .version as a fallback for environments where git is
722+
# unavailable or tags are pruned later.
723+
git -C "$nemoclaw_src" describe --tags --match 'v*' 2>/dev/null \
724+
| sed 's/^v//' >"$nemoclaw_src/.version" || true
696725
spin "Preparing OpenClaw package" bash -c "$(declare -f info warn pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$nemoclaw_src" \
697726
|| warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken"
698727
spin "Installing NemoClaw dependencies" bash -c "cd \"$nemoclaw_src\" && npm install --ignore-scripts"

scripts/check-version-tag-sync.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env bash
2+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Pre-push hook: when pushing a v* tag, verify that package.json at the
6+
# tagged commit has a matching version. Blocks the push if they differ.
7+
#
8+
# Usage (called by prek as a pre-push hook):
9+
# echo "<local-ref> <local-sha> <remote-ref> <remote-sha>" | bash scripts/check-version-tag-sync.sh
10+
#
11+
# Manual check (no stdin needed — compares latest v* tag with package.json):
12+
# bash scripts/check-version-tag-sync.sh --check
13+
14+
set -euo pipefail
15+
16+
RED=$'\033[1;31m'
17+
GREEN=$'\033[32m'
18+
DIM=$'\033[2m'
19+
RESET=$'\033[0m'
20+
21+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
22+
23+
# Extract the "version" field from the package.json at a given commit.
24+
version_at_commit() {
25+
local sha="$1"
26+
git -C "$ROOT" show "${sha}:package.json" 2>/dev/null \
27+
| sed -nE 's/^[[:space:]]*"version":[[:space:]]*"([^"]+)".*/\1/p' \
28+
| head -1
29+
}
30+
31+
check_tag() {
32+
local tag="$1" sha="$2"
33+
local tag_version="${tag#v}"
34+
local pkg_version
35+
pkg_version="$(version_at_commit "$sha")"
36+
37+
if [[ -z "$pkg_version" ]]; then
38+
echo "${RED}${RESET} Tag ${tag}: could not read package.json at ${sha:0:8}" >&2
39+
return 1
40+
fi
41+
42+
if [[ "$pkg_version" != "$tag_version" ]]; then
43+
cat >&2 <<EOF
44+
45+
${RED}✗ Version mismatch for tag ${tag}${RESET}
46+
47+
Tag version: ${tag_version}
48+
package.json version: ${pkg_version}
49+
50+
Update package.json before tagging:
51+
52+
${DIM}npm version ${tag_version} --no-git-tag-version
53+
git add package.json
54+
git commit --amend --no-edit
55+
git tag -f ${tag}${RESET}
56+
57+
EOF
58+
return 1
59+
fi
60+
61+
echo "${GREEN}${RESET} Tag ${tag} matches package.json (${pkg_version})"
62+
return 0
63+
}
64+
65+
# ------------------------------------------------------------------
66+
# --check mode: compare the latest v* tag against current package.json
67+
# ------------------------------------------------------------------
68+
if [[ "${1:-}" == "--check" ]]; then
69+
latest_tag="$(git -C "$ROOT" describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)"
70+
if [[ -z "$latest_tag" ]]; then
71+
echo "${DIM}No v* tags found — nothing to check.${RESET}"
72+
exit 0
73+
fi
74+
sha="$(git -C "$ROOT" rev-list -1 "$latest_tag")"
75+
check_tag "$latest_tag" "$sha"
76+
exit $?
77+
fi
78+
79+
# ------------------------------------------------------------------
80+
# Pre-push mode: read pushed refs from stdin
81+
# ------------------------------------------------------------------
82+
errors=0
83+
84+
while IFS=' ' read -r local_ref local_sha _remote_ref _remote_sha; do
85+
# Only care about v* tag pushes
86+
case "$local_ref" in
87+
refs/tags/v*)
88+
tag="${local_ref#refs/tags/}"
89+
check_tag "$tag" "$local_sha" || errors=$((errors + 1))
90+
;;
91+
esac
92+
done
93+
94+
if ((errors > 0)); then
95+
exit 1
96+
fi

test/install-preflight.test.js

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -902,8 +902,14 @@ exit 0`,
902902
});
903903

904904
expect(result.status).toBe(0);
905-
// git should NOT have been called at all in the source-checkout path
906-
expect(fs.existsSync(gitLog)).toBe(false);
905+
// git clone / git fetch should NOT have been called in the source-checkout path.
906+
// git may be called for version resolution (git describe), so we check
907+
// that no clone or fetch was attempted rather than no git calls at all.
908+
if (fs.existsSync(gitLog)) {
909+
const gitCalls = fs.readFileSync(gitLog, "utf-8");
910+
expect(gitCalls).not.toMatch(/clone/);
911+
expect(gitCalls).not.toMatch(/fetch/);
912+
}
907913
// And curl for the releases API should NOT have been called
908914
expect(`${result.stdout}${result.stderr}`).not.toMatch(/curl should not be called/);
909915
});
@@ -1043,18 +1049,43 @@ describe("installer pure helpers", () => {
10431049

10441050
// -- resolve_installer_version --
10451051

1046-
it("resolve_installer_version: reads version from package.json", () => {
1052+
it("resolve_installer_version: reads version from git or package.json", () => {
10471053
const r = callInstallerFn("resolve_installer_version");
1048-
// Should read from the repo's actual package.json
1049-
expect(r.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
1054+
// May return clean semver ("0.0.2") or git describe format ("0.0.2-3-gabcdef1")
1055+
expect(r.stdout.trim()).toMatch(/^\d+\.\d+\.\d+(-.+)?$/);
1056+
});
1057+
1058+
it("resolve_installer_version: falls back to package.json when git tags are unavailable", () => {
1059+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-resolve-ver-pkg-"));
1060+
fs.mkdirSync(path.join(tmp, ".git"));
1061+
fs.writeFileSync(
1062+
path.join(tmp, "package.json"),
1063+
`${JSON.stringify({ version: "0.5.0" }, null, 2)}\n`,
1064+
);
1065+
// source overwrites SCRIPT_DIR, so we re-set it after sourcing.
1066+
// The temp dir advertises git metadata but has no usable tags,
1067+
// so the function should fall back to package.json instead of exiting.
1068+
const r = spawnSync(
1069+
"bash",
1070+
["-c", `source "${INSTALLER}" 2>/dev/null; SCRIPT_DIR="${tmp}"; resolve_installer_version`],
1071+
{
1072+
cwd: tmp,
1073+
encoding: "utf-8",
1074+
env: { HOME: tmp, PATH: TEST_SYSTEM_PATH },
1075+
},
1076+
);
1077+
expect(r.status).toBe(0);
1078+
expect(r.stdout.trim()).toBe("0.5.0");
10501079
});
10511080

10521081
it("resolve_installer_version: falls back to DEFAULT when no package.json", () => {
10531082
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-resolve-ver-"));
1054-
// source from a directory with no package.json — SCRIPT_DIR will be wrong
1083+
// source overwrites SCRIPT_DIR, so we re-set it after sourcing.
1084+
// The temp dir has no .git, no .version, and no package.json,
1085+
// so the function should fall back to DEFAULT_NEMOCLAW_VERSION.
10551086
const r = spawnSync(
10561087
"bash",
1057-
["-c", `SCRIPT_DIR="${tmp}"; source "${INSTALLER}" 2>/dev/null; resolve_installer_version`],
1088+
["-c", `source "${INSTALLER}" 2>/dev/null; SCRIPT_DIR="${tmp}"; resolve_installer_version`],
10581089
{
10591090
cwd: tmp,
10601091
encoding: "utf-8",

0 commit comments

Comments
 (0)