@tenonhq/dovetail-servicenow 0.0.30 → 0.0.31
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/README.md +43 -2
- package/dist/cli.js +111 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -1
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +35 -0
- package/dist/mcp/schemas.d.ts +25 -0
- package/dist/mcp/schemas.js +14 -1
- package/dist/publishApp.d.ts +173 -0
- package/dist/publishApp.js +664 -0
- package/dist/table/formSession.d.ts +8 -0
- package/dist/table/formSession.js +12 -4
- package/dist/table/index.d.ts +1 -1
- package/dist/table/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -406,6 +406,45 @@ Programmatic: `invokeRest({ method, path, body, confirm })` is exported, and the
|
|
|
406
406
|
client gained `now.put` / `now.delete` / `now.invoke` (the latter returns
|
|
407
407
|
`{ status, body }` verbatim) alongside the existing `now.get` / `now.post`.
|
|
408
408
|
|
|
409
|
+
### Publish an app to the Store / application repository
|
|
410
|
+
|
|
411
|
+
Publish a scoped application to the **ServiceNow Store**, the **company
|
|
412
|
+
application repository**, or both — headlessly, with the publish's progress
|
|
413
|
+
tracker polled to completion.
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
# Dry-run — the DEFAULT: resolves the app, prints the plan, publishes NOTHING
|
|
417
|
+
npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 --target both
|
|
418
|
+
|
|
419
|
+
# Publish for real (store, then repo, same version)
|
|
420
|
+
npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 \
|
|
421
|
+
--target both --dev-notes "July release" --confirm --json
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Store publish is EXTERNALLY VISIBLE on the ServiceNow Store — treat
|
|
425
|
+
`--target store --confirm` as a release.** `publish-app` is dry-run by default:
|
|
426
|
+
without `--confirm` it prints the resolved plan and exits `1` (a deliberate
|
|
427
|
+
refusal). `--target both` runs store then repo sequentially and short-circuits
|
|
428
|
+
if the store leg fails.
|
|
429
|
+
|
|
430
|
+
The two targets ride different transports:
|
|
431
|
+
|
|
432
|
+
- **store** replays the `sys_app` form's upload flow (`xmlhttp.do` +
|
|
433
|
+
`sn_appauthor.ScopedAppUploaderAJAX`) over a form-login session — basic auth
|
|
434
|
+
alone no-ops there. It needs the Store account credentials in the loaded env
|
|
435
|
+
file as `SN_STORE_USERNAME` / `SN_STORE_PASSWORD`. The password is **never
|
|
436
|
+
accepted as a flag** and never appears in output, dry-run previews included.
|
|
437
|
+
- **repo** uses the supported CI/CD REST API (`POST /api/sn_cicd/app_repo/publish`
|
|
438
|
+
+ `GET /api/sn_cicd/progress/{id}`) over basic auth. The API user needs the
|
|
439
|
+
`sn_cicd` role (or admin).
|
|
440
|
+
|
|
441
|
+
`--app` accepts a scope name, `sys_app` sys_id, or app name; `--version` must be
|
|
442
|
+
above the currently published version. The result carries the progress-tracker
|
|
443
|
+
id, per-step states ("Packaging application", "Uploading application"), the
|
|
444
|
+
Store `appLink`, and the publish's update-set sys_id where the instance reports
|
|
445
|
+
one. Exit codes: `0` published or dry-run, `1` bad args/unconfirmed, `2`
|
|
446
|
+
failed/timeout. Programmatic: `publishApp({ app, version, target, confirm })`.
|
|
447
|
+
|
|
409
448
|
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
410
449
|
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
|
411
450
|
the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
|
|
@@ -488,8 +527,10 @@ publish is read back and verified), `flow_publish` (compile a flow/subflow snaps
|
|
|
488
527
|
publish, grafting a template), `flow_test` (validate or run a flow), and
|
|
489
528
|
`flow_edit` (patch a flow), plus `invoke_rest` (invoke an arbitrary authenticated
|
|
490
529
|
REST operation — Scripted REST included — with GET/POST/PUT/DELETE; dry-run by
|
|
491
|
-
default, response passed through verbatim, bodies never logged)
|
|
492
|
-
|
|
530
|
+
default, response passed through verbatim, bodies never logged) and `app_publish`
|
|
531
|
+
(publish a scoped app to the ServiceNow Store and/or the application repository;
|
|
532
|
+
dry-run by default, Store credentials env-only — they never transit tool
|
|
533
|
+
arguments). It reads ServiceNow credentials from the same env vars as the CLI.
|
|
493
534
|
|
|
494
535
|
```bash
|
|
495
536
|
npx dove-sn mcp --smoke # list the registered tools and exit
|
package/dist/cli.js
CHANGED
|
@@ -83,6 +83,7 @@ const table_1 = require("./table");
|
|
|
83
83
|
const setField_1 = require("./setField");
|
|
84
84
|
const createRecord_1 = require("./createRecord");
|
|
85
85
|
const invokeRest_1 = require("./invokeRest");
|
|
86
|
+
const publishApp_1 = require("./publishApp");
|
|
86
87
|
const hostAssets_1 = require("./hostAssets");
|
|
87
88
|
const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
|
|
88
89
|
function parseArgs(argv) {
|
|
@@ -918,6 +919,15 @@ function printHelp() {
|
|
|
918
919
|
" [--script-input <name>] [--update-set <sys_id>] [--apply] [--json])\n" +
|
|
919
920
|
" edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
|
|
920
921
|
" (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
|
|
922
|
+
" publish-app Publish a scoped app to the ServiceNow Store and/or the company\n" +
|
|
923
|
+
" application repository, then poll the publish to completion.\n" +
|
|
924
|
+
" STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store.\n" +
|
|
925
|
+
" DRY-RUN BY DEFAULT — nothing is published without --confirm\n" +
|
|
926
|
+
" (--app <scope|sys_id|name> --version <v> --target store|repo|both\n" +
|
|
927
|
+
" [--dev-notes <text>] [--store-user <email>] [--timeout-ms <n>]\n" +
|
|
928
|
+
" [--dry-run] [--json] [--confirm])\n" +
|
|
929
|
+
" Store creds: SN_STORE_USERNAME/SN_STORE_PASSWORD in the --env file;\n" +
|
|
930
|
+
" the password is never a flag. Repo publish needs the sn_cicd role.\n" +
|
|
921
931
|
" mcp Run the MCP stdio server (--smoke lists tools and exits)\n" +
|
|
922
932
|
"\nGlobal flags:\n" +
|
|
923
933
|
" --env <path> Load credentials from a specific .env file (also --env-file,\n" +
|
|
@@ -1511,6 +1521,104 @@ async function runInvokeRest(flags) {
|
|
|
1511
1521
|
}
|
|
1512
1522
|
return 0;
|
|
1513
1523
|
}
|
|
1524
|
+
/**
|
|
1525
|
+
* dove-sn publish-app:
|
|
1526
|
+
* --app <scope|sys_id|name> Required. The sys_app to publish.
|
|
1527
|
+
* --version <v> Required. Version to publish (e.g. 6.0.20260716).
|
|
1528
|
+
* --target store|repo|both Required. STORE PUBLISH IS EXTERNALLY VISIBLE.
|
|
1529
|
+
* [--dev-notes <text>] Optional developer notes.
|
|
1530
|
+
* [--store-user <email>] Store account email (else SN_STORE_USERNAME).
|
|
1531
|
+
* The password comes ONLY from SN_STORE_PASSWORD —
|
|
1532
|
+
* there is no flag for it, ever.
|
|
1533
|
+
* [--timeout-ms <n>] Progress-poll budget (default 120000).
|
|
1534
|
+
* [--dry-run] [--json] [--confirm]
|
|
1535
|
+
*
|
|
1536
|
+
* DRY-RUN unless --confirm: without it the resolved plan is printed and the
|
|
1537
|
+
* command exits 1 (a deliberate refusal, not success). --target both publishes
|
|
1538
|
+
* store then repo sequentially with the same version and short-circuits if the
|
|
1539
|
+
* store leg fails; --json emits an array of per-target results.
|
|
1540
|
+
* Exit codes: 0 published/dry-run, 1 bad args/unconfirmed, 2 failed/timeout.
|
|
1541
|
+
*/
|
|
1542
|
+
async function runPublishApp(flags) {
|
|
1543
|
+
var app = flags.app;
|
|
1544
|
+
var version = flags.version;
|
|
1545
|
+
var target = flags.target;
|
|
1546
|
+
if (!app || !version || !target) {
|
|
1547
|
+
process.stderr.write("publish-app: --app, --version and --target store|repo|both are required\n");
|
|
1548
|
+
return 1;
|
|
1549
|
+
}
|
|
1550
|
+
if (target !== "store" && target !== "repo" && target !== "both") {
|
|
1551
|
+
process.stderr.write("publish-app: --target must be store, repo or both (got '" +
|
|
1552
|
+
target +
|
|
1553
|
+
"')\n");
|
|
1554
|
+
return 1;
|
|
1555
|
+
}
|
|
1556
|
+
var targets = target === "both" ? ["store", "repo"] : [target];
|
|
1557
|
+
var dryRun = flags["dry-run"] === "true";
|
|
1558
|
+
var confirmed = flags.confirm === "true";
|
|
1559
|
+
var client = (0, client_1.createClient)({});
|
|
1560
|
+
var results = [];
|
|
1561
|
+
var exitCode = 0;
|
|
1562
|
+
for (var i = 0; i < targets.length; i += 1) {
|
|
1563
|
+
var params = {
|
|
1564
|
+
client: client,
|
|
1565
|
+
app: app,
|
|
1566
|
+
version: version,
|
|
1567
|
+
target: targets[i],
|
|
1568
|
+
confirm: confirmed,
|
|
1569
|
+
dryRun: dryRun,
|
|
1570
|
+
};
|
|
1571
|
+
if (flags["dev-notes"])
|
|
1572
|
+
params.devNotes = flags["dev-notes"];
|
|
1573
|
+
if (flags["store-user"])
|
|
1574
|
+
params.storeUsername = flags["store-user"];
|
|
1575
|
+
if (flags["timeout-ms"]) {
|
|
1576
|
+
// A NaN timeout would make the poll-loop budget check always false —
|
|
1577
|
+
// an infinite loop. Validate here, exit 1 on garbage.
|
|
1578
|
+
var timeoutMs = Number(flags["timeout-ms"]);
|
|
1579
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
1580
|
+
process.stderr.write("publish-app: --timeout-ms must be a positive integer (got '" +
|
|
1581
|
+
flags["timeout-ms"] +
|
|
1582
|
+
"')\n");
|
|
1583
|
+
return 1;
|
|
1584
|
+
}
|
|
1585
|
+
params.timeoutMs = timeoutMs;
|
|
1586
|
+
}
|
|
1587
|
+
var result = await (0, publishApp_1.publishApp)(params);
|
|
1588
|
+
results.push(result);
|
|
1589
|
+
if (flags.json !== "true") {
|
|
1590
|
+
process.stdout.write("[" +
|
|
1591
|
+
result.status +
|
|
1592
|
+
"] " +
|
|
1593
|
+
result.target +
|
|
1594
|
+
" — " +
|
|
1595
|
+
result.appName +
|
|
1596
|
+
" (" +
|
|
1597
|
+
result.appScope +
|
|
1598
|
+
") v" +
|
|
1599
|
+
result.version +
|
|
1600
|
+
(result.appLink ? " — " + result.appLink : "") +
|
|
1601
|
+
(result.updateSetSysId
|
|
1602
|
+
? " — update set " + result.updateSetSysId
|
|
1603
|
+
: "") +
|
|
1604
|
+
"\n" +
|
|
1605
|
+
result.note +
|
|
1606
|
+
"\n");
|
|
1607
|
+
}
|
|
1608
|
+
if (result.status === "failed" || result.status === "timeout") {
|
|
1609
|
+
exitCode = 2;
|
|
1610
|
+
break; // Short-circuit: never repo-publish after a failed store leg.
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (flags.json === "true") {
|
|
1614
|
+
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
1615
|
+
}
|
|
1616
|
+
if (exitCode === 0 && !dryRun && !confirmed) {
|
|
1617
|
+
process.stderr.write("publish-app: refusing to publish without --confirm (the plan above is a dry-run).\n");
|
|
1618
|
+
return 1;
|
|
1619
|
+
}
|
|
1620
|
+
return exitCode;
|
|
1621
|
+
}
|
|
1514
1622
|
async function main() {
|
|
1515
1623
|
var parsed = parseArgs(process.argv.slice(2));
|
|
1516
1624
|
// Load credentials before any command runs. `--env`/`--env-file` (or the
|
|
@@ -1585,6 +1693,9 @@ async function main() {
|
|
|
1585
1693
|
await runSetRelatedLists(parsed.flags);
|
|
1586
1694
|
return 0;
|
|
1587
1695
|
}
|
|
1696
|
+
if (parsed.command === "publish-app") {
|
|
1697
|
+
return await runPublishApp(parsed.flags);
|
|
1698
|
+
}
|
|
1588
1699
|
if (parsed.command === "mcp") {
|
|
1589
1700
|
return await runMcp(parsed.flags);
|
|
1590
1701
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,3 +27,5 @@ export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setFi
|
|
|
27
27
|
export type { CreateRecordParams, CreateRecordResult } from "./createRecord";
|
|
28
28
|
export { invokeRest, INVOKE_REST_METHODS } from "./invokeRest";
|
|
29
29
|
export type { InvokeRestParams, InvokeRestResult } from "./invokeRest";
|
|
30
|
+
export { publishApp, buildStartFields, parseXmlAnswer, parseProgressTree, classifyProgress, flattenSteps, harvestProgressResults, parseCicdPublishResponse, parseCicdProgress, DEFAULT_PUBLISH_TIMEOUT_MS, PUBLISH_POLL_DELAYS_MS, } from "./publishApp";
|
|
31
|
+
export type { PublishAppParams, PublishAppResult, PublishTarget, PublishStep, ProgressNode, CicdProgress, PublishTransport, } from "./publishApp";
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
|
|
10
|
-
exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
|
|
10
|
+
exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
|
|
11
11
|
var client_1 = require("./client");
|
|
12
12
|
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
|
|
13
13
|
var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
|
|
@@ -82,3 +82,15 @@ Object.defineProperty(exports, "createRecord", { enumerable: true, get: function
|
|
|
82
82
|
var invokeRest_1 = require("./invokeRest");
|
|
83
83
|
Object.defineProperty(exports, "invokeRest", { enumerable: true, get: function () { return invokeRest_1.invokeRest; } });
|
|
84
84
|
Object.defineProperty(exports, "INVOKE_REST_METHODS", { enumerable: true, get: function () { return invokeRest_1.INVOKE_REST_METHODS; } });
|
|
85
|
+
var publishApp_1 = require("./publishApp");
|
|
86
|
+
Object.defineProperty(exports, "publishApp", { enumerable: true, get: function () { return publishApp_1.publishApp; } });
|
|
87
|
+
Object.defineProperty(exports, "buildStartFields", { enumerable: true, get: function () { return publishApp_1.buildStartFields; } });
|
|
88
|
+
Object.defineProperty(exports, "parseXmlAnswer", { enumerable: true, get: function () { return publishApp_1.parseXmlAnswer; } });
|
|
89
|
+
Object.defineProperty(exports, "parseProgressTree", { enumerable: true, get: function () { return publishApp_1.parseProgressTree; } });
|
|
90
|
+
Object.defineProperty(exports, "classifyProgress", { enumerable: true, get: function () { return publishApp_1.classifyProgress; } });
|
|
91
|
+
Object.defineProperty(exports, "flattenSteps", { enumerable: true, get: function () { return publishApp_1.flattenSteps; } });
|
|
92
|
+
Object.defineProperty(exports, "harvestProgressResults", { enumerable: true, get: function () { return publishApp_1.harvestProgressResults; } });
|
|
93
|
+
Object.defineProperty(exports, "parseCicdPublishResponse", { enumerable: true, get: function () { return publishApp_1.parseCicdPublishResponse; } });
|
|
94
|
+
Object.defineProperty(exports, "parseCicdProgress", { enumerable: true, get: function () { return publishApp_1.parseCicdProgress; } });
|
|
95
|
+
Object.defineProperty(exports, "DEFAULT_PUBLISH_TIMEOUT_MS", { enumerable: true, get: function () { return publishApp_1.DEFAULT_PUBLISH_TIMEOUT_MS; } });
|
|
96
|
+
Object.defineProperty(exports, "PUBLISH_POLL_DELAYS_MS", { enumerable: true, get: function () { return publishApp_1.PUBLISH_POLL_DELAYS_MS; } });
|
package/dist/mcp/registry.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
|
|
12
12
|
import type { ServiceNowClient } from "../client";
|
|
13
|
-
export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_field", "create_record", "host_assets", "invoke_rest"];
|
|
13
|
+
export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish"];
|
|
14
14
|
export type ToolName = (typeof TOOL_NAMES)[number];
|
|
15
15
|
export interface RegistryDeps {
|
|
16
16
|
/** Optional client injection for tests; defaults to createClient({}). */
|
package/dist/mcp/registry.js
CHANGED
|
@@ -31,6 +31,7 @@ const hostAssets_1 = require("../hostAssets");
|
|
|
31
31
|
const setField_1 = require("../setField");
|
|
32
32
|
const createRecord_1 = require("../createRecord");
|
|
33
33
|
const invokeRest_1 = require("../invokeRest");
|
|
34
|
+
const publishApp_1 = require("../publishApp");
|
|
34
35
|
const schemas_1 = require("./schemas");
|
|
35
36
|
exports.TOOL_NAMES = [
|
|
36
37
|
"create_view",
|
|
@@ -53,6 +54,7 @@ exports.TOOL_NAMES = [
|
|
|
53
54
|
"create_record",
|
|
54
55
|
"host_assets",
|
|
55
56
|
"invoke_rest",
|
|
57
|
+
"app_publish",
|
|
56
58
|
];
|
|
57
59
|
// Annotation presets (READ_ONLY / WRITE_ADDITIVE_IDEMPOTENT / WRITE_CREATE /
|
|
58
60
|
// WRITE_OVERWRITE / WRITE_EXECUTE) come from @tenonhq/dovetail-mcp-kit.
|
|
@@ -491,6 +493,39 @@ function buildDescriptors(deps = {}) {
|
|
|
491
493
|
return (0, invokeRest_1.invokeRest)(params);
|
|
492
494
|
},
|
|
493
495
|
},
|
|
496
|
+
{
|
|
497
|
+
name: "app_publish",
|
|
498
|
+
annotations: dovetail_mcp_kit_1.WRITE_EXECUTE,
|
|
499
|
+
description: "Publish a scoped ServiceNow application to ONE target per call — the ServiceNow Store " +
|
|
500
|
+
"(target 'store') or the company Application Repository (target 'repo'); call twice to " +
|
|
501
|
+
"publish to both — then poll the publish to completion. STORE PUBLISH IS EXTERNALLY " +
|
|
502
|
+
"VISIBLE on the ServiceNow Store — treat it as a release. DRY-RUN BY DEFAULT: without " +
|
|
503
|
+
"confirm:true the resolved plan (app, current version, target) is returned and nothing is " +
|
|
504
|
+
"published. target 'store' replays the sys_app form's upload flow over a form-login session " +
|
|
505
|
+
"and requires SN_STORE_USERNAME/SN_STORE_PASSWORD in the server's env file — credentials " +
|
|
506
|
+
"never transit tool arguments. target 'repo' uses the supported CI/CD REST API " +
|
|
507
|
+
"(/api/sn_cicd/app_repo/publish) and requires the sn_cicd role. app is a scope name, " +
|
|
508
|
+
"sys_app sys_id, or app name; version must be above the currently published version. The " +
|
|
509
|
+
"result carries the progress-tracker id, per-step states, the Store appLink, and the " +
|
|
510
|
+
"publish's update-set sys_id where the instance reports one.",
|
|
511
|
+
shape: schemas_1.publishAppSchema.shape,
|
|
512
|
+
handler: async function (args) {
|
|
513
|
+
var p = schemas_1.publishAppSchema.parse(args);
|
|
514
|
+
var params = {
|
|
515
|
+
app: p.app,
|
|
516
|
+
version: p.version,
|
|
517
|
+
target: p.target,
|
|
518
|
+
devNotes: p.devNotes,
|
|
519
|
+
confirm: p.confirm,
|
|
520
|
+
dryRun: p.dryRun,
|
|
521
|
+
timeoutMs: p.timeoutMs,
|
|
522
|
+
};
|
|
523
|
+
if (deps.client) {
|
|
524
|
+
params.client = deps.client;
|
|
525
|
+
}
|
|
526
|
+
return (0, publishApp_1.publishApp)(params);
|
|
527
|
+
},
|
|
528
|
+
},
|
|
494
529
|
];
|
|
495
530
|
}
|
|
496
531
|
function registerAllTools(server, deps = {}) {
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -958,6 +958,31 @@ export declare var createRecordSchema: z.ZodObject<{
|
|
|
958
958
|
dryRun?: boolean | undefined;
|
|
959
959
|
ifAbsentQuery?: string | undefined;
|
|
960
960
|
}>;
|
|
961
|
+
export declare var publishAppSchema: z.ZodObject<{
|
|
962
|
+
app: z.ZodString;
|
|
963
|
+
version: z.ZodString;
|
|
964
|
+
devNotes: z.ZodOptional<z.ZodString>;
|
|
965
|
+
target: z.ZodUnion<[z.ZodLiteral<"store">, z.ZodLiteral<"repo">]>;
|
|
966
|
+
confirm: z.ZodOptional<z.ZodBoolean>;
|
|
967
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
968
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
969
|
+
}, "strip", z.ZodTypeAny, {
|
|
970
|
+
target: "store" | "repo";
|
|
971
|
+
version: string;
|
|
972
|
+
app: string;
|
|
973
|
+
confirm?: boolean | undefined;
|
|
974
|
+
dryRun?: boolean | undefined;
|
|
975
|
+
devNotes?: string | undefined;
|
|
976
|
+
timeoutMs?: number | undefined;
|
|
977
|
+
}, {
|
|
978
|
+
target: "store" | "repo";
|
|
979
|
+
version: string;
|
|
980
|
+
app: string;
|
|
981
|
+
confirm?: boolean | undefined;
|
|
982
|
+
dryRun?: boolean | undefined;
|
|
983
|
+
devNotes?: string | undefined;
|
|
984
|
+
timeoutMs?: number | undefined;
|
|
985
|
+
}>;
|
|
961
986
|
export declare var invokeRestSchema: z.ZodObject<{
|
|
962
987
|
method: z.ZodEffects<z.ZodEnum<["GET", "POST", "PUT", "DELETE"]>, "GET" | "DELETE" | "POST" | "PUT", unknown>;
|
|
963
988
|
path: z.ZodString;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* own file so registry.ts stays focused on wiring.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.invokeRestSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
|
|
7
|
+
exports.invokeRestSchema = exports.publishAppSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
|
|
8
8
|
const zod_1 = require("zod");
|
|
9
9
|
exports.createViewSchema = zod_1.z.object({
|
|
10
10
|
name: zod_1.z.string().min(1),
|
|
@@ -261,6 +261,19 @@ exports.createRecordSchema = zod_1.z.object({
|
|
|
261
261
|
ifAbsentQuery: zod_1.z.string().optional(),
|
|
262
262
|
dryRun: zod_1.z.boolean().optional(),
|
|
263
263
|
});
|
|
264
|
+
// app_publish: publish a scoped app to the ServiceNow Store or the company
|
|
265
|
+
// application repository. Deliberately NO credential fields — the Store
|
|
266
|
+
// account resolves from SN_STORE_USERNAME/SN_STORE_PASSWORD inside the verb,
|
|
267
|
+
// so credentials never transit MCP arguments or telemetry.
|
|
268
|
+
exports.publishAppSchema = zod_1.z.object({
|
|
269
|
+
app: zod_1.z.string().min(1),
|
|
270
|
+
version: zod_1.z.string().min(1),
|
|
271
|
+
devNotes: zod_1.z.string().optional(),
|
|
272
|
+
target: zod_1.z.union([zod_1.z.literal("store"), zod_1.z.literal("repo")]),
|
|
273
|
+
confirm: zod_1.z.boolean().optional(),
|
|
274
|
+
dryRun: zod_1.z.boolean().optional(),
|
|
275
|
+
timeoutMs: zod_1.z.number().int().positive().optional(),
|
|
276
|
+
});
|
|
264
277
|
// invoke_rest: transport primitive for arbitrary authenticated REST operations
|
|
265
278
|
// (Scripted REST included). The dry-run-unless-confirm gate lives in invokeRest
|
|
266
279
|
// itself; the regex here rejects absolute URLs and non-/api/ paths early.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dove-sn publish-app — publish a scoped application to the ServiceNow Store
|
|
3
|
+
* and/or the company Application Repository, headlessly, then poll the
|
|
4
|
+
* publish's progress tracker until it terminates.
|
|
5
|
+
*
|
|
6
|
+
* Two engines, one result shape:
|
|
7
|
+
*
|
|
8
|
+
* STORE — replays the sys_app form's "Publish to ServiceNow Store" flow
|
|
9
|
+
* (ground truth: HAR capture, 2026-07-16): a form-login session
|
|
10
|
+
* POSTs /xmlhttp.do with sysparm_processor=
|
|
11
|
+
* sn_appauthor.ScopedAppUploaderAJAX & sysparm_name=start (plus the
|
|
12
|
+
* Store account credentials), then polls AJAXProgressStatusChecker/
|
|
13
|
+
* getStatus with the returned execution sys_id. Basic auth alone
|
|
14
|
+
* NO-OPS on xmlhttp.do — the form session + X-UserToken is required
|
|
15
|
+
* (proven by the sn-undo-default-push headless revert).
|
|
16
|
+
*
|
|
17
|
+
* REPO — uses the supported CI/CD REST API: POST /api/sn_cicd/app_repo/publish
|
|
18
|
+
* (basic auth via the shared client), then polls
|
|
19
|
+
* GET /api/sn_cicd/progress/{id}. Requires the sn_cicd role (or admin).
|
|
20
|
+
*
|
|
21
|
+
* STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store — the verb
|
|
22
|
+
* refuses to run either engine without confirm:true (dry-run is the default).
|
|
23
|
+
*
|
|
24
|
+
* Store account credentials resolve ONLY inside this module, from
|
|
25
|
+
* SN_STORE_USERNAME / SN_STORE_PASSWORD (or injected params for tests). The
|
|
26
|
+
* password is never logged, never echoed into notes or dry-run previews, and
|
|
27
|
+
* never accepted as a CLI flag.
|
|
28
|
+
*/
|
|
29
|
+
import type { ServiceNowClient } from "./client";
|
|
30
|
+
import type { FormAuth, FormSession, PostResult } from "./table";
|
|
31
|
+
export type PublishTarget = "store" | "repo";
|
|
32
|
+
export interface PublishTransport {
|
|
33
|
+
openSession?: (auth: FormAuth) => Promise<FormSession>;
|
|
34
|
+
post?: (auth: FormAuth, session: FormSession, path: string, fields: Record<string, string>) => Promise<PostResult>;
|
|
35
|
+
sleep?: (ms: number) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface PublishAppParams {
|
|
38
|
+
/** Injectable for tests; defaults to createClient({}). */
|
|
39
|
+
client?: ServiceNowClient;
|
|
40
|
+
/** App selector: scope name (x_cadso_*), sys_app sys_id, or app name. */
|
|
41
|
+
app: string;
|
|
42
|
+
/** Version to publish, e.g. 6.0.20260716. Required — callers derive defaults. */
|
|
43
|
+
version: string;
|
|
44
|
+
devNotes?: string;
|
|
45
|
+
/** One target per call — "both" is CLI-level sequencing. */
|
|
46
|
+
target: PublishTarget;
|
|
47
|
+
/** Store account email; falls back to SN_STORE_USERNAME (store target only). */
|
|
48
|
+
storeUsername?: string;
|
|
49
|
+
/** Test seam only — the CLI never passes this; env SN_STORE_PASSWORD is the real source. */
|
|
50
|
+
storePassword?: string;
|
|
51
|
+
instance?: string;
|
|
52
|
+
user?: string;
|
|
53
|
+
password?: string;
|
|
54
|
+
/** The live gate: without confirm:true the result is a dry-run plan. */
|
|
55
|
+
confirm?: boolean;
|
|
56
|
+
dryRun?: boolean;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
/** Test seam — defaults to the real formSession functions + setTimeout sleep. */
|
|
59
|
+
transport?: PublishTransport;
|
|
60
|
+
}
|
|
61
|
+
export interface PublishStep {
|
|
62
|
+
name: string;
|
|
63
|
+
state: string;
|
|
64
|
+
percentComplete: string;
|
|
65
|
+
message: string;
|
|
66
|
+
}
|
|
67
|
+
/** The AJAXProgressStatusChecker getStatus answer, decoded. state "2" = success. */
|
|
68
|
+
export interface ProgressNode {
|
|
69
|
+
state: string;
|
|
70
|
+
percent_complete?: string;
|
|
71
|
+
message?: string;
|
|
72
|
+
name?: string;
|
|
73
|
+
result?: Record<string, string> | null;
|
|
74
|
+
children?: Array<ProgressNode>;
|
|
75
|
+
}
|
|
76
|
+
export interface PublishAppResult {
|
|
77
|
+
status: "dry-run" | "published" | "failed" | "timeout";
|
|
78
|
+
target: PublishTarget;
|
|
79
|
+
appSysId: string;
|
|
80
|
+
appScope: string;
|
|
81
|
+
appName: string;
|
|
82
|
+
/** sys_app.version at plan time (before the publish). */
|
|
83
|
+
versionBefore: string;
|
|
84
|
+
/** The version that was (or would be) published. */
|
|
85
|
+
version: string;
|
|
86
|
+
/** Store: the progress-tracker sys_id. Repo: the CI/CD progress id. */
|
|
87
|
+
executionSysId: string;
|
|
88
|
+
/** Store leg only — root result["sys_update_set.sys_id"]; "" when absent. */
|
|
89
|
+
updateSetSysId: string;
|
|
90
|
+
/** Store: tpp.servicenow.com editapplication URL. Repo: "". */
|
|
91
|
+
appLink: string;
|
|
92
|
+
steps: Array<PublishStep>;
|
|
93
|
+
polls: number;
|
|
94
|
+
/** Root progress message (failure diagnostics live here). */
|
|
95
|
+
message: string;
|
|
96
|
+
/** Dry-run only — the request that would be sent, credentials MASKED. */
|
|
97
|
+
requestPreview?: Record<string, string>;
|
|
98
|
+
note: string;
|
|
99
|
+
}
|
|
100
|
+
export declare var DEFAULT_PUBLISH_TIMEOUT_MS: number;
|
|
101
|
+
/** Repeat the LAST delay until the budget is spent — a publish can outrun 15s. */
|
|
102
|
+
export declare var PUBLISH_POLL_DELAYS_MS: ReadonlyArray<number>;
|
|
103
|
+
/**
|
|
104
|
+
* Build the exact ScopedAppUploaderAJAX start-call field map the browser sends
|
|
105
|
+
* (HAR ground truth). The repo variant is kept for parity/fixtures even though
|
|
106
|
+
* the repo leg ships on the CI/CD API: publish_to_store=false, empty username,
|
|
107
|
+
* and NO sysparm_password key at all.
|
|
108
|
+
*/
|
|
109
|
+
export declare function buildStartFields(p: {
|
|
110
|
+
appSysId: string;
|
|
111
|
+
version: string;
|
|
112
|
+
devNotes: string;
|
|
113
|
+
target: PublishTarget;
|
|
114
|
+
storeUsername: string;
|
|
115
|
+
storePassword: string;
|
|
116
|
+
}): Record<string, string>;
|
|
117
|
+
/**
|
|
118
|
+
* Extract the `answer` attribute from an xmlhttp.do response envelope
|
|
119
|
+
* (`<xml answer="..." .../>`) and entity-decode it. A missing/empty answer is
|
|
120
|
+
* returned as an error, never thrown — callers fold it into a failed result.
|
|
121
|
+
*/
|
|
122
|
+
export declare function parseXmlAnswer(xml: string): {
|
|
123
|
+
answer: string;
|
|
124
|
+
error: string;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Parse a decoded getStatus answer (a JSON progress tree). Throws a clear
|
|
128
|
+
* error on malformed JSON or an unexpected shape — the poll loop surfaces it
|
|
129
|
+
* as a failed result rather than a raw stack.
|
|
130
|
+
*/
|
|
131
|
+
export declare function parseProgressTree(decodedAnswer: string): ProgressNode;
|
|
132
|
+
/**
|
|
133
|
+
* Terminal-state classification for the AJAX progress tracker.
|
|
134
|
+
* "2" = Succeeded (HAR ground truth). "3"/"4" = Failed/Cancelled per the
|
|
135
|
+
* GlideExecutionTracker state model. Anything else keeps polling until the
|
|
136
|
+
* timeout budget — conservative on purpose: an unknown state must not be
|
|
137
|
+
* reported as success OR failure.
|
|
138
|
+
*/
|
|
139
|
+
export declare function classifyProgress(root: ProgressNode): {
|
|
140
|
+
terminal: boolean;
|
|
141
|
+
success: boolean;
|
|
142
|
+
};
|
|
143
|
+
/** Flatten the progress tree's children into an ordered step list. */
|
|
144
|
+
export declare function flattenSteps(root: ProgressNode): Array<PublishStep>;
|
|
145
|
+
/**
|
|
146
|
+
* Harvest the interesting result values from the progress tree, walking
|
|
147
|
+
* root-down: the FIRST non-empty appLink/appName/update-set sys_id encountered
|
|
148
|
+
* wins and is never overwritten. In the observed HAR trees each value appears
|
|
149
|
+
* on exactly one node, so first-wins is equivalent — the rule just needs to be
|
|
150
|
+
* deterministic.
|
|
151
|
+
*/
|
|
152
|
+
export declare function harvestProgressResults(root: ProgressNode): {
|
|
153
|
+
appLink: string;
|
|
154
|
+
appName: string;
|
|
155
|
+
updateSetSysId: string;
|
|
156
|
+
};
|
|
157
|
+
/** CI/CD publish response: harvest the progress id (and echo status_label). */
|
|
158
|
+
export declare function parseCicdPublishResponse(body: unknown): {
|
|
159
|
+
progressId: string;
|
|
160
|
+
statusLabel: string;
|
|
161
|
+
error: string;
|
|
162
|
+
};
|
|
163
|
+
export interface CicdProgress {
|
|
164
|
+
/** "0" pending, "1" running, "2" successful, "3" failed, "4" cancelled. */
|
|
165
|
+
status: string;
|
|
166
|
+
statusLabel: string;
|
|
167
|
+
statusMessage: string;
|
|
168
|
+
statusDetail: string;
|
|
169
|
+
percentComplete: string;
|
|
170
|
+
}
|
|
171
|
+
/** CI/CD progress response → a typed snapshot. Unknown shapes become status "". */
|
|
172
|
+
export declare function parseCicdProgress(body: unknown): CicdProgress;
|
|
173
|
+
export declare function publishApp(params: PublishAppParams): Promise<PublishAppResult>;
|
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dove-sn publish-app — publish a scoped application to the ServiceNow Store
|
|
4
|
+
* and/or the company Application Repository, headlessly, then poll the
|
|
5
|
+
* publish's progress tracker until it terminates.
|
|
6
|
+
*
|
|
7
|
+
* Two engines, one result shape:
|
|
8
|
+
*
|
|
9
|
+
* STORE — replays the sys_app form's "Publish to ServiceNow Store" flow
|
|
10
|
+
* (ground truth: HAR capture, 2026-07-16): a form-login session
|
|
11
|
+
* POSTs /xmlhttp.do with sysparm_processor=
|
|
12
|
+
* sn_appauthor.ScopedAppUploaderAJAX & sysparm_name=start (plus the
|
|
13
|
+
* Store account credentials), then polls AJAXProgressStatusChecker/
|
|
14
|
+
* getStatus with the returned execution sys_id. Basic auth alone
|
|
15
|
+
* NO-OPS on xmlhttp.do — the form session + X-UserToken is required
|
|
16
|
+
* (proven by the sn-undo-default-push headless revert).
|
|
17
|
+
*
|
|
18
|
+
* REPO — uses the supported CI/CD REST API: POST /api/sn_cicd/app_repo/publish
|
|
19
|
+
* (basic auth via the shared client), then polls
|
|
20
|
+
* GET /api/sn_cicd/progress/{id}. Requires the sn_cicd role (or admin).
|
|
21
|
+
*
|
|
22
|
+
* STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store — the verb
|
|
23
|
+
* refuses to run either engine without confirm:true (dry-run is the default).
|
|
24
|
+
*
|
|
25
|
+
* Store account credentials resolve ONLY inside this module, from
|
|
26
|
+
* SN_STORE_USERNAME / SN_STORE_PASSWORD (or injected params for tests). The
|
|
27
|
+
* password is never logged, never echoed into notes or dry-run previews, and
|
|
28
|
+
* never accepted as a CLI flag.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = void 0;
|
|
32
|
+
exports.buildStartFields = buildStartFields;
|
|
33
|
+
exports.parseXmlAnswer = parseXmlAnswer;
|
|
34
|
+
exports.parseProgressTree = parseProgressTree;
|
|
35
|
+
exports.classifyProgress = classifyProgress;
|
|
36
|
+
exports.flattenSteps = flattenSteps;
|
|
37
|
+
exports.harvestProgressResults = harvestProgressResults;
|
|
38
|
+
exports.parseCicdPublishResponse = parseCicdPublishResponse;
|
|
39
|
+
exports.parseCicdProgress = parseCicdProgress;
|
|
40
|
+
exports.publishApp = publishApp;
|
|
41
|
+
const client_1 = require("./client");
|
|
42
|
+
const table_1 = require("./table");
|
|
43
|
+
exports.DEFAULT_PUBLISH_TIMEOUT_MS = 120000;
|
|
44
|
+
/** Repeat the LAST delay until the budget is spent — a publish can outrun 15s. */
|
|
45
|
+
exports.PUBLISH_POLL_DELAYS_MS = [
|
|
46
|
+
1000, 1000, 2000, 2000, 5000,
|
|
47
|
+
];
|
|
48
|
+
var SYS_ID_RE = /^[0-9a-f]{32}$/;
|
|
49
|
+
var MASK = "***";
|
|
50
|
+
/**
|
|
51
|
+
* Build the exact ScopedAppUploaderAJAX start-call field map the browser sends
|
|
52
|
+
* (HAR ground truth). The repo variant is kept for parity/fixtures even though
|
|
53
|
+
* the repo leg ships on the CI/CD API: publish_to_store=false, empty username,
|
|
54
|
+
* and NO sysparm_password key at all.
|
|
55
|
+
*/
|
|
56
|
+
function buildStartFields(p) {
|
|
57
|
+
var fields = {
|
|
58
|
+
sysparm_processor: "sn_appauthor.ScopedAppUploaderAJAX",
|
|
59
|
+
sysparm_name: "start",
|
|
60
|
+
sysparm_scope: "global",
|
|
61
|
+
sysparm_want_session_messages: "true",
|
|
62
|
+
sysparm_sys_id: p.appSysId,
|
|
63
|
+
// The UI sends a single space when the notes box is empty — mirror it.
|
|
64
|
+
sysparm_dev_notes: p.devNotes || " ",
|
|
65
|
+
sysparm_version: p.version,
|
|
66
|
+
sysparm_target_upload_URL: "",
|
|
67
|
+
sysparm_is_customization: "",
|
|
68
|
+
"ni.nolog.x_referer": "ignore",
|
|
69
|
+
x_referer: "sys_app.do?sys_id=" + p.appSysId,
|
|
70
|
+
};
|
|
71
|
+
if (p.target === "store") {
|
|
72
|
+
fields.sysparm_publish_to_store = "true";
|
|
73
|
+
fields.sysparm_username = p.storeUsername;
|
|
74
|
+
fields.sysparm_password = p.storePassword;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
fields.sysparm_publish_to_store = "false";
|
|
78
|
+
fields.sysparm_username = "";
|
|
79
|
+
}
|
|
80
|
+
return fields;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extract the `answer` attribute from an xmlhttp.do response envelope
|
|
84
|
+
* (`<xml answer="..." .../>`) and entity-decode it. A missing/empty answer is
|
|
85
|
+
* returned as an error, never thrown — callers fold it into a failed result.
|
|
86
|
+
*/
|
|
87
|
+
function parseXmlAnswer(xml) {
|
|
88
|
+
if (typeof xml !== "string" || xml.length === 0) {
|
|
89
|
+
return { answer: "", error: "empty xmlhttp.do response body" };
|
|
90
|
+
}
|
|
91
|
+
var m = /\banswer\s*=\s*"([^"]*)"/.exec(xml);
|
|
92
|
+
if (!m) {
|
|
93
|
+
return {
|
|
94
|
+
answer: "",
|
|
95
|
+
error: "no answer attribute in xmlhttp.do response: " + xml.slice(0, 200),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { answer: (0, table_1.decodeHtmlEntities)(m[1]), error: "" };
|
|
99
|
+
}
|
|
100
|
+
function isProgressNode(v) {
|
|
101
|
+
return (typeof v === "object" &&
|
|
102
|
+
v !== null &&
|
|
103
|
+
typeof v.state === "string");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse a decoded getStatus answer (a JSON progress tree). Throws a clear
|
|
107
|
+
* error on malformed JSON or an unexpected shape — the poll loop surfaces it
|
|
108
|
+
* as a failed result rather than a raw stack.
|
|
109
|
+
*/
|
|
110
|
+
function parseProgressTree(decodedAnswer) {
|
|
111
|
+
var parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(decodedAnswer);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
throw new Error("publish-app: getStatus answer is not valid JSON: " +
|
|
117
|
+
decodedAnswer.slice(0, 200));
|
|
118
|
+
}
|
|
119
|
+
if (!isProgressNode(parsed)) {
|
|
120
|
+
throw new Error("publish-app: getStatus answer is not a progress tree (no state field): " +
|
|
121
|
+
decodedAnswer.slice(0, 200));
|
|
122
|
+
}
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Terminal-state classification for the AJAX progress tracker.
|
|
127
|
+
* "2" = Succeeded (HAR ground truth). "3"/"4" = Failed/Cancelled per the
|
|
128
|
+
* GlideExecutionTracker state model. Anything else keeps polling until the
|
|
129
|
+
* timeout budget — conservative on purpose: an unknown state must not be
|
|
130
|
+
* reported as success OR failure.
|
|
131
|
+
*/
|
|
132
|
+
function classifyProgress(root) {
|
|
133
|
+
if (root.state === "2")
|
|
134
|
+
return { terminal: true, success: true };
|
|
135
|
+
if (root.state === "3" || root.state === "4") {
|
|
136
|
+
return { terminal: true, success: false };
|
|
137
|
+
}
|
|
138
|
+
return { terminal: false, success: false };
|
|
139
|
+
}
|
|
140
|
+
/** Flatten the progress tree's children into an ordered step list. */
|
|
141
|
+
function flattenSteps(root) {
|
|
142
|
+
var out = [];
|
|
143
|
+
function walk(node) {
|
|
144
|
+
if (node !== root && node.name) {
|
|
145
|
+
out.push({
|
|
146
|
+
name: node.name,
|
|
147
|
+
state: node.state,
|
|
148
|
+
percentComplete: node.percent_complete || "",
|
|
149
|
+
message: node.message || "",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
var children = node.children;
|
|
153
|
+
if (Array.isArray(children)) {
|
|
154
|
+
for (var i = 0; i < children.length; i += 1) {
|
|
155
|
+
if (isProgressNode(children[i]))
|
|
156
|
+
walk(children[i]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
walk(root);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Harvest the interesting result values from the progress tree, walking
|
|
165
|
+
* root-down: the FIRST non-empty appLink/appName/update-set sys_id encountered
|
|
166
|
+
* wins and is never overwritten. In the observed HAR trees each value appears
|
|
167
|
+
* on exactly one node, so first-wins is equivalent — the rule just needs to be
|
|
168
|
+
* deterministic.
|
|
169
|
+
*/
|
|
170
|
+
function harvestProgressResults(root) {
|
|
171
|
+
var appLink = "";
|
|
172
|
+
var appName = "";
|
|
173
|
+
var updateSetSysId = "";
|
|
174
|
+
function walk(node) {
|
|
175
|
+
var result = node.result;
|
|
176
|
+
if (result && typeof result === "object") {
|
|
177
|
+
if (typeof result.appLink === "string" && result.appLink && !appLink) {
|
|
178
|
+
appLink = result.appLink;
|
|
179
|
+
}
|
|
180
|
+
if (typeof result.appName === "string" && result.appName && !appName) {
|
|
181
|
+
appName = result.appName;
|
|
182
|
+
}
|
|
183
|
+
var us = result["sys_update_set.sys_id"];
|
|
184
|
+
if (typeof us === "string" && us && !updateSetSysId) {
|
|
185
|
+
updateSetSysId = us;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
var children = node.children;
|
|
189
|
+
if (Array.isArray(children)) {
|
|
190
|
+
for (var i = 0; i < children.length; i += 1) {
|
|
191
|
+
if (isProgressNode(children[i]))
|
|
192
|
+
walk(children[i]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
walk(root);
|
|
197
|
+
return { appLink: appLink, appName: appName, updateSetSysId: updateSetSysId };
|
|
198
|
+
}
|
|
199
|
+
/** CI/CD publish response: harvest the progress id (and echo status_label). */
|
|
200
|
+
function parseCicdPublishResponse(body) {
|
|
201
|
+
if (typeof body !== "object" || body === null) {
|
|
202
|
+
return { progressId: "", statusLabel: "", error: "empty CI/CD response" };
|
|
203
|
+
}
|
|
204
|
+
var result = body.result;
|
|
205
|
+
if (typeof result !== "object" || result === null) {
|
|
206
|
+
return {
|
|
207
|
+
progressId: "",
|
|
208
|
+
statusLabel: "",
|
|
209
|
+
error: "CI/CD response has no result object",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
var r = result;
|
|
213
|
+
var id = "";
|
|
214
|
+
if (r.links &&
|
|
215
|
+
typeof r.links === "object" &&
|
|
216
|
+
r.links.progress &&
|
|
217
|
+
typeof r.links.progress === "object" &&
|
|
218
|
+
typeof r.links.progress.id === "string") {
|
|
219
|
+
id = r.links.progress.id;
|
|
220
|
+
}
|
|
221
|
+
var label = typeof r.status_label === "string" ? r.status_label : "";
|
|
222
|
+
if (!id) {
|
|
223
|
+
return {
|
|
224
|
+
progressId: "",
|
|
225
|
+
statusLabel: label,
|
|
226
|
+
error: "CI/CD response carries no progress id",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return { progressId: id, statusLabel: label, error: "" };
|
|
230
|
+
}
|
|
231
|
+
/** CI/CD progress response → a typed snapshot. Unknown shapes become status "". */
|
|
232
|
+
function parseCicdProgress(body) {
|
|
233
|
+
var empty = {
|
|
234
|
+
status: "",
|
|
235
|
+
statusLabel: "",
|
|
236
|
+
statusMessage: "",
|
|
237
|
+
statusDetail: "",
|
|
238
|
+
percentComplete: "",
|
|
239
|
+
};
|
|
240
|
+
if (typeof body !== "object" || body === null)
|
|
241
|
+
return empty;
|
|
242
|
+
var result = body.result;
|
|
243
|
+
if (typeof result !== "object" || result === null)
|
|
244
|
+
return empty;
|
|
245
|
+
var r = result;
|
|
246
|
+
function str(v) {
|
|
247
|
+
if (typeof v === "string")
|
|
248
|
+
return v;
|
|
249
|
+
if (typeof v === "number")
|
|
250
|
+
return String(v);
|
|
251
|
+
return "";
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
status: str(r.status),
|
|
255
|
+
statusLabel: str(r.status_label),
|
|
256
|
+
statusMessage: str(r.status_message),
|
|
257
|
+
statusDetail: str(r.status_detail),
|
|
258
|
+
percentComplete: str(r.percent_complete),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function realSleep(ms) {
|
|
262
|
+
return new Promise(function (resolve) {
|
|
263
|
+
setTimeout(resolve, ms);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function resolveApp(client, selector) {
|
|
267
|
+
var fields = ["sys_id", "scope", "name", "version"];
|
|
268
|
+
var queries = [];
|
|
269
|
+
if (SYS_ID_RE.test(selector)) {
|
|
270
|
+
queries.push("sys_id=" + selector);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
queries.push("scope=" + selector);
|
|
274
|
+
queries.push("name=" + selector);
|
|
275
|
+
}
|
|
276
|
+
for (var i = 0; i < queries.length; i += 1) {
|
|
277
|
+
var rows = await client.table.query("sys_app", queries[i], { limit: 2, fields: fields });
|
|
278
|
+
if (rows.length === 1) {
|
|
279
|
+
return {
|
|
280
|
+
sysId: rows[0].sys_id || "",
|
|
281
|
+
scope: rows[0].scope || "",
|
|
282
|
+
name: rows[0].name || "",
|
|
283
|
+
version: rows[0].version || "",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (rows.length > 1) {
|
|
287
|
+
throw new Error("publish-app: app selector '" +
|
|
288
|
+
selector +
|
|
289
|
+
"' matches more than one sys_app row — use the scope or sys_id.");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
throw new Error("publish-app: no sys_app row matches '" +
|
|
293
|
+
selector +
|
|
294
|
+
"' (tried " +
|
|
295
|
+
queries.join(", then ") +
|
|
296
|
+
").");
|
|
297
|
+
}
|
|
298
|
+
function baseResult(target, app, version) {
|
|
299
|
+
return {
|
|
300
|
+
status: "failed",
|
|
301
|
+
target: target,
|
|
302
|
+
appSysId: app.sysId,
|
|
303
|
+
appScope: app.scope,
|
|
304
|
+
appName: app.name,
|
|
305
|
+
versionBefore: app.version,
|
|
306
|
+
version: version,
|
|
307
|
+
executionSysId: "",
|
|
308
|
+
updateSetSysId: "",
|
|
309
|
+
appLink: "",
|
|
310
|
+
steps: [],
|
|
311
|
+
polls: 0,
|
|
312
|
+
message: "",
|
|
313
|
+
note: "",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function resolveStoreCreds(params) {
|
|
317
|
+
var username = params.storeUsername || process.env.SN_STORE_USERNAME || "";
|
|
318
|
+
var password = params.storePassword || process.env.SN_STORE_PASSWORD || "";
|
|
319
|
+
if (!username || !password) {
|
|
320
|
+
throw new Error("publish-app: Store credentials missing — set SN_STORE_USERNAME and " +
|
|
321
|
+
"SN_STORE_PASSWORD in the env file the verb loads (--env). The " +
|
|
322
|
+
"password is never accepted as a flag.");
|
|
323
|
+
}
|
|
324
|
+
return { username: username, password: password };
|
|
325
|
+
}
|
|
326
|
+
async function publishToStore(params, app) {
|
|
327
|
+
var result = baseResult("store", app, params.version);
|
|
328
|
+
var creds = resolveStoreCreds(params);
|
|
329
|
+
var transport = params.transport || {};
|
|
330
|
+
var openSession = transport.openSession || table_1.openFormSession;
|
|
331
|
+
var post = transport.post || table_1.postForm;
|
|
332
|
+
var sleepFn = transport.sleep || realSleep;
|
|
333
|
+
var timeoutMs = params.timeoutMs != null ? params.timeoutMs : exports.DEFAULT_PUBLISH_TIMEOUT_MS;
|
|
334
|
+
var auth = (0, table_1.resolveFormAuth)({
|
|
335
|
+
instance: params.instance,
|
|
336
|
+
user: params.user,
|
|
337
|
+
password: params.password,
|
|
338
|
+
});
|
|
339
|
+
var session = await openSession(auth);
|
|
340
|
+
var startFields = buildStartFields({
|
|
341
|
+
appSysId: app.sysId,
|
|
342
|
+
version: params.version,
|
|
343
|
+
devNotes: params.devNotes || "",
|
|
344
|
+
target: "store",
|
|
345
|
+
storeUsername: creds.username,
|
|
346
|
+
storePassword: creds.password,
|
|
347
|
+
});
|
|
348
|
+
var startRes = await post(auth, session, "/xmlhttp.do", startFields);
|
|
349
|
+
if (startRes.status < 200 || startRes.status >= 300) {
|
|
350
|
+
result.message = "start call returned HTTP " + startRes.status;
|
|
351
|
+
result.note =
|
|
352
|
+
"publish-app: the ScopedAppUploaderAJAX start call failed (HTTP " +
|
|
353
|
+
startRes.status +
|
|
354
|
+
"): " +
|
|
355
|
+
(startRes.body || "").slice(0, 200);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
var startAnswer = parseXmlAnswer(startRes.body);
|
|
359
|
+
if (startAnswer.error || !SYS_ID_RE.test(startAnswer.answer)) {
|
|
360
|
+
result.message = startAnswer.error || "answer is not a sys_id";
|
|
361
|
+
result.note =
|
|
362
|
+
"publish-app: the start call did not return an execution sys_id — " +
|
|
363
|
+
(startAnswer.error ||
|
|
364
|
+
"got '" + startAnswer.answer.slice(0, 64) + "'") +
|
|
365
|
+
". The upload never started.";
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
result.executionSysId = startAnswer.answer;
|
|
369
|
+
var startedAt = Date.now();
|
|
370
|
+
var pollIndex = 0;
|
|
371
|
+
while (true) {
|
|
372
|
+
var delay = exports.PUBLISH_POLL_DELAYS_MS[Math.min(pollIndex, exports.PUBLISH_POLL_DELAYS_MS.length - 1)];
|
|
373
|
+
if (Date.now() - startedAt + delay > timeoutMs) {
|
|
374
|
+
result.status = "timeout";
|
|
375
|
+
result.note =
|
|
376
|
+
"publish-app: progress tracker " +
|
|
377
|
+
result.executionSysId +
|
|
378
|
+
" did not terminate within " +
|
|
379
|
+
timeoutMs +
|
|
380
|
+
"ms after " +
|
|
381
|
+
result.polls +
|
|
382
|
+
" poll(s). Check /sys_execution_tracker.do?sys_id=" +
|
|
383
|
+
result.executionSysId +
|
|
384
|
+
" on the instance.";
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
await sleepFn(delay);
|
|
388
|
+
pollIndex += 1;
|
|
389
|
+
result.polls += 1;
|
|
390
|
+
var pollRes = await post(auth, session, "/xmlhttp.do", {
|
|
391
|
+
sysparm_processor: "AJAXProgressStatusChecker",
|
|
392
|
+
sysparm_name: "getStatus",
|
|
393
|
+
sysparm_scope: "global",
|
|
394
|
+
sysparm_want_session_messages: "true",
|
|
395
|
+
sysparm_execution_id: result.executionSysId,
|
|
396
|
+
"ni.nolog.x_referer": "ignore",
|
|
397
|
+
x_referer: "sys_app.do?sys_id=" + app.sysId,
|
|
398
|
+
});
|
|
399
|
+
if (pollRes.status < 200 || pollRes.status >= 300) {
|
|
400
|
+
result.message = "getStatus returned HTTP " + pollRes.status;
|
|
401
|
+
result.note =
|
|
402
|
+
"publish-app: progress poll failed (HTTP " +
|
|
403
|
+
pollRes.status +
|
|
404
|
+
") — tracker " +
|
|
405
|
+
result.executionSysId +
|
|
406
|
+
" may still be running on the instance.";
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
var pollAnswer = parseXmlAnswer(pollRes.body);
|
|
410
|
+
if (pollAnswer.error) {
|
|
411
|
+
result.message = pollAnswer.error;
|
|
412
|
+
result.note =
|
|
413
|
+
"publish-app: could not read the progress answer — " + pollAnswer.error;
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
var tree;
|
|
417
|
+
try {
|
|
418
|
+
tree = parseProgressTree(pollAnswer.answer);
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
result.message = e instanceof Error ? e.message : String(e);
|
|
422
|
+
result.note = result.message;
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
var state = classifyProgress(tree);
|
|
426
|
+
if (!state.terminal)
|
|
427
|
+
continue;
|
|
428
|
+
result.steps = flattenSteps(tree);
|
|
429
|
+
result.message = tree.message || "";
|
|
430
|
+
var harvested = harvestProgressResults(tree);
|
|
431
|
+
result.appLink = harvested.appLink;
|
|
432
|
+
result.updateSetSysId = harvested.updateSetSysId;
|
|
433
|
+
if (state.success) {
|
|
434
|
+
result.status = "published";
|
|
435
|
+
result.note =
|
|
436
|
+
"Published " +
|
|
437
|
+
app.name +
|
|
438
|
+
" v" +
|
|
439
|
+
params.version +
|
|
440
|
+
" to the ServiceNow Store" +
|
|
441
|
+
(result.appLink ? " — " + result.appLink : "") +
|
|
442
|
+
" (" +
|
|
443
|
+
result.polls +
|
|
444
|
+
" poll(s)).";
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
result.status = "failed";
|
|
448
|
+
result.note =
|
|
449
|
+
"publish-app: store publish terminated in state " +
|
|
450
|
+
tree.state +
|
|
451
|
+
(result.message ? " — " + result.message : "") +
|
|
452
|
+
". Tracker " +
|
|
453
|
+
result.executionSysId +
|
|
454
|
+
".";
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function publishToRepo(params, app, client) {
|
|
460
|
+
var result = baseResult("repo", app, params.version);
|
|
461
|
+
var transport = params.transport || {};
|
|
462
|
+
var sleepFn = transport.sleep || realSleep;
|
|
463
|
+
var timeoutMs = params.timeoutMs != null ? params.timeoutMs : exports.DEFAULT_PUBLISH_TIMEOUT_MS;
|
|
464
|
+
var path = "/api/sn_cicd/app_repo/publish?sys_id=" +
|
|
465
|
+
encodeURIComponent(app.sysId) +
|
|
466
|
+
"&version=" +
|
|
467
|
+
encodeURIComponent(params.version);
|
|
468
|
+
if (params.devNotes) {
|
|
469
|
+
path += "&dev_notes=" + encodeURIComponent(params.devNotes);
|
|
470
|
+
}
|
|
471
|
+
var res = await client.now.invoke({ method: "POST", path: path });
|
|
472
|
+
if (res.status === 401 || res.status === 403) {
|
|
473
|
+
result.message = "HTTP " + res.status + " from the CI/CD API";
|
|
474
|
+
result.note =
|
|
475
|
+
"publish-app: the CI/CD publish was refused (HTTP " +
|
|
476
|
+
res.status +
|
|
477
|
+
") — the API user needs the sn_cicd role (or admin) and the CI/CD " +
|
|
478
|
+
"plugin must be active.";
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
if (res.status < 200 || res.status >= 300) {
|
|
482
|
+
result.message = "HTTP " + res.status + " from the CI/CD API";
|
|
483
|
+
result.note =
|
|
484
|
+
"publish-app: POST /api/sn_cicd/app_repo/publish failed (HTTP " +
|
|
485
|
+
res.status +
|
|
486
|
+
"): " +
|
|
487
|
+
JSON.stringify(res.body).slice(0, 300) +
|
|
488
|
+
" — a version at or below the app's current repo version is the usual cause.";
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
var publishRes = parseCicdPublishResponse(res.body);
|
|
492
|
+
if (publishRes.error) {
|
|
493
|
+
result.message = publishRes.error;
|
|
494
|
+
result.note =
|
|
495
|
+
"publish-app: the CI/CD publish response is missing its progress link — " +
|
|
496
|
+
publishRes.error;
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
result.executionSysId = publishRes.progressId;
|
|
500
|
+
var startedAt = Date.now();
|
|
501
|
+
var pollIndex = 0;
|
|
502
|
+
while (true) {
|
|
503
|
+
var delay = exports.PUBLISH_POLL_DELAYS_MS[Math.min(pollIndex, exports.PUBLISH_POLL_DELAYS_MS.length - 1)];
|
|
504
|
+
if (Date.now() - startedAt + delay > timeoutMs) {
|
|
505
|
+
result.status = "timeout";
|
|
506
|
+
result.note =
|
|
507
|
+
"publish-app: CI/CD progress " +
|
|
508
|
+
result.executionSysId +
|
|
509
|
+
" did not terminate within " +
|
|
510
|
+
timeoutMs +
|
|
511
|
+
"ms after " +
|
|
512
|
+
result.polls +
|
|
513
|
+
" poll(s). Check GET /api/sn_cicd/progress/" +
|
|
514
|
+
result.executionSysId +
|
|
515
|
+
".";
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
await sleepFn(delay);
|
|
519
|
+
pollIndex += 1;
|
|
520
|
+
result.polls += 1;
|
|
521
|
+
var progRes = await client.now.invoke({
|
|
522
|
+
method: "GET",
|
|
523
|
+
path: "/api/sn_cicd/progress/" + encodeURIComponent(result.executionSysId),
|
|
524
|
+
});
|
|
525
|
+
if (progRes.status < 200 || progRes.status >= 300) {
|
|
526
|
+
result.message = "HTTP " + progRes.status + " from the CI/CD progress API";
|
|
527
|
+
result.note =
|
|
528
|
+
"publish-app: CI/CD progress poll failed (HTTP " +
|
|
529
|
+
progRes.status +
|
|
530
|
+
") — progress id " +
|
|
531
|
+
result.executionSysId +
|
|
532
|
+
".";
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
var progress = parseCicdProgress(progRes.body);
|
|
536
|
+
result.message = progress.statusMessage || progress.statusLabel;
|
|
537
|
+
if (progress.status === "2") {
|
|
538
|
+
result.status = "published";
|
|
539
|
+
result.steps = [
|
|
540
|
+
{
|
|
541
|
+
name: "CI/CD app_repo publish",
|
|
542
|
+
state: progress.status,
|
|
543
|
+
percentComplete: progress.percentComplete,
|
|
544
|
+
message: result.message,
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
result.note =
|
|
548
|
+
"Published " +
|
|
549
|
+
app.name +
|
|
550
|
+
" v" +
|
|
551
|
+
params.version +
|
|
552
|
+
" to the application repository (" +
|
|
553
|
+
result.polls +
|
|
554
|
+
" poll(s)).";
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
if (progress.status === "3" || progress.status === "4") {
|
|
558
|
+
result.status = "failed";
|
|
559
|
+
result.steps = [
|
|
560
|
+
{
|
|
561
|
+
name: "CI/CD app_repo publish",
|
|
562
|
+
state: progress.status,
|
|
563
|
+
percentComplete: progress.percentComplete,
|
|
564
|
+
message: progress.statusDetail || result.message,
|
|
565
|
+
},
|
|
566
|
+
];
|
|
567
|
+
result.note =
|
|
568
|
+
"publish-app: CI/CD publish terminated " +
|
|
569
|
+
(progress.statusLabel || "in state " + progress.status) +
|
|
570
|
+
(progress.statusDetail ? " — " + progress.statusDetail : "") +
|
|
571
|
+
".";
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
// "0" pending / "1" running / unknown — keep polling until the budget ends.
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function maskedPreview(target, app, params) {
|
|
578
|
+
if (target === "store") {
|
|
579
|
+
var username = params.storeUsername || process.env.SN_STORE_USERNAME || "";
|
|
580
|
+
var hasPassword = Boolean(params.storePassword || process.env.SN_STORE_PASSWORD);
|
|
581
|
+
var fields = buildStartFields({
|
|
582
|
+
appSysId: app.sysId,
|
|
583
|
+
version: params.version,
|
|
584
|
+
devNotes: params.devNotes || "",
|
|
585
|
+
target: "store",
|
|
586
|
+
storeUsername: username || MASK,
|
|
587
|
+
storePassword: hasPassword ? MASK : "(MISSING — set SN_STORE_PASSWORD)",
|
|
588
|
+
});
|
|
589
|
+
// Belt and braces: the preview never carries a real password.
|
|
590
|
+
fields.sysparm_password = hasPassword
|
|
591
|
+
? MASK
|
|
592
|
+
: "(MISSING — set SN_STORE_PASSWORD)";
|
|
593
|
+
return fields;
|
|
594
|
+
}
|
|
595
|
+
var preview = {
|
|
596
|
+
method: "POST",
|
|
597
|
+
path: "/api/sn_cicd/app_repo/publish?sys_id=" +
|
|
598
|
+
app.sysId +
|
|
599
|
+
"&version=" +
|
|
600
|
+
params.version,
|
|
601
|
+
};
|
|
602
|
+
if (params.devNotes)
|
|
603
|
+
preview.dev_notes = params.devNotes;
|
|
604
|
+
return preview;
|
|
605
|
+
}
|
|
606
|
+
async function publishApp(params) {
|
|
607
|
+
if (!params || typeof params !== "object") {
|
|
608
|
+
throw new Error("publish-app: params object is required.");
|
|
609
|
+
}
|
|
610
|
+
if (!params.app) {
|
|
611
|
+
throw new Error("publish-app: app (scope, sys_id, or name) is required.");
|
|
612
|
+
}
|
|
613
|
+
if (!params.version) {
|
|
614
|
+
throw new Error("publish-app: version is required.");
|
|
615
|
+
}
|
|
616
|
+
if (params.target !== "store" && params.target !== "repo") {
|
|
617
|
+
throw new Error("publish-app: target must be 'store' or 'repo' (got '" +
|
|
618
|
+
String(params.target) +
|
|
619
|
+
"').");
|
|
620
|
+
}
|
|
621
|
+
// A NaN/non-positive timeout would make the poll-loop budget check always
|
|
622
|
+
// false — an infinite loop. Refuse it here so every caller is covered.
|
|
623
|
+
if (params.timeoutMs != null &&
|
|
624
|
+
(typeof params.timeoutMs !== "number" ||
|
|
625
|
+
!isFinite(params.timeoutMs) ||
|
|
626
|
+
params.timeoutMs <= 0)) {
|
|
627
|
+
throw new Error("publish-app: timeoutMs must be a positive number (got '" +
|
|
628
|
+
String(params.timeoutMs) +
|
|
629
|
+
"').");
|
|
630
|
+
}
|
|
631
|
+
var live = params.confirm === true && params.dryRun !== true;
|
|
632
|
+
// Fail fast on missing Store creds BEFORE any network work — a live run that
|
|
633
|
+
// resolves the app and only then discovers the missing password wastes the
|
|
634
|
+
// caller's confirm. Dry-runs still render (with the gap flagged in the preview).
|
|
635
|
+
if (live && params.target === "store") {
|
|
636
|
+
resolveStoreCreds(params);
|
|
637
|
+
}
|
|
638
|
+
var client = params.client || (0, client_1.createClient)({});
|
|
639
|
+
var app = await resolveApp(client, params.app);
|
|
640
|
+
if (!live) {
|
|
641
|
+
var dry = baseResult(params.target, app, params.version);
|
|
642
|
+
dry.status = "dry-run";
|
|
643
|
+
dry.requestPreview = maskedPreview(params.target, app, params);
|
|
644
|
+
dry.note =
|
|
645
|
+
"dry-run: nothing was published. Would publish " +
|
|
646
|
+
app.name +
|
|
647
|
+
" (" +
|
|
648
|
+
app.scope +
|
|
649
|
+
", current version " +
|
|
650
|
+
app.version +
|
|
651
|
+
") as v" +
|
|
652
|
+
params.version +
|
|
653
|
+
" to the " +
|
|
654
|
+
(params.target === "store"
|
|
655
|
+
? "ServiceNow Store (EXTERNALLY VISIBLE)"
|
|
656
|
+
: "application repository") +
|
|
657
|
+
". Re-run with confirm to publish.";
|
|
658
|
+
return dry;
|
|
659
|
+
}
|
|
660
|
+
if (params.target === "store") {
|
|
661
|
+
return publishToStore(params, app);
|
|
662
|
+
}
|
|
663
|
+
return publishToRepo(params, app, client);
|
|
664
|
+
}
|
|
@@ -78,6 +78,14 @@ export declare function getNewRecordForm(auth: FormAuth, session: FormSession):
|
|
|
78
78
|
* handles double/single-quoted attributes in either order. Exported for testing.
|
|
79
79
|
*/
|
|
80
80
|
export declare function parseFormInputs(html: string): Record<string, string>;
|
|
81
|
+
/**
|
|
82
|
+
* Decode the HTML entities ServiceNow uses in attribute values. Unescape &
|
|
83
|
+
* LAST. Doing it first double-unescapes inputs like "&lt;" ("&lt;" ->
|
|
84
|
+
* "<" -> "<"), the js/double-escaping vulnerability. With the ampersand
|
|
85
|
+
* handled last, each entity is unescaped exactly once. Exported for the
|
|
86
|
+
* xmlhttp.do consumers (publishApp's progress-tree answers arrive entity-encoded).
|
|
87
|
+
*/
|
|
88
|
+
export declare function decodeHtmlEntities(s: string): string;
|
|
81
89
|
export interface PostResult {
|
|
82
90
|
status: number;
|
|
83
91
|
location: string;
|
|
@@ -20,6 +20,7 @@ exports.setCurrentApplication = setCurrentApplication;
|
|
|
20
20
|
exports.getRecordForm = getRecordForm;
|
|
21
21
|
exports.getNewRecordForm = getNewRecordForm;
|
|
22
22
|
exports.parseFormInputs = parseFormInputs;
|
|
23
|
+
exports.decodeHtmlEntities = decodeHtmlEntities;
|
|
23
24
|
exports.postForm = postForm;
|
|
24
25
|
/** Env precedence mirrors client.ts: explicit > SN_* > SN_DEV_* > SN_PROD_*. */
|
|
25
26
|
function resolveFormAuth(cfg) {
|
|
@@ -246,10 +247,14 @@ function attr(tag, name) {
|
|
|
246
247
|
return decodeHtml(sq[1]);
|
|
247
248
|
return null;
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Decode the HTML entities ServiceNow uses in attribute values. Unescape &
|
|
252
|
+
* LAST. Doing it first double-unescapes inputs like "&lt;" ("&lt;" ->
|
|
253
|
+
* "<" -> "<"), the js/double-escaping vulnerability. With the ampersand
|
|
254
|
+
* handled last, each entity is unescaped exactly once. Exported for the
|
|
255
|
+
* xmlhttp.do consumers (publishApp's progress-tree answers arrive entity-encoded).
|
|
256
|
+
*/
|
|
257
|
+
function decodeHtmlEntities(s) {
|
|
253
258
|
return s
|
|
254
259
|
.replace(/</g, "<")
|
|
255
260
|
.replace(/>/g, ">")
|
|
@@ -257,6 +262,9 @@ function decodeHtml(s) {
|
|
|
257
262
|
.replace(/'/g, "'")
|
|
258
263
|
.replace(/&/g, "&");
|
|
259
264
|
}
|
|
265
|
+
function decodeHtml(s) {
|
|
266
|
+
return decodeHtmlEntities(s);
|
|
267
|
+
}
|
|
260
268
|
/** POST a form-urlencoded field map to a `.do` path. Follows nothing — returns the 302. */
|
|
261
269
|
async function postForm(auth, session, path, fields) {
|
|
262
270
|
var B = base(auth);
|
package/dist/table/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { buildColumnXml, xmlEscape } from "./buildColumnXml";
|
|
|
9
9
|
export type { NormalizedColumn } from "./buildColumnXml";
|
|
10
10
|
export { normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, showInMenuKey, listEditKey, TYPE_MAP, } from "./buildTableSave";
|
|
11
11
|
export type { ColumnSpec, AccessFlags, OverlaySpec } from "./buildTableSave";
|
|
12
|
-
export { resolveFormAuth, openFormSession, setCurrentApplication, getRecordForm, getNewRecordForm, parseFormInputs, postForm, scrapeCk, } from "./formSession";
|
|
12
|
+
export { resolveFormAuth, openFormSession, setCurrentApplication, getRecordForm, getNewRecordForm, parseFormInputs, postForm, scrapeCk, decodeHtmlEntities, } from "./formSession";
|
|
13
13
|
export type { FormAuth, FormSession, HarvestedForm, PostResult, } from "./formSession";
|
|
14
14
|
export { addColumn, deriveElement } from "./addColumn";
|
|
15
15
|
export type { AddColumnParams, AddColumnResult } from "./addColumn";
|
package/dist/table/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* live-validation caveat.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
|
|
8
|
+
exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
|
|
9
9
|
var createTable_1 = require("./createTable");
|
|
10
10
|
Object.defineProperty(exports, "createTable", { enumerable: true, get: function () { return createTable_1.createTable; } });
|
|
11
11
|
Object.defineProperty(exports, "projectTableGraph", { enumerable: true, get: function () { return createTable_1.projectTableGraph; } });
|
|
@@ -33,6 +33,7 @@ Object.defineProperty(exports, "getNewRecordForm", { enumerable: true, get: func
|
|
|
33
33
|
Object.defineProperty(exports, "parseFormInputs", { enumerable: true, get: function () { return formSession_1.parseFormInputs; } });
|
|
34
34
|
Object.defineProperty(exports, "postForm", { enumerable: true, get: function () { return formSession_1.postForm; } });
|
|
35
35
|
Object.defineProperty(exports, "scrapeCk", { enumerable: true, get: function () { return formSession_1.scrapeCk; } });
|
|
36
|
+
Object.defineProperty(exports, "decodeHtmlEntities", { enumerable: true, get: function () { return formSession_1.decodeHtmlEntities; } });
|
|
36
37
|
var addColumn_1 = require("./addColumn");
|
|
37
38
|
Object.defineProperty(exports, "addColumn", { enumerable: true, get: function () { return addColumn_1.addColumn; } });
|
|
38
39
|
Object.defineProperty(exports, "deriveElement", { enumerable: true, get: function () { return addColumn_1.deriveElement; } });
|