@tenonhq/dovetail-servicenow 0.0.39 → 0.0.40
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 +35 -9
- package/dist/cli.js +41 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -2
- package/dist/mcp/registry.js +19 -10
- package/dist/mcp/schemas.d.ts +12 -3
- package/dist/mcp/schemas.js +9 -1
- package/dist/publishApp.d.ts +128 -16
- package/dist/publishApp.js +463 -100
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -516,11 +516,11 @@ Programmatic: `invokeRest({ method, path, body, confirm })` is exported, and the
|
|
|
516
516
|
client gained `now.put` / `now.delete` / `now.invoke` (the latter returns
|
|
517
517
|
`{ status, body }` verbatim) alongside the existing `now.get` / `now.post`.
|
|
518
518
|
|
|
519
|
-
### Publish an app to the Store / application repository
|
|
519
|
+
### Publish an app to the Store / application repository / an update set
|
|
520
520
|
|
|
521
521
|
Publish a scoped application to the **ServiceNow Store**, the **company
|
|
522
|
-
application repository**, or
|
|
523
|
-
tracker polled to completion.
|
|
522
|
+
application repository**, and/or **into a new update set** — headlessly, with
|
|
523
|
+
each publish's progress tracker polled to completion.
|
|
524
524
|
|
|
525
525
|
```bash
|
|
526
526
|
# Dry-run — the DEFAULT: resolves the app, prints the plan, publishes NOTHING
|
|
@@ -529,15 +529,21 @@ npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 --target bot
|
|
|
529
529
|
# Publish for real (store, then repo, same version)
|
|
530
530
|
npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260716 \
|
|
531
531
|
--target both --dev-notes "July release" --confirm --json
|
|
532
|
+
|
|
533
|
+
# Release flow on an instance WITHOUT the sn_cicd plugin: publish to the company
|
|
534
|
+
# repository over the UI uploader, then capture the app into a dated update set.
|
|
535
|
+
npx dove-sn publish-app --app x_cadso_filter --version 6.0.20260729 \
|
|
536
|
+
--target repo-ui,update-set --update-set-description 20260729 --confirm --json
|
|
532
537
|
```
|
|
533
538
|
|
|
534
539
|
**Store publish is EXTERNALLY VISIBLE on the ServiceNow Store — treat
|
|
535
540
|
`--target store --confirm` as a release.** `publish-app` is dry-run by default:
|
|
536
541
|
without `--confirm` it prints the resolved plan and exits `1` (a deliberate
|
|
537
|
-
refusal). `--target
|
|
538
|
-
|
|
542
|
+
refusal). `--target` takes one target, a comma-separated list, or `both` (an
|
|
543
|
+
alias for `store,repo`); targets run **in the order given** and short-circuit on
|
|
544
|
+
the first failure.
|
|
539
545
|
|
|
540
|
-
The
|
|
546
|
+
The targets ride different transports:
|
|
541
547
|
|
|
542
548
|
- **store** replays the `sys_app` form's upload flow (`xmlhttp.do` +
|
|
543
549
|
`sn_appauthor.ScopedAppUploaderAJAX`) over a form-login session — basic auth
|
|
@@ -547,13 +553,33 @@ The two targets ride different transports:
|
|
|
547
553
|
- **repo** uses the supported CI/CD REST API (`POST /api/sn_cicd/app_repo/publish`
|
|
548
554
|
+ `GET /api/sn_cicd/progress/{id}`) over basic auth. The API user needs the
|
|
549
555
|
`sn_cicd` role (or admin).
|
|
556
|
+
- **repo-ui** reaches the *same* company repository as `repo`, but over the UI
|
|
557
|
+
uploader (`sysparm_publish_to_store=false`) instead of REST. Use it when the
|
|
558
|
+
instance has no CI/CD plugin — `tenonworkshop`, for instance, has no `sn_cicd`
|
|
559
|
+
scope and no `app_repo` service, so `repo` 404s there while `repo-ui` works.
|
|
560
|
+
It needs no Store credentials.
|
|
561
|
+
- **update-set** publishes the app *into a newly created update set* via the
|
|
562
|
+
two-call `com.snc.apps.AppsAjaxProcessor` flow (`createUpdateSet` →
|
|
563
|
+
`publishToUpdateSet`). There is no REST equivalent. `--update-set-name`
|
|
564
|
+
defaults to the app's name (the dialog's field is readonly, so that is what
|
|
565
|
+
the UI submits); `--update-set-description` is conventionally the release date
|
|
566
|
+
stamp `YYYYMMDD`, which makes a whole release one query
|
|
567
|
+
(`sys_update_set` where `description=20260729`). `--include-data` maps to the
|
|
568
|
+
dialog's "Include demo data" box and defaults **off**, matching the value the
|
|
569
|
+
UI actually puts on the wire.
|
|
570
|
+
|
|
571
|
+
Ordering matters when you combine them: the repo publish is what bumps
|
|
572
|
+
`sys_app.version`, so put it **before** `update-set` if you want the set
|
|
573
|
+
captured at the new version.
|
|
550
574
|
|
|
551
575
|
`--app` accepts a scope name, `sys_app` sys_id, or app name; `--version` must be
|
|
552
576
|
above the currently published version. The result carries the progress-tracker
|
|
553
577
|
id, per-step states ("Packaging application", "Uploading application"), the
|
|
554
|
-
Store `appLink`, and the
|
|
555
|
-
|
|
556
|
-
|
|
578
|
+
Store `appLink`, and the update-set sys_id — for the `update-set` target that is
|
|
579
|
+
recorded as soon as the set is created, so it survives a later failure and you
|
|
580
|
+
can always find (or delete) the set. Exit codes: `0` published or dry-run, `1`
|
|
581
|
+
bad args/unconfirmed, `2` failed/timeout. Programmatic:
|
|
582
|
+
`publishApp({ app, version, target, confirm })`.
|
|
557
583
|
|
|
558
584
|
### Export an update set (or a whole app) to importable XML
|
|
559
585
|
|
package/dist/cli.js
CHANGED
|
@@ -1043,15 +1043,18 @@ function printHelp() {
|
|
|
1043
1043
|
" [--script-input <name>] [--update-set <sys_id>] [--apply] [--json])\n" +
|
|
1044
1044
|
" edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
|
|
1045
1045
|
" (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
|
|
1046
|
-
" publish-app Publish a scoped app to the ServiceNow Store
|
|
1047
|
-
"
|
|
1046
|
+
" publish-app Publish a scoped app to the ServiceNow Store, the company application\n" +
|
|
1047
|
+
" repository, and/or a new update set, then poll each to completion.\n" +
|
|
1048
1048
|
" STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store.\n" +
|
|
1049
1049
|
" DRY-RUN BY DEFAULT — nothing is published without --confirm\n" +
|
|
1050
|
-
" (--app <scope|sys_id|name> --version <v
|
|
1050
|
+
" (--app <scope|sys_id|name> --version <v>\n" +
|
|
1051
|
+
" --target store|repo|repo-ui|update-set|both (comma-separated ok)\n" +
|
|
1051
1052
|
" [--dev-notes <text>] [--store-user <email>] [--timeout-ms <n>]\n" +
|
|
1052
|
-
" [--
|
|
1053
|
+
" [--update-set-name <name>] [--update-set-description <text>]\n" +
|
|
1054
|
+
" [--include-data] [--dry-run] [--json] [--confirm])\n" +
|
|
1053
1055
|
" Store creds: SN_STORE_USERNAME/SN_STORE_PASSWORD in the --env file;\n" +
|
|
1054
|
-
" the password is never a flag.
|
|
1056
|
+
" the password is never a flag. 'repo' needs the sn_cicd plugin+role;\n" +
|
|
1057
|
+
" 'repo-ui' reaches the same repository over the UI uploader instead.\n" +
|
|
1055
1058
|
" export-update-set Export an update set to importable <unload> XML, with secret\n" +
|
|
1056
1059
|
" values replaced by __SET_DURING_INSTALL__ (no opt-out).\n" +
|
|
1057
1060
|
" assemble mode is READ-ONLY; complete mode marks the set\n" +
|
|
@@ -1833,8 +1836,19 @@ async function runInvokeRest(flags) {
|
|
|
1833
1836
|
* dove-sn publish-app:
|
|
1834
1837
|
* --app <scope|sys_id|name> Required. The sys_app to publish.
|
|
1835
1838
|
* --version <v> Required. Version to publish (e.g. 6.0.20260716).
|
|
1836
|
-
* --target store|repo|
|
|
1837
|
-
*
|
|
1839
|
+
* --target <t[,t...]> Required. store | repo | repo-ui | update-set |
|
|
1840
|
+
* both (= store,repo). STORE IS EXTERNALLY VISIBLE.
|
|
1841
|
+
* repo = CI/CD REST API (needs the sn_cicd plugin)
|
|
1842
|
+
* repo-ui= same destination over the UI uploader,
|
|
1843
|
+
* for instances without sn_cicd
|
|
1844
|
+
* update-set = publish the app INTO a new update set
|
|
1845
|
+
* [--dev-notes <text>] Optional developer notes (uploader targets).
|
|
1846
|
+
* [--update-set-name <name>] update-set only. Defaults to the app's name.
|
|
1847
|
+
* [--update-set-description <text>]
|
|
1848
|
+
* update-set only. Tenon convention is the release
|
|
1849
|
+
* date stamp (YYYYMMDD) so a release is one query.
|
|
1850
|
+
* [--include-data] update-set only. Include demo data (default off,
|
|
1851
|
+
* matching the observed wire value).
|
|
1838
1852
|
* [--store-user <email>] Store account email (else SN_STORE_USERNAME).
|
|
1839
1853
|
* The password comes ONLY from SN_STORE_PASSWORD —
|
|
1840
1854
|
* there is no flag for it, ever.
|
|
@@ -1842,9 +1856,9 @@ async function runInvokeRest(flags) {
|
|
|
1842
1856
|
* [--dry-run] [--json] [--confirm]
|
|
1843
1857
|
*
|
|
1844
1858
|
* DRY-RUN unless --confirm: without it the resolved plan is printed and the
|
|
1845
|
-
* command exits 1 (a deliberate refusal, not success).
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1859
|
+
* command exits 1 (a deliberate refusal, not success). Multiple targets run
|
|
1860
|
+
* sequentially with the same version and short-circuit on the first failure;
|
|
1861
|
+
* --json emits an array of per-target results.
|
|
1848
1862
|
* Exit codes: 0 published/dry-run, 1 bad args/unconfirmed, 2 failed/timeout.
|
|
1849
1863
|
*/
|
|
1850
1864
|
async function runPublishApp(flags) {
|
|
@@ -1852,16 +1866,17 @@ async function runPublishApp(flags) {
|
|
|
1852
1866
|
var version = flags.version;
|
|
1853
1867
|
var target = flags.target;
|
|
1854
1868
|
if (!app || !version || !target) {
|
|
1855
|
-
process.stderr.write("publish-app: --app, --version and --target
|
|
1869
|
+
process.stderr.write("publish-app: --app, --version and --target <" +
|
|
1870
|
+
publishApp_1.PUBLISH_TARGETS.join("|") +
|
|
1871
|
+
"|both> are required\n");
|
|
1856
1872
|
return 1;
|
|
1857
1873
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
"')\n");
|
|
1874
|
+
var parsedTargets = (0, publishApp_1.parsePublishTargets)(target);
|
|
1875
|
+
if (parsedTargets.error) {
|
|
1876
|
+
process.stderr.write("publish-app: " + parsedTargets.error + "\n");
|
|
1862
1877
|
return 1;
|
|
1863
1878
|
}
|
|
1864
|
-
var targets =
|
|
1879
|
+
var targets = parsedTargets.targets;
|
|
1865
1880
|
var dryRun = flags["dry-run"] === "true";
|
|
1866
1881
|
var confirmed = flags.confirm === "true";
|
|
1867
1882
|
var client = (0, client_1.createClient)({});
|
|
@@ -1880,6 +1895,16 @@ async function runPublishApp(flags) {
|
|
|
1880
1895
|
params.devNotes = flags["dev-notes"];
|
|
1881
1896
|
if (flags["store-user"])
|
|
1882
1897
|
params.storeUsername = flags["store-user"];
|
|
1898
|
+
if (flags["update-set-name"]) {
|
|
1899
|
+
params.updateSetName = flags["update-set-name"];
|
|
1900
|
+
}
|
|
1901
|
+
// Read with !== undefined, not truthiness: an intentionally empty
|
|
1902
|
+
// description must stay empty rather than silently fall back.
|
|
1903
|
+
if (flags["update-set-description"] !== undefined) {
|
|
1904
|
+
params.updateSetDescription = flags["update-set-description"];
|
|
1905
|
+
}
|
|
1906
|
+
if (flags["include-data"] === "true")
|
|
1907
|
+
params.includeData = true;
|
|
1883
1908
|
if (flags["timeout-ms"]) {
|
|
1884
1909
|
// A NaN timeout would make the poll-loop budget check always false —
|
|
1885
1910
|
// an infinite loop. Validate here, exit 1 on garbage.
|
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setFi
|
|
|
29
29
|
export type { CreateRecordParams, CreateRecordResult } from "./createRecord";
|
|
30
30
|
export { invokeRest, INVOKE_REST_METHODS } from "./invokeRest";
|
|
31
31
|
export type { InvokeRestParams, InvokeRestResult } from "./invokeRest";
|
|
32
|
-
export { publishApp, buildStartFields, parseXmlAnswer, parseProgressTree, classifyProgress, flattenSteps, harvestProgressResults, parseCicdPublishResponse, parseCicdProgress, DEFAULT_PUBLISH_TIMEOUT_MS, PUBLISH_POLL_DELAYS_MS, } from "./publishApp";
|
|
32
|
+
export { publishApp, buildStartFields, buildCreateUpdateSetFields, buildPublishToUpdateSetFields, resolveUpdateSetNaming, describeTarget, parsePublishTargets, PUBLISH_TARGETS, parseXmlAnswer, parseProgressTree, classifyProgress, flattenSteps, harvestProgressResults, parseCicdPublishResponse, parseCicdProgress, DEFAULT_PUBLISH_TIMEOUT_MS, PUBLISH_POLL_DELAYS_MS, } from "./publishApp";
|
|
33
33
|
export type { PublishAppParams, PublishAppResult, PublishTarget, PublishStep, ProgressNode, CicdProgress, PublishTransport, } from "./publishApp";
|
|
34
34
|
export { exportUpdateSet, renderUpdateXmlRow, renderRemoteUpdateSet, renderUnload, countUnloadRecords, countUpdateXml, fetchUpdateXmlRows, refreshTypeFields, parseStatsCount, formatUnloadDate, xmlEscape, UPDATE_XML_FIELDS, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, DEFAULT_MAX_ROWS, } from "./exportUpdateSet";
|
|
35
35
|
export type { ExportUpdateSetParams, ExportUpdateSetResult, ExportMode, ExportTransport, } from "./exportUpdateSet";
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
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.formatRemoveChoicesResult = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.ChoiceWriteError = exports.removeChoicesFromField = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.assertWriteAllowed = exports.evaluateWriteGate = exports.isCiEnvironment = exports.resolveExecutionContext = exports.createClient = void 0;
|
|
10
|
-
exports.
|
|
11
|
-
exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = void 0;
|
|
10
|
+
exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = 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.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = void 0;
|
|
11
|
+
exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = void 0;
|
|
12
12
|
var client_1 = require("./client");
|
|
13
13
|
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
|
|
14
14
|
var executionContext_1 = require("./executionContext");
|
|
@@ -99,6 +99,12 @@ Object.defineProperty(exports, "INVOKE_REST_METHODS", { enumerable: true, get: f
|
|
|
99
99
|
var publishApp_1 = require("./publishApp");
|
|
100
100
|
Object.defineProperty(exports, "publishApp", { enumerable: true, get: function () { return publishApp_1.publishApp; } });
|
|
101
101
|
Object.defineProperty(exports, "buildStartFields", { enumerable: true, get: function () { return publishApp_1.buildStartFields; } });
|
|
102
|
+
Object.defineProperty(exports, "buildCreateUpdateSetFields", { enumerable: true, get: function () { return publishApp_1.buildCreateUpdateSetFields; } });
|
|
103
|
+
Object.defineProperty(exports, "buildPublishToUpdateSetFields", { enumerable: true, get: function () { return publishApp_1.buildPublishToUpdateSetFields; } });
|
|
104
|
+
Object.defineProperty(exports, "resolveUpdateSetNaming", { enumerable: true, get: function () { return publishApp_1.resolveUpdateSetNaming; } });
|
|
105
|
+
Object.defineProperty(exports, "describeTarget", { enumerable: true, get: function () { return publishApp_1.describeTarget; } });
|
|
106
|
+
Object.defineProperty(exports, "parsePublishTargets", { enumerable: true, get: function () { return publishApp_1.parsePublishTargets; } });
|
|
107
|
+
Object.defineProperty(exports, "PUBLISH_TARGETS", { enumerable: true, get: function () { return publishApp_1.PUBLISH_TARGETS; } });
|
|
102
108
|
Object.defineProperty(exports, "parseXmlAnswer", { enumerable: true, get: function () { return publishApp_1.parseXmlAnswer; } });
|
|
103
109
|
Object.defineProperty(exports, "parseProgressTree", { enumerable: true, get: function () { return publishApp_1.parseProgressTree; } });
|
|
104
110
|
Object.defineProperty(exports, "classifyProgress", { enumerable: true, get: function () { return publishApp_1.classifyProgress; } });
|
package/dist/mcp/registry.js
CHANGED
|
@@ -624,18 +624,24 @@ function buildDescriptors(deps = {}) {
|
|
|
624
624
|
{
|
|
625
625
|
name: "app_publish",
|
|
626
626
|
annotations: dovetail_mcp_kit_1.WRITE_EXECUTE,
|
|
627
|
-
description: "Publish a scoped ServiceNow application to ONE target per call
|
|
628
|
-
"
|
|
629
|
-
"
|
|
630
|
-
"
|
|
631
|
-
"
|
|
632
|
-
"
|
|
633
|
-
"
|
|
634
|
-
"
|
|
635
|
-
"
|
|
627
|
+
description: "Publish a scoped ServiceNow application to ONE target per call, then poll the publish to " +
|
|
628
|
+
"completion; call repeatedly to hit several targets. STORE PUBLISH IS EXTERNALLY VISIBLE " +
|
|
629
|
+
"on the ServiceNow Store — treat it as a release. DRY-RUN BY DEFAULT: without confirm:true " +
|
|
630
|
+
"the resolved plan (app, current version, target) is returned and nothing is published. " +
|
|
631
|
+
"Targets: 'store' replays the sys_app form's upload flow over a form-login session and " +
|
|
632
|
+
"requires SN_STORE_USERNAME/SN_STORE_PASSWORD in the server's env file — credentials never " +
|
|
633
|
+
"transit tool arguments. 'repo' publishes to the company Application Repository via the " +
|
|
634
|
+
"supported CI/CD REST API (/api/sn_cicd/app_repo/publish) and requires the sn_cicd plugin " +
|
|
635
|
+
"and role. 'repo-ui' reaches the SAME company repository over the UI uploader instead — use " +
|
|
636
|
+
"it on instances without sn_cicd, where 'repo' 404s. 'update-set' publishes the app INTO a " +
|
|
637
|
+
"newly created update set via the two-call AppsAjaxProcessor flow (no REST equivalent " +
|
|
638
|
+
"exists); updateSetName defaults to the app's name and updateSetDescription is conventionally " +
|
|
639
|
+
"the release date stamp (YYYYMMDD) so a whole release is one sys_update_set query. " +
|
|
640
|
+
"includeData (default false) is the dialog's 'Include demo data' box. app is a scope name, " +
|
|
636
641
|
"sys_app sys_id, or app name; version must be above the currently published version. The " +
|
|
637
642
|
"result carries the progress-tracker id, per-step states, the Store appLink, and the " +
|
|
638
|
-
"
|
|
643
|
+
"update-set sys_id (for 'update-set' this is set as soon as the set is created, so it " +
|
|
644
|
+
"survives a later failure).",
|
|
639
645
|
shape: schemas_1.publishAppSchema.shape,
|
|
640
646
|
handler: async function (args) {
|
|
641
647
|
var p = schemas_1.publishAppSchema.parse(args);
|
|
@@ -644,6 +650,9 @@ function buildDescriptors(deps = {}) {
|
|
|
644
650
|
version: p.version,
|
|
645
651
|
target: p.target,
|
|
646
652
|
devNotes: p.devNotes,
|
|
653
|
+
updateSetName: p.updateSetName,
|
|
654
|
+
updateSetDescription: p.updateSetDescription,
|
|
655
|
+
includeData: p.includeData,
|
|
647
656
|
confirm: p.confirm,
|
|
648
657
|
dryRun: p.dryRun,
|
|
649
658
|
timeoutMs: p.timeoutMs,
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -1099,26 +1099,35 @@ export declare var publishAppSchema: z.ZodObject<{
|
|
|
1099
1099
|
app: z.ZodString;
|
|
1100
1100
|
version: z.ZodString;
|
|
1101
1101
|
devNotes: z.ZodOptional<z.ZodString>;
|
|
1102
|
-
target: z.ZodUnion<[z.ZodLiteral<"store">, z.ZodLiteral<"repo">]>;
|
|
1102
|
+
target: z.ZodUnion<[z.ZodLiteral<"store">, z.ZodLiteral<"repo">, z.ZodLiteral<"repo-ui">, z.ZodLiteral<"update-set">]>;
|
|
1103
|
+
updateSetName: z.ZodOptional<z.ZodString>;
|
|
1104
|
+
updateSetDescription: z.ZodOptional<z.ZodString>;
|
|
1105
|
+
includeData: z.ZodOptional<z.ZodBoolean>;
|
|
1103
1106
|
confirm: z.ZodOptional<z.ZodBoolean>;
|
|
1104
1107
|
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
1105
1108
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
1106
1109
|
}, "strip", z.ZodTypeAny, {
|
|
1107
|
-
target: "store" | "repo";
|
|
1110
|
+
target: "store" | "repo" | "repo-ui" | "update-set";
|
|
1108
1111
|
version: string;
|
|
1109
1112
|
app: string;
|
|
1110
1113
|
confirm?: boolean | undefined;
|
|
1111
1114
|
dryRun?: boolean | undefined;
|
|
1115
|
+
includeData?: boolean | undefined;
|
|
1112
1116
|
timeoutMs?: number | undefined;
|
|
1113
1117
|
devNotes?: string | undefined;
|
|
1118
|
+
updateSetName?: string | undefined;
|
|
1119
|
+
updateSetDescription?: string | undefined;
|
|
1114
1120
|
}, {
|
|
1115
|
-
target: "store" | "repo";
|
|
1121
|
+
target: "store" | "repo" | "repo-ui" | "update-set";
|
|
1116
1122
|
version: string;
|
|
1117
1123
|
app: string;
|
|
1118
1124
|
confirm?: boolean | undefined;
|
|
1119
1125
|
dryRun?: boolean | undefined;
|
|
1126
|
+
includeData?: boolean | undefined;
|
|
1120
1127
|
timeoutMs?: number | undefined;
|
|
1121
1128
|
devNotes?: string | undefined;
|
|
1129
|
+
updateSetName?: string | undefined;
|
|
1130
|
+
updateSetDescription?: string | undefined;
|
|
1122
1131
|
}>;
|
|
1123
1132
|
export declare var invokeRestSchema: z.ZodObject<{
|
|
1124
1133
|
method: z.ZodEffects<z.ZodEnum<["GET", "POST", "PUT", "DELETE"]>, "GET" | "DELETE" | "POST" | "PUT", unknown>;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -339,7 +339,15 @@ exports.publishAppSchema = zod_1.z.object({
|
|
|
339
339
|
app: zod_1.z.string().min(1),
|
|
340
340
|
version: zod_1.z.string().min(1),
|
|
341
341
|
devNotes: zod_1.z.string().optional(),
|
|
342
|
-
target: zod_1.z.union([
|
|
342
|
+
target: zod_1.z.union([
|
|
343
|
+
zod_1.z.literal("store"),
|
|
344
|
+
zod_1.z.literal("repo"),
|
|
345
|
+
zod_1.z.literal("repo-ui"),
|
|
346
|
+
zod_1.z.literal("update-set"),
|
|
347
|
+
]),
|
|
348
|
+
updateSetName: zod_1.z.string().optional(),
|
|
349
|
+
updateSetDescription: zod_1.z.string().optional(),
|
|
350
|
+
includeData: zod_1.z.boolean().optional(),
|
|
343
351
|
confirm: zod_1.z.boolean().optional(),
|
|
344
352
|
dryRun: zod_1.z.boolean().optional(),
|
|
345
353
|
timeoutMs: zod_1.z.number().int().positive().optional(),
|
package/dist/publishApp.d.ts
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dove-sn publish-app — publish a scoped application to the ServiceNow Store
|
|
3
|
-
*
|
|
4
|
-
* publish's progress tracker until it terminates.
|
|
2
|
+
* dove-sn publish-app — publish a scoped application to the ServiceNow Store,
|
|
3
|
+
* the company Application Repository, and/or a new update set, headlessly, then
|
|
4
|
+
* poll each publish's progress tracker until it terminates.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Three engines, one result shape:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* sn_appauthor.ScopedAppUploaderAJAX & sysparm_name=start
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* UPLOADER — replays the sys_app form's upload flow (ground truth: HAR
|
|
9
|
+
* captures 2026-07-16 and 2026-07-29): a form-login session POSTs
|
|
10
|
+
* /xmlhttp.do with sysparm_processor=
|
|
11
|
+
* sn_appauthor.ScopedAppUploaderAJAX & sysparm_name=start, then polls
|
|
12
|
+
* AJAXProgressStatusChecker/getStatus with the returned execution
|
|
13
|
+
* sys_id. Basic auth alone NO-OPS on xmlhttp.do — the form session +
|
|
14
|
+
* X-UserToken is required (proven by the sn-undo-default-push
|
|
15
|
+
* headless revert). Serves BOTH the "store" target
|
|
16
|
+
* (sysparm_publish_to_store=true, plus Store credentials) and the
|
|
17
|
+
* "repo-ui" target (=false, no credentials) — the destination is one
|
|
18
|
+
* flag apart.
|
|
16
19
|
*
|
|
17
20
|
* REPO — uses the supported CI/CD REST API: POST /api/sn_cicd/app_repo/publish
|
|
18
21
|
* (basic auth via the shared client), then polls
|
|
19
22
|
* GET /api/sn_cicd/progress/{id}. Requires the sn_cicd role (or admin).
|
|
23
|
+
* NOTE: the plugin is absent on some instances (tenonworkshop has no
|
|
24
|
+
* sn_cicd scope and no app_repo service) — use "repo-ui" there.
|
|
25
|
+
*
|
|
26
|
+
* UPDATE-SET — replays the "Publish to Update Set" dialog: two POSTs to
|
|
27
|
+
* /xmlhttp.do against com.snc.apps.AppsAjaxProcessor —
|
|
28
|
+
* sysparm_function=createUpdateSet (answers the new sys_update_set
|
|
29
|
+
* sys_id), then =publishToUpdateSet (answers a progress worker id) —
|
|
30
|
+
* polled through the same AJAXProgressStatusChecker as the uploader.
|
|
31
|
+
* There is no REST equivalent for this operation.
|
|
20
32
|
*
|
|
21
33
|
* STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store — the verb
|
|
22
34
|
* refuses to run either engine without confirm:true (dry-run is the default).
|
|
@@ -28,7 +40,31 @@
|
|
|
28
40
|
*/
|
|
29
41
|
import type { ServiceNowClient } from "./client";
|
|
30
42
|
import type { FormAuth, FormSession, PostResult } from "./table";
|
|
31
|
-
|
|
43
|
+
/**
|
|
44
|
+
* - "store" — ServiceNow Store, via ScopedAppUploaderAJAX (UI replay).
|
|
45
|
+
* - "repo" — company app repository, via the sn_cicd CI/CD REST API.
|
|
46
|
+
* - "repo-ui" — company app repository, via ScopedAppUploaderAJAX (UI replay).
|
|
47
|
+
* Use when the CI/CD plugin is absent: tenonworkshop has no
|
|
48
|
+
* sn_cicd scope and no app_repo service, so "repo" 404s there
|
|
49
|
+
* while the UI path (HAR-proven) works.
|
|
50
|
+
* - "update-set" — publish the app INTO a new update set, via the two-call
|
|
51
|
+
* com.snc.apps.AppsAjaxProcessor flow. No REST equivalent.
|
|
52
|
+
*/
|
|
53
|
+
export type PublishTarget = "store" | "repo" | "repo-ui" | "update-set";
|
|
54
|
+
export declare var PUBLISH_TARGETS: ReadonlyArray<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Expand a CLI --target value into an ordered target list.
|
|
57
|
+
*
|
|
58
|
+
* "both" stays an alias for store,repo (back-compat). Anything else may be a
|
|
59
|
+
* comma-separated list, so one invocation can publish to the app repository AND
|
|
60
|
+
* capture the app into an update set — the order given is the order run.
|
|
61
|
+
* Returns { targets } on success or { error } with a caller-printable message;
|
|
62
|
+
* never throws, so the CLI can exit 1 cleanly on bad input.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parsePublishTargets(raw: string): {
|
|
65
|
+
targets: Array<PublishTarget>;
|
|
66
|
+
error: string;
|
|
67
|
+
};
|
|
32
68
|
export interface PublishTransport {
|
|
33
69
|
openSession?: (auth: FormAuth) => Promise<FormSession>;
|
|
34
70
|
post?: (auth: FormAuth, session: FormSession, path: string, fields: Record<string, string>) => Promise<PostResult>;
|
|
@@ -48,6 +84,24 @@ export interface PublishAppParams {
|
|
|
48
84
|
storeUsername?: string;
|
|
49
85
|
/** Test seam only — the CLI never passes this; env SN_STORE_PASSWORD is the real source. */
|
|
50
86
|
storePassword?: string;
|
|
87
|
+
/**
|
|
88
|
+
* update-set target only — the name of the update set to create. Defaults to
|
|
89
|
+
* the app's name, which is exactly what the UI dialog submits (its app_name
|
|
90
|
+
* input is readonly), so the default reproduces the manual flow.
|
|
91
|
+
*/
|
|
92
|
+
updateSetName?: string;
|
|
93
|
+
/**
|
|
94
|
+
* update-set target only — the update set's description. Tenon convention is
|
|
95
|
+
* the release date stamp (YYYYMMDD) so a whole release is one query:
|
|
96
|
+
* sys_update_set where description=<stamp>.
|
|
97
|
+
*/
|
|
98
|
+
updateSetDescription?: string;
|
|
99
|
+
/**
|
|
100
|
+
* update-set target only — the dialog's "Include demo data" checkbox. The
|
|
101
|
+
* checkbox renders checked, but the captured HAR sent an EMPTY string, so the
|
|
102
|
+
* default here reproduces the observed wire value rather than the markup.
|
|
103
|
+
*/
|
|
104
|
+
includeData?: boolean;
|
|
51
105
|
instance?: string;
|
|
52
106
|
user?: string;
|
|
53
107
|
password?: string;
|
|
@@ -83,11 +137,20 @@ export interface PublishAppResult {
|
|
|
83
137
|
versionBefore: string;
|
|
84
138
|
/** The version that was (or would be) published. */
|
|
85
139
|
version: string;
|
|
86
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Store/repo-ui/update-set: the progress-tracker sys_id (the update-set leg's
|
|
142
|
+
* <workerid>). Repo: the CI/CD progress id.
|
|
143
|
+
*/
|
|
87
144
|
executionSysId: string;
|
|
88
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Store: harvested from root result["sys_update_set.sys_id"].
|
|
147
|
+
* update-set: the sys_update_set created by createUpdateSet — the whole point
|
|
148
|
+
* of the call, so it is set BEFORE the publish is polled and survives a
|
|
149
|
+
* failure (the set exists on the instance either way, and the caller needs
|
|
150
|
+
* the sys_id to clean up). "" when absent.
|
|
151
|
+
*/
|
|
89
152
|
updateSetSysId: string;
|
|
90
|
-
/** Store: tpp.servicenow.com editapplication URL.
|
|
153
|
+
/** Store: tpp.servicenow.com editapplication URL. Everything else: "". */
|
|
91
154
|
appLink: string;
|
|
92
155
|
steps: Array<PublishStep>;
|
|
93
156
|
polls: number;
|
|
@@ -114,6 +177,36 @@ export declare function buildStartFields(p: {
|
|
|
114
177
|
storeUsername: string;
|
|
115
178
|
storePassword: string;
|
|
116
179
|
}): Record<string, string>;
|
|
180
|
+
/**
|
|
181
|
+
* Field map for AppsAjaxProcessor.createUpdateSet — call 1 of the two-call
|
|
182
|
+
* "Publish to Update Set" flow (HAR ground truth). The answer attribute is the
|
|
183
|
+
* new sys_update_set sys_id.
|
|
184
|
+
*
|
|
185
|
+
* The app sys_id rides as `sysparm_appid` HERE but as `sysparm_sys_id` in call
|
|
186
|
+
* 2 (and `sysparm_app_id` in the version validator). The platform spells it
|
|
187
|
+
* differently per function and they are NOT interchangeable.
|
|
188
|
+
*/
|
|
189
|
+
export declare function buildCreateUpdateSetFields(p: {
|
|
190
|
+
appSysId: string;
|
|
191
|
+
updateSetName: string;
|
|
192
|
+
description: string;
|
|
193
|
+
}): Record<string, string>;
|
|
194
|
+
/**
|
|
195
|
+
* Field map for AppsAjaxProcessor.publishToUpdateSet — call 2. The answer
|
|
196
|
+
* attribute is the progress-worker sys_id (echoed as a <workerid> element).
|
|
197
|
+
*
|
|
198
|
+
* `sysparm_name` is the literal "start", NOT a name: the dialog composes an
|
|
199
|
+
* "<app> - <version>" label, but the generic progress viewer overwrites
|
|
200
|
+
* sysparm_name with "start" before the request leaves the browser, so the
|
|
201
|
+
* composed label never reaches the wire. The set keeps the name call 1 gave it.
|
|
202
|
+
*/
|
|
203
|
+
export declare function buildPublishToUpdateSetFields(p: {
|
|
204
|
+
appSysId: string;
|
|
205
|
+
updateSetSysId: string;
|
|
206
|
+
version: string;
|
|
207
|
+
description: string;
|
|
208
|
+
includeData: boolean;
|
|
209
|
+
}): Record<string, string>;
|
|
117
210
|
/**
|
|
118
211
|
* Extract the `answer` attribute from an xmlhttp.do response envelope
|
|
119
212
|
* (`<xml answer="..." .../>`) and entity-decode it. A missing/empty answer is
|
|
@@ -170,4 +263,23 @@ export interface CicdProgress {
|
|
|
170
263
|
}
|
|
171
264
|
/** CI/CD progress response → a typed snapshot. Unknown shapes become status "". */
|
|
172
265
|
export declare function parseCicdProgress(body: unknown): CicdProgress;
|
|
266
|
+
/**
|
|
267
|
+
* Resolve the update set's name and description.
|
|
268
|
+
*
|
|
269
|
+
* The name defaults to the app's name because the dialog's `app_name` input is
|
|
270
|
+
* READONLY — the bare app name is what the UI submits, and the live record
|
|
271
|
+
* confirms it (HAR createUpdateSet answer 2d368e8f… → sys_update_set named
|
|
272
|
+
* "@tenon/ui-side-modal"). The description defaults to empty; callers wanting
|
|
273
|
+
* the release-query convention pass the YYYYMMDD stamp.
|
|
274
|
+
*/
|
|
275
|
+
export declare function resolveUpdateSetNaming(p: {
|
|
276
|
+
updateSetName?: string;
|
|
277
|
+
updateSetDescription?: string;
|
|
278
|
+
appName: string;
|
|
279
|
+
}): {
|
|
280
|
+
name: string;
|
|
281
|
+
description: string;
|
|
282
|
+
};
|
|
283
|
+
/** Human phrase for a target — used in dry-run notes and CLI output. */
|
|
284
|
+
export declare function describeTarget(target: PublishTarget): string;
|
|
173
285
|
export declare function publishApp(params: PublishAppParams): Promise<PublishAppResult>;
|
package/dist/publishApp.js
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* dove-sn publish-app — publish a scoped application to the ServiceNow Store
|
|
4
|
-
*
|
|
5
|
-
* publish's progress tracker until it terminates.
|
|
3
|
+
* dove-sn publish-app — publish a scoped application to the ServiceNow Store,
|
|
4
|
+
* the company Application Repository, and/or a new update set, headlessly, then
|
|
5
|
+
* poll each publish's progress tracker until it terminates.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Three engines, one result shape:
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* sn_appauthor.ScopedAppUploaderAJAX & sysparm_name=start
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* UPLOADER — replays the sys_app form's upload flow (ground truth: HAR
|
|
10
|
+
* captures 2026-07-16 and 2026-07-29): a form-login session POSTs
|
|
11
|
+
* /xmlhttp.do with sysparm_processor=
|
|
12
|
+
* sn_appauthor.ScopedAppUploaderAJAX & sysparm_name=start, then polls
|
|
13
|
+
* AJAXProgressStatusChecker/getStatus with the returned execution
|
|
14
|
+
* sys_id. Basic auth alone NO-OPS on xmlhttp.do — the form session +
|
|
15
|
+
* X-UserToken is required (proven by the sn-undo-default-push
|
|
16
|
+
* headless revert). Serves BOTH the "store" target
|
|
17
|
+
* (sysparm_publish_to_store=true, plus Store credentials) and the
|
|
18
|
+
* "repo-ui" target (=false, no credentials) — the destination is one
|
|
19
|
+
* flag apart.
|
|
17
20
|
*
|
|
18
21
|
* REPO — uses the supported CI/CD REST API: POST /api/sn_cicd/app_repo/publish
|
|
19
22
|
* (basic auth via the shared client), then polls
|
|
20
23
|
* GET /api/sn_cicd/progress/{id}. Requires the sn_cicd role (or admin).
|
|
24
|
+
* NOTE: the plugin is absent on some instances (tenonworkshop has no
|
|
25
|
+
* sn_cicd scope and no app_repo service) — use "repo-ui" there.
|
|
26
|
+
*
|
|
27
|
+
* UPDATE-SET — replays the "Publish to Update Set" dialog: two POSTs to
|
|
28
|
+
* /xmlhttp.do against com.snc.apps.AppsAjaxProcessor —
|
|
29
|
+
* sysparm_function=createUpdateSet (answers the new sys_update_set
|
|
30
|
+
* sys_id), then =publishToUpdateSet (answers a progress worker id) —
|
|
31
|
+
* polled through the same AJAXProgressStatusChecker as the uploader.
|
|
32
|
+
* There is no REST equivalent for this operation.
|
|
21
33
|
*
|
|
22
34
|
* STORE PUBLISH IS EXTERNALLY VISIBLE on the ServiceNow Store — the verb
|
|
23
35
|
* refuses to run either engine without confirm:true (dry-run is the default).
|
|
@@ -28,8 +40,11 @@
|
|
|
28
40
|
* never accepted as a CLI flag.
|
|
29
41
|
*/
|
|
30
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = void 0;
|
|
43
|
+
exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.PUBLISH_TARGETS = void 0;
|
|
44
|
+
exports.parsePublishTargets = parsePublishTargets;
|
|
32
45
|
exports.buildStartFields = buildStartFields;
|
|
46
|
+
exports.buildCreateUpdateSetFields = buildCreateUpdateSetFields;
|
|
47
|
+
exports.buildPublishToUpdateSetFields = buildPublishToUpdateSetFields;
|
|
33
48
|
exports.parseXmlAnswer = parseXmlAnswer;
|
|
34
49
|
exports.parseProgressTree = parseProgressTree;
|
|
35
50
|
exports.classifyProgress = classifyProgress;
|
|
@@ -37,9 +52,56 @@ exports.flattenSteps = flattenSteps;
|
|
|
37
52
|
exports.harvestProgressResults = harvestProgressResults;
|
|
38
53
|
exports.parseCicdPublishResponse = parseCicdPublishResponse;
|
|
39
54
|
exports.parseCicdProgress = parseCicdProgress;
|
|
55
|
+
exports.resolveUpdateSetNaming = resolveUpdateSetNaming;
|
|
56
|
+
exports.describeTarget = describeTarget;
|
|
40
57
|
exports.publishApp = publishApp;
|
|
41
58
|
const client_1 = require("./client");
|
|
42
59
|
const table_1 = require("./table");
|
|
60
|
+
exports.PUBLISH_TARGETS = [
|
|
61
|
+
"store",
|
|
62
|
+
"repo",
|
|
63
|
+
"repo-ui",
|
|
64
|
+
"update-set",
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Expand a CLI --target value into an ordered target list.
|
|
68
|
+
*
|
|
69
|
+
* "both" stays an alias for store,repo (back-compat). Anything else may be a
|
|
70
|
+
* comma-separated list, so one invocation can publish to the app repository AND
|
|
71
|
+
* capture the app into an update set — the order given is the order run.
|
|
72
|
+
* Returns { targets } on success or { error } with a caller-printable message;
|
|
73
|
+
* never throws, so the CLI can exit 1 cleanly on bad input.
|
|
74
|
+
*/
|
|
75
|
+
function parsePublishTargets(raw) {
|
|
76
|
+
var value = String(raw || "").trim();
|
|
77
|
+
if (value === "both") {
|
|
78
|
+
return { targets: ["store", "repo"], error: "" };
|
|
79
|
+
}
|
|
80
|
+
var parts = value
|
|
81
|
+
.split(",")
|
|
82
|
+
.map(function (s) {
|
|
83
|
+
return s.trim();
|
|
84
|
+
})
|
|
85
|
+
.filter(function (s) {
|
|
86
|
+
return s.length > 0;
|
|
87
|
+
});
|
|
88
|
+
if (parts.length === 0) {
|
|
89
|
+
return { targets: [], error: "--target is empty" };
|
|
90
|
+
}
|
|
91
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
92
|
+
if (exports.PUBLISH_TARGETS.indexOf(parts[i]) === -1) {
|
|
93
|
+
return {
|
|
94
|
+
targets: [],
|
|
95
|
+
error: "--target must be one of " +
|
|
96
|
+
exports.PUBLISH_TARGETS.join(", ") +
|
|
97
|
+
", 'both', or a comma-separated list of them (got '" +
|
|
98
|
+
parts[i] +
|
|
99
|
+
"')",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { targets: parts, error: "" };
|
|
104
|
+
}
|
|
43
105
|
exports.DEFAULT_PUBLISH_TIMEOUT_MS = 120000;
|
|
44
106
|
/** Repeat the LAST delay until the budget is spent — a publish can outrun 15s. */
|
|
45
107
|
exports.PUBLISH_POLL_DELAYS_MS = [
|
|
@@ -79,6 +141,64 @@ function buildStartFields(p) {
|
|
|
79
141
|
}
|
|
80
142
|
return fields;
|
|
81
143
|
}
|
|
144
|
+
var APPS_AJAX_PROCESSOR = "com.snc.apps.AppsAjaxProcessor";
|
|
145
|
+
/**
|
|
146
|
+
* Field map for AppsAjaxProcessor.createUpdateSet — call 1 of the two-call
|
|
147
|
+
* "Publish to Update Set" flow (HAR ground truth). The answer attribute is the
|
|
148
|
+
* new sys_update_set sys_id.
|
|
149
|
+
*
|
|
150
|
+
* The app sys_id rides as `sysparm_appid` HERE but as `sysparm_sys_id` in call
|
|
151
|
+
* 2 (and `sysparm_app_id` in the version validator). The platform spells it
|
|
152
|
+
* differently per function and they are NOT interchangeable.
|
|
153
|
+
*/
|
|
154
|
+
function buildCreateUpdateSetFields(p) {
|
|
155
|
+
return {
|
|
156
|
+
sysparm_processor: APPS_AJAX_PROCESSOR,
|
|
157
|
+
sysparm_scope: "global",
|
|
158
|
+
sysparm_want_session_messages: "true",
|
|
159
|
+
sysparm_function: "createUpdateSet",
|
|
160
|
+
sysparm_name: p.updateSetName,
|
|
161
|
+
sysparm_appid: p.appSysId,
|
|
162
|
+
sysparm_description: p.description,
|
|
163
|
+
// "false" = create the set but do NOT adopt it as the session's current
|
|
164
|
+
// update set. A batch that flipped the session set would silently capture
|
|
165
|
+
// unrelated writes into the last app's set.
|
|
166
|
+
sysparm_current: "false",
|
|
167
|
+
"ni.nolog.x_referer": "ignore",
|
|
168
|
+
x_referer: "sys_app.do?sys_id=" + p.appSysId,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Field map for AppsAjaxProcessor.publishToUpdateSet — call 2. The answer
|
|
173
|
+
* attribute is the progress-worker sys_id (echoed as a <workerid> element).
|
|
174
|
+
*
|
|
175
|
+
* `sysparm_name` is the literal "start", NOT a name: the dialog composes an
|
|
176
|
+
* "<app> - <version>" label, but the generic progress viewer overwrites
|
|
177
|
+
* sysparm_name with "start" before the request leaves the browser, so the
|
|
178
|
+
* composed label never reaches the wire. The set keeps the name call 1 gave it.
|
|
179
|
+
*/
|
|
180
|
+
function buildPublishToUpdateSetFields(p) {
|
|
181
|
+
return {
|
|
182
|
+
sysparm_processor: APPS_AJAX_PROCESSOR,
|
|
183
|
+
sysparm_scope: "global",
|
|
184
|
+
sysparm_want_session_messages: "true",
|
|
185
|
+
sysparm_function: "publishToUpdateSet",
|
|
186
|
+
sysparm_update_set_id: p.updateSetSysId,
|
|
187
|
+
sysparm_sys_id: p.appSysId,
|
|
188
|
+
sysparm_name: "start",
|
|
189
|
+
sysparm_version: p.version,
|
|
190
|
+
sysparm_description: p.description,
|
|
191
|
+
// The captured HAR sent an EMPTY string here even though the "Include demo
|
|
192
|
+
// data" checkbox renders checked — empty is the observed wire default and
|
|
193
|
+
// "true" is the explicit opt-in.
|
|
194
|
+
sysparm_include_data: p.includeData ? "true" : "",
|
|
195
|
+
sysparm_progress_name: "Publishing application",
|
|
196
|
+
sysparm_ajax_processor: APPS_AJAX_PROCESSOR,
|
|
197
|
+
sysparm_show_done_button: "true",
|
|
198
|
+
"ni.nolog.x_referer": "ignore",
|
|
199
|
+
x_referer: "sys_app.do?sys_id=" + p.appSysId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
82
202
|
/**
|
|
83
203
|
* Extract the `answer` attribute from an xmlhttp.do response envelope
|
|
84
204
|
* (`<xml answer="..." .../>`) and entity-decode it. A missing/empty answer is
|
|
@@ -323,78 +443,68 @@ function resolveStoreCreds(params) {
|
|
|
323
443
|
}
|
|
324
444
|
return { username: username, password: password };
|
|
325
445
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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;
|
|
446
|
+
/**
|
|
447
|
+
* Resolve the update set's name and description.
|
|
448
|
+
*
|
|
449
|
+
* The name defaults to the app's name because the dialog's `app_name` input is
|
|
450
|
+
* READONLY — the bare app name is what the UI submits, and the live record
|
|
451
|
+
* confirms it (HAR createUpdateSet answer 2d368e8f… → sys_update_set named
|
|
452
|
+
* "@tenon/ui-side-modal"). The description defaults to empty; callers wanting
|
|
453
|
+
* the release-query convention pass the YYYYMMDD stamp.
|
|
454
|
+
*/
|
|
455
|
+
function resolveUpdateSetNaming(p) {
|
|
456
|
+
var name = typeof p.updateSetName === "string" ? p.updateSetName.trim() : "";
|
|
457
|
+
if (!name)
|
|
458
|
+
name = String(p.appName || "").trim();
|
|
459
|
+
if (!name) {
|
|
460
|
+
throw new Error("publish-app: the update-set target needs an update set name, and the " +
|
|
461
|
+
"resolved app has no name — pass updateSetName explicitly.");
|
|
367
462
|
}
|
|
368
|
-
|
|
463
|
+
var description = typeof p.updateSetDescription === "string" ? p.updateSetDescription : "";
|
|
464
|
+
return { name: name, description: description };
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Poll AJAXProgressStatusChecker until the tracker reaches a terminal state.
|
|
468
|
+
*
|
|
469
|
+
* Mutates `result` (polls / status / message / note / steps) and returns the
|
|
470
|
+
* terminal progress tree — or `null` when the caller should return immediately
|
|
471
|
+
* because the outcome is already written onto `result` (timeout, transport
|
|
472
|
+
* failure, or an unparseable answer).
|
|
473
|
+
*
|
|
474
|
+
* Shared by every UI-replay target: the Store upload, the app-repo upload, and
|
|
475
|
+
* the update-set publish all report through this one tracker endpoint.
|
|
476
|
+
*/
|
|
477
|
+
async function pollAjaxProgress(p) {
|
|
478
|
+
var result = p.result;
|
|
369
479
|
var startedAt = Date.now();
|
|
370
480
|
var pollIndex = 0;
|
|
371
481
|
while (true) {
|
|
372
482
|
var delay = exports.PUBLISH_POLL_DELAYS_MS[Math.min(pollIndex, exports.PUBLISH_POLL_DELAYS_MS.length - 1)];
|
|
373
|
-
if (Date.now() - startedAt + delay > timeoutMs) {
|
|
483
|
+
if (Date.now() - startedAt + delay > p.timeoutMs) {
|
|
374
484
|
result.status = "timeout";
|
|
375
485
|
result.note =
|
|
376
486
|
"publish-app: progress tracker " +
|
|
377
487
|
result.executionSysId +
|
|
378
488
|
" did not terminate within " +
|
|
379
|
-
timeoutMs +
|
|
489
|
+
p.timeoutMs +
|
|
380
490
|
"ms after " +
|
|
381
491
|
result.polls +
|
|
382
492
|
" poll(s). Check /sys_execution_tracker.do?sys_id=" +
|
|
383
493
|
result.executionSysId +
|
|
384
494
|
" on the instance.";
|
|
385
|
-
return
|
|
495
|
+
return null;
|
|
386
496
|
}
|
|
387
|
-
await sleepFn(delay);
|
|
497
|
+
await p.sleepFn(delay);
|
|
388
498
|
pollIndex += 1;
|
|
389
499
|
result.polls += 1;
|
|
390
|
-
var pollRes = await post(auth, session, "/xmlhttp.do", {
|
|
500
|
+
var pollRes = await p.post(p.auth, p.session, "/xmlhttp.do", {
|
|
391
501
|
sysparm_processor: "AJAXProgressStatusChecker",
|
|
392
502
|
sysparm_name: "getStatus",
|
|
393
503
|
sysparm_scope: "global",
|
|
394
504
|
sysparm_want_session_messages: "true",
|
|
395
505
|
sysparm_execution_id: result.executionSysId,
|
|
396
506
|
"ni.nolog.x_referer": "ignore",
|
|
397
|
-
x_referer: "sys_app.do?sys_id=" +
|
|
507
|
+
x_referer: "sys_app.do?sys_id=" + p.appSysId,
|
|
398
508
|
});
|
|
399
509
|
if (pollRes.status < 200 || pollRes.status >= 300) {
|
|
400
510
|
result.message = "getStatus returned HTTP " + pollRes.status;
|
|
@@ -404,14 +514,14 @@ async function publishToStore(params, app) {
|
|
|
404
514
|
") — tracker " +
|
|
405
515
|
result.executionSysId +
|
|
406
516
|
" may still be running on the instance.";
|
|
407
|
-
return
|
|
517
|
+
return null;
|
|
408
518
|
}
|
|
409
519
|
var pollAnswer = parseXmlAnswer(pollRes.body);
|
|
410
520
|
if (pollAnswer.error) {
|
|
411
521
|
result.message = pollAnswer.error;
|
|
412
522
|
result.note =
|
|
413
523
|
"publish-app: could not read the progress answer — " + pollAnswer.error;
|
|
414
|
-
return
|
|
524
|
+
return null;
|
|
415
525
|
}
|
|
416
526
|
var tree;
|
|
417
527
|
try {
|
|
@@ -420,41 +530,238 @@ async function publishToStore(params, app) {
|
|
|
420
530
|
catch (e) {
|
|
421
531
|
result.message = e instanceof Error ? e.message : String(e);
|
|
422
532
|
result.note = result.message;
|
|
423
|
-
return
|
|
533
|
+
return null;
|
|
424
534
|
}
|
|
425
|
-
|
|
426
|
-
if (!state.terminal)
|
|
535
|
+
if (!classifyProgress(tree).terminal)
|
|
427
536
|
continue;
|
|
428
537
|
result.steps = flattenSteps(tree);
|
|
429
538
|
result.message = tree.message || "";
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
539
|
+
return tree;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* ScopedAppUploaderAJAX upload — the engine behind BOTH the Store publish and
|
|
544
|
+
* the company-app-repo publish. The two differ only by
|
|
545
|
+
* `sysparm_publish_to_store` and whether Store credentials are attached; the
|
|
546
|
+
* session, the start call, and the progress tracker are identical.
|
|
547
|
+
*/
|
|
548
|
+
async function publishViaUploader(params, app, target) {
|
|
549
|
+
var result = baseResult(target, app, params.version);
|
|
550
|
+
// repo-ui never carries Store credentials: publish_to_store=false makes the
|
|
551
|
+
// processor ignore them, and requiring SN_STORE_* would block an internal
|
|
552
|
+
// publish on a Store account it does not need.
|
|
553
|
+
var creds = target === "store"
|
|
554
|
+
? resolveStoreCreds(params)
|
|
555
|
+
: { username: "", password: "" };
|
|
556
|
+
var transport = params.transport || {};
|
|
557
|
+
var openSession = transport.openSession || table_1.openFormSession;
|
|
558
|
+
var post = transport.post || table_1.postForm;
|
|
559
|
+
var sleepFn = transport.sleep || realSleep;
|
|
560
|
+
var timeoutMs = params.timeoutMs != null ? params.timeoutMs : exports.DEFAULT_PUBLISH_TIMEOUT_MS;
|
|
561
|
+
var auth = (0, table_1.resolveFormAuth)({
|
|
562
|
+
instance: params.instance,
|
|
563
|
+
user: params.user,
|
|
564
|
+
password: params.password,
|
|
565
|
+
});
|
|
566
|
+
var session = await openSession(auth);
|
|
567
|
+
var startFields = buildStartFields({
|
|
568
|
+
appSysId: app.sysId,
|
|
569
|
+
version: params.version,
|
|
570
|
+
devNotes: params.devNotes || "",
|
|
571
|
+
target: target,
|
|
572
|
+
storeUsername: creds.username,
|
|
573
|
+
storePassword: creds.password,
|
|
574
|
+
});
|
|
575
|
+
var startRes = await post(auth, session, "/xmlhttp.do", startFields);
|
|
576
|
+
if (startRes.status < 200 || startRes.status >= 300) {
|
|
577
|
+
result.message = "start call returned HTTP " + startRes.status;
|
|
578
|
+
result.note =
|
|
579
|
+
"publish-app: the ScopedAppUploaderAJAX start call failed (HTTP " +
|
|
580
|
+
startRes.status +
|
|
581
|
+
"): " +
|
|
582
|
+
(startRes.body || "").slice(0, 200);
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
var startAnswer = parseXmlAnswer(startRes.body);
|
|
586
|
+
if (startAnswer.error || !SYS_ID_RE.test(startAnswer.answer)) {
|
|
587
|
+
result.message = startAnswer.error || "answer is not a sys_id";
|
|
588
|
+
result.note =
|
|
589
|
+
"publish-app: the start call did not return an execution sys_id — " +
|
|
590
|
+
(startAnswer.error || "got '" + startAnswer.answer.slice(0, 64) + "'") +
|
|
591
|
+
". The upload never started.";
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
result.executionSysId = startAnswer.answer;
|
|
595
|
+
var tree = await pollAjaxProgress({
|
|
596
|
+
result: result,
|
|
597
|
+
auth: auth,
|
|
598
|
+
session: session,
|
|
599
|
+
post: post,
|
|
600
|
+
sleepFn: sleepFn,
|
|
601
|
+
appSysId: app.sysId,
|
|
602
|
+
timeoutMs: timeoutMs,
|
|
603
|
+
});
|
|
604
|
+
if (!tree)
|
|
605
|
+
return result;
|
|
606
|
+
var harvested = harvestProgressResults(tree);
|
|
607
|
+
result.appLink = harvested.appLink;
|
|
608
|
+
result.updateSetSysId = harvested.updateSetSysId;
|
|
609
|
+
if (classifyProgress(tree).success) {
|
|
610
|
+
result.status = "published";
|
|
611
|
+
result.note =
|
|
612
|
+
"Published " +
|
|
613
|
+
app.name +
|
|
614
|
+
" v" +
|
|
615
|
+
params.version +
|
|
616
|
+
" to " +
|
|
617
|
+
(target === "store"
|
|
618
|
+
? "the ServiceNow Store"
|
|
619
|
+
: "the company application repository") +
|
|
620
|
+
(result.appLink ? " — " + result.appLink : "") +
|
|
621
|
+
" (" +
|
|
622
|
+
result.polls +
|
|
623
|
+
" poll(s)).";
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
result.status = "failed";
|
|
627
|
+
result.note =
|
|
628
|
+
"publish-app: " +
|
|
629
|
+
(target === "store" ? "store" : "app-repo") +
|
|
630
|
+
" publish terminated in state " +
|
|
631
|
+
tree.state +
|
|
632
|
+
(result.message ? " — " + result.message : "") +
|
|
633
|
+
". Tracker " +
|
|
634
|
+
result.executionSysId +
|
|
635
|
+
".";
|
|
636
|
+
}
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* "Publish to Update Set" — the two-call AppsAjaxProcessor flow, replayed over
|
|
641
|
+
* the same form session. Call 1 creates the sys_update_set; call 2 publishes
|
|
642
|
+
* the app into it under a progress worker.
|
|
643
|
+
*
|
|
644
|
+
* `updateSetSysId` is recorded the moment call 1 answers, BEFORE the publish is
|
|
645
|
+
* polled: if call 2 or the worker fails, the set still exists on the instance
|
|
646
|
+
* and the caller needs its sys_id to inspect or delete it.
|
|
647
|
+
*/
|
|
648
|
+
async function publishToUpdateSet(params, app) {
|
|
649
|
+
var result = baseResult("update-set", app, params.version);
|
|
650
|
+
var naming = resolveUpdateSetNaming({
|
|
651
|
+
updateSetName: params.updateSetName,
|
|
652
|
+
updateSetDescription: params.updateSetDescription,
|
|
653
|
+
appName: app.name,
|
|
654
|
+
});
|
|
655
|
+
var transport = params.transport || {};
|
|
656
|
+
var openSession = transport.openSession || table_1.openFormSession;
|
|
657
|
+
var post = transport.post || table_1.postForm;
|
|
658
|
+
var sleepFn = transport.sleep || realSleep;
|
|
659
|
+
var timeoutMs = params.timeoutMs != null ? params.timeoutMs : exports.DEFAULT_PUBLISH_TIMEOUT_MS;
|
|
660
|
+
var auth = (0, table_1.resolveFormAuth)({
|
|
661
|
+
instance: params.instance,
|
|
662
|
+
user: params.user,
|
|
663
|
+
password: params.password,
|
|
664
|
+
});
|
|
665
|
+
var session = await openSession(auth);
|
|
666
|
+
// --- call 1: createUpdateSet ---
|
|
667
|
+
var createRes = await post(auth, session, "/xmlhttp.do", buildCreateUpdateSetFields({
|
|
668
|
+
appSysId: app.sysId,
|
|
669
|
+
updateSetName: naming.name,
|
|
670
|
+
description: naming.description,
|
|
671
|
+
}));
|
|
672
|
+
if (createRes.status < 200 || createRes.status >= 300) {
|
|
673
|
+
result.message = "createUpdateSet returned HTTP " + createRes.status;
|
|
674
|
+
result.note =
|
|
675
|
+
"publish-app: createUpdateSet failed (HTTP " +
|
|
676
|
+
createRes.status +
|
|
677
|
+
"): " +
|
|
678
|
+
(createRes.body || "").slice(0, 200) +
|
|
679
|
+
". No update set was created.";
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
var createAnswer = parseXmlAnswer(createRes.body);
|
|
683
|
+
if (createAnswer.error || !SYS_ID_RE.test(createAnswer.answer)) {
|
|
684
|
+
result.message = createAnswer.error || "answer is not a sys_id";
|
|
685
|
+
result.note =
|
|
686
|
+
"publish-app: createUpdateSet did not return an update set sys_id — " +
|
|
687
|
+
(createAnswer.error || "got '" + createAnswer.answer.slice(0, 64) + "'") +
|
|
688
|
+
". Nothing was published.";
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
result.updateSetSysId = createAnswer.answer;
|
|
692
|
+
// --- call 2: publishToUpdateSet ---
|
|
693
|
+
var publishRes = await post(auth, session, "/xmlhttp.do", buildPublishToUpdateSetFields({
|
|
694
|
+
appSysId: app.sysId,
|
|
695
|
+
updateSetSysId: result.updateSetSysId,
|
|
696
|
+
version: params.version,
|
|
697
|
+
description: naming.description,
|
|
698
|
+
includeData: params.includeData === true,
|
|
699
|
+
}));
|
|
700
|
+
if (publishRes.status < 200 || publishRes.status >= 300) {
|
|
701
|
+
result.message = "publishToUpdateSet returned HTTP " + publishRes.status;
|
|
702
|
+
result.note =
|
|
703
|
+
"publish-app: publishToUpdateSet failed (HTTP " +
|
|
704
|
+
publishRes.status +
|
|
705
|
+
"): " +
|
|
706
|
+
(publishRes.body || "").slice(0, 200) +
|
|
707
|
+
". Update set " +
|
|
708
|
+
result.updateSetSysId +
|
|
709
|
+
" was created but is EMPTY.";
|
|
710
|
+
return result;
|
|
711
|
+
}
|
|
712
|
+
var publishAnswer = parseXmlAnswer(publishRes.body);
|
|
713
|
+
if (publishAnswer.error || !SYS_ID_RE.test(publishAnswer.answer)) {
|
|
714
|
+
result.message = publishAnswer.error || "answer is not a sys_id";
|
|
715
|
+
result.note =
|
|
716
|
+
"publish-app: publishToUpdateSet did not return a worker sys_id — " +
|
|
717
|
+
(publishAnswer.error ||
|
|
718
|
+
"got '" + publishAnswer.answer.slice(0, 64) + "'") +
|
|
719
|
+
". Update set " +
|
|
720
|
+
result.updateSetSysId +
|
|
721
|
+
" was created but is EMPTY.";
|
|
456
722
|
return result;
|
|
457
723
|
}
|
|
724
|
+
result.executionSysId = publishAnswer.answer;
|
|
725
|
+
var tree = await pollAjaxProgress({
|
|
726
|
+
result: result,
|
|
727
|
+
auth: auth,
|
|
728
|
+
session: session,
|
|
729
|
+
post: post,
|
|
730
|
+
sleepFn: sleepFn,
|
|
731
|
+
appSysId: app.sysId,
|
|
732
|
+
timeoutMs: timeoutMs,
|
|
733
|
+
});
|
|
734
|
+
if (!tree)
|
|
735
|
+
return result;
|
|
736
|
+
if (classifyProgress(tree).success) {
|
|
737
|
+
result.status = "published";
|
|
738
|
+
result.note =
|
|
739
|
+
"Published " +
|
|
740
|
+
app.name +
|
|
741
|
+
" v" +
|
|
742
|
+
params.version +
|
|
743
|
+
" into update set '" +
|
|
744
|
+
naming.name +
|
|
745
|
+
"' (" +
|
|
746
|
+
result.updateSetSysId +
|
|
747
|
+
(naming.description ? ", description '" + naming.description + "'" : "") +
|
|
748
|
+
") (" +
|
|
749
|
+
result.polls +
|
|
750
|
+
" poll(s)).";
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
result.status = "failed";
|
|
754
|
+
result.note =
|
|
755
|
+
"publish-app: update-set publish terminated in state " +
|
|
756
|
+
tree.state +
|
|
757
|
+
(result.message ? " — " + result.message : "") +
|
|
758
|
+
". Tracker " +
|
|
759
|
+
result.executionSysId +
|
|
760
|
+
". Update set " +
|
|
761
|
+
result.updateSetSysId +
|
|
762
|
+
" exists and may be incomplete.";
|
|
763
|
+
}
|
|
764
|
+
return result;
|
|
458
765
|
}
|
|
459
766
|
async function publishToRepo(params, app, client) {
|
|
460
767
|
var result = baseResult("repo", app, params.version);
|
|
@@ -523,7 +830,8 @@ async function publishToRepo(params, app, client) {
|
|
|
523
830
|
path: "/api/sn_cicd/progress/" + encodeURIComponent(result.executionSysId),
|
|
524
831
|
});
|
|
525
832
|
if (progRes.status < 200 || progRes.status >= 300) {
|
|
526
|
-
result.message =
|
|
833
|
+
result.message =
|
|
834
|
+
"HTTP " + progRes.status + " from the CI/CD progress API";
|
|
527
835
|
result.note =
|
|
528
836
|
"publish-app: CI/CD progress poll failed (HTTP " +
|
|
529
837
|
progRes.status +
|
|
@@ -592,6 +900,46 @@ function maskedPreview(target, app, params) {
|
|
|
592
900
|
: "(MISSING — set SN_STORE_PASSWORD)";
|
|
593
901
|
return fields;
|
|
594
902
|
}
|
|
903
|
+
if (target === "repo-ui") {
|
|
904
|
+
// No credentials are involved at all — publish_to_store=false.
|
|
905
|
+
return buildStartFields({
|
|
906
|
+
appSysId: app.sysId,
|
|
907
|
+
version: params.version,
|
|
908
|
+
devNotes: params.devNotes || "",
|
|
909
|
+
target: "repo-ui",
|
|
910
|
+
storeUsername: "",
|
|
911
|
+
storePassword: "",
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
if (target === "update-set") {
|
|
915
|
+
var naming = resolveUpdateSetNaming({
|
|
916
|
+
updateSetName: params.updateSetName,
|
|
917
|
+
updateSetDescription: params.updateSetDescription,
|
|
918
|
+
appName: app.name,
|
|
919
|
+
});
|
|
920
|
+
// Two calls, so the preview is flattened with a call-N prefix — the second
|
|
921
|
+
// call's real sysparm_update_set_id is only known at run time.
|
|
922
|
+
var flat = {};
|
|
923
|
+
var create = buildCreateUpdateSetFields({
|
|
924
|
+
appSysId: app.sysId,
|
|
925
|
+
updateSetName: naming.name,
|
|
926
|
+
description: naming.description,
|
|
927
|
+
});
|
|
928
|
+
Object.keys(create).forEach(function (k) {
|
|
929
|
+
flat["1." + k] = create[k];
|
|
930
|
+
});
|
|
931
|
+
var publish = buildPublishToUpdateSetFields({
|
|
932
|
+
appSysId: app.sysId,
|
|
933
|
+
updateSetSysId: "(sys_id returned by call 1)",
|
|
934
|
+
version: params.version,
|
|
935
|
+
description: naming.description,
|
|
936
|
+
includeData: params.includeData === true,
|
|
937
|
+
});
|
|
938
|
+
Object.keys(publish).forEach(function (k) {
|
|
939
|
+
flat["2." + k] = publish[k];
|
|
940
|
+
});
|
|
941
|
+
return flat;
|
|
942
|
+
}
|
|
595
943
|
var preview = {
|
|
596
944
|
method: "POST",
|
|
597
945
|
path: "/api/sn_cicd/app_repo/publish?sys_id=" +
|
|
@@ -603,6 +951,16 @@ function maskedPreview(target, app, params) {
|
|
|
603
951
|
preview.dev_notes = params.devNotes;
|
|
604
952
|
return preview;
|
|
605
953
|
}
|
|
954
|
+
/** Human phrase for a target — used in dry-run notes and CLI output. */
|
|
955
|
+
function describeTarget(target) {
|
|
956
|
+
if (target === "store")
|
|
957
|
+
return "ServiceNow Store (EXTERNALLY VISIBLE)";
|
|
958
|
+
if (target === "repo")
|
|
959
|
+
return "application repository (CI/CD REST API)";
|
|
960
|
+
if (target === "repo-ui")
|
|
961
|
+
return "company application repository (UI replay)";
|
|
962
|
+
return "new update set";
|
|
963
|
+
}
|
|
606
964
|
async function publishApp(params) {
|
|
607
965
|
if (!params || typeof params !== "object") {
|
|
608
966
|
throw new Error("publish-app: params object is required.");
|
|
@@ -613,8 +971,12 @@ async function publishApp(params) {
|
|
|
613
971
|
if (!params.version) {
|
|
614
972
|
throw new Error("publish-app: version is required.");
|
|
615
973
|
}
|
|
616
|
-
if (params.target !== "store" &&
|
|
617
|
-
|
|
974
|
+
if (params.target !== "store" &&
|
|
975
|
+
params.target !== "repo" &&
|
|
976
|
+
params.target !== "repo-ui" &&
|
|
977
|
+
params.target !== "update-set") {
|
|
978
|
+
throw new Error("publish-app: target must be 'store', 'repo', 'repo-ui', or " +
|
|
979
|
+
"'update-set' (got '" +
|
|
618
980
|
String(params.target) +
|
|
619
981
|
"').");
|
|
620
982
|
}
|
|
@@ -651,14 +1013,15 @@ async function publishApp(params) {
|
|
|
651
1013
|
") as v" +
|
|
652
1014
|
params.version +
|
|
653
1015
|
" to the " +
|
|
654
|
-
(params.target
|
|
655
|
-
? "ServiceNow Store (EXTERNALLY VISIBLE)"
|
|
656
|
-
: "application repository") +
|
|
1016
|
+
describeTarget(params.target) +
|
|
657
1017
|
". Re-run with confirm to publish.";
|
|
658
1018
|
return dry;
|
|
659
1019
|
}
|
|
660
|
-
if (params.target === "store") {
|
|
661
|
-
return
|
|
1020
|
+
if (params.target === "store" || params.target === "repo-ui") {
|
|
1021
|
+
return publishViaUploader(params, app, params.target);
|
|
1022
|
+
}
|
|
1023
|
+
if (params.target === "update-set") {
|
|
1024
|
+
return publishToUpdateSet(params, app);
|
|
662
1025
|
}
|
|
663
1026
|
return publishToRepo(params, app, client);
|
|
664
1027
|
}
|