From ba6437896dcbe0e359c6087d5140df4ac65b9263 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 03:20:04 -0400 Subject: [PATCH 1/2] fix(release): bump every sdk npm package in lockstep, incl. @nself/cli-api (1.4.15) bump-version.sh listed sdk/ts and sdk/ts-sdk by hand and skipped sdk/ts-cli-api, so the v1.4.15 release left it at 1.4.14 and SDK Version Sync has been red on main since. The bumper and the three lockstep checks now glob sdk/*/package.json, so a new package is covered from the day it exists. The release fixture tests run the real bumper over one fixture per sdk package and fail when any is left behind. sdk/ts-cli-api is brought to 1.4.15 to complete the already released 1.4.15. No tag, release or npm publish. --- .github/workflows/sdk-version-sync.yml | 8 +++-- .github/workflows/version-lockstep.yml | 31 ++++++++++--------- scripts/bump-version.sh | 42 ++++++++++++++------------ scripts/check-version-lockstep.sh | 20 +++++++----- scripts/release/fixtures/cases4.sh | 19 ++++++++++-- 5 files changed, 73 insertions(+), 47 deletions(-) diff --git a/.github/workflows/sdk-version-sync.yml b/.github/workflows/sdk-version-sync.yml index 32964d542..a01e45771 100644 --- a/.github/workflows/sdk-version-sync.yml +++ b/.github/workflows/sdk-version-sync.yml @@ -84,11 +84,13 @@ jobs: exit 1 fi - - name: Verify @nself/sdk and @nself/cli-api versions (sdk/ts-sdk, sdk/ts-cli-api) + - name: Verify every sdk npm package version (sdk/*/package.json) run: | TARGET='${{ steps.target.outputs.target }}' - for dir in sdk/ts-sdk sdk/ts-cli-api; do - V=$(node -p "require('./${dir}/package.json').version") + # Globbed, not listed: a package missing from a hand-kept list was skipped by the bumper. + for pkg in sdk/*/package.json; do + dir="${pkg%/package.json}" + V=$(node -p "require('./${pkg}').version") echo "${dir} declared: ${V}" if [ "${V}" != "${TARGET}" ]; then echo "MISMATCH: ${dir} is ${V}, expected ${TARGET}" diff --git a/.github/workflows/version-lockstep.yml b/.github/workflows/version-lockstep.yml index 6806280a7..25de1d87d 100644 --- a/.github/workflows/version-lockstep.yml +++ b/.github/workflows/version-lockstep.yml @@ -29,8 +29,7 @@ on: - '.github/VERSION' - 'internal/version/version.go' - 'sdk/go/doc.go' - - 'sdk/ts/package.json' - - 'sdk/ts-sdk/package.json' + - 'sdk/*/package.json' - 'sdk/py/pyproject.toml' - '.github/workflows/version-lockstep.yml' - 'scripts/check-version-lockstep.sh' @@ -136,20 +135,22 @@ jobs: | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | head -1) check "cli/sdk/go/doc.go" "${V}" 1 - # 4. cli/sdk/ts/package.json (@nself/plugin-sdk) - V=$(node -p "require('${NSELF_REPO_ROOT}/sdk/ts/package.json').version") - check "cli/sdk/ts/package.json" "${V}" 1 - - # 4b. cli/sdk/ts-sdk/package.json (@nself/sdk) + # 4. cli/sdk/*/package.json (@nself/plugin-sdk, @nself/sdk, @nself/cli-api, ...) # - # There are TWO TypeScript packages and only the first was listed - # here, so a release bumped one and left the other behind. Its publish - # workflow then saw its version already on npm, no-opped as - # "already published", and reported success — and the SDK Version - # Coherence check caught the drift a day later on its schedule, - # after the release had shipped. - V=$(node -p "require('${NSELF_REPO_ROOT}/sdk/ts-sdk/package.json').version") - check "cli/sdk/ts-sdk/package.json" "${V}" 1 + # Discovered by glob, never listed by hand: a hand-kept list let a release + # bump one npm package and leave another behind (sdk/ts-sdk once, then + # sdk/ts-cli-api at v1.4.15). bump-version.sh uses the same glob. + n=0 + for pkg in "${NSELF_REPO_ROOT}"/sdk/*/package.json; do + [ -f "${pkg}" ] || continue + n=$((n + 1)) + V=$(node -p "require('${pkg}').version") + check "cli/${pkg#${NSELF_REPO_ROOT}/}" "${V}" 1 + done + if [ "${n}" -eq 0 ]; then + printf '::error::no sdk/*/package.json found\n' + exit 1 + fi # 5. cli/sdk/py/pyproject.toml V=$(grep -E '^version\s*=' "${NSELF_REPO_ROOT}/sdk/py/pyproject.toml" \ diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 3d5de79f8..b76b30810 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -24,8 +24,8 @@ # 1. cli/.github/VERSION # 2. cli/internal/version/version.go Version string = "x.y.z" (type-annotated) # 3. cli/sdk/go/doc.go const Version = "x.y.z" -# 4. cli/sdk/ts/package.json "version": "x.y.z" (via jq) -# 5. cli/sdk/ts-sdk/package.json "version": "x.y.z" (via jq) +# 4. cli/sdk/*/package.json "version": "x.y.z" (via jq; every TS package +# found under sdk/: ts, ts-sdk, ts-cli-api, ...) # 6. cli/sdk/py/pyproject.toml version = "x.y.z" under [project] # 7. admin/package.json "version": "x.y.z" (via jq) # 8. admin/src/lib/cli-version.ts CLI_VERSION = 'x.y.z' @@ -102,7 +102,7 @@ fi # ── Tool availability checks ────────────────────────────────────────────────── if ! command -v jq >/dev/null 2>&1; then - printf 'ERROR: jq is required for JSON file bumps (sdk/ts/package.json, sdk/ts-sdk/package.json, admin/package.json)\n' >&2 + printf 'ERROR: jq is required for JSON file bumps (sdk/*/package.json, admin/package.json)\n' >&2 printf ' Install: brew install jq (macOS) | apt-get install jq (Linux)\n' >&2 exit 1 fi @@ -114,9 +114,7 @@ VERSION_FILE="${REPO_ROOT}/.github/VERSION" OLD_VERSION="$(tr -d '[:space:]' < "${VERSION_FILE}" 2>/dev/null || true)" VERSION_GO="${REPO_ROOT}/internal/version/version.go" SDK_GO="${REPO_ROOT}/sdk/go/doc.go" -SDK_TS="${REPO_ROOT}/sdk/ts/package.json" SDK_PY="${REPO_ROOT}/sdk/py/pyproject.toml" -SDK_TS_SDK="${REPO_ROOT}/sdk/ts-sdk/package.json" # SDK_FLUTTER removed 2026-06-30 (#159, ASI Policy 2) ADMIN_PKG="${NSELF_ROOT}/admin/package.json" ADMIN_CLIVER="${NSELF_ROOT}/admin/src/lib/cli-version.ts" @@ -248,17 +246,24 @@ update_sdk_go_doc() { report "${f}" "${old}" "${NEW_VERSION}" } -update_sdk_ts_pkg() { - update_json_version "${SDK_TS}" -} - -# sdk/ts-sdk/package.json — the @nself/sdk consumer client (NselfClient), -# distinct from sdk/ts (@nself/plugin-sdk). Publishes in lockstep with the -# CLI per sdk-ts-sdk-publish.yml ("Triggers on: cli release tag (v*)"). -# Added 2026-07-06 after #179 (missing from this bumper caused @nself/sdk -# to lag at 1.1.9 while VERSION moved to 1.2.4 — SDK Version Coherence fail). -update_sdk_ts_sdk_pkg() { - update_json_version "${SDK_TS_SDK}" +# Every sdk/*/package.json is one npm package that ships in lockstep with the CLI +# (sdk/ts = @nself/plugin-sdk, sdk/ts-sdk = @nself/sdk, sdk/ts-cli-api = @nself/cli-api). +# The packages are discovered, never listed by hand: a hand-kept list lost sdk/ts-sdk +# once (#179, @nself/sdk lagged at 1.1.9) and sdk/ts-cli-api again at v1.4.15, where +# SDK Version Sync went red on main. A new sdk//package.json is bumped from the +# moment it exists. The same glob drives the lockstep checks in version-lockstep.yml +# and check-version-lockstep.sh. +update_sdk_ts_pkgs() { + local f n=0 + for f in "${REPO_ROOT}"/sdk/*/package.json; do + [ -f "${f}" ] || continue + update_json_version "${f}" + n=$((n + 1)) + done + if [ "${n}" -eq 0 ]; then + printf 'ERROR: no sdk/*/package.json found under %s\n' "${REPO_ROOT}" >&2 + exit 1 + fi } # pyproject.toml shape: `version = "2.0.0"` under [project] @@ -558,13 +563,12 @@ if [ "${HOMEBREW}" = "true" ]; then exit 0 fi -printf 'Phase 1 — updating 11 lockstep files + 4 version-hash goldens (Homebrew deferred to phase 2)\n\n' +printf 'Phase 1 — updating the lockstep files (every sdk/*/package.json included) + 4 version-hash goldens (Homebrew deferred to phase 2)\n\n' update_version_file update_version_go update_sdk_go_doc -update_sdk_ts_pkg -update_sdk_ts_sdk_pkg +update_sdk_ts_pkgs update_sdk_py_pyproject update_admin_pkg update_admin_cliver diff --git a/scripts/check-version-lockstep.sh b/scripts/check-version-lockstep.sh index 6eff76b1a..3a193d72f 100755 --- a/scripts/check-version-lockstep.sh +++ b/scripts/check-version-lockstep.sh @@ -10,7 +10,7 @@ # 1. cli/internal/version/version.go (Go constant) # 2. cli/.github/VERSION (plain text) # 3. cli/sdk/go/doc.go (Go const) -# 4. cli/sdk/ts/package.json (JSON version field) +# 4. cli/sdk/*/package.json (JSON version field; every sdk npm package) # 5. cli/sdk/py/pyproject.toml (TOML version field) # 6. admin/package.json (JSON version field) # 7. admin/src/lib/cli-version.ts (TS constant) @@ -170,18 +170,22 @@ else log_warn "cli/sdk/go/doc.go" "(missing)" fi -# 4. cli/sdk/ts/package.json (warn-only) -FILE4="${CLI_ROOT}/sdk/ts/package.json" -if [ -f "${FILE4}" ]; then +# 4. cli/sdk/*/package.json (warn-only): every npm package under sdk/ (ts, ts-sdk, +# ts-cli-api, ...) is discovered by glob, the same way bump-version.sh bumps them. +SDK_PKG_N=0 +for FILE4 in "${CLI_ROOT}"/sdk/*/package.json; do + [ -f "${FILE4}" ] || continue + SDK_PKG_N=$((SDK_PKG_N + 1)) if command -v python3 >/dev/null 2>&1; then - V4=$(python3 -c "import json,sys; d=json.load(open('${FILE4}')); print(d['version'])" 2>/dev/null || true) + V4=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d['version'])" "${FILE4}" 2>/dev/null || true) else V4=$(grep -E '"version"\s*:' "${FILE4}" \ | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' | head -1) fi - check_version "cli/sdk/ts/package.json" "${V4}" 0 -else - log_warn "cli/sdk/ts/package.json" "(missing)" + check_version "cli/${FILE4#"${CLI_ROOT}/"}" "${V4}" 0 +done +if [ "${SDK_PKG_N}" -eq 0 ]; then + log_warn "cli/sdk/*/package.json" "(none found)" fi # 5. cli/sdk/py/pyproject.toml (warn-only) diff --git a/scripts/release/fixtures/cases4.sh b/scripts/release/fixtures/cases4.sh index 27b57e66b..1bae19e5a 100644 --- a/scripts/release/fixtures/cases4.sh +++ b/scripts/release/fixtures/cases4.sh @@ -15,13 +15,17 @@ mk_bump_env() { B=$(mktemp -d "$BASE/bump.XXXXXX"); C=$B/nself/cli; A=$B/nself/admin # mk_env's TMPDIR was deleted with its env; bump-version.sh's `mktemp -d` needs a live one (GNU mktemp fails without). mkdir -p "$BASE/tmp"; export TMPDIR=$BASE/tmp - mkdir -p "$C/scripts" "$C/internal/version" "$C/sdk/go" "$C/sdk/ts" "$C/sdk/ts-sdk" "$C/sdk/py" "$C/.github" \ + mkdir -p "$C/scripts" "$C/internal/version" "$C/sdk/go" "$C/sdk/py" "$C/.github" \ "$C/internal/build" "$C/cmd/commands" "$A/src/lib" "$B/bin" cp "$ROOT/scripts/bump-version.sh" "$C/scripts/" echo 1.4.13 > "$C/.github/VERSION" printf 'package version\n\nvar (\n\tVersion string = "1.4.13"\n)\n' > "$C/internal/version/version.go" printf 'package sdk\n\nconst Version = "1.4.13"\n' > "$C/sdk/go/doc.go" - for f in sdk/ts sdk/ts-sdk; do echo '{"version":"1.4.13"}' > "$C/$f/package.json"; done + # One fixture package per sdk/*/package.json in the real tree, so a package added there + # is part of every bump test without touching this file. + for f in "$ROOT"/sdk/*/package.json; do + d=$(basename "$(dirname "$f")"); mkdir -p "$C/sdk/$d"; echo '{"version":"1.4.13"}' > "$C/sdk/$d/package.json" + done printf '[project]\nversion = "1.4.13"\n' > "$C/sdk/py/pyproject.toml" echo '{"version":"1.4.13"}' > "$A/package.json" echo "export const CLI_VERSION = '1.4.13'" > "$A/src/lib/cli-version.ts" @@ -65,6 +69,17 @@ check "bump kept the other golden lines" gold_all 'other keep' check "bump ran the build golden with UPDATE_GOLDEN=1" grep -q '^UPDATE_GOLDEN=1 test .*./internal/build ' "$GO_LOG" check "bump ran the cmd golden with UPDATE_GOLDEN=1" grep -q '^UPDATE_GOLDEN=1 test .*./cmd/commands ' "$GO_LOG" check "bump re-ran both tests plain" test "$(grep -c '^UPDATE_GOLDEN= test' "$GO_LOG")" -eq 2 +sdk_pkgs_at() { local f; for f in "$C"/sdk/*/package.json; do grep -q "\"$1\"" "$f" || return 1; done; } +check "bump found the real sdk npm packages (ts-sdk, ts-cli-api among them)" \ + test -f "$C/sdk/ts-sdk/package.json" -a -f "$C/sdk/ts-cli-api/package.json" +check "bump moved every sdk/*/package.json, none skipped" sdk_pkgs_at 9.9.9 +rm -rf "$B" + +# 13b. a package the bumper skips is caught: sdk_pkgs_at fails when one stays behind ------- +mk_bump_env +run_bump 9.9.9 +echo '{"version":"1.4.14"}' > "$C/sdk/ts-cli-api/package.json" +check "a lagging sdk package fails the lockstep assertion" test "$(sdk_pkgs_at 9.9.9 && echo ok || echo lag)" = lag rm -rf "$B" # 14. other drift is refused and the goldens come back ------------------------------------- From e25b3d4b60d46221fe1d46f7116fdcba5ed2a55f Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:45:39 -0400 Subject: [PATCH 2/2] fix(ci): isolate host fixtures and handle notice writes --- cmd/commands/canon_engine_test.go | 2 +- internal/build/plugins_entitlement_mount_test.go | 6 ++++++ internal/license/entitlement/write_test.go | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/commands/canon_engine_test.go b/cmd/commands/canon_engine_test.go index 4b8cc223c..b8577b1d5 100644 --- a/cmd/commands/canon_engine_test.go +++ b/cmd/commands/canon_engine_test.go @@ -776,7 +776,7 @@ func TestCanonEnginePrepare(t *testing.T) { t.Helper() compattest.Set(t, v15) savedArgs, savedMount := os.Args, mountInstalledPlugins - defer func() { os.Args, mountInstalledPlugins = savedArgs, savedMount }() + t.Cleanup(func() { os.Args, mountInstalledPlugins = savedArgs, savedMount }) mountInstalledPlugins = func(*cobra.Command) {} if mount { mountInstalledPlugins = func(r *cobra.Command) { diff --git a/internal/build/plugins_entitlement_mount_test.go b/internal/build/plugins_entitlement_mount_test.go index b1581d833..41a6ce63e 100644 --- a/internal/build/plugins_entitlement_mount_test.go +++ b/internal/build/plugins_entitlement_mount_test.go @@ -90,6 +90,12 @@ func TestMountIgnoresOverride(t *testing.T) { } func TestMountEmptyFileE125(t *testing.T) { + // Missing-token behavior must not renew a license configured on the test host. + t.Setenv("NSELF_PING_API_URL", "http://127.0.0.1:1") + t.Setenv("NSELF_PLUGIN_LICENSE_KEY", "synthetic-license-key") + for i := 1; i <= 10; i++ { + t.Setenv("NSELF_LICENSE_KEY_"+strconv.Itoa(i), "") + } compattest.Set(t, true) // No licence key from the host: a stored or env key would renew a real JWT online. t.Setenv("HOME", t.TempDir()) diff --git a/internal/license/entitlement/write_test.go b/internal/license/entitlement/write_test.go index be67a5f5e..292dfa9f4 100644 --- a/internal/license/entitlement/write_test.go +++ b/internal/license/entitlement/write_test.go @@ -102,6 +102,12 @@ func TestEntitlementRefreshWrites(t *testing.T) { } func TestBuildE125UnderV15(t *testing.T) { + // Missing-token behavior must not renew a license configured on the test host. + t.Setenv("NSELF_PING_API_URL", "http://127.0.0.1:1") + t.Setenv("NSELF_PLUGIN_LICENSE_KEY", "synthetic-license-key") + for i := 1; i <= 10; i++ { + t.Setenv("NSELF_LICENSE_KEY_"+strconv.Itoa(i), "") + } compattest.Both(t, func(t *testing.T) { // No licence key from the host: a stored or env key would renew a real JWT online. t.Setenv("HOME", t.TempDir())