@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 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 both — headlessly, with the publish's progress
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 both` runs store then repo sequentially and short-circuits
538
- if the store leg fails.
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 two targets ride different transports:
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 publish's update-set sys_id where the instance reports
555
- one. Exit codes: `0` published or dry-run, `1` bad args/unconfirmed, `2`
556
- failed/timeout. Programmatic: `publishApp({ app, version, target, confirm })`.
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 and/or the company\n" +
1047
- " application repository, then poll the publish to completion.\n" +
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> --target store|repo|both\n" +
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
- " [--dry-run] [--json] [--confirm])\n" +
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. Repo publish needs the sn_cicd role.\n" +
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|both Required. STORE PUBLISH IS EXTERNALLY VISIBLE.
1837
- * [--dev-notes <text>] Optional developer notes.
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). --target both publishes
1846
- * store then repo sequentially with the same version and short-circuits if the
1847
- * store leg fails; --json emits an array of per-target results.
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 store|repo|both are required\n");
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
- if (target !== "store" && target !== "repo" && target !== "both") {
1859
- process.stderr.write("publish-app: --target must be store, repo or both (got '" +
1860
- target +
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 = target === "both" ? ["store", "repo"] : [target];
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.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = 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.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 = 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; } });
@@ -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 — the ServiceNow Store " +
628
- "(target 'store') or the company Application Repository (target 'repo'); call twice to " +
629
- "publish to both — then poll the publish to completion. STORE PUBLISH IS EXTERNALLY " +
630
- "VISIBLE on the ServiceNow Store — treat it as a release. DRY-RUN BY DEFAULT: without " +
631
- "confirm:true the resolved plan (app, current version, target) is returned and nothing is " +
632
- "published. target 'store' replays the sys_app form's upload flow over a form-login session " +
633
- "and requires SN_STORE_USERNAME/SN_STORE_PASSWORD in the server's env file — credentials " +
634
- "never transit tool arguments. target 'repo' uses the supported CI/CD REST API " +
635
- "(/api/sn_cicd/app_repo/publish) and requires the sn_cicd role. app is a scope name, " +
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
- "publish's update-set sys_id where the instance reports one.",
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,
@@ -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>;
@@ -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([zod_1.z.literal("store"), zod_1.z.literal("repo")]),
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(),
@@ -1,22 +1,34 @@
1
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.
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
- * Two engines, one result shape:
6
+ * Three engines, one result shape:
7
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).
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
- export type PublishTarget = "store" | "repo";
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
- /** Store: the progress-tracker sys_id. Repo: the CI/CD progress id. */
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
- /** Store leg only — root result["sys_update_set.sys_id"]; "" when absent. */
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. Repo: "". */
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>;
@@ -1,23 +1,35 @@
1
1
  "use strict";
2
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.
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
- * Two engines, one result shape:
7
+ * Three engines, one result shape:
8
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).
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
- 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;
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
- result.executionSysId = startAnswer.answer;
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 result;
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=" + app.sysId,
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 result;
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 result;
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 result;
533
+ return null;
424
534
  }
425
- var state = classifyProgress(tree);
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
- 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
- }
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 = "HTTP " + progRes.status + " from the CI/CD progress API";
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" && params.target !== "repo") {
617
- throw new Error("publish-app: target must be 'store' or 'repo' (got '" +
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 === "store"
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 publishToStore(params, app);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.39",
3
+ "version": "0.0.40",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },