@superblocksteam/sdk 2.0.137-next.0 → 2.0.138-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +64 -59
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +54 -33
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.d.mts +26 -1
- package/dist/cli-replacement/dependency-install-classifier.d.mts.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.mjs +58 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.test.mjs +33 -1
- package/dist/cli-replacement/dependency-install-classifier.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.d.mts +4 -2
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.mjs +32 -5
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.mjs +142 -65
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.d.mts.map +1 -1
- package/dist/cli-replacement/home-npmrc.mjs +13 -5
- package/dist/cli-replacement/home-npmrc.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.test.mjs +49 -17
- package/dist/cli-replacement/home-npmrc.test.mjs.map +1 -1
- package/dist/cli-replacement/package-json-snapshot.d.mts +1 -0
- package/dist/cli-replacement/package-json-snapshot.d.mts.map +1 -1
- package/dist/cli-replacement/package-json-snapshot.mjs +35 -0
- package/dist/cli-replacement/package-json-snapshot.mjs.map +1 -1
- package/dist/cli-replacement/package-json-snapshot.test.mjs +104 -0
- package/dist/cli-replacement/package-json-snapshot.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.d.mts +17 -7
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +18 -12
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +15 -6
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/prepare-in-place-restart.d.mts +31 -0
- package/dist/dev-utils/prepare-in-place-restart.d.mts.map +1 -0
- package/dist/dev-utils/prepare-in-place-restart.mjs +64 -0
- package/dist/dev-utils/prepare-in-place-restart.mjs.map +1 -0
- package/dist/dev-utils/prepare-in-place-restart.test.d.mts +2 -0
- package/dist/dev-utils/prepare-in-place-restart.test.d.mts.map +1 -0
- package/dist/dev-utils/prepare-in-place-restart.test.mjs +46 -0
- package/dist/dev-utils/prepare-in-place-restart.test.mjs.map +1 -0
- package/dist/telemetry/index.d.ts +12 -0
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +26 -12
- package/dist/telemetry/index.js.map +1 -1
- package/package.json +6 -6
- package/src/cli-replacement/automatic-upgrades.test.ts +68 -58
- package/src/cli-replacement/automatic-upgrades.ts +81 -59
- package/src/cli-replacement/dependency-install-classifier.mts +74 -1
- package/src/cli-replacement/dependency-install-classifier.test.mts +49 -0
- package/src/cli-replacement/dev.interception.test.mts +39 -7
- package/src/cli-replacement/dev.mts +154 -62
- package/src/cli-replacement/home-npmrc.mts +12 -4
- package/src/cli-replacement/home-npmrc.test.mts +57 -17
- package/src/cli-replacement/package-json-snapshot.mts +50 -0
- package/src/cli-replacement/package-json-snapshot.test.mts +121 -0
- package/src/dev-utils/dev-server-metrics.mts +48 -21
- package/src/dev-utils/dev-server.mts +22 -5
- package/src/dev-utils/prepare-in-place-restart.mts +99 -0
- package/src/dev-utils/prepare-in-place-restart.test.mts +69 -0
- package/src/telemetry/index.ts +24 -10
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superblocksteam/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.138-next.0",
|
|
4
4
|
"description": "Superblocks JS SDK",
|
|
5
5
|
"homepage": "https://www.superblocks.com",
|
|
6
6
|
"license": "Superblocks Community Software License",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"vite-tsconfig-paths": "^6.0.4",
|
|
49
49
|
"winston": "^3.17.0",
|
|
50
50
|
"yaml": "^2.7.1",
|
|
51
|
-
"@superblocksteam/library-shared": "2.0.
|
|
52
|
-
"@superblocksteam/
|
|
53
|
-
"@superblocksteam/
|
|
54
|
-
"@superblocksteam/
|
|
55
|
-
"@superblocksteam/vite-plugin-file-sync": "2.0.
|
|
51
|
+
"@superblocksteam/library-shared": "2.0.138-next.0",
|
|
52
|
+
"@superblocksteam/shared": "0.9595.0",
|
|
53
|
+
"@superblocksteam/telemetry": "2.0.138-next.0",
|
|
54
|
+
"@superblocksteam/util": "2.0.138-next.0",
|
|
55
|
+
"@superblocksteam/vite-plugin-file-sync": "2.0.138-next.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@eslint/js": "^9.39.2",
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
buildInstallEnv,
|
|
26
26
|
checkVersionsAndWritePackageJson,
|
|
27
27
|
} from "./automatic-upgrades.js";
|
|
28
|
+
import { UpgradeFailed } from "./dependency-install-classifier.mjs";
|
|
28
29
|
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
30
31
|
// Mocks
|
|
@@ -564,9 +565,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
564
565
|
new Error("Command failed: pnpm install ..."),
|
|
565
566
|
);
|
|
566
567
|
|
|
567
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
568
|
-
/Command failed: pnpm install/,
|
|
569
|
-
);
|
|
568
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
570
569
|
|
|
571
570
|
// The structured log line fires before the rejection so the operator
|
|
572
571
|
// sees the reason code in the crash-loop output. Reason is encoded
|
|
@@ -604,9 +603,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
604
603
|
new Error("Command failed: pnpm install ..."),
|
|
605
604
|
);
|
|
606
605
|
|
|
607
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
608
|
-
/Command failed: pnpm install/,
|
|
609
|
-
);
|
|
606
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
610
607
|
|
|
611
608
|
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
612
609
|
expect.stringMatching(
|
|
@@ -640,9 +637,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
640
637
|
new Error("Command failed: pnpm install ..."),
|
|
641
638
|
);
|
|
642
639
|
|
|
643
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
644
|
-
/Command failed: pnpm install/,
|
|
645
|
-
);
|
|
640
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
646
641
|
|
|
647
642
|
const blockedCall = loggerMock.error.mock.calls.find(
|
|
648
643
|
(c) =>
|
|
@@ -670,9 +665,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
670
665
|
new Error("Command failed: pnpm install ..."),
|
|
671
666
|
);
|
|
672
667
|
|
|
673
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
674
|
-
/Command failed: pnpm install/,
|
|
675
|
-
);
|
|
668
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
676
669
|
|
|
677
670
|
const blockedCall = loggerMock.error.mock.calls.find(
|
|
678
671
|
(c) =>
|
|
@@ -717,9 +710,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
717
710
|
new Error("Command failed: pnpm install ..."),
|
|
718
711
|
);
|
|
719
712
|
|
|
720
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
721
|
-
/Command failed: pnpm install/,
|
|
722
|
-
);
|
|
713
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
723
714
|
|
|
724
715
|
const blockedCall = loggerMock.error.mock.calls.find(
|
|
725
716
|
(c) =>
|
|
@@ -758,9 +749,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
758
749
|
new Error("Command failed: pnpm install ..."),
|
|
759
750
|
);
|
|
760
751
|
|
|
761
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
762
|
-
/Command failed: pnpm install/,
|
|
763
|
-
);
|
|
752
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
764
753
|
|
|
765
754
|
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
766
755
|
expect.stringMatching(/\[npm-install-blocked\] reason=not_in_registry/),
|
|
@@ -776,9 +765,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
776
765
|
new Error("Command failed: pnpm install ..."),
|
|
777
766
|
);
|
|
778
767
|
|
|
779
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
780
|
-
/Command failed: pnpm install/,
|
|
781
|
-
);
|
|
768
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
782
769
|
|
|
783
770
|
// The classifier returns null for ERESOLVE — no structured log fires.
|
|
784
771
|
expect(loggerMock.error).not.toHaveBeenCalledWith(
|
|
@@ -809,9 +796,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
809
796
|
].join("\n"),
|
|
810
797
|
});
|
|
811
798
|
|
|
812
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
813
|
-
/Command failed: pnpm install/,
|
|
814
|
-
);
|
|
799
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
815
800
|
|
|
816
801
|
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
817
802
|
expect.stringMatching(
|
|
@@ -834,9 +819,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
834
819
|
new Error("Command failed: pnpm install ..."),
|
|
835
820
|
);
|
|
836
821
|
|
|
837
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
838
|
-
/Command failed: pnpm install/,
|
|
839
|
-
);
|
|
822
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
840
823
|
|
|
841
824
|
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
842
825
|
expect.stringMatching(
|
|
@@ -928,9 +911,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
928
911
|
new Error("Command failed: pnpm install ..."),
|
|
929
912
|
);
|
|
930
913
|
|
|
931
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
932
|
-
/Command failed: pnpm install/,
|
|
933
|
-
);
|
|
914
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
934
915
|
|
|
935
916
|
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
936
917
|
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
@@ -958,9 +939,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
958
939
|
new Error("Command failed: pnpm install ..."),
|
|
959
940
|
);
|
|
960
941
|
|
|
961
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
962
|
-
/Command failed: pnpm install/,
|
|
963
|
-
);
|
|
942
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
964
943
|
|
|
965
944
|
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
966
945
|
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
@@ -974,6 +953,44 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
974
953
|
expect(args.registryHost).toBe(privateHost);
|
|
975
954
|
});
|
|
976
955
|
|
|
956
|
+
it("threads hasAnyRegistryConfigured into the classified error and the metric so private-registry upgrade failures get the admin guidance, not the public-npm copy", async () => {
|
|
957
|
+
// E404 against a private host = the org has a registry configured but it
|
|
958
|
+
// can't serve the target version. The caller derives
|
|
959
|
+
// hasAnyRegistryConfigured=true and forwards it here; without the
|
|
960
|
+
// threading both the overlay copy and the metric would wrongly read
|
|
961
|
+
// "no registry configured".
|
|
962
|
+
await mockNpmFallbackExec(
|
|
963
|
+
[
|
|
964
|
+
"npm error code E404",
|
|
965
|
+
"npm error 404 Not Found - GET https://npm.private.example.com/@superblocksteam%2fcli - Not found",
|
|
966
|
+
].join("\n"),
|
|
967
|
+
new Error("Command failed: pnpm install ..."),
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
const result = await checkVersionsAndWritePackageJson(
|
|
971
|
+
mockLockService,
|
|
972
|
+
mockConfig,
|
|
973
|
+
false,
|
|
974
|
+
false,
|
|
975
|
+
// appDir omitted (defaults to process.cwd()); pass the registry
|
|
976
|
+
// tri-state in the new trailing position.
|
|
977
|
+
undefined,
|
|
978
|
+
true,
|
|
979
|
+
);
|
|
980
|
+
const err = await Promise.all(result.upgradePromises).catch((e) => e);
|
|
981
|
+
|
|
982
|
+
// The classified error drives the overlay copy: true → "ask an org
|
|
983
|
+
// admin to add the version to your registry".
|
|
984
|
+
expect(err).toBeInstanceOf(UpgradeFailed);
|
|
985
|
+
expect(err.serverError.category).toBe("not_in_registry");
|
|
986
|
+
expect(err.serverError.hasAnyRegistryConfigured).toBe(true);
|
|
987
|
+
|
|
988
|
+
// The same signal lands on the install metric so `auto_upgrade`
|
|
989
|
+
// failures attribute to the configured-registry bucket.
|
|
990
|
+
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
991
|
+
expect(recordNpmInstallMock.mock.calls[0]![0].configured).toBe(true);
|
|
992
|
+
});
|
|
993
|
+
|
|
977
994
|
it("APPS-4544: emits outcome=other for an unclassified failure (ERESOLVE) so the metric reflects the failure even without a NpmInstallBlocked match", async () => {
|
|
978
995
|
await mockNpmFallbackExec(
|
|
979
996
|
[
|
|
@@ -983,9 +1000,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
983
1000
|
new Error("Command failed: pnpm install ..."),
|
|
984
1001
|
);
|
|
985
1002
|
|
|
986
|
-
await expect(runAndAwaitUpgrade()).rejects.toThrow(
|
|
987
|
-
/Command failed: pnpm install/,
|
|
988
|
-
);
|
|
1003
|
+
await expect(runAndAwaitUpgrade()).rejects.toThrow(UpgradeFailed);
|
|
989
1004
|
|
|
990
1005
|
expect(recordNpmInstallMock).toHaveBeenCalledTimes(1);
|
|
991
1006
|
const args = recordNpmInstallMock.mock.calls[0]![0];
|
|
@@ -1053,7 +1068,7 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
1053
1068
|
// The validation failure rejects the upgrade promise (so dev.mts
|
|
1054
1069
|
// won't restart into a stale CLI)...
|
|
1055
1070
|
await expect(Promise.all(result.upgradePromises)).rejects.toThrow(
|
|
1056
|
-
|
|
1071
|
+
UpgradeFailed,
|
|
1057
1072
|
);
|
|
1058
1073
|
|
|
1059
1074
|
// ...and the metric still fires exactly once, flipped to `other`.
|
|
@@ -1240,26 +1255,21 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
1240
1255
|
// it so the test doesn't surface as an unhandled rejection,
|
|
1241
1256
|
// then assert on the span attribute the `catch` set.
|
|
1242
1257
|
await expect(Promise.all(result.upgradePromises)).rejects.toThrow(
|
|
1243
|
-
|
|
1258
|
+
UpgradeFailed,
|
|
1244
1259
|
);
|
|
1245
1260
|
|
|
1246
1261
|
expect(spanSetAttributeMock).toHaveBeenCalledWith("outcome", "failure");
|
|
1247
1262
|
});
|
|
1248
1263
|
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1257
|
-
|
|
1258
|
-
// routes through `dev.mts` to `process.exit(1)` and crash-loops the
|
|
1259
|
-
// live-edit pod. The no-op dev-server restart that still follows, graceful
|
|
1260
|
-
// degrade, and user surfacing of the failed upgrade are tracked by
|
|
1261
|
-
// APPS-4457, so this test asserts the promise RESOLVES.
|
|
1262
|
-
it("APPS-4544: emits outcome=other and fails the span (without throwing) when no global install command can be resolved", async () => {
|
|
1264
|
+
// When the detected package manager can't be turned into a global install
|
|
1265
|
+
// command, `resolveCommand` returns undefined. It still emits a
|
|
1266
|
+
// `runner=auto_upgrade` failure sample and flips the `upgradeCli` span to
|
|
1267
|
+
// `outcome=failure` so the no-op stays visible in metrics and traces, and
|
|
1268
|
+
// it throws `UpgradeFailed` so the upgrade promise REJECTS. The dev.mts
|
|
1269
|
+
// join routes that marker to the degrade path (keep serving the current
|
|
1270
|
+
// version) — NOT `process.exit(1)` — and the rejection also stops the
|
|
1271
|
+
// `cliUpdated=true` enqueue from triggering a pointless restart loop.
|
|
1272
|
+
it("emits outcome=other, fails the span, and rejects with UpgradeFailed when no global install command can be resolved", async () => {
|
|
1263
1273
|
const { exec } = await import("node:child_process");
|
|
1264
1274
|
const execMock = vi.mocked(exec);
|
|
1265
1275
|
execMock.mockImplementation(((
|
|
@@ -1302,12 +1312,12 @@ describe("checkVersionsAndWritePackageJson", () => {
|
|
|
1302
1312
|
false,
|
|
1303
1313
|
);
|
|
1304
1314
|
|
|
1305
|
-
// The upgrade promise must
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
// degrade
|
|
1309
|
-
await expect(Promise.all(result.upgradePromises)).
|
|
1310
|
-
|
|
1315
|
+
// The upgrade promise must REJECT with the `UpgradeFailed` marker so the
|
|
1316
|
+
// dev.mts join degrades (keep serving the current version) instead of
|
|
1317
|
+
// restarting on the no-op. `handleStartupError` maps the marker to
|
|
1318
|
+
// "degrade", so this rejection never reaches `process.exit(1)`.
|
|
1319
|
+
await expect(Promise.all(result.upgradePromises)).rejects.toBeInstanceOf(
|
|
1320
|
+
UpgradeFailed,
|
|
1311
1321
|
);
|
|
1312
1322
|
|
|
1313
1323
|
// A failure sample is still recorded for the auto_upgrade runner so the
|
|
@@ -25,6 +25,10 @@ import type { ResponseMeta } from "../socket/handlers.js";
|
|
|
25
25
|
import { getTracer } from "../telemetry/index.js";
|
|
26
26
|
import { getErrorMeta, getLogger } from "../telemetry/logging.js";
|
|
27
27
|
import type { ApplicationConfigWithTokenConfigAndUserInfo } from "../types/common.js";
|
|
28
|
+
import {
|
|
29
|
+
classifyUpgradeError,
|
|
30
|
+
UpgradeFailed,
|
|
31
|
+
} from "./dependency-install-classifier.mjs";
|
|
28
32
|
import { superblocksLogsPath, superblocksNpmrcPath } from "./home-npmrc.mjs";
|
|
29
33
|
import { stripUpgradedCliLockfiles } from "./post-upgrade-lockfile-strip.mjs";
|
|
30
34
|
import {
|
|
@@ -176,12 +180,14 @@ async function upgradeCliWithOclif(targetVersion: string): Promise<boolean> {
|
|
|
176
180
|
}
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
183
|
+
// Resolves when an install command ran and post-install validation passed.
|
|
184
|
+
// Every failure throws `UpgradeFailed` carrying a classified, non-blocking
|
|
185
|
+
// `DependencyUpgradeError` — the no-command branch, an exec failure, and a
|
|
186
|
+
// post-install version mismatch all degrade the same way. The dev.mts join
|
|
187
|
+
// routes that marker to the degrade path, so the live-edit pod keeps serving
|
|
188
|
+
// the currently installed version instead of crash-looping. The auto-upgrade
|
|
189
|
+
// is best-effort: the running version still works, so a failed upgrade is
|
|
190
|
+
// never fatal.
|
|
185
191
|
async function upgradeCliWithPackageManager(
|
|
186
192
|
pm: DetectResult,
|
|
187
193
|
targetVersion: string,
|
|
@@ -191,35 +197,33 @@ async function upgradeCliWithPackageManager(
|
|
|
191
197
|
// `.superblocks/logs`). Optional so callers/tests that don't care keep
|
|
192
198
|
// npm's default `_logs` location.
|
|
193
199
|
logsDir?: string,
|
|
194
|
-
|
|
200
|
+
// Currently installed version, carried into the classified error so the
|
|
201
|
+
// overlay can say "still on X; couldn't reach Y". Best-effort context.
|
|
202
|
+
currentVersion?: string,
|
|
203
|
+
): Promise<void> {
|
|
195
204
|
const packageName = alias
|
|
196
205
|
? `@superblocksteam/cli@npm:${alias}@${targetVersion}`
|
|
197
206
|
: `@superblocksteam/cli@${targetVersion}`;
|
|
198
207
|
|
|
208
|
+
// Context shared by every classification path below: the package being
|
|
209
|
+
// upgraded plus whether a registry is configured.
|
|
210
|
+
const parseCtx = {
|
|
211
|
+
requestedPackages: [{ name: packageName }],
|
|
212
|
+
hasAnyRegistryConfigured,
|
|
213
|
+
};
|
|
214
|
+
const versions = { currentVersion, targetVersion };
|
|
215
|
+
|
|
199
216
|
const installArgs = buildGlobalInstallArgs({ agent: pm.agent }, packageName);
|
|
200
217
|
|
|
201
218
|
const installCommand = resolveCommand(pm.agent, "global", installArgs);
|
|
202
219
|
|
|
203
220
|
if (!installCommand) {
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// `
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// Emit a failure sample so the histogram carries a `runner="auto_upgrade"`
|
|
212
|
-
// failure sample for this path, then return `false` so the caller flips
|
|
213
|
-
// the `upgradeCli` span to `outcome=failure`. `durationMs` is 0 because no
|
|
214
|
-
// install process ran; `outcome="other"` mirrors the unclassified-failure
|
|
215
|
-
// fallback used by the exec/validation paths below.
|
|
216
|
-
//
|
|
217
|
-
// We deliberately do NOT throw: a rejected upgrade promise routes through
|
|
218
|
-
// `dev.mts` to `process.exit(1)`, crash-looping the live-edit pod. The
|
|
219
|
-
// no-op dev-server restart that still follows (`cliUpdated` is set
|
|
220
|
-
// unconditionally at enqueue time in `checkVersionsAndWritePackageJson`),
|
|
221
|
-
// plus degrading gracefully (continue on the current version) and
|
|
222
|
-
// surfacing the failed upgrade to the user, are tracked by APPS-4457.
|
|
221
|
+
// We couldn't build a global install command for the detected package
|
|
222
|
+
// manager, so no upgrade ran. Emit a failure sample (durationMs 0 — nothing
|
|
223
|
+
// ran) so the metric doesn't read this no-op as a success, then throw
|
|
224
|
+
// `UpgradeFailed` so the dev.mts join degrades (keep serving the current
|
|
225
|
+
// version) instead of leaving `cliUpdated=true` to trigger a pointless
|
|
226
|
+
// dev-server restart loop.
|
|
223
227
|
logger.error("Could not determine how to upgrade Superblocks packages.");
|
|
224
228
|
recordNpmInstall({
|
|
225
229
|
runner: "auto_upgrade",
|
|
@@ -228,7 +232,15 @@ async function upgradeCliWithPackageManager(
|
|
|
228
232
|
registryHost: undefined,
|
|
229
233
|
durationMs: 0,
|
|
230
234
|
});
|
|
231
|
-
|
|
235
|
+
throw new UpgradeFailed(
|
|
236
|
+
classifyUpgradeError(
|
|
237
|
+
{
|
|
238
|
+
message: "Could not determine how to upgrade Superblocks packages.",
|
|
239
|
+
},
|
|
240
|
+
parseCtx,
|
|
241
|
+
versions,
|
|
242
|
+
),
|
|
243
|
+
);
|
|
232
244
|
}
|
|
233
245
|
|
|
234
246
|
const { command, args } = installCommand;
|
|
@@ -284,10 +296,7 @@ async function upgradeCliWithPackageManager(
|
|
|
284
296
|
const errStderr = (error as { stderr?: unknown }).stderr;
|
|
285
297
|
const stderrText =
|
|
286
298
|
stderrBuffer || (typeof errStderr === "string" ? errStderr : "");
|
|
287
|
-
const blocked = classifyNpmExecStderr(stderrText,
|
|
288
|
-
requestedPackages: [{ name: packageName }],
|
|
289
|
-
hasAnyRegistryConfigured,
|
|
290
|
-
});
|
|
299
|
+
const blocked = classifyNpmExecStderr(stderrText, parseCtx);
|
|
291
300
|
// Surface the structured `NpmInstallBlocked.reason` (e.g.
|
|
292
301
|
// `registry_unreachable`, `registry_auth_failed`) and raw host to
|
|
293
302
|
// the install metric below; the emitter coerces both at the emit
|
|
@@ -399,7 +408,24 @@ async function upgradeCliWithPackageManager(
|
|
|
399
408
|
if (outcome === "success") {
|
|
400
409
|
outcome = "other";
|
|
401
410
|
}
|
|
402
|
-
|
|
411
|
+
// Both the exec failure (rejected with the raw npm error, re-thrown by
|
|
412
|
+
// `await promise`) and a post-install version mismatch land here. Classify
|
|
413
|
+
// into a non-blocking `DependencyUpgradeError` and throw the `UpgradeFailed`
|
|
414
|
+
// marker so the dev.mts join degrades instead of crash-looping. `stderrBuffer`
|
|
415
|
+
// holds the streamed npm stderr; fall back to the error's own fields when a
|
|
416
|
+
// fast-failing subprocess exited before the listener attached.
|
|
417
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
418
|
+
throw new UpgradeFailed(
|
|
419
|
+
classifyUpgradeError(
|
|
420
|
+
{
|
|
421
|
+
stdout: e.stdout,
|
|
422
|
+
stderr: stderrBuffer || e.stderr,
|
|
423
|
+
message: e.message,
|
|
424
|
+
},
|
|
425
|
+
parseCtx,
|
|
426
|
+
versions,
|
|
427
|
+
),
|
|
428
|
+
);
|
|
403
429
|
} finally {
|
|
404
430
|
// Both the happy path and every throw above (exec failure → callback
|
|
405
431
|
// sets outcome, post-install validation throw → catch sets outcome to
|
|
@@ -418,9 +444,7 @@ async function upgradeCliWithPackageManager(
|
|
|
418
444
|
}
|
|
419
445
|
|
|
420
446
|
// Reached only when the install ran and post-install validation passed
|
|
421
|
-
// (every failure above throws).
|
|
422
|
-
// happened so it leaves the `upgradeCli` span `outcome=success`.
|
|
423
|
-
return true;
|
|
447
|
+
// (every failure above throws `UpgradeFailed`).
|
|
424
448
|
}
|
|
425
449
|
|
|
426
450
|
export async function checkVersionsAndWritePackageJson(
|
|
@@ -434,6 +458,14 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
434
458
|
// unaffected; `dev()` passes its `cwd` explicitly for consistency with
|
|
435
459
|
// the startup install.
|
|
436
460
|
appDir: string = process.cwd(),
|
|
461
|
+
// Whether the org routes installs through a configured npm registry, as a
|
|
462
|
+
// tri-state: `true` (configured/stale), `false` (deliberately none),
|
|
463
|
+
// `undefined` (couldn't resolve). The caller derives it from the same
|
|
464
|
+
// registry config the startup install classifier uses and forwards it into
|
|
465
|
+
// the CLI auto-upgrade, so a failure classifies/renders with the right
|
|
466
|
+
// guidance ("ask an org admin" vs "temporary") and records the correct
|
|
467
|
+
// `configured` install metric instead of always defaulting to `false`.
|
|
468
|
+
hasAnyRegistryConfigured?: boolean,
|
|
437
469
|
): Promise<{
|
|
438
470
|
cliUpdated: boolean;
|
|
439
471
|
upgradePromises: Promise<void>[];
|
|
@@ -521,16 +553,16 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
521
553
|
return;
|
|
522
554
|
}
|
|
523
555
|
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
556
|
+
// The auto-upgrade runs the global `npm install -g @superblocksteam/cli@...`
|
|
557
|
+
// and classifies its failures the same way the per-app install does. On
|
|
558
|
+
// any failure (network block, registry/auth/TLS error, version mismatch,
|
|
559
|
+
// or no resolvable install command) it throws the `UpgradeFailed` marker
|
|
560
|
+
// carrying a classified `DependencyUpgradeError`. The dev.mts join routes
|
|
561
|
+
// that marker to the degrade path, so the pod keeps serving the current
|
|
562
|
+
// version instead of crash-looping — the upgrade is best-effort. The
|
|
563
|
+
// early `process.exit(1)` below is reserved for genuinely-unusable cases
|
|
564
|
+
// (no package manager, an unparseable version, a corrupt package.json)
|
|
565
|
+
// that are not upgrade failures.
|
|
534
566
|
try {
|
|
535
567
|
if (
|
|
536
568
|
!skipCliUpgrade &&
|
|
@@ -600,30 +632,20 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
600
632
|
logger.info(
|
|
601
633
|
`Falling back to package manager upgrade for CLI`,
|
|
602
634
|
);
|
|
603
|
-
|
|
635
|
+
await traceFunction({
|
|
604
636
|
name: "upgradePackageWithPackageManager - cli",
|
|
605
637
|
tracer,
|
|
606
638
|
fn: async () => {
|
|
607
|
-
|
|
639
|
+
await upgradeCliWithPackageManager(
|
|
608
640
|
packageManager,
|
|
609
641
|
targetVersions.cli,
|
|
610
642
|
currentCliInfo.alias,
|
|
611
|
-
|
|
643
|
+
hasAnyRegistryConfigured,
|
|
612
644
|
superblocksLogsPath(appDir),
|
|
645
|
+
currentCliInfo.version,
|
|
613
646
|
);
|
|
614
647
|
},
|
|
615
648
|
});
|
|
616
|
-
if (!cliUpgradeRan) {
|
|
617
|
-
// No global install command could be resolved → the CLI
|
|
618
|
-
// upgrade was a no-op (the failure metric was already
|
|
619
|
-
// emitted inline). Flip the span to `failure` so the trace
|
|
620
|
-
// is accurate, but do NOT throw: throwing routes through
|
|
621
|
-
// `dev.mts` to `process.exit(1)` and crash-loops the pod.
|
|
622
|
-
// The no-op dev-server restart that still follows
|
|
623
|
-
// (`cliUpdated = true` below), graceful degrade, and user
|
|
624
|
-
// surfacing of the failed upgrade are tracked by APPS-4457.
|
|
625
|
-
outcome = "failure";
|
|
626
|
-
}
|
|
627
649
|
}
|
|
628
650
|
// The freshly fetched tarball may ship bundled lockfiles with
|
|
629
651
|
// stale public-host `resolved` URLs; strip them so the next
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DependencyInstallError,
|
|
3
|
+
DependencyUpgradeError,
|
|
4
|
+
} from "@superblocksteam/library-shared/types";
|
|
2
5
|
import {
|
|
3
6
|
classifyNpmExecStderr,
|
|
4
7
|
parseNpmJsonDiagnostic,
|
|
@@ -23,6 +26,19 @@ export class InitialInstallFailed extends Error {
|
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/** Marker thrown by the best-effort CLI/library auto-upgrade so the dev.mts
|
|
30
|
+
* catch can DEGRADE (keep serving the current version) instead of crash-looping
|
|
31
|
+
* the pod. Carries the classified, non-blocking upgrade error. */
|
|
32
|
+
export class UpgradeFailed extends Error {
|
|
33
|
+
readonly serverError: DependencyUpgradeError;
|
|
34
|
+
constructor(serverError: DependencyUpgradeError) {
|
|
35
|
+
super(`dependency upgrade failed (${serverError.category})`);
|
|
36
|
+
this.name = "UpgradeFailed";
|
|
37
|
+
this.serverError = serverError;
|
|
38
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
export interface ExecFailure {
|
|
27
43
|
stdout?: string;
|
|
28
44
|
stderr?: string;
|
|
@@ -130,3 +146,60 @@ export function classifyInitialInstallError(
|
|
|
130
146
|
rawError,
|
|
131
147
|
};
|
|
132
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Classify a failed CLI/library auto-upgrade exec into a non-blocking
|
|
152
|
+
* `DependencyUpgradeError`. Always returns an error (never null) — every
|
|
153
|
+
* upgrade failure degrades, because the pod keeps running the current version.
|
|
154
|
+
*
|
|
155
|
+
* Mirrors `classifyInitialInstallError` but maps to the upgrade error's
|
|
156
|
+
* narrower category set: registry/network reasons keep their classification;
|
|
157
|
+
* everything else (no install command, post-install version mismatch,
|
|
158
|
+
* unclassified exec error) folds to `unknown`. We deliberately do NOT surface
|
|
159
|
+
* `dependency_conflict` / `disk_full` here — a global `-g` CLI install does not
|
|
160
|
+
* resolve the app's dependency tree, and the remediation is uniform (an org
|
|
161
|
+
* admin makes the target version installable), so finer buckets would not
|
|
162
|
+
* change the user-facing guidance.
|
|
163
|
+
*/
|
|
164
|
+
export function classifyUpgradeError(
|
|
165
|
+
failure: ExecFailure,
|
|
166
|
+
ctx: ParseContext,
|
|
167
|
+
versions: { currentVersion?: string; targetVersion?: string } = {},
|
|
168
|
+
): DependencyUpgradeError {
|
|
169
|
+
const stdout = failure.stdout ?? "";
|
|
170
|
+
const stderr = failure.stderr ?? "";
|
|
171
|
+
const rawError = scrubSecrets(
|
|
172
|
+
parseNpmJsonDiagnostic(stdout) || stderr || failure.message || "",
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const base = {
|
|
176
|
+
type: "dev-server/dependency-upgrade" as const,
|
|
177
|
+
timestamp: new Date().toISOString(),
|
|
178
|
+
currentVersion: versions.currentVersion,
|
|
179
|
+
targetVersion: versions.targetVersion,
|
|
180
|
+
rawError,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const blocked =
|
|
184
|
+
parseNpmJsonError(stdout, ctx) ?? classifyNpmExecStderr(stderr, ctx);
|
|
185
|
+
if (blocked) {
|
|
186
|
+
return {
|
|
187
|
+
...base,
|
|
188
|
+
// `NpmInstallBlockedReason` is exactly the four registry/network reasons,
|
|
189
|
+
// each of which is also a `DependencyUpgradeError` category, so the reason
|
|
190
|
+
// doubles as the category directly (REASON_TO_CATEGORY is identity here).
|
|
191
|
+
category: blocked.reason,
|
|
192
|
+
npmErrorCode: blocked.npmErrorCode,
|
|
193
|
+
hasAnyRegistryConfigured: blocked.hasAnyRegistryConfigured,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const combined = `${stdout}\n${stderr}`;
|
|
198
|
+
const unknownCode = combined.match(/npm (?:error|ERR!) code (\S+)/)?.[1];
|
|
199
|
+
return {
|
|
200
|
+
...base,
|
|
201
|
+
category: "unknown",
|
|
202
|
+
npmErrorCode: unknownCode,
|
|
203
|
+
hasAnyRegistryConfigured: ctx.hasAnyRegistryConfigured,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -2,7 +2,9 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
InitialInstallFailed,
|
|
5
|
+
UpgradeFailed,
|
|
5
6
|
classifyInitialInstallError,
|
|
7
|
+
classifyUpgradeError,
|
|
6
8
|
} from "./dependency-install-classifier.mjs";
|
|
7
9
|
|
|
8
10
|
const ERESOLVE_JSON = JSON.stringify({
|
|
@@ -109,3 +111,50 @@ describe("classifyInitialInstallError", () => {
|
|
|
109
111
|
expect(marker.serverError).toBe(se);
|
|
110
112
|
});
|
|
111
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("classifyUpgradeError", () => {
|
|
116
|
+
it("maps a registry block to its reason and carries the versions", () => {
|
|
117
|
+
const e = classifyUpgradeError(
|
|
118
|
+
{
|
|
119
|
+
stdout: "",
|
|
120
|
+
stderr:
|
|
121
|
+
"npm error code E401\nnpm error 401 Unauthorized - GET https://reg.corp/@superblocksteam%2fcli",
|
|
122
|
+
message: "Command failed",
|
|
123
|
+
},
|
|
124
|
+
{ hasAnyRegistryConfigured: true },
|
|
125
|
+
{ currentVersion: "2.0.133", targetVersion: "2.0.134" },
|
|
126
|
+
);
|
|
127
|
+
expect(e.type).toBe("dev-server/dependency-upgrade");
|
|
128
|
+
expect(e.category).toBe("registry_auth_failed");
|
|
129
|
+
expect(e.currentVersion).toBe("2.0.133");
|
|
130
|
+
expect(e.targetVersion).toBe("2.0.134");
|
|
131
|
+
expect(e.rawError.length).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("folds anything unclassified to unknown — an odd exec code or the no-command message", () => {
|
|
135
|
+
const exec = classifyUpgradeError(
|
|
136
|
+
{
|
|
137
|
+
stdout: "",
|
|
138
|
+
stderr: "npm error code EWEIRD\nsomething broke",
|
|
139
|
+
message: "Command failed",
|
|
140
|
+
},
|
|
141
|
+
{},
|
|
142
|
+
);
|
|
143
|
+
expect(exec.category).toBe("unknown");
|
|
144
|
+
expect(exec.npmErrorCode).toBe("EWEIRD");
|
|
145
|
+
|
|
146
|
+
const noCommand = classifyUpgradeError(
|
|
147
|
+
{ message: "Could not determine how to upgrade Superblocks packages." },
|
|
148
|
+
{},
|
|
149
|
+
);
|
|
150
|
+
expect(noCommand.category).toBe("unknown");
|
|
151
|
+
expect(noCommand.rawError).toContain("Could not determine");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("UpgradeFailed carries the classified error", () => {
|
|
155
|
+
const se = classifyUpgradeError({ stderr: "npm error code EWEIRD" }, {});
|
|
156
|
+
const marker = new UpgradeFailed(se);
|
|
157
|
+
expect(marker).toBeInstanceOf(Error);
|
|
158
|
+
expect(marker.serverError).toBe(se);
|
|
159
|
+
});
|
|
160
|
+
});
|