@tenonhq/dovetail-core 0.0.95 → 0.0.97
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/dist/appUtils.js
CHANGED
|
@@ -118,7 +118,13 @@ const processFilesInManRec = async (recPath, rec, forceWrite) => {
|
|
|
118
118
|
const processRecsInManTable = async (tablePath, table, forceWrite, onRecordProcessed) => {
|
|
119
119
|
const { records } = table;
|
|
120
120
|
const recKeys = Object.keys(records);
|
|
121
|
-
|
|
121
|
+
// Derive the folder name via toSafeFolderName, not the raw record.name. The
|
|
122
|
+
// download path normalizes manifest keys upstream (processManifest), but the
|
|
123
|
+
// refresh path (processMissingFiles) feeds this writer the raw bulkDownload
|
|
124
|
+
// response, whose .name is the unmodified ServiceNow display name — which can
|
|
125
|
+
// be filesystem-unsafe (e.g. a `<table>.*` ACL). toSafeFolderName is
|
|
126
|
+
// idempotent on already-safe names, so re-applying it here is harmless.
|
|
127
|
+
const recKeyToPath = (key) => path_1.default.join(tablePath, (0, exports.toSafeFolderName)(records[key]));
|
|
122
128
|
const recPathPromises = recKeys
|
|
123
129
|
.map(recKeyToPath)
|
|
124
130
|
.map(fUtils.createDirRecursively);
|
|
@@ -511,11 +517,16 @@ const refreshAllFiles = async (newManifest, sourcePath, options = {}) => {
|
|
|
511
517
|
var recs = filesToProcess[tableName].records;
|
|
512
518
|
var recKeys = Object.keys(recs);
|
|
513
519
|
await Promise.all(recKeys.map(function (k) {
|
|
514
|
-
|
|
520
|
+
// toSafeFolderName, not raw recs[k].name: the bulkDownload response
|
|
521
|
+
// carries the unmodified ServiceNow display name, which may be
|
|
522
|
+
// filesystem-unsafe (e.g. a `<table>.*` ACL). Without this, refresh
|
|
523
|
+
// recreates Windows-illegal folders that normalizeManifestKeys
|
|
524
|
+
// already removed from the manifest.
|
|
525
|
+
return fUtils.createDirRecursively(path_1.default.join(tablePath, (0, exports.toSafeFolderName)(recs[k])));
|
|
515
526
|
}));
|
|
516
527
|
await (0, genericUtils_1.processBatched)(recKeys, constants_1.CONCURRENCY_RECORDS, async function (recKey) {
|
|
517
528
|
var rec = recs[recKey];
|
|
518
|
-
var recPath = path_1.default.join(tablePath,
|
|
529
|
+
var recPath = path_1.default.join(tablePath, (0, exports.toSafeFolderName)(rec));
|
|
519
530
|
// Split server-provided metadata off from the regular files so we can
|
|
520
531
|
// track whether any regular file actually changed — metaData shouldn't
|
|
521
532
|
// be the trigger for "this record changed" since we stamp it on every
|
package/dist/snClient.js
CHANGED
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.unwrapSNResponse = exports.defaultClient = exports.snClient = exports.processPushResponse = exports.retryOnHttpErr = exports.retryOnErr = void 0;
|
|
7
7
|
exports.setBenchmarkSink = setBenchmarkSink;
|
|
8
8
|
exports._resetDovetailApiFallback = _resetDovetailApiFallback;
|
|
9
|
-
exports.
|
|
9
|
+
exports._isUsingLegacyPath = _isUsingLegacyPath;
|
|
10
10
|
exports._callDovetailApi = _callDovetailApi;
|
|
11
11
|
exports.unwrapTableAPIFirstItem = unwrapTableAPIFirstItem;
|
|
12
12
|
const axios_1 = __importDefault(require("axios"));
|
|
@@ -56,28 +56,28 @@ function _getHttpStatus(e) {
|
|
|
56
56
|
}
|
|
57
57
|
return undefined;
|
|
58
58
|
}
|
|
59
|
-
// Dovetail
|
|
60
|
-
// /api/cadso/
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
let
|
|
59
|
+
// Dovetail's core Scripted REST API now lives in the Dovetail scoped
|
|
60
|
+
// application at /api/cadso/dovetail_core/*. Older instances still expose the
|
|
61
|
+
// previous global-scope path /api/cadso/dovetail/*. We try dovetail_core first;
|
|
62
|
+
// if it 404s we fall back to the legacy /api/cadso/dovetail/* path for the rest
|
|
63
|
+
// of the session, with a one-time deprecation warning.
|
|
64
|
+
let _dovetailApiUseLegacyPath = false;
|
|
65
65
|
function _dovetailEndpoint(op) {
|
|
66
|
-
return
|
|
67
|
-
? "api/cadso/
|
|
68
|
-
: "api/cadso/
|
|
66
|
+
return _dovetailApiUseLegacyPath
|
|
67
|
+
? "api/cadso/dovetail/" + op
|
|
68
|
+
: "api/cadso/dovetail_core/" + op;
|
|
69
69
|
}
|
|
70
70
|
// Exported for tests only: reset the latched fallback state between test cases.
|
|
71
71
|
function _resetDovetailApiFallback() {
|
|
72
|
-
|
|
72
|
+
_dovetailApiUseLegacyPath = false;
|
|
73
73
|
}
|
|
74
74
|
// Exported for tests only: inspect the latched fallback state.
|
|
75
|
-
function
|
|
76
|
-
return
|
|
75
|
+
function _isUsingLegacyPath() {
|
|
76
|
+
return _dovetailApiUseLegacyPath;
|
|
77
77
|
}
|
|
78
78
|
// SN returns 400 with this body when a Scripted REST endpoint does not exist
|
|
79
|
-
// at the requested URI. The
|
|
80
|
-
// 404, so the same "endpoint missing" condition presents as an opaque 400 to
|
|
79
|
+
// at the requested URI. The dovetail_core -> dovetail fallback above only fires
|
|
80
|
+
// on 404, so the same "endpoint missing" condition presents as an opaque 400 to
|
|
81
81
|
// callers. Detect it and surface a clear hint.
|
|
82
82
|
var _SN_MISSING_ENDPOINT_BODY = "Requested URI does not represent any resource";
|
|
83
83
|
function _summarizeResponseBody(data) {
|
|
@@ -101,7 +101,7 @@ function _enrichDovetailApiError(e, attemptedEndpoint) {
|
|
|
101
101
|
var body = e.response ? _summarizeResponseBody(e.response.data) : "";
|
|
102
102
|
var missingEndpoint = status === 400 && body.indexOf(_SN_MISSING_ENDPOINT_BODY) !== -1;
|
|
103
103
|
var hint = missingEndpoint
|
|
104
|
-
? " — Scripted REST endpoint not found on this instance.
|
|
104
|
+
? " — Scripted REST endpoint not found on this instance. Install the Dovetail application's Scripted REST APIs (dovetail_core / dovetail_sync / dovetail_promote)."
|
|
105
105
|
: "";
|
|
106
106
|
e.message =
|
|
107
107
|
"Dovetail REST call failed: " +
|
|
@@ -117,11 +117,11 @@ async function _callDovetailApi(op, call) {
|
|
|
117
117
|
return await call(endpoint);
|
|
118
118
|
}
|
|
119
119
|
catch (e) {
|
|
120
|
-
if (!
|
|
121
|
-
Logger_1.logger.warn("[deprecation] /api/cadso/
|
|
122
|
-
" returned 404. Falling back to legacy /api/cadso/
|
|
123
|
-
".
|
|
124
|
-
|
|
120
|
+
if (!_dovetailApiUseLegacyPath && _getHttpStatus(e) === 404) {
|
|
121
|
+
Logger_1.logger.warn("[deprecation] /api/cadso/dovetail_core/" + op +
|
|
122
|
+
" returned 404. Falling back to legacy /api/cadso/dovetail/" + op +
|
|
123
|
+
". Install the Dovetail application's Scripted REST APIs to silence this warning.");
|
|
124
|
+
_dovetailApiUseLegacyPath = true;
|
|
125
125
|
var legacyEndpoint = _dovetailEndpoint(op);
|
|
126
126
|
try {
|
|
127
127
|
return await call(legacyEndpoint);
|
|
@@ -290,11 +290,11 @@ const snClient = (baseURL, username, password) => {
|
|
|
290
290
|
});
|
|
291
291
|
const client = (0, axios_rate_limit_1.default)(rawAxios, { maxRPS: 20 });
|
|
292
292
|
const getAppList = () => {
|
|
293
|
-
const endpoint = "api/
|
|
293
|
+
const endpoint = "api/cadso/dovetail_sync/getAppList";
|
|
294
294
|
return client.get(endpoint);
|
|
295
295
|
};
|
|
296
296
|
const updateATFfile = (contents, sysId) => {
|
|
297
|
-
const endpoint = "api/
|
|
297
|
+
const endpoint = "api/cadso/dovetail_sync/pushATFfile";
|
|
298
298
|
try {
|
|
299
299
|
return client.post(endpoint, { file: contents, sys_id: sysId });
|
|
300
300
|
}
|
|
@@ -350,7 +350,7 @@ const snClient = (baseURL, username, password) => {
|
|
|
350
350
|
});
|
|
351
351
|
};
|
|
352
352
|
const getCurrentScope = () => {
|
|
353
|
-
const endpoint = "api/
|
|
353
|
+
const endpoint = "api/cadso/dovetail_sync/getCurrentScope";
|
|
354
354
|
return client.get(endpoint);
|
|
355
355
|
};
|
|
356
356
|
const createUpdateSet = (updateSetName, scopeSysId, description) => {
|
|
@@ -390,13 +390,13 @@ const snClient = (baseURL, username, password) => {
|
|
|
390
390
|
});
|
|
391
391
|
};
|
|
392
392
|
const getMissingFiles = (missingFiles, tableOptions) => {
|
|
393
|
-
const endpoint = `api/
|
|
393
|
+
const endpoint = `api/cadso/dovetail_sync/bulkDownload`;
|
|
394
394
|
const tableCount = Object.keys(missingFiles).length;
|
|
395
395
|
FileLogger_1.fileLogger.debug("Bulk downloading files for " + tableCount + " tables");
|
|
396
396
|
return client.post(endpoint, { missingFiles, tableOptions });
|
|
397
397
|
};
|
|
398
398
|
const getManifest = (scope, config, withFiles = false) => {
|
|
399
|
-
const endpoint = `api/
|
|
399
|
+
const endpoint = `api/cadso/dovetail_sync/getManifest/${scope}`;
|
|
400
400
|
const { includes = {}, excludes = {}, tableOptions = {}, scopes = {}, } = config;
|
|
401
401
|
// Strip _ directive keys before sending to ServiceNow API
|
|
402
402
|
var cleanIncludes = _stripUnderscoreKeys(includes);
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Regression: the refresh path must write filesystem-SAFE folder names.
|
|
4
|
+
*
|
|
5
|
+
* normalizeManifestKeys() (PR #117) re-keys the manifest object so a record
|
|
6
|
+
* with a Windows-illegal display name (e.g. a table-wildcard ACL named
|
|
7
|
+
* `x_cadso_work_campaign.*`) maps to its sys_id. But the refresh writer
|
|
8
|
+
* (`refreshAllFiles`) builds folders from the raw `getMissingFiles`
|
|
9
|
+
* (bulkDownload) server response, whose `.name` is the unmodified display
|
|
10
|
+
* name — so it recreated the illegal `<table>.*` folders on every refresh,
|
|
11
|
+
* silently breaking the folder ≡ manifest-key invariant and aborting
|
|
12
|
+
* `git clone` on Windows.
|
|
13
|
+
*
|
|
14
|
+
* These tests assert the writer routes through toSafeFolderName: a `.*` ACL
|
|
15
|
+
* record lands in a sys_id folder, while a safe name stays human-readable.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
// ---------- mocks (must come before importing the module under test) ----------
|
|
55
|
+
var mockLogger = {
|
|
56
|
+
info: jest.fn(),
|
|
57
|
+
debug: jest.fn(),
|
|
58
|
+
warn: jest.fn(),
|
|
59
|
+
error: jest.fn(),
|
|
60
|
+
success: jest.fn(),
|
|
61
|
+
getLogLevel: function () { return "warn"; },
|
|
62
|
+
};
|
|
63
|
+
var mockFileLogger = {
|
|
64
|
+
debug: jest.fn(),
|
|
65
|
+
info: jest.fn(),
|
|
66
|
+
warn: jest.fn(),
|
|
67
|
+
error: jest.fn(),
|
|
68
|
+
};
|
|
69
|
+
var mockClient = {
|
|
70
|
+
getManifest: jest.fn(),
|
|
71
|
+
getMissingFiles: jest.fn(),
|
|
72
|
+
};
|
|
73
|
+
jest.mock("../Logger", function () { return { logger: mockLogger }; });
|
|
74
|
+
jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
|
|
75
|
+
jest.mock("../snClient", function () {
|
|
76
|
+
return {
|
|
77
|
+
defaultClient: function () { return mockClient; },
|
|
78
|
+
unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
|
|
79
|
+
processPushResponse: jest.fn(),
|
|
80
|
+
retryOnErr: jest.fn(),
|
|
81
|
+
retryOnHttpErr: jest.fn(),
|
|
82
|
+
unwrapTableAPIFirstItem: jest.fn(),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
var mockConfig = {
|
|
86
|
+
getConfig: jest.fn().mockReturnValue({
|
|
87
|
+
scopes: { x_cadso_work: {} },
|
|
88
|
+
tableOptions: {},
|
|
89
|
+
}),
|
|
90
|
+
getManifest: jest.fn().mockResolvedValue({
|
|
91
|
+
x_cadso_work: { scope: "x_cadso_work", tables: {} },
|
|
92
|
+
}),
|
|
93
|
+
getSourcePathForScope: jest.fn(),
|
|
94
|
+
getSourcePath: jest.fn(),
|
|
95
|
+
getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
|
|
96
|
+
resolveConfigForScope: jest.fn().mockImplementation(function () {
|
|
97
|
+
return {
|
|
98
|
+
tables: ["sys_security_acl"],
|
|
99
|
+
fieldOverrides: {},
|
|
100
|
+
apiIncludes: {},
|
|
101
|
+
apiExcludes: {},
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
isMultiScopeManifest: jest.fn().mockReturnValue(true),
|
|
105
|
+
updateManifest: jest.fn(),
|
|
106
|
+
};
|
|
107
|
+
jest.mock("../config", function () { return mockConfig; });
|
|
108
|
+
jest.mock("progress", function () {
|
|
109
|
+
return jest.fn().mockImplementation(function () {
|
|
110
|
+
return { tick: jest.fn() };
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// Keep real FileUtils so we assert against the actual file system; stub only
|
|
114
|
+
// writeScopeManifest so no manifest file is written.
|
|
115
|
+
jest.mock("../FileUtils", function () {
|
|
116
|
+
var actual = jest.requireActual("../FileUtils");
|
|
117
|
+
return Object.assign({}, actual, {
|
|
118
|
+
writeScopeManifest: jest.fn().mockResolvedValue(undefined),
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
const AppUtils = __importStar(require("../appUtils"));
|
|
122
|
+
// ---------- helpers ----------
|
|
123
|
+
var tmpRoot;
|
|
124
|
+
var ACL_SYS_ID = "bc91c2a5f334621048dff31590812771";
|
|
125
|
+
function listAclFolders() {
|
|
126
|
+
var dir = path.join(tmpRoot, "sys_security_acl");
|
|
127
|
+
try {
|
|
128
|
+
return fs.readdirSync(dir).sort();
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
describe("refresh — folder names are filesystem-safe (sys_id fallback)", function () {
|
|
135
|
+
beforeEach(function () {
|
|
136
|
+
jest.clearAllMocks();
|
|
137
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dove-safe-folder-"));
|
|
138
|
+
mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
|
|
139
|
+
mockConfig.getSourcePath.mockReturnValue(tmpRoot);
|
|
140
|
+
});
|
|
141
|
+
afterEach(function () {
|
|
142
|
+
try {
|
|
143
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
catch (e) { }
|
|
146
|
+
});
|
|
147
|
+
test("a `<table>.*` ACL record is written to its sys_id folder, never `.*`", async function () {
|
|
148
|
+
// Manifest as the server returns it: keyed by the illegal display name.
|
|
149
|
+
// normalizeManifestKeys re-keys this to sys_id inside syncManifest.
|
|
150
|
+
mockClient.getManifest.mockResolvedValue({
|
|
151
|
+
scope: "x_cadso_work",
|
|
152
|
+
tables: {
|
|
153
|
+
sys_security_acl: {
|
|
154
|
+
records: {
|
|
155
|
+
"x_cadso_work_campaign.*": {
|
|
156
|
+
name: "x_cadso_work_campaign.*",
|
|
157
|
+
sys_id: ACL_SYS_ID,
|
|
158
|
+
files: [{ name: "script", type: "js" }],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// bulkDownload echoes the RAW display name back — the bug source. The
|
|
165
|
+
// writer must not trust it for the folder path.
|
|
166
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
167
|
+
sys_security_acl: {
|
|
168
|
+
records: {
|
|
169
|
+
"x_cadso_work_campaign.*": {
|
|
170
|
+
name: "x_cadso_work_campaign.*",
|
|
171
|
+
sys_id: ACL_SYS_ID,
|
|
172
|
+
files: [{ name: "script", type: "js", content: "// acl" }],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
await AppUtils.syncManifest("x_cadso_work");
|
|
178
|
+
var folders = listAclFolders();
|
|
179
|
+
expect(folders).toContain(ACL_SYS_ID);
|
|
180
|
+
expect(folders).not.toContain("x_cadso_work_campaign.*");
|
|
181
|
+
// The file landed inside the sys_id folder.
|
|
182
|
+
expect(fs.readFileSync(path.join(tmpRoot, "sys_security_acl", ACL_SYS_ID, "script.js"), "utf8")).toBe("// acl");
|
|
183
|
+
});
|
|
184
|
+
test("a filesystem-safe ACL name stays human-readable (no needless sys_id fallback)", async function () {
|
|
185
|
+
var SAFE_SYS_ID = "aa11bb22cc33dd44ee55ff6600112233";
|
|
186
|
+
mockClient.getManifest.mockResolvedValue({
|
|
187
|
+
scope: "x_cadso_work",
|
|
188
|
+
tables: {
|
|
189
|
+
sys_security_acl: {
|
|
190
|
+
records: {
|
|
191
|
+
x_cadso_work_campaign: {
|
|
192
|
+
name: "x_cadso_work_campaign",
|
|
193
|
+
sys_id: SAFE_SYS_ID,
|
|
194
|
+
files: [{ name: "script", type: "js" }],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
201
|
+
sys_security_acl: {
|
|
202
|
+
records: {
|
|
203
|
+
x_cadso_work_campaign: {
|
|
204
|
+
name: "x_cadso_work_campaign",
|
|
205
|
+
sys_id: SAFE_SYS_ID,
|
|
206
|
+
files: [{ name: "script", type: "js", content: "// safe" }],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
await AppUtils.syncManifest("x_cadso_work");
|
|
212
|
+
var folders = listAclFolders();
|
|
213
|
+
expect(folders).toContain("x_cadso_work_campaign");
|
|
214
|
+
expect(folders).not.toContain(SAFE_SYS_ID);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Tests for the /api/cadso/
|
|
2
|
+
// Tests for the /api/cadso/dovetail_core/* -> /api/cadso/dovetail/* 404 fallback
|
|
3
3
|
// in _callDovetailApi (snClient.ts). Covers: first call hits the dovetail
|
|
4
4
|
// endpoint, 404 latches the legacy path with a one-time warning, subsequent
|
|
5
5
|
// calls skip dovetail, and non-404 errors do NOT trigger fallback.
|
|
@@ -35,21 +35,21 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
35
35
|
(0, snClient_1._resetDovetailApiFallback)();
|
|
36
36
|
jest.clearAllMocks();
|
|
37
37
|
});
|
|
38
|
-
it("first request hits /api/cadso/
|
|
38
|
+
it("first request hits /api/cadso/dovetail_core/<op>", async function () {
|
|
39
39
|
var seen = [];
|
|
40
40
|
var call = jest.fn(async function (endpoint) {
|
|
41
41
|
seen.push(endpoint);
|
|
42
42
|
return makeAxiosResponse({ ok: true });
|
|
43
43
|
});
|
|
44
44
|
await (0, snClient_1._callDovetailApi)("changeScope", call);
|
|
45
|
-
expect(seen).toEqual(["api/cadso/
|
|
46
|
-
expect((0, snClient_1.
|
|
45
|
+
expect(seen).toEqual(["api/cadso/dovetail_core/changeScope"]);
|
|
46
|
+
expect((0, snClient_1._isUsingLegacyPath)()).toBe(false);
|
|
47
47
|
});
|
|
48
|
-
it("on 404, retries against legacy /api/cadso/
|
|
48
|
+
it("on 404, retries against legacy /api/cadso/dovetail/<op> and warns once", async function () {
|
|
49
49
|
var seen = [];
|
|
50
50
|
var call = jest.fn(async function (endpoint) {
|
|
51
51
|
seen.push(endpoint);
|
|
52
|
-
if (endpoint.indexOf("api/cadso/
|
|
52
|
+
if (endpoint.indexOf("api/cadso/dovetail_core/") === 0) {
|
|
53
53
|
throw makeAxiosError(404);
|
|
54
54
|
}
|
|
55
55
|
return makeAxiosResponse({ ok: true });
|
|
@@ -57,20 +57,20 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
57
57
|
var result = await (0, snClient_1._callDovetailApi)("changeScope", call);
|
|
58
58
|
expect(result.data).toEqual({ ok: true });
|
|
59
59
|
expect(seen).toEqual([
|
|
60
|
+
"api/cadso/dovetail_core/changeScope",
|
|
60
61
|
"api/cadso/dovetail/changeScope",
|
|
61
|
-
"api/cadso/claude/changeScope",
|
|
62
62
|
]);
|
|
63
|
-
expect((0, snClient_1.
|
|
63
|
+
expect((0, snClient_1._isUsingLegacyPath)()).toBe(true);
|
|
64
64
|
expect(mockLogger.warn).toHaveBeenCalledTimes(1);
|
|
65
65
|
expect(mockLogger.warn.mock.calls[0][0]).toContain("[deprecation]");
|
|
66
|
+
expect(mockLogger.warn.mock.calls[0][0]).toContain("/api/cadso/dovetail_core/changeScope");
|
|
66
67
|
expect(mockLogger.warn.mock.calls[0][0]).toContain("/api/cadso/dovetail/changeScope");
|
|
67
|
-
expect(mockLogger.warn.mock.calls[0][0]).toContain("/api/cadso/claude/changeScope");
|
|
68
68
|
});
|
|
69
69
|
it("after latching, subsequent calls skip dovetail and warn no more", async function () {
|
|
70
70
|
var seen = [];
|
|
71
71
|
var call = jest.fn(async function (endpoint) {
|
|
72
72
|
seen.push(endpoint);
|
|
73
|
-
if (endpoint.indexOf("api/cadso/
|
|
73
|
+
if (endpoint.indexOf("api/cadso/dovetail_core/") === 0) {
|
|
74
74
|
throw makeAxiosError(404);
|
|
75
75
|
}
|
|
76
76
|
return makeAxiosResponse({ ok: true });
|
|
@@ -81,8 +81,8 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
81
81
|
await (0, snClient_1._callDovetailApi)("changeScope", call);
|
|
82
82
|
await (0, snClient_1._callDovetailApi)("changeUpdateSet", call);
|
|
83
83
|
expect(seen).toEqual([
|
|
84
|
-
"api/cadso/
|
|
85
|
-
"api/cadso/
|
|
84
|
+
"api/cadso/dovetail/changeScope",
|
|
85
|
+
"api/cadso/dovetail/changeUpdateSet",
|
|
86
86
|
]);
|
|
87
87
|
expect(mockLogger.warn).not.toHaveBeenCalled();
|
|
88
88
|
});
|
|
@@ -94,7 +94,7 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
94
94
|
response: { status: 401 },
|
|
95
95
|
});
|
|
96
96
|
expect(call).toHaveBeenCalledTimes(1);
|
|
97
|
-
expect((0, snClient_1.
|
|
97
|
+
expect((0, snClient_1._isUsingLegacyPath)()).toBe(false);
|
|
98
98
|
expect(mockLogger.warn).not.toHaveBeenCalled();
|
|
99
99
|
});
|
|
100
100
|
it("does NOT fall back on 500 (server error)", async function () {
|
|
@@ -105,7 +105,7 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
105
105
|
response: { status: 500 },
|
|
106
106
|
});
|
|
107
107
|
expect(call).toHaveBeenCalledTimes(1);
|
|
108
|
-
expect((0, snClient_1.
|
|
108
|
+
expect((0, snClient_1._isUsingLegacyPath)()).toBe(false);
|
|
109
109
|
expect(mockLogger.warn).not.toHaveBeenCalled();
|
|
110
110
|
});
|
|
111
111
|
it("does NOT fall back on a network error with no response", async function () {
|
|
@@ -114,7 +114,7 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
114
114
|
});
|
|
115
115
|
await expect((0, snClient_1._callDovetailApi)("changeScope", call)).rejects.toThrow("ECONNREFUSED");
|
|
116
116
|
expect(call).toHaveBeenCalledTimes(1);
|
|
117
|
-
expect((0, snClient_1.
|
|
117
|
+
expect((0, snClient_1._isUsingLegacyPath)()).toBe(false);
|
|
118
118
|
expect(mockLogger.warn).not.toHaveBeenCalled();
|
|
119
119
|
});
|
|
120
120
|
describe("descriptive error enrichment", function () {
|
|
@@ -129,7 +129,7 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
129
129
|
catch (e) {
|
|
130
130
|
expect(e.response.status).toBe(401);
|
|
131
131
|
expect(e.message).toContain("HTTP 401");
|
|
132
|
-
expect(e.message).toContain("api/cadso/
|
|
132
|
+
expect(e.message).toContain("api/cadso/dovetail_core/changeScope");
|
|
133
133
|
expect(e.message).toContain("User Not Authenticated");
|
|
134
134
|
}
|
|
135
135
|
});
|
|
@@ -148,8 +148,8 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
148
148
|
catch (e) {
|
|
149
149
|
expect(e.response.status).toBe(400);
|
|
150
150
|
expect(e.message).toContain("HTTP 400");
|
|
151
|
-
expect(e.message).toContain("api/cadso/
|
|
152
|
-
expect(e.message).toContain("
|
|
151
|
+
expect(e.message).toContain("api/cadso/dovetail_core/changeScope");
|
|
152
|
+
expect(e.message).toContain("Install the Dovetail application's Scripted REST APIs");
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
it("does NOT add the missing-endpoint hint for unrelated 400s", async function () {
|
|
@@ -163,14 +163,14 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
163
163
|
catch (e) {
|
|
164
164
|
expect(e.message).toContain("HTTP 400");
|
|
165
165
|
expect(e.message).toContain("Invalid scope name");
|
|
166
|
-
expect(e.message).not.toContain("
|
|
166
|
+
expect(e.message).not.toContain("Install the Dovetail application's Scripted REST APIs");
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
it("after fallback latches, errors from the legacy path are also enriched", async function () {
|
|
170
170
|
var calls = [];
|
|
171
171
|
var call = jest.fn(async function (endpoint) {
|
|
172
172
|
calls.push(endpoint);
|
|
173
|
-
if (endpoint.indexOf("api/cadso/
|
|
173
|
+
if (endpoint.indexOf("api/cadso/dovetail_core/") === 0) {
|
|
174
174
|
throw makeAxiosError(404, {});
|
|
175
175
|
}
|
|
176
176
|
throw makeAxiosError(500, { error: { message: "boom" } });
|
|
@@ -181,12 +181,12 @@ describe("_callDovetailApi 404 fallback", function () {
|
|
|
181
181
|
}
|
|
182
182
|
catch (e) {
|
|
183
183
|
expect(calls).toEqual([
|
|
184
|
+
"api/cadso/dovetail_core/changeScope",
|
|
184
185
|
"api/cadso/dovetail/changeScope",
|
|
185
|
-
"api/cadso/claude/changeScope",
|
|
186
186
|
]);
|
|
187
187
|
expect(e.response.status).toBe(500);
|
|
188
188
|
expect(e.message).toContain("HTTP 500");
|
|
189
|
-
expect(e.message).toContain("api/cadso/
|
|
189
|
+
expect(e.message).toContain("api/cadso/dovetail/changeScope");
|
|
190
190
|
expect(e.message).toContain("boom");
|
|
191
191
|
}
|
|
192
192
|
});
|
|
@@ -38,7 +38,7 @@ function makeAxiosError(overrides) {
|
|
|
38
38
|
error.isAxiosError = true;
|
|
39
39
|
error.config = {
|
|
40
40
|
method: overrides.method || "post",
|
|
41
|
-
url: overrides.url || "api/
|
|
41
|
+
url: overrides.url || "api/cadso/dovetail_sync/getManifest/x_cadso_example",
|
|
42
42
|
};
|
|
43
43
|
error.response = {
|
|
44
44
|
status: overrides.status,
|
|
@@ -74,7 +74,7 @@ describe("unwrapSNResponse — error handling", function () {
|
|
|
74
74
|
var axErr = makeAxiosError({
|
|
75
75
|
status: 500,
|
|
76
76
|
method: "post",
|
|
77
|
-
url: "api/
|
|
77
|
+
url: "api/cadso/dovetail_sync/getManifest/x_cadso_automate",
|
|
78
78
|
data: snBody,
|
|
79
79
|
statusText: "Internal Server Error",
|
|
80
80
|
});
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,333 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-
|
|
3
|
+
"generated_at": "2026-06-01T21:28:08.741Z",
|
|
4
4
|
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-core@0.0.96",
|
|
8
|
+
"package": "@tenonhq/dovetail-core",
|
|
9
|
+
"version": "0.0.96",
|
|
10
|
+
"prev_version": "0.0.95",
|
|
11
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
14
|
+
"tag": "dovetail-core@0.0.96"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "a3ea5ead4",
|
|
20
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
21
|
+
"pr": 132
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"sha": "93d180eef",
|
|
25
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.9 [skip ci]"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"sha": "d32523991",
|
|
29
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.8 [skip ci]"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"sha": "ba1c67775",
|
|
33
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.36 [skip ci]"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"sha": "9901285c6",
|
|
37
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.35 [skip ci]"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"sha": "55de31527",
|
|
41
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.34 [skip ci]"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"sha": "776f1fc53",
|
|
45
|
+
"subject": "chore(release): @tenonhq/dovetail-sawmill@0.0.7 [skip ci]"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"sha": "407d6d75e",
|
|
49
|
+
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"exports_delta": null,
|
|
53
|
+
"narrative": null
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"$schema": "dovetail-release-event/v1",
|
|
57
|
+
"event_id": "dovetail-servicenow@0.0.10",
|
|
58
|
+
"package": "@tenonhq/dovetail-servicenow",
|
|
59
|
+
"version": "0.0.10",
|
|
60
|
+
"prev_version": "0.0.9",
|
|
61
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
62
|
+
"git": {
|
|
63
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
64
|
+
"tag": "dovetail-servicenow@0.0.10"
|
|
65
|
+
},
|
|
66
|
+
"semver_bump": "patch",
|
|
67
|
+
"commits": [
|
|
68
|
+
{
|
|
69
|
+
"sha": "a3ea5ead4",
|
|
70
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
71
|
+
"pr": 132
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"sha": "93d180eef",
|
|
75
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.9 [skip ci]"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"exports_delta": null,
|
|
79
|
+
"narrative": null
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"$schema": "dovetail-release-event/v1",
|
|
83
|
+
"event_id": "dovetail-sawmill@0.0.8",
|
|
84
|
+
"package": "@tenonhq/dovetail-sawmill",
|
|
85
|
+
"version": "0.0.8",
|
|
86
|
+
"prev_version": "0.0.7",
|
|
87
|
+
"published_at": "2026-06-01T21:28:08.724Z",
|
|
88
|
+
"git": {
|
|
89
|
+
"sha": "a3ea5ead48b0bd4c324e0847e7e768a43520c7ec",
|
|
90
|
+
"tag": "dovetail-sawmill@0.0.8"
|
|
91
|
+
},
|
|
92
|
+
"semver_bump": "patch",
|
|
93
|
+
"commits": [
|
|
94
|
+
{
|
|
95
|
+
"sha": "a3ea5ead4",
|
|
96
|
+
"subject": "feat(client): point Dovetail clients at the scoped dovetail_* APIs (#132)",
|
|
97
|
+
"pr": 132
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"sha": "776f1fc53",
|
|
101
|
+
"subject": "chore(release): @tenonhq/dovetail-sawmill@0.0.7 [skip ci]"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"exports_delta": null,
|
|
105
|
+
"narrative": null
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"$schema": "dovetail-release-event/v1",
|
|
109
|
+
"event_id": "dovetail-servicenow@0.0.9",
|
|
110
|
+
"package": "@tenonhq/dovetail-servicenow",
|
|
111
|
+
"version": "0.0.9",
|
|
112
|
+
"prev_version": "0.0.8",
|
|
113
|
+
"published_at": "2026-05-31T16:52:41.024Z",
|
|
114
|
+
"git": {
|
|
115
|
+
"sha": "6e62bec706d10158b1f1496e3e2246a927c1c0b5",
|
|
116
|
+
"tag": "dovetail-servicenow@0.0.9"
|
|
117
|
+
},
|
|
118
|
+
"semver_bump": "patch",
|
|
119
|
+
"commits": [
|
|
120
|
+
{
|
|
121
|
+
"sha": "6e62bec70",
|
|
122
|
+
"subject": "fix(servicenow): editFlow persistence + copyFlow (dropped from #129 squash-merge) (#130)",
|
|
123
|
+
"pr": 130
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"sha": "d32523991",
|
|
127
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.8 [skip ci]"
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"exports_delta": null,
|
|
131
|
+
"narrative": null
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"$schema": "dovetail-release-event/v1",
|
|
135
|
+
"event_id": "dovetail-servicenow@0.0.8",
|
|
136
|
+
"package": "@tenonhq/dovetail-servicenow",
|
|
137
|
+
"version": "0.0.8",
|
|
138
|
+
"prev_version": "0.0.7",
|
|
139
|
+
"published_at": "2026-05-31T03:07:57.716Z",
|
|
140
|
+
"git": {
|
|
141
|
+
"sha": "41b87ff39f4dcb7f6fd5bbbd3d3d26446d450bc6",
|
|
142
|
+
"tag": "dovetail-servicenow@0.0.8"
|
|
143
|
+
},
|
|
144
|
+
"semver_bump": "patch",
|
|
145
|
+
"commits": [
|
|
146
|
+
{
|
|
147
|
+
"sha": "41b87ff39",
|
|
148
|
+
"subject": "feat(servicenow): view + test + publish + edit flows/subflows/actions (Dovetail Flow Capability) (#129)",
|
|
149
|
+
"pr": 129
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"sha": "cbd551e00",
|
|
153
|
+
"subject": "chore(release): @tenonhq/dovetail-servicenow@0.0.7 [skip ci]"
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"exports_delta": null,
|
|
157
|
+
"narrative": null
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"$schema": "dovetail-release-event/v1",
|
|
161
|
+
"event_id": "dovetail-dashboard@0.0.36",
|
|
162
|
+
"package": "@tenonhq/dovetail-dashboard",
|
|
163
|
+
"version": "0.0.36",
|
|
164
|
+
"prev_version": "0.0.35",
|
|
165
|
+
"published_at": "2026-05-30T10:12:42.799Z",
|
|
166
|
+
"git": {
|
|
167
|
+
"sha": "fd04f052c3426e1591fcd3383c5ace29f2d9121f",
|
|
168
|
+
"tag": "dovetail-dashboard@0.0.36"
|
|
169
|
+
},
|
|
170
|
+
"semver_bump": "patch",
|
|
171
|
+
"commits": [
|
|
172
|
+
{
|
|
173
|
+
"sha": "fd04f052c",
|
|
174
|
+
"subject": "feat(dashboard): TODO side panel on claude-plans page (#127)",
|
|
175
|
+
"pr": 127
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"sha": "9901285c6",
|
|
179
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.35 [skip ci]"
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"exports_delta": null,
|
|
183
|
+
"narrative": null
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"$schema": "dovetail-release-event/v1",
|
|
187
|
+
"event_id": "dovetail-dashboard@0.0.35",
|
|
188
|
+
"package": "@tenonhq/dovetail-dashboard",
|
|
189
|
+
"version": "0.0.35",
|
|
190
|
+
"prev_version": "0.0.34",
|
|
191
|
+
"published_at": "2026-05-30T09:01:34.128Z",
|
|
192
|
+
"git": {
|
|
193
|
+
"sha": "31178f2465068e24d4804cc467976f9ede3c9788",
|
|
194
|
+
"tag": "dovetail-dashboard@0.0.35"
|
|
195
|
+
},
|
|
196
|
+
"semver_bump": "patch",
|
|
197
|
+
"commits": [
|
|
198
|
+
{
|
|
199
|
+
"sha": "31178f246",
|
|
200
|
+
"subject": "fix(todo): apply PR #124 review findings (R-1…R-4) + unblock republish (#125)",
|
|
201
|
+
"pr": 125
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"sha": "55de31527",
|
|
205
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.34 [skip ci]"
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"exports_delta": null,
|
|
209
|
+
"narrative": null
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"$schema": "dovetail-release-event/v1",
|
|
213
|
+
"event_id": "dovetail-dashboard@0.0.34",
|
|
214
|
+
"package": "@tenonhq/dovetail-dashboard",
|
|
215
|
+
"version": "0.0.34",
|
|
216
|
+
"prev_version": "0.0.33",
|
|
217
|
+
"published_at": "2026-05-30T08:18:23.493Z",
|
|
218
|
+
"git": {
|
|
219
|
+
"sha": "d8151d39e4fa684a3bfee0b9f5bc6f51859bffe4",
|
|
220
|
+
"tag": "dovetail-dashboard@0.0.34"
|
|
221
|
+
},
|
|
222
|
+
"semver_bump": "patch",
|
|
223
|
+
"commits": [
|
|
224
|
+
{
|
|
225
|
+
"sha": "d8151d39e",
|
|
226
|
+
"subject": "feat(todo): TODO MCP package + dashboard priority checklist (#124)",
|
|
227
|
+
"pr": 124
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"sha": "da705804d",
|
|
231
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.33 [skip ci]"
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"exports_delta": null,
|
|
235
|
+
"narrative": null
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"$schema": "dovetail-release-event/v1",
|
|
239
|
+
"event_id": "dovetail-sawmill@0.0.7",
|
|
240
|
+
"package": "@tenonhq/dovetail-sawmill",
|
|
241
|
+
"version": "0.0.7",
|
|
242
|
+
"prev_version": "0.0.6",
|
|
243
|
+
"published_at": "2026-05-29T21:44:48.823Z",
|
|
244
|
+
"git": {
|
|
245
|
+
"sha": "1e949a30823f064a9dc603455f16cf5b10bc61d3",
|
|
246
|
+
"tag": "dovetail-sawmill@0.0.7"
|
|
247
|
+
},
|
|
248
|
+
"semver_bump": "patch",
|
|
249
|
+
"commits": [
|
|
250
|
+
{
|
|
251
|
+
"sha": "1e949a308",
|
|
252
|
+
"subject": "docs: Dovetail adoption pack (Claude operating guide, onboarding, INDEX, drift fix) (#121)",
|
|
253
|
+
"pr": 121
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"sha": "9332fdf7a",
|
|
257
|
+
"subject": "chore(release): publish 20 packages [skip ci]"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"sha": "9046da774",
|
|
261
|
+
"subject": "chore(release): publish 19 packages [skip ci]"
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
"exports_delta": null,
|
|
265
|
+
"narrative": null
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"$schema": "dovetail-release-event/v1",
|
|
269
|
+
"event_id": "dovetail-schema@0.0.9",
|
|
270
|
+
"package": "@tenonhq/dovetail-schema",
|
|
271
|
+
"version": "0.0.9",
|
|
272
|
+
"prev_version": "0.0.8",
|
|
273
|
+
"published_at": "2026-05-29T21:30:43.647Z",
|
|
274
|
+
"git": {
|
|
275
|
+
"sha": "dac76cc7b8e6a346aa54a84c7b50c442323301fb",
|
|
276
|
+
"tag": "dovetail-schema@0.0.9"
|
|
277
|
+
},
|
|
278
|
+
"semver_bump": "patch",
|
|
279
|
+
"commits": [
|
|
280
|
+
{
|
|
281
|
+
"sha": "dac76cc7b",
|
|
282
|
+
"subject": "feat(schema): snapshots + structured drift diff for dove schema (#122)",
|
|
283
|
+
"pr": 122
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"sha": "9332fdf7a",
|
|
287
|
+
"subject": "chore(release): publish 20 packages [skip ci]"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"sha": "9046da774",
|
|
291
|
+
"subject": "chore(release): publish 19 packages [skip ci]"
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"exports_delta": null,
|
|
295
|
+
"narrative": null
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"$schema": "dovetail-release-event/v1",
|
|
299
|
+
"event_id": "dovetail-core@0.0.95",
|
|
300
|
+
"package": "@tenonhq/dovetail-core",
|
|
301
|
+
"version": "0.0.95",
|
|
302
|
+
"prev_version": "0.0.94",
|
|
303
|
+
"published_at": "2026-05-29T21:30:43.647Z",
|
|
304
|
+
"git": {
|
|
305
|
+
"sha": "dac76cc7b8e6a346aa54a84c7b50c442323301fb",
|
|
306
|
+
"tag": "dovetail-core@0.0.95"
|
|
307
|
+
},
|
|
308
|
+
"semver_bump": "patch",
|
|
309
|
+
"commits": [
|
|
310
|
+
{
|
|
311
|
+
"sha": "dac76cc7b",
|
|
312
|
+
"subject": "feat(schema): snapshots + structured drift diff for dove schema (#122)",
|
|
313
|
+
"pr": 122
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"sha": "da705804d",
|
|
317
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.33 [skip ci]"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"sha": "92213e1f8",
|
|
321
|
+
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"sha": "1d43f9122",
|
|
325
|
+
"subject": "chore(release): @tenonhq/dovetail-core@0.0.94 [skip ci]"
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
"exports_delta": null,
|
|
329
|
+
"narrative": null
|
|
330
|
+
},
|
|
5
331
|
{
|
|
6
332
|
"$schema": "dovetail-release-event/v1",
|
|
7
333
|
"event_id": "dovetail-dashboard@0.0.33",
|