@postman-cs/onboarding-repo-sync 2.10.8 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,15 @@ The example permissions let `GITHUB_TOKEN` commit generated artifacts and update
69
69
 
70
70
  `postman-access-token` is required: every asset operation (environment create/get/update, collection read, mock, monitor) plus workspace-to-repository linking and system environment association runs through the access-token gateway. Use `postman-resolve-service-token-action` to mint it at runtime from a [Postman service account](https://learning.postman.com/docs/administration/service-accounts/) PMAK. Without it the action fails fast — the PMAK is not an asset-routing fallback; it only mints/re-mints the access token, powers the generated CI workflow's `postman login --with-api-key`, and mints the CI `POSTMAN_API_KEY` secret. See [docs/credentials.md](docs/credentials.md).
71
71
 
72
+ To manage the complete values for an environment, use a rich entry in the same input. String entries remain backward-compatible and receive generated values; rich entries replace the environment values exactly as supplied (plus the action's branch-ownership marker when applicable).
73
+
74
+ ```yaml
75
+ environments-json: >-
76
+ [{"slug":"dev","values":[{"key":"baseUrl","value":"https://dev.example.com"},{"key":"jwtToken","value":"","type":"secret"}]}]
77
+ ```
78
+
79
+ Secret-typed values must be empty. Supply the real secret as a masked runtime variable from the customer's vault when running the collection; repo-sync neither reads nor persists that value. `value`, `type`, and `enabled` default to `""`, `"default"`, and `true`. `env-runtime-urls-json` and generated credential slots apply only to string entries because a rich entry is the complete desired definition.
80
+
72
81
  ### Disable CI workflow generation
73
82
 
74
83
  For existing repositories that already own their CI workflow, disable workflow generation:
@@ -169,7 +178,7 @@ with:
169
178
  | `smoke-collection-id` | Smoke collection ID used for monitor creation. | no | |
170
179
  | `contract-collection-id` | Contract collection ID used for exported artifacts. | no | |
171
180
  | `onboarding-scope` | Onboarding scope. Use full for the complete pipeline or spec-only for repository linking and workspace/spec state only. | no | `full` |
172
- | `prebuilt-collections-json` | Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without cloud export. | no | `""` |
181
+ | `prebuilt-collections-json` | Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without a cloud snapshot read. | no | `""` |
173
182
  | `collection-sync-mode` | Collection sync lifecycle mode (refresh or version). | no | `refresh` |
174
183
  | `spec-sync-mode` | Spec sync lifecycle mode (update or version). | no | `update` |
175
184
  | `release-label` | Optional release label used for versioned naming. | no | |
@@ -178,7 +187,7 @@ with:
178
187
  | `mock-visibility` | Required mock access policy. Public is anonymous; private requires a runtime x-api-key supplied by the caller and is never persisted by repo-sync. | no | `private` |
179
188
  | `mock-environment-enabled` | Create or update a dedicated manual-validation environment whose baseUrl is the validated mock URL. This environment is excluded from runtime CI selection and never contains a mock credential. | no | `false` |
180
189
  | `monitor-cron` | Cron expression for monitor scheduling (e.g. '0 */6 * * *'). When empty, the monitor is created disabled and triggered to run once per workflow invocation (and once on every subsequent run). | no | `""` |
181
- | `environments-json` | JSON array of environment slugs to create or update. | no | `["prod"]` |
190
+ | `environments-json` | JSON array of environment slugs or full definitions ({slug, values}) to create or replace. Secret-typed values must be empty runtime slots. | no | `["prod"]` |
182
191
  | `git-provider` | Git provider override ('github', 'gitlab', 'bitbucket', 'azure-devops'). Auto-detected from environment when omitted. | no | |
183
192
  | `ado-token` | Azure DevOps personal access token or system token used to push commits in Azure Pipelines. Defaults to SYSTEM_ACCESSTOKEN when available. | no | |
184
193
  | `repo-url` | Explicit repository URL (GitHub, GitLab, or Azure DevOps). Defaults to the URL inferred from runner environment when omitted. For commit-and-push it must identify the checked-out origin. | no | |
package/action.yml CHANGED
@@ -43,7 +43,7 @@ inputs:
43
43
  required: false
44
44
  default: full
45
45
  prebuilt-collections-json:
46
- description: Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without cloud export.
46
+ description: Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without a cloud snapshot read.
47
47
  required: false
48
48
  default: ''
49
49
  collection-sync-mode:
@@ -76,7 +76,7 @@ inputs:
76
76
  required: false
77
77
  default: ""
78
78
  environments-json:
79
- description: JSON array of environment slugs to create or update.
79
+ description: JSON array of environment slugs or full definitions ({slug, values}) to create or replace. Secret-typed values must be empty runtime slots.
80
80
  required: false
81
81
  default: '["prod"]'
82
82
  git-provider:
package/dist/action.cjs CHANGED
@@ -117719,6 +117719,7 @@ var path6 = __toESM(require("node:path"), 1);
117719
117719
  var V2 = __toESM(require_v2(), 1);
117720
117720
  var import_transforms = __toESM(require_transforms(), 1);
117721
117721
  var import_v3 = __toESM(require_node2(), 1);
117722
+ var V2_COLLECTION_MODEL = V2.Collection;
117722
117723
  function asArray(value) {
117723
117724
  return Array.isArray(value) ? value : [];
117724
117725
  }
@@ -117739,13 +117740,23 @@ function normalizeGraphqlRequests(node) {
117739
117740
  }
117740
117741
  }
117741
117742
  async function convertAndSplitCollection(v2Collection, outputDir) {
117742
- const model = V2.Collection;
117743
- const parsed = model.parse(v2Collection ?? {});
117744
- const v3 = (0, import_transforms.transform)(model, import_transforms.FormatVersion.V3, parsed);
117743
+ const v3 = convertV2CollectionToV3Collection(v2Collection);
117744
+ await writeSplitCollection(v3, outputDir);
117745
+ }
117746
+ function convertV2CollectionToV3Collection(v2Collection) {
117747
+ const parsed = V2_COLLECTION_MODEL.parse(v2Collection ?? {});
117748
+ const v3 = (0, import_transforms.transform)(
117749
+ V2_COLLECTION_MODEL,
117750
+ import_transforms.FormatVersion.V3,
117751
+ parsed
117752
+ );
117745
117753
  for (const item of asArray(v3.items)) {
117746
117754
  normalizeGraphqlRequests(item);
117747
117755
  }
117748
- await writeSplitCollection(v3, outputDir);
117756
+ return v3;
117757
+ }
117758
+ function assertV2CollectionModel(value) {
117759
+ V2_COLLECTION_MODEL.parse(value ?? {});
117749
117760
  }
117750
117761
  function normalizeV3ExportIds(node) {
117751
117762
  if (!node || typeof node !== "object") return;
@@ -121303,7 +121314,7 @@ var postmanRepoSyncActionContract = {
121303
121314
  allowedValues: ["full", "spec-only"]
121304
121315
  },
121305
121316
  "prebuilt-collections-json": {
121306
- description: "Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without cloud export.",
121317
+ description: "Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without a cloud snapshot read.",
121307
121318
  required: false,
121308
121319
  default: ""
121309
121320
  },
@@ -121346,7 +121357,7 @@ var postmanRepoSyncActionContract = {
121346
121357
  default: ""
121347
121358
  },
121348
121359
  "environments-json": {
121349
- description: "JSON array of environment slugs to create or update.",
121360
+ description: "JSON array of environment slugs or full definitions ({slug, values}) to create or replace. Secret-typed values must be empty runtime slots.",
121350
121361
  required: false,
121351
121362
  default: '["prod"]'
121352
121363
  },
@@ -127837,8 +127848,8 @@ var PostmanGatewayAssetsClient = class {
127837
127848
  }
127838
127849
  /**
127839
127850
  * Validate and normalize a collection public UID for routes that require the
127840
- * full owner-prefixed UID (`<owner>-<uuid>`, 6 hyphen segments): collection
127841
- * GET export, collection-root PATCH/read, and private-mock runtime auth.
127851
+ * full owner-prefixed UID (`<owner>-<uuid>`, 6 hyphen segments): populated
127852
+ * collection snapshot, collection-root PATCH/read, and private-mock runtime auth.
127842
127853
  *
127843
127854
  * - Trims once.
127844
127855
  * - Rejects empty values.
@@ -128150,32 +128161,52 @@ var PostmanGatewayAssetsClient = class {
128150
128161
  );
128151
128162
  return match?.uid ? { uid: match.uid, name: match.name } : null;
128152
128163
  }
128153
- // --- collection read (service: collection, v3 export) ---
128164
+ // --- collection read (service: sync, populated v2.1 snapshot) ---
128154
128165
  //
128155
- // The gateway `collection` service does NOT serve spec-generated uids on the
128156
- // v2 `/collections/:uid` path (404 RESOURCE_NOT_FOUND, live-probed). It DOES
128157
- // serve `GET /v3/collections/:id/export`, which returns the canonical v3
128158
- // collection IR (`{ data: { collection: { ... } } }`). That v3 IR is fed
128159
- // straight to `convertAndSplitV3Collection` never round-tripped back to v2.
128160
- // The full owner-prefixed public UID must be sent verbatim; the bare model id
128161
- // is rejected on root mutation routes (403 FORBIDDEN) and must not be used
128162
- // here either, so the caller is responsible for passing the public UID.
128166
+ // This is the canonical full collection read used by
128167
+ // api-specification-service before whole-tree Sync updates. Collection v3
128168
+ // `/export` performs an expensive projection and has returned 500s for fresh
128169
+ // roots under provider load. The populated Sync read returns the complete
128170
+ // v2.1 model directly and is transformed in memory through the official
128171
+ // runtime.models V2 -> V3 pipeline before repository materialization.
128163
128172
  /**
128164
- * Fetch a collection's v3 IR through the gateway v3 export endpoint.
128165
- * Returns the `data.collection` object (canonical v3 shape with `$kind`
128166
- * discriminators, `items`, `variables`, `references`). Caller writes it to
128167
- * disk via `convertAndSplitV3Collection`. PMAK is never used for collection
128168
- * reads.
128173
+ * Fetch one complete v2.1 collection through a single retry-free populated
128174
+ * Sync GET. The response must be a v2.1 model for the requested public UID;
128175
+ * malformed or foreign snapshots fail closed. PMAK is never used.
128169
128176
  */
128170
128177
  async getCollection(uid) {
128171
128178
  const id = this.requireCollectionPublicUid(uid);
128172
128179
  const response = await this.gateway.requestJson({
128173
- service: "collection",
128180
+ service: "sync",
128174
128181
  method: "get",
128175
- path: `/v3/collections/${id}/export`
128176
- });
128177
- const data2 = this.asRecord(response?.data);
128178
- return data2?.collection ?? null;
128182
+ path: `/collection/${encodeURIComponent(id)}`,
128183
+ query: { populate: "true", format: "2.1.0", uid: "false" },
128184
+ retry: "none",
128185
+ fallback: "none"
128186
+ });
128187
+ const collection = this.asRecord(response?.data);
128188
+ const info2 = this.asRecord(collection?.info);
128189
+ const schema = typeof info2?.schema === "string" ? info2.schema.trim() : "";
128190
+ if (!collection || !info2 || typeof info2.name !== "string" || !info2.name.trim() || !/\/collection\/v2\.1\.0\/collection\.json\/?$/i.test(schema) || !Array.isArray(collection.item)) {
128191
+ throw new Error(
128192
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync read did not return a complete v2.1 collection"
128193
+ );
128194
+ }
128195
+ try {
128196
+ assertV2CollectionModel(collection);
128197
+ } catch (error2) {
128198
+ throw new Error(
128199
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync payload failed v2.1 schema validation",
128200
+ { cause: error2 }
128201
+ );
128202
+ }
128203
+ const observedId = String(info2._postman_id ?? collection.id ?? "").trim();
128204
+ if (!observedId || this.toModelId(observedId).toLowerCase() !== this.toModelId(id).toLowerCase()) {
128205
+ throw new Error(
128206
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync read returned a different collection identity"
128207
+ );
128208
+ }
128209
+ return collection;
128179
128210
  }
128180
128211
  // --- mocks (service: mock) ---
128181
128212
  async createMock(workspaceId, name, collectionUid, environmentUid, requestedVisibility = "private") {
@@ -129567,13 +129598,117 @@ function parseJsonMap(raw) {
129567
129598
  ])
129568
129599
  );
129569
129600
  }
129570
- function parseJsonArray(raw) {
129571
- if (!raw.trim()) return [];
129572
- const parsed = JSON.parse(raw);
129601
+ function parseEnvironmentInputs(raw) {
129602
+ if (!raw.trim()) return { environments: [], definitions: /* @__PURE__ */ Object.create(null) };
129603
+ let parsed;
129604
+ try {
129605
+ parsed = JSON.parse(raw);
129606
+ } catch {
129607
+ throw new Error("environments-json must contain valid JSON");
129608
+ }
129573
129609
  if (!Array.isArray(parsed)) {
129574
- throw new Error("Expected JSON array");
129610
+ throw new Error("environments-json must be a JSON array");
129611
+ }
129612
+ const environments = [];
129613
+ const definitions = /* @__PURE__ */ Object.create(null);
129614
+ const seen = /* @__PURE__ */ new Map();
129615
+ parsed.forEach((entry, environmentIndex) => {
129616
+ const label = `environments-json[${environmentIndex}]`;
129617
+ let slug;
129618
+ let rich = false;
129619
+ if (typeof entry === "string") {
129620
+ slug = entry;
129621
+ } else {
129622
+ if (!isPlainObject2(entry)) {
129623
+ throw new Error(`${label} must be a slug string or an environment definition`);
129624
+ }
129625
+ const unknownFields = Object.keys(entry).filter((key) => key !== "slug" && key !== "values");
129626
+ if (unknownFields.length > 0) {
129627
+ throw new Error(`${label} contains unsupported field "${unknownFields[0]}"`);
129628
+ }
129629
+ if (typeof entry.slug !== "string" || !Array.isArray(entry.values)) {
129630
+ throw new Error(`${label} must contain a string slug and a values array`);
129631
+ }
129632
+ slug = entry.slug;
129633
+ rich = true;
129634
+ const keys = /* @__PURE__ */ new Set();
129635
+ definitions[slug] = entry.values.map((value, valueIndex) => {
129636
+ const valueLabel = `${label}.values[${valueIndex}]`;
129637
+ if (!isPlainObject2(value)) {
129638
+ throw new Error(`${valueLabel} must be an object`);
129639
+ }
129640
+ const unknownValueFields = Object.keys(value).filter(
129641
+ (key) => !["key", "value", "type", "enabled"].includes(key)
129642
+ );
129643
+ if (unknownValueFields.length > 0) {
129644
+ throw new Error(`${valueLabel} contains unsupported field "${unknownValueFields[0]}"`);
129645
+ }
129646
+ if (typeof value.key !== "string" || !value.key.trim() || value.key !== value.key.trim()) {
129647
+ throw new Error(`${valueLabel}.key must be a non-empty string without surrounding whitespace`);
129648
+ }
129649
+ if (value.key === "x-pm-onboarding") {
129650
+ throw new Error(`${valueLabel}.key is reserved by repo-sync`);
129651
+ }
129652
+ if (keys.has(value.key)) {
129653
+ throw new Error(`${label} contains duplicate variable key "${value.key}"`);
129654
+ }
129655
+ keys.add(value.key);
129656
+ if (value.value !== void 0 && typeof value.value !== "string") {
129657
+ throw new Error(`${valueLabel}.value must be a string when provided`);
129658
+ }
129659
+ if (value.type !== void 0 && value.type !== "default" && value.type !== "secret") {
129660
+ throw new Error(`${valueLabel}.type must be "default" or "secret"`);
129661
+ }
129662
+ if (value.enabled !== void 0 && typeof value.enabled !== "boolean") {
129663
+ throw new Error(`${valueLabel}.enabled must be a boolean when provided`);
129664
+ }
129665
+ const type = value.type ?? "default";
129666
+ const normalizedValue = value.value ?? "";
129667
+ if (type === "secret" && normalizedValue) {
129668
+ throw new Error(`${valueLabel} cannot contain a populated secret value; inject it at runtime`);
129669
+ }
129670
+ return { key: value.key, value: normalizedValue, type, enabled: value.enabled ?? true };
129671
+ });
129672
+ }
129673
+ if (rich && !/^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/.test(slug)) {
129674
+ throw new Error(`${label} rich slug must use only letters, numbers, dots, hyphens, or underscores`);
129675
+ }
129676
+ if (rich && Object.prototype.hasOwnProperty.call(Object.prototype, slug)) {
129677
+ throw new Error(`${label} rich slug conflicts with a reserved object property`);
129678
+ }
129679
+ if (rich && /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(slug)) {
129680
+ throw new Error(`${label} rich slug conflicts with a Windows device name`);
129681
+ }
129682
+ if (!rich && !slug.trim()) {
129683
+ throw new Error(`${label} slug must be a non-empty string`);
129684
+ }
129685
+ const priorWasRich = seen.get(slug);
129686
+ if (priorWasRich !== void 0 && (rich || priorWasRich)) {
129687
+ throw new Error(`environments-json contains duplicate slug "${slug}"`);
129688
+ }
129689
+ if (priorWasRich === void 0) seen.set(slug, rich);
129690
+ environments.push(slug);
129691
+ });
129692
+ return { environments, definitions };
129693
+ }
129694
+ function normalizeProgrammaticEnvironmentDefinitions(inputs) {
129695
+ const definitions = inputs.environmentDefinitions;
129696
+ if (!definitions) return inputs;
129697
+ const prototype = Object.getPrototypeOf(definitions);
129698
+ if (prototype !== Object.prototype && prototype !== null) {
129699
+ throw new Error("environmentDefinitions must be a plain object");
129700
+ }
129701
+ const entries = inputs.environments.map(
129702
+ (slug) => Object.prototype.hasOwnProperty.call(definitions, slug) ? { slug, values: definitions[slug] } : slug
129703
+ );
129704
+ let raw;
129705
+ try {
129706
+ raw = JSON.stringify(entries);
129707
+ } catch {
129708
+ throw new Error("environmentDefinitions must contain JSON-serializable values");
129575
129709
  }
129576
- return parsed.map((entry) => String(entry));
129710
+ const parsed = parseEnvironmentInputs(raw);
129711
+ return { ...inputs, environments: parsed.environments, environmentDefinitions: parsed.definitions };
129577
129712
  }
129578
129713
  function readInput(actionCore, name, required = false) {
129579
129714
  return actionCore.getInput(name, { required }).trim();
@@ -129670,7 +129805,7 @@ function resolveInputs(env = process.env) {
129670
129805
  },
129671
129806
  env
129672
129807
  );
129673
- const environments = parseJsonArray(getInput2("environments-json", env) || '["prod"]');
129808
+ const parsedEnvironments = parseEnvironmentInputs(getInput2("environments-json", env) || '["prod"]');
129674
129809
  const secretsResolverProvider = parseSecretsResolverProvider(getInput2("secrets-resolver", env));
129675
129810
  const systemEnvMap = parseJsonMap(getInput2("system-env-map-json", env) || "{}");
129676
129811
  const environmentUids = parseJsonMap(getInput2("environment-uids-json", env) || "{}");
@@ -129708,7 +129843,8 @@ function resolveInputs(env = process.env) {
129708
129843
  secretsResolverProvider,
129709
129844
  specSyncMode: normalizeSpecSyncMode(getInput2("spec-sync-mode", env) || "update"),
129710
129845
  releaseLabel: normalizeReleaseLabel(getInput2("release-label", env)) || void 0,
129711
- environments: environments.length > 0 ? environments : ["prod"],
129846
+ environments: parsedEnvironments.environments.length > 0 ? parsedEnvironments.environments : ["prod"],
129847
+ environmentDefinitions: parsedEnvironments.definitions,
129712
129848
  repoUrl: repoContext.repoUrl || "",
129713
129849
  integrationBackend: getInput2("integration-backend", env) || "bifrost",
129714
129850
  workspaceLinkEnabled: parseBooleanInput(getInput2("workspace-link-enabled", env), true),
@@ -130376,7 +130512,8 @@ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMar
130376
130512
  }
130377
130513
  } catch {
130378
130514
  }
130379
- const values2 = buildEnvironmentValues(envName, runtimeUrl, {
130515
+ const definedValues2 = inputs.environmentDefinitions?.[envName];
130516
+ const values2 = definedValues2 ? definedValues2.map((value) => ({ ...value })) : buildEnvironmentValues(envName, runtimeUrl, {
130380
130517
  secretsResolverProvider: inputs.secretsResolverProvider,
130381
130518
  preservedCredentialValues: priorValues
130382
130519
  });
@@ -130399,7 +130536,8 @@ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMar
130399
130536
  dependencies.core.setOutput("environment-uids-json", JSON.stringify(envUids));
130400
130537
  continue;
130401
130538
  }
130402
- const values = buildEnvironmentValues(envName, runtimeUrl, {
130539
+ const definedValues = inputs.environmentDefinitions?.[envName];
130540
+ const values = definedValues ? definedValues.map((value) => ({ ...value })) : buildEnvironmentValues(envName, runtimeUrl, {
130403
130541
  secretsResolverProvider: inputs.secretsResolverProvider
130404
130542
  });
130405
130543
  if (assetMarker) values.push({ key: "x-pm-onboarding", value: JSON.stringify(assetMarker), type: "default" });
@@ -131183,13 +131321,14 @@ function applyPrivateMockPrebuiltPlans(plans) {
131183
131321
  }
131184
131322
  }
131185
131323
  async function preparePrivateMockCloudCollection(role, collectionId, postman) {
131186
- const col = await postman.getCollection(collectionId);
131187
- const { collection } = applyPrivateMockExportCleanup(col, {
131324
+ const snapshot = await postman.getCollection(collectionId);
131325
+ const v3Collection = convertV2CollectionToV3Collection(snapshot);
131326
+ const { collection } = applyPrivateMockExportCleanup(v3Collection, {
131188
131327
  stripManagedBlocks: isPrivateMockLegacyExportCleanupEnabled()
131189
131328
  });
131190
131329
  if (!verifyPrivateMockRootHook(collection)) {
131191
131330
  throw new Error(
131192
- `PRIVATE_MOCK_AUTH_ROOT_UNVERIFIED: Managed root hook missing from exported ${role} collection ${collectionId}`
131331
+ `PRIVATE_MOCK_AUTH_ROOT_UNVERIFIED: Managed root hook missing from populated ${role} collection snapshot ${collectionId}`
131193
131332
  );
131194
131333
  }
131195
131334
  return collection;
@@ -131235,7 +131374,7 @@ async function acquireCollectionArtifact(options) {
131235
131374
  return { reusePrebuilt: true };
131236
131375
  }
131237
131376
  if (entry) {
131238
- core.info(privateMockAuth ? `Private mock has no locally reconciled exact prebuilt ${role} collection; exporting from cloud` : `Prebuilt ${role} collection entry present but did not exactly match; exporting from cloud`);
131377
+ core.info(privateMockAuth ? `Private mock has no locally reconciled exact prebuilt ${role} collection; reading populated cloud snapshot` : `Prebuilt ${role} collection entry present but did not exactly match; reading populated cloud snapshot`);
131239
131378
  }
131240
131379
  const cloud = privateMockAuth ? await preparePrivateMockCloudCollection(role, collectionId, postman) : await postman.getCollection(collectionId);
131241
131380
  return { reusePrebuilt: false, cloudCollection: cloud };
@@ -131589,6 +131728,7 @@ async function commitAndPushGeneratedFiles(inputs, dependencies, privateMockAuth
131589
131728
  };
131590
131729
  }
131591
131730
  async function runRepoSync(inputs, dependencies, executionContext) {
131731
+ inputs = normalizeProgrammaticEnvironmentDefinitions(inputs);
131592
131732
  const telemetry = createTelemetryContext({ action: "postman-repo-sync-action", actionVersion: resolveActionVersion2(), logger: dependencies.core });
131593
131733
  telemetry.setTeamId(dependencies.teamId);
131594
131734
  const logger = resolveRepoSyncLogger(dependencies);
@@ -132482,9 +132622,9 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
132482
132622
  getEnvironment: gatewayAssets.getEnvironment.bind(gatewayAssets),
132483
132623
  updateEnvironment: gatewayAssets.updateEnvironment.bind(gatewayAssets),
132484
132624
  findEnvironmentByName: gatewayAssets.findEnvironmentByName.bind(gatewayAssets),
132485
- // Collection read via the v3 export endpoint — returns canonical v3 IR,
132486
- // written to disk by `convertAndSplitAnyCollection`. PMAK is never used for
132487
- // collection reads.
132625
+ // Collection read via one retry-free populated Sync GET — returns a full
132626
+ // v2.1 snapshot that the official runtime.models transform converts to v3
132627
+ // before repository materialization. PMAK is never used for reads.
132488
132628
  getCollection: gatewayAssets.getCollection.bind(gatewayAssets),
132489
132629
  // Mocks via the `mock` service, collection-based monitors via the `monitors`
132490
132630
  // service (jobTemplates). Both reference the collection by its full public
package/dist/cli.cjs CHANGED
@@ -115824,6 +115824,7 @@ var path = __toESM(require("node:path"), 1);
115824
115824
  var V2 = __toESM(require_v2(), 1);
115825
115825
  var import_transforms = __toESM(require_transforms(), 1);
115826
115826
  var import_v3 = __toESM(require_node2(), 1);
115827
+ var V2_COLLECTION_MODEL = V2.Collection;
115827
115828
  function asArray(value) {
115828
115829
  return Array.isArray(value) ? value : [];
115829
115830
  }
@@ -115844,13 +115845,23 @@ function normalizeGraphqlRequests(node) {
115844
115845
  }
115845
115846
  }
115846
115847
  async function convertAndSplitCollection(v2Collection, outputDir) {
115847
- const model = V2.Collection;
115848
- const parsed = model.parse(v2Collection ?? {});
115849
- const v3 = (0, import_transforms.transform)(model, import_transforms.FormatVersion.V3, parsed);
115848
+ const v3 = convertV2CollectionToV3Collection(v2Collection);
115849
+ await writeSplitCollection(v3, outputDir);
115850
+ }
115851
+ function convertV2CollectionToV3Collection(v2Collection) {
115852
+ const parsed = V2_COLLECTION_MODEL.parse(v2Collection ?? {});
115853
+ const v3 = (0, import_transforms.transform)(
115854
+ V2_COLLECTION_MODEL,
115855
+ import_transforms.FormatVersion.V3,
115856
+ parsed
115857
+ );
115850
115858
  for (const item of asArray(v3.items)) {
115851
115859
  normalizeGraphqlRequests(item);
115852
115860
  }
115853
- await writeSplitCollection(v3, outputDir);
115861
+ return v3;
115862
+ }
115863
+ function assertV2CollectionModel(value) {
115864
+ V2_COLLECTION_MODEL.parse(value ?? {});
115854
115865
  }
115855
115866
  function normalizeV3ExportIds(node) {
115856
115867
  if (!node || typeof node !== "object") return;
@@ -119408,7 +119419,7 @@ var postmanRepoSyncActionContract = {
119408
119419
  allowedValues: ["full", "spec-only"]
119409
119420
  },
119410
119421
  "prebuilt-collections-json": {
119411
- description: "Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without cloud export.",
119422
+ description: "Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without a cloud snapshot read.",
119412
119423
  required: false,
119413
119424
  default: ""
119414
119425
  },
@@ -119451,7 +119462,7 @@ var postmanRepoSyncActionContract = {
119451
119462
  default: ""
119452
119463
  },
119453
119464
  "environments-json": {
119454
- description: "JSON array of environment slugs to create or update.",
119465
+ description: "JSON array of environment slugs or full definitions ({slug, values}) to create or replace. Secret-typed values must be empty runtime slots.",
119455
119466
  required: false,
119456
119467
  default: '["prod"]'
119457
119468
  },
@@ -125942,8 +125953,8 @@ var PostmanGatewayAssetsClient = class {
125942
125953
  }
125943
125954
  /**
125944
125955
  * Validate and normalize a collection public UID for routes that require the
125945
- * full owner-prefixed UID (`<owner>-<uuid>`, 6 hyphen segments): collection
125946
- * GET export, collection-root PATCH/read, and private-mock runtime auth.
125956
+ * full owner-prefixed UID (`<owner>-<uuid>`, 6 hyphen segments): populated
125957
+ * collection snapshot, collection-root PATCH/read, and private-mock runtime auth.
125947
125958
  *
125948
125959
  * - Trims once.
125949
125960
  * - Rejects empty values.
@@ -126255,32 +126266,52 @@ var PostmanGatewayAssetsClient = class {
126255
126266
  );
126256
126267
  return match?.uid ? { uid: match.uid, name: match.name } : null;
126257
126268
  }
126258
- // --- collection read (service: collection, v3 export) ---
126269
+ // --- collection read (service: sync, populated v2.1 snapshot) ---
126259
126270
  //
126260
- // The gateway `collection` service does NOT serve spec-generated uids on the
126261
- // v2 `/collections/:uid` path (404 RESOURCE_NOT_FOUND, live-probed). It DOES
126262
- // serve `GET /v3/collections/:id/export`, which returns the canonical v3
126263
- // collection IR (`{ data: { collection: { ... } } }`). That v3 IR is fed
126264
- // straight to `convertAndSplitV3Collection` never round-tripped back to v2.
126265
- // The full owner-prefixed public UID must be sent verbatim; the bare model id
126266
- // is rejected on root mutation routes (403 FORBIDDEN) and must not be used
126267
- // here either, so the caller is responsible for passing the public UID.
126271
+ // This is the canonical full collection read used by
126272
+ // api-specification-service before whole-tree Sync updates. Collection v3
126273
+ // `/export` performs an expensive projection and has returned 500s for fresh
126274
+ // roots under provider load. The populated Sync read returns the complete
126275
+ // v2.1 model directly and is transformed in memory through the official
126276
+ // runtime.models V2 -> V3 pipeline before repository materialization.
126268
126277
  /**
126269
- * Fetch a collection's v3 IR through the gateway v3 export endpoint.
126270
- * Returns the `data.collection` object (canonical v3 shape with `$kind`
126271
- * discriminators, `items`, `variables`, `references`). Caller writes it to
126272
- * disk via `convertAndSplitV3Collection`. PMAK is never used for collection
126273
- * reads.
126278
+ * Fetch one complete v2.1 collection through a single retry-free populated
126279
+ * Sync GET. The response must be a v2.1 model for the requested public UID;
126280
+ * malformed or foreign snapshots fail closed. PMAK is never used.
126274
126281
  */
126275
126282
  async getCollection(uid) {
126276
126283
  const id = this.requireCollectionPublicUid(uid);
126277
126284
  const response = await this.gateway.requestJson({
126278
- service: "collection",
126285
+ service: "sync",
126279
126286
  method: "get",
126280
- path: `/v3/collections/${id}/export`
126281
- });
126282
- const data2 = this.asRecord(response?.data);
126283
- return data2?.collection ?? null;
126287
+ path: `/collection/${encodeURIComponent(id)}`,
126288
+ query: { populate: "true", format: "2.1.0", uid: "false" },
126289
+ retry: "none",
126290
+ fallback: "none"
126291
+ });
126292
+ const collection = this.asRecord(response?.data);
126293
+ const info = this.asRecord(collection?.info);
126294
+ const schema = typeof info?.schema === "string" ? info.schema.trim() : "";
126295
+ if (!collection || !info || typeof info.name !== "string" || !info.name.trim() || !/\/collection\/v2\.1\.0\/collection\.json\/?$/i.test(schema) || !Array.isArray(collection.item)) {
126296
+ throw new Error(
126297
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync read did not return a complete v2.1 collection"
126298
+ );
126299
+ }
126300
+ try {
126301
+ assertV2CollectionModel(collection);
126302
+ } catch (error) {
126303
+ throw new Error(
126304
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync payload failed v2.1 schema validation",
126305
+ { cause: error }
126306
+ );
126307
+ }
126308
+ const observedId = String(info._postman_id ?? collection.id ?? "").trim();
126309
+ if (!observedId || this.toModelId(observedId).toLowerCase() !== this.toModelId(id).toLowerCase()) {
126310
+ throw new Error(
126311
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync read returned a different collection identity"
126312
+ );
126313
+ }
126314
+ return collection;
126284
126315
  }
126285
126316
  // --- mocks (service: mock) ---
126286
126317
  async createMock(workspaceId, name, collectionUid, environmentUid, requestedVisibility = "private") {
@@ -127620,13 +127651,117 @@ function parseJsonMap(raw) {
127620
127651
  ])
127621
127652
  );
127622
127653
  }
127623
- function parseJsonArray(raw) {
127624
- if (!raw.trim()) return [];
127625
- const parsed = JSON.parse(raw);
127654
+ function parseEnvironmentInputs(raw) {
127655
+ if (!raw.trim()) return { environments: [], definitions: /* @__PURE__ */ Object.create(null) };
127656
+ let parsed;
127657
+ try {
127658
+ parsed = JSON.parse(raw);
127659
+ } catch {
127660
+ throw new Error("environments-json must contain valid JSON");
127661
+ }
127626
127662
  if (!Array.isArray(parsed)) {
127627
- throw new Error("Expected JSON array");
127663
+ throw new Error("environments-json must be a JSON array");
127664
+ }
127665
+ const environments = [];
127666
+ const definitions = /* @__PURE__ */ Object.create(null);
127667
+ const seen = /* @__PURE__ */ new Map();
127668
+ parsed.forEach((entry, environmentIndex) => {
127669
+ const label = `environments-json[${environmentIndex}]`;
127670
+ let slug;
127671
+ let rich = false;
127672
+ if (typeof entry === "string") {
127673
+ slug = entry;
127674
+ } else {
127675
+ if (!isPlainObject2(entry)) {
127676
+ throw new Error(`${label} must be a slug string or an environment definition`);
127677
+ }
127678
+ const unknownFields = Object.keys(entry).filter((key) => key !== "slug" && key !== "values");
127679
+ if (unknownFields.length > 0) {
127680
+ throw new Error(`${label} contains unsupported field "${unknownFields[0]}"`);
127681
+ }
127682
+ if (typeof entry.slug !== "string" || !Array.isArray(entry.values)) {
127683
+ throw new Error(`${label} must contain a string slug and a values array`);
127684
+ }
127685
+ slug = entry.slug;
127686
+ rich = true;
127687
+ const keys = /* @__PURE__ */ new Set();
127688
+ definitions[slug] = entry.values.map((value, valueIndex) => {
127689
+ const valueLabel = `${label}.values[${valueIndex}]`;
127690
+ if (!isPlainObject2(value)) {
127691
+ throw new Error(`${valueLabel} must be an object`);
127692
+ }
127693
+ const unknownValueFields = Object.keys(value).filter(
127694
+ (key) => !["key", "value", "type", "enabled"].includes(key)
127695
+ );
127696
+ if (unknownValueFields.length > 0) {
127697
+ throw new Error(`${valueLabel} contains unsupported field "${unknownValueFields[0]}"`);
127698
+ }
127699
+ if (typeof value.key !== "string" || !value.key.trim() || value.key !== value.key.trim()) {
127700
+ throw new Error(`${valueLabel}.key must be a non-empty string without surrounding whitespace`);
127701
+ }
127702
+ if (value.key === "x-pm-onboarding") {
127703
+ throw new Error(`${valueLabel}.key is reserved by repo-sync`);
127704
+ }
127705
+ if (keys.has(value.key)) {
127706
+ throw new Error(`${label} contains duplicate variable key "${value.key}"`);
127707
+ }
127708
+ keys.add(value.key);
127709
+ if (value.value !== void 0 && typeof value.value !== "string") {
127710
+ throw new Error(`${valueLabel}.value must be a string when provided`);
127711
+ }
127712
+ if (value.type !== void 0 && value.type !== "default" && value.type !== "secret") {
127713
+ throw new Error(`${valueLabel}.type must be "default" or "secret"`);
127714
+ }
127715
+ if (value.enabled !== void 0 && typeof value.enabled !== "boolean") {
127716
+ throw new Error(`${valueLabel}.enabled must be a boolean when provided`);
127717
+ }
127718
+ const type = value.type ?? "default";
127719
+ const normalizedValue = value.value ?? "";
127720
+ if (type === "secret" && normalizedValue) {
127721
+ throw new Error(`${valueLabel} cannot contain a populated secret value; inject it at runtime`);
127722
+ }
127723
+ return { key: value.key, value: normalizedValue, type, enabled: value.enabled ?? true };
127724
+ });
127725
+ }
127726
+ if (rich && !/^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/.test(slug)) {
127727
+ throw new Error(`${label} rich slug must use only letters, numbers, dots, hyphens, or underscores`);
127728
+ }
127729
+ if (rich && Object.prototype.hasOwnProperty.call(Object.prototype, slug)) {
127730
+ throw new Error(`${label} rich slug conflicts with a reserved object property`);
127731
+ }
127732
+ if (rich && /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(slug)) {
127733
+ throw new Error(`${label} rich slug conflicts with a Windows device name`);
127734
+ }
127735
+ if (!rich && !slug.trim()) {
127736
+ throw new Error(`${label} slug must be a non-empty string`);
127737
+ }
127738
+ const priorWasRich = seen.get(slug);
127739
+ if (priorWasRich !== void 0 && (rich || priorWasRich)) {
127740
+ throw new Error(`environments-json contains duplicate slug "${slug}"`);
127741
+ }
127742
+ if (priorWasRich === void 0) seen.set(slug, rich);
127743
+ environments.push(slug);
127744
+ });
127745
+ return { environments, definitions };
127746
+ }
127747
+ function normalizeProgrammaticEnvironmentDefinitions(inputs) {
127748
+ const definitions = inputs.environmentDefinitions;
127749
+ if (!definitions) return inputs;
127750
+ const prototype = Object.getPrototypeOf(definitions);
127751
+ if (prototype !== Object.prototype && prototype !== null) {
127752
+ throw new Error("environmentDefinitions must be a plain object");
127753
+ }
127754
+ const entries = inputs.environments.map(
127755
+ (slug) => Object.prototype.hasOwnProperty.call(definitions, slug) ? { slug, values: definitions[slug] } : slug
127756
+ );
127757
+ let raw;
127758
+ try {
127759
+ raw = JSON.stringify(entries);
127760
+ } catch {
127761
+ throw new Error("environmentDefinitions must contain JSON-serializable values");
127628
127762
  }
127629
- return parsed.map((entry) => String(entry));
127763
+ const parsed = parseEnvironmentInputs(raw);
127764
+ return { ...inputs, environments: parsed.environments, environmentDefinitions: parsed.definitions };
127630
127765
  }
127631
127766
  function normalizeRepoWriteMode(value) {
127632
127767
  if (value === "none" || value === "commit-only" || value === "commit-and-push") {
@@ -127720,7 +127855,7 @@ function resolveInputs(env = process.env) {
127720
127855
  },
127721
127856
  env
127722
127857
  );
127723
- const environments = parseJsonArray(getInput("environments-json", env) || '["prod"]');
127858
+ const parsedEnvironments = parseEnvironmentInputs(getInput("environments-json", env) || '["prod"]');
127724
127859
  const secretsResolverProvider = parseSecretsResolverProvider(getInput("secrets-resolver", env));
127725
127860
  const systemEnvMap = parseJsonMap(getInput("system-env-map-json", env) || "{}");
127726
127861
  const environmentUids = parseJsonMap(getInput("environment-uids-json", env) || "{}");
@@ -127758,7 +127893,8 @@ function resolveInputs(env = process.env) {
127758
127893
  secretsResolverProvider,
127759
127894
  specSyncMode: normalizeSpecSyncMode(getInput("spec-sync-mode", env) || "update"),
127760
127895
  releaseLabel: normalizeReleaseLabel(getInput("release-label", env)) || void 0,
127761
- environments: environments.length > 0 ? environments : ["prod"],
127896
+ environments: parsedEnvironments.environments.length > 0 ? parsedEnvironments.environments : ["prod"],
127897
+ environmentDefinitions: parsedEnvironments.definitions,
127762
127898
  repoUrl: repoContext.repoUrl || "",
127763
127899
  integrationBackend: getInput("integration-backend", env) || "bifrost",
127764
127900
  workspaceLinkEnabled: parseBooleanInput(getInput("workspace-link-enabled", env), true),
@@ -128285,7 +128421,8 @@ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMar
128285
128421
  }
128286
128422
  } catch {
128287
128423
  }
128288
- const values2 = buildEnvironmentValues(envName, runtimeUrl, {
128424
+ const definedValues2 = inputs.environmentDefinitions?.[envName];
128425
+ const values2 = definedValues2 ? definedValues2.map((value) => ({ ...value })) : buildEnvironmentValues(envName, runtimeUrl, {
128289
128426
  secretsResolverProvider: inputs.secretsResolverProvider,
128290
128427
  preservedCredentialValues: priorValues
128291
128428
  });
@@ -128308,7 +128445,8 @@ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMar
128308
128445
  dependencies.core.setOutput("environment-uids-json", JSON.stringify(envUids));
128309
128446
  continue;
128310
128447
  }
128311
- const values = buildEnvironmentValues(envName, runtimeUrl, {
128448
+ const definedValues = inputs.environmentDefinitions?.[envName];
128449
+ const values = definedValues ? definedValues.map((value) => ({ ...value })) : buildEnvironmentValues(envName, runtimeUrl, {
128312
128450
  secretsResolverProvider: inputs.secretsResolverProvider
128313
128451
  });
128314
128452
  if (assetMarker) values.push({ key: "x-pm-onboarding", value: JSON.stringify(assetMarker), type: "default" });
@@ -129092,13 +129230,14 @@ function applyPrivateMockPrebuiltPlans(plans) {
129092
129230
  }
129093
129231
  }
129094
129232
  async function preparePrivateMockCloudCollection(role, collectionId, postman) {
129095
- const col = await postman.getCollection(collectionId);
129096
- const { collection } = applyPrivateMockExportCleanup(col, {
129233
+ const snapshot = await postman.getCollection(collectionId);
129234
+ const v3Collection = convertV2CollectionToV3Collection(snapshot);
129235
+ const { collection } = applyPrivateMockExportCleanup(v3Collection, {
129097
129236
  stripManagedBlocks: isPrivateMockLegacyExportCleanupEnabled()
129098
129237
  });
129099
129238
  if (!verifyPrivateMockRootHook(collection)) {
129100
129239
  throw new Error(
129101
- `PRIVATE_MOCK_AUTH_ROOT_UNVERIFIED: Managed root hook missing from exported ${role} collection ${collectionId}`
129240
+ `PRIVATE_MOCK_AUTH_ROOT_UNVERIFIED: Managed root hook missing from populated ${role} collection snapshot ${collectionId}`
129102
129241
  );
129103
129242
  }
129104
129243
  return collection;
@@ -129144,7 +129283,7 @@ async function acquireCollectionArtifact(options) {
129144
129283
  return { reusePrebuilt: true };
129145
129284
  }
129146
129285
  if (entry) {
129147
- core.info(privateMockAuth ? `Private mock has no locally reconciled exact prebuilt ${role} collection; exporting from cloud` : `Prebuilt ${role} collection entry present but did not exactly match; exporting from cloud`);
129286
+ core.info(privateMockAuth ? `Private mock has no locally reconciled exact prebuilt ${role} collection; reading populated cloud snapshot` : `Prebuilt ${role} collection entry present but did not exactly match; reading populated cloud snapshot`);
129148
129287
  }
129149
129288
  const cloud = privateMockAuth ? await preparePrivateMockCloudCollection(role, collectionId, postman) : await postman.getCollection(collectionId);
129150
129289
  return { reusePrebuilt: false, cloudCollection: cloud };
@@ -129498,6 +129637,7 @@ async function commitAndPushGeneratedFiles(inputs, dependencies, privateMockAuth
129498
129637
  };
129499
129638
  }
129500
129639
  async function runRepoSync(inputs, dependencies, executionContext) {
129640
+ inputs = normalizeProgrammaticEnvironmentDefinitions(inputs);
129501
129641
  const telemetry = createTelemetryContext({ action: "postman-repo-sync-action", actionVersion: resolveActionVersion2(), logger: dependencies.core });
129502
129642
  telemetry.setTeamId(dependencies.teamId);
129503
129643
  const logger = resolveRepoSyncLogger(dependencies);
@@ -130391,9 +130531,9 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
130391
130531
  getEnvironment: gatewayAssets.getEnvironment.bind(gatewayAssets),
130392
130532
  updateEnvironment: gatewayAssets.updateEnvironment.bind(gatewayAssets),
130393
130533
  findEnvironmentByName: gatewayAssets.findEnvironmentByName.bind(gatewayAssets),
130394
- // Collection read via the v3 export endpoint — returns canonical v3 IR,
130395
- // written to disk by `convertAndSplitAnyCollection`. PMAK is never used for
130396
- // collection reads.
130534
+ // Collection read via one retry-free populated Sync GET — returns a full
130535
+ // v2.1 snapshot that the official runtime.models transform converts to v3
130536
+ // before repository materialization. PMAK is never used for reads.
130397
130537
  getCollection: gatewayAssets.getCollection.bind(gatewayAssets),
130398
130538
  // Mocks via the `mock` service, collection-based monitors via the `monitors`
130399
130539
  // service (jobTemplates). Both reference the collection by its full public
package/dist/index.cjs CHANGED
@@ -117745,6 +117745,7 @@ var path6 = __toESM(require("node:path"), 1);
117745
117745
  var V2 = __toESM(require_v2(), 1);
117746
117746
  var import_transforms = __toESM(require_transforms(), 1);
117747
117747
  var import_v3 = __toESM(require_node2(), 1);
117748
+ var V2_COLLECTION_MODEL = V2.Collection;
117748
117749
  function asArray(value) {
117749
117750
  return Array.isArray(value) ? value : [];
117750
117751
  }
@@ -117765,13 +117766,23 @@ function normalizeGraphqlRequests(node) {
117765
117766
  }
117766
117767
  }
117767
117768
  async function convertAndSplitCollection(v2Collection, outputDir) {
117768
- const model = V2.Collection;
117769
- const parsed = model.parse(v2Collection ?? {});
117770
- const v3 = (0, import_transforms.transform)(model, import_transforms.FormatVersion.V3, parsed);
117769
+ const v3 = convertV2CollectionToV3Collection(v2Collection);
117770
+ await writeSplitCollection(v3, outputDir);
117771
+ }
117772
+ function convertV2CollectionToV3Collection(v2Collection) {
117773
+ const parsed = V2_COLLECTION_MODEL.parse(v2Collection ?? {});
117774
+ const v3 = (0, import_transforms.transform)(
117775
+ V2_COLLECTION_MODEL,
117776
+ import_transforms.FormatVersion.V3,
117777
+ parsed
117778
+ );
117771
117779
  for (const item of asArray(v3.items)) {
117772
117780
  normalizeGraphqlRequests(item);
117773
117781
  }
117774
- await writeSplitCollection(v3, outputDir);
117782
+ return v3;
117783
+ }
117784
+ function assertV2CollectionModel(value) {
117785
+ V2_COLLECTION_MODEL.parse(value ?? {});
117775
117786
  }
117776
117787
  function normalizeV3ExportIds(node) {
117777
117788
  if (!node || typeof node !== "object") return;
@@ -121329,7 +121340,7 @@ var postmanRepoSyncActionContract = {
121329
121340
  allowedValues: ["full", "spec-only"]
121330
121341
  },
121331
121342
  "prebuilt-collections-json": {
121332
- description: "Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without cloud export.",
121343
+ description: "Optional digest-bound JSON manifest of unique baseline, smoke, or contract roles with confined repo-relative path, SHA-256 artifact digest of the on-disk v3 collection tree (sorted relative-path + NUL + bytes + NUL), and canonical cloud ID. The optional payloadDigest field is the semantic v2 payload digest carried for provenance (format-validated only, not the reuse gate). Exact role, path, cloudId, and artifactDigest matches reuse the on-disk tree without a cloud snapshot read.",
121333
121344
  required: false,
121334
121345
  default: ""
121335
121346
  },
@@ -121372,7 +121383,7 @@ var postmanRepoSyncActionContract = {
121372
121383
  default: ""
121373
121384
  },
121374
121385
  "environments-json": {
121375
- description: "JSON array of environment slugs to create or update.",
121386
+ description: "JSON array of environment slugs or full definitions ({slug, values}) to create or replace. Secret-typed values must be empty runtime slots.",
121376
121387
  required: false,
121377
121388
  default: '["prod"]'
121378
121389
  },
@@ -127863,8 +127874,8 @@ var PostmanGatewayAssetsClient = class {
127863
127874
  }
127864
127875
  /**
127865
127876
  * Validate and normalize a collection public UID for routes that require the
127866
- * full owner-prefixed UID (`<owner>-<uuid>`, 6 hyphen segments): collection
127867
- * GET export, collection-root PATCH/read, and private-mock runtime auth.
127877
+ * full owner-prefixed UID (`<owner>-<uuid>`, 6 hyphen segments): populated
127878
+ * collection snapshot, collection-root PATCH/read, and private-mock runtime auth.
127868
127879
  *
127869
127880
  * - Trims once.
127870
127881
  * - Rejects empty values.
@@ -128176,32 +128187,52 @@ var PostmanGatewayAssetsClient = class {
128176
128187
  );
128177
128188
  return match?.uid ? { uid: match.uid, name: match.name } : null;
128178
128189
  }
128179
- // --- collection read (service: collection, v3 export) ---
128190
+ // --- collection read (service: sync, populated v2.1 snapshot) ---
128180
128191
  //
128181
- // The gateway `collection` service does NOT serve spec-generated uids on the
128182
- // v2 `/collections/:uid` path (404 RESOURCE_NOT_FOUND, live-probed). It DOES
128183
- // serve `GET /v3/collections/:id/export`, which returns the canonical v3
128184
- // collection IR (`{ data: { collection: { ... } } }`). That v3 IR is fed
128185
- // straight to `convertAndSplitV3Collection` never round-tripped back to v2.
128186
- // The full owner-prefixed public UID must be sent verbatim; the bare model id
128187
- // is rejected on root mutation routes (403 FORBIDDEN) and must not be used
128188
- // here either, so the caller is responsible for passing the public UID.
128192
+ // This is the canonical full collection read used by
128193
+ // api-specification-service before whole-tree Sync updates. Collection v3
128194
+ // `/export` performs an expensive projection and has returned 500s for fresh
128195
+ // roots under provider load. The populated Sync read returns the complete
128196
+ // v2.1 model directly and is transformed in memory through the official
128197
+ // runtime.models V2 -> V3 pipeline before repository materialization.
128189
128198
  /**
128190
- * Fetch a collection's v3 IR through the gateway v3 export endpoint.
128191
- * Returns the `data.collection` object (canonical v3 shape with `$kind`
128192
- * discriminators, `items`, `variables`, `references`). Caller writes it to
128193
- * disk via `convertAndSplitV3Collection`. PMAK is never used for collection
128194
- * reads.
128199
+ * Fetch one complete v2.1 collection through a single retry-free populated
128200
+ * Sync GET. The response must be a v2.1 model for the requested public UID;
128201
+ * malformed or foreign snapshots fail closed. PMAK is never used.
128195
128202
  */
128196
128203
  async getCollection(uid) {
128197
128204
  const id = this.requireCollectionPublicUid(uid);
128198
128205
  const response = await this.gateway.requestJson({
128199
- service: "collection",
128206
+ service: "sync",
128200
128207
  method: "get",
128201
- path: `/v3/collections/${id}/export`
128202
- });
128203
- const data2 = this.asRecord(response?.data);
128204
- return data2?.collection ?? null;
128208
+ path: `/collection/${encodeURIComponent(id)}`,
128209
+ query: { populate: "true", format: "2.1.0", uid: "false" },
128210
+ retry: "none",
128211
+ fallback: "none"
128212
+ });
128213
+ const collection = this.asRecord(response?.data);
128214
+ const info2 = this.asRecord(collection?.info);
128215
+ const schema = typeof info2?.schema === "string" ? info2.schema.trim() : "";
128216
+ if (!collection || !info2 || typeof info2.name !== "string" || !info2.name.trim() || !/\/collection\/v2\.1\.0\/collection\.json\/?$/i.test(schema) || !Array.isArray(collection.item)) {
128217
+ throw new Error(
128218
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync read did not return a complete v2.1 collection"
128219
+ );
128220
+ }
128221
+ try {
128222
+ assertV2CollectionModel(collection);
128223
+ } catch (error2) {
128224
+ throw new Error(
128225
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync payload failed v2.1 schema validation",
128226
+ { cause: error2 }
128227
+ );
128228
+ }
128229
+ const observedId = String(info2._postman_id ?? collection.id ?? "").trim();
128230
+ if (!observedId || this.toModelId(observedId).toLowerCase() !== this.toModelId(id).toLowerCase()) {
128231
+ throw new Error(
128232
+ "COLLECTION_SNAPSHOT_INVALID: populated Sync read returned a different collection identity"
128233
+ );
128234
+ }
128235
+ return collection;
128205
128236
  }
128206
128237
  // --- mocks (service: mock) ---
128207
128238
  async createMock(workspaceId, name, collectionUid, environmentUid, requestedVisibility = "private") {
@@ -129593,13 +129624,117 @@ function parseJsonMap(raw) {
129593
129624
  ])
129594
129625
  );
129595
129626
  }
129596
- function parseJsonArray(raw) {
129597
- if (!raw.trim()) return [];
129598
- const parsed = JSON.parse(raw);
129627
+ function parseEnvironmentInputs(raw) {
129628
+ if (!raw.trim()) return { environments: [], definitions: /* @__PURE__ */ Object.create(null) };
129629
+ let parsed;
129630
+ try {
129631
+ parsed = JSON.parse(raw);
129632
+ } catch {
129633
+ throw new Error("environments-json must contain valid JSON");
129634
+ }
129599
129635
  if (!Array.isArray(parsed)) {
129600
- throw new Error("Expected JSON array");
129636
+ throw new Error("environments-json must be a JSON array");
129637
+ }
129638
+ const environments = [];
129639
+ const definitions = /* @__PURE__ */ Object.create(null);
129640
+ const seen = /* @__PURE__ */ new Map();
129641
+ parsed.forEach((entry, environmentIndex) => {
129642
+ const label = `environments-json[${environmentIndex}]`;
129643
+ let slug;
129644
+ let rich = false;
129645
+ if (typeof entry === "string") {
129646
+ slug = entry;
129647
+ } else {
129648
+ if (!isPlainObject2(entry)) {
129649
+ throw new Error(`${label} must be a slug string or an environment definition`);
129650
+ }
129651
+ const unknownFields = Object.keys(entry).filter((key) => key !== "slug" && key !== "values");
129652
+ if (unknownFields.length > 0) {
129653
+ throw new Error(`${label} contains unsupported field "${unknownFields[0]}"`);
129654
+ }
129655
+ if (typeof entry.slug !== "string" || !Array.isArray(entry.values)) {
129656
+ throw new Error(`${label} must contain a string slug and a values array`);
129657
+ }
129658
+ slug = entry.slug;
129659
+ rich = true;
129660
+ const keys = /* @__PURE__ */ new Set();
129661
+ definitions[slug] = entry.values.map((value, valueIndex) => {
129662
+ const valueLabel = `${label}.values[${valueIndex}]`;
129663
+ if (!isPlainObject2(value)) {
129664
+ throw new Error(`${valueLabel} must be an object`);
129665
+ }
129666
+ const unknownValueFields = Object.keys(value).filter(
129667
+ (key) => !["key", "value", "type", "enabled"].includes(key)
129668
+ );
129669
+ if (unknownValueFields.length > 0) {
129670
+ throw new Error(`${valueLabel} contains unsupported field "${unknownValueFields[0]}"`);
129671
+ }
129672
+ if (typeof value.key !== "string" || !value.key.trim() || value.key !== value.key.trim()) {
129673
+ throw new Error(`${valueLabel}.key must be a non-empty string without surrounding whitespace`);
129674
+ }
129675
+ if (value.key === "x-pm-onboarding") {
129676
+ throw new Error(`${valueLabel}.key is reserved by repo-sync`);
129677
+ }
129678
+ if (keys.has(value.key)) {
129679
+ throw new Error(`${label} contains duplicate variable key "${value.key}"`);
129680
+ }
129681
+ keys.add(value.key);
129682
+ if (value.value !== void 0 && typeof value.value !== "string") {
129683
+ throw new Error(`${valueLabel}.value must be a string when provided`);
129684
+ }
129685
+ if (value.type !== void 0 && value.type !== "default" && value.type !== "secret") {
129686
+ throw new Error(`${valueLabel}.type must be "default" or "secret"`);
129687
+ }
129688
+ if (value.enabled !== void 0 && typeof value.enabled !== "boolean") {
129689
+ throw new Error(`${valueLabel}.enabled must be a boolean when provided`);
129690
+ }
129691
+ const type = value.type ?? "default";
129692
+ const normalizedValue = value.value ?? "";
129693
+ if (type === "secret" && normalizedValue) {
129694
+ throw new Error(`${valueLabel} cannot contain a populated secret value; inject it at runtime`);
129695
+ }
129696
+ return { key: value.key, value: normalizedValue, type, enabled: value.enabled ?? true };
129697
+ });
129698
+ }
129699
+ if (rich && !/^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/.test(slug)) {
129700
+ throw new Error(`${label} rich slug must use only letters, numbers, dots, hyphens, or underscores`);
129701
+ }
129702
+ if (rich && Object.prototype.hasOwnProperty.call(Object.prototype, slug)) {
129703
+ throw new Error(`${label} rich slug conflicts with a reserved object property`);
129704
+ }
129705
+ if (rich && /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(slug)) {
129706
+ throw new Error(`${label} rich slug conflicts with a Windows device name`);
129707
+ }
129708
+ if (!rich && !slug.trim()) {
129709
+ throw new Error(`${label} slug must be a non-empty string`);
129710
+ }
129711
+ const priorWasRich = seen.get(slug);
129712
+ if (priorWasRich !== void 0 && (rich || priorWasRich)) {
129713
+ throw new Error(`environments-json contains duplicate slug "${slug}"`);
129714
+ }
129715
+ if (priorWasRich === void 0) seen.set(slug, rich);
129716
+ environments.push(slug);
129717
+ });
129718
+ return { environments, definitions };
129719
+ }
129720
+ function normalizeProgrammaticEnvironmentDefinitions(inputs) {
129721
+ const definitions = inputs.environmentDefinitions;
129722
+ if (!definitions) return inputs;
129723
+ const prototype = Object.getPrototypeOf(definitions);
129724
+ if (prototype !== Object.prototype && prototype !== null) {
129725
+ throw new Error("environmentDefinitions must be a plain object");
129726
+ }
129727
+ const entries = inputs.environments.map(
129728
+ (slug) => Object.prototype.hasOwnProperty.call(definitions, slug) ? { slug, values: definitions[slug] } : slug
129729
+ );
129730
+ let raw;
129731
+ try {
129732
+ raw = JSON.stringify(entries);
129733
+ } catch {
129734
+ throw new Error("environmentDefinitions must contain JSON-serializable values");
129601
129735
  }
129602
- return parsed.map((entry) => String(entry));
129736
+ const parsed = parseEnvironmentInputs(raw);
129737
+ return { ...inputs, environments: parsed.environments, environmentDefinitions: parsed.definitions };
129603
129738
  }
129604
129739
  function readInput(actionCore, name, required = false) {
129605
129740
  return actionCore.getInput(name, { required }).trim();
@@ -129696,7 +129831,7 @@ function resolveInputs(env = process.env) {
129696
129831
  },
129697
129832
  env
129698
129833
  );
129699
- const environments = parseJsonArray(getInput2("environments-json", env) || '["prod"]');
129834
+ const parsedEnvironments = parseEnvironmentInputs(getInput2("environments-json", env) || '["prod"]');
129700
129835
  const secretsResolverProvider = parseSecretsResolverProvider(getInput2("secrets-resolver", env));
129701
129836
  const systemEnvMap = parseJsonMap(getInput2("system-env-map-json", env) || "{}");
129702
129837
  const environmentUids = parseJsonMap(getInput2("environment-uids-json", env) || "{}");
@@ -129734,7 +129869,8 @@ function resolveInputs(env = process.env) {
129734
129869
  secretsResolverProvider,
129735
129870
  specSyncMode: normalizeSpecSyncMode(getInput2("spec-sync-mode", env) || "update"),
129736
129871
  releaseLabel: normalizeReleaseLabel(getInput2("release-label", env)) || void 0,
129737
- environments: environments.length > 0 ? environments : ["prod"],
129872
+ environments: parsedEnvironments.environments.length > 0 ? parsedEnvironments.environments : ["prod"],
129873
+ environmentDefinitions: parsedEnvironments.definitions,
129738
129874
  repoUrl: repoContext.repoUrl || "",
129739
129875
  integrationBackend: getInput2("integration-backend", env) || "bifrost",
129740
129876
  workspaceLinkEnabled: parseBooleanInput(getInput2("workspace-link-enabled", env), true),
@@ -130402,7 +130538,8 @@ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMar
130402
130538
  }
130403
130539
  } catch {
130404
130540
  }
130405
- const values2 = buildEnvironmentValues(envName, runtimeUrl, {
130541
+ const definedValues2 = inputs.environmentDefinitions?.[envName];
130542
+ const values2 = definedValues2 ? definedValues2.map((value) => ({ ...value })) : buildEnvironmentValues(envName, runtimeUrl, {
130406
130543
  secretsResolverProvider: inputs.secretsResolverProvider,
130407
130544
  preservedCredentialValues: priorValues
130408
130545
  });
@@ -130425,7 +130562,8 @@ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMar
130425
130562
  dependencies.core.setOutput("environment-uids-json", JSON.stringify(envUids));
130426
130563
  continue;
130427
130564
  }
130428
- const values = buildEnvironmentValues(envName, runtimeUrl, {
130565
+ const definedValues = inputs.environmentDefinitions?.[envName];
130566
+ const values = definedValues ? definedValues.map((value) => ({ ...value })) : buildEnvironmentValues(envName, runtimeUrl, {
130429
130567
  secretsResolverProvider: inputs.secretsResolverProvider
130430
130568
  });
130431
130569
  if (assetMarker) values.push({ key: "x-pm-onboarding", value: JSON.stringify(assetMarker), type: "default" });
@@ -131209,13 +131347,14 @@ function applyPrivateMockPrebuiltPlans(plans) {
131209
131347
  }
131210
131348
  }
131211
131349
  async function preparePrivateMockCloudCollection(role, collectionId, postman) {
131212
- const col = await postman.getCollection(collectionId);
131213
- const { collection } = applyPrivateMockExportCleanup(col, {
131350
+ const snapshot = await postman.getCollection(collectionId);
131351
+ const v3Collection = convertV2CollectionToV3Collection(snapshot);
131352
+ const { collection } = applyPrivateMockExportCleanup(v3Collection, {
131214
131353
  stripManagedBlocks: isPrivateMockLegacyExportCleanupEnabled()
131215
131354
  });
131216
131355
  if (!verifyPrivateMockRootHook(collection)) {
131217
131356
  throw new Error(
131218
- `PRIVATE_MOCK_AUTH_ROOT_UNVERIFIED: Managed root hook missing from exported ${role} collection ${collectionId}`
131357
+ `PRIVATE_MOCK_AUTH_ROOT_UNVERIFIED: Managed root hook missing from populated ${role} collection snapshot ${collectionId}`
131219
131358
  );
131220
131359
  }
131221
131360
  return collection;
@@ -131261,7 +131400,7 @@ async function acquireCollectionArtifact(options) {
131261
131400
  return { reusePrebuilt: true };
131262
131401
  }
131263
131402
  if (entry) {
131264
- core.info(privateMockAuth ? `Private mock has no locally reconciled exact prebuilt ${role} collection; exporting from cloud` : `Prebuilt ${role} collection entry present but did not exactly match; exporting from cloud`);
131403
+ core.info(privateMockAuth ? `Private mock has no locally reconciled exact prebuilt ${role} collection; reading populated cloud snapshot` : `Prebuilt ${role} collection entry present but did not exactly match; reading populated cloud snapshot`);
131265
131404
  }
131266
131405
  const cloud = privateMockAuth ? await preparePrivateMockCloudCollection(role, collectionId, postman) : await postman.getCollection(collectionId);
131267
131406
  return { reusePrebuilt: false, cloudCollection: cloud };
@@ -131615,6 +131754,7 @@ async function commitAndPushGeneratedFiles(inputs, dependencies, privateMockAuth
131615
131754
  };
131616
131755
  }
131617
131756
  async function runRepoSync(inputs, dependencies, executionContext) {
131757
+ inputs = normalizeProgrammaticEnvironmentDefinitions(inputs);
131618
131758
  const telemetry = createTelemetryContext({ action: "postman-repo-sync-action", actionVersion: resolveActionVersion2(), logger: dependencies.core });
131619
131759
  telemetry.setTeamId(dependencies.teamId);
131620
131760
  const logger = resolveRepoSyncLogger(dependencies);
@@ -132508,9 +132648,9 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
132508
132648
  getEnvironment: gatewayAssets.getEnvironment.bind(gatewayAssets),
132509
132649
  updateEnvironment: gatewayAssets.updateEnvironment.bind(gatewayAssets),
132510
132650
  findEnvironmentByName: gatewayAssets.findEnvironmentByName.bind(gatewayAssets),
132511
- // Collection read via the v3 export endpoint — returns canonical v3 IR,
132512
- // written to disk by `convertAndSplitAnyCollection`. PMAK is never used for
132513
- // collection reads.
132651
+ // Collection read via one retry-free populated Sync GET — returns a full
132652
+ // v2.1 snapshot that the official runtime.models transform converts to v3
132653
+ // before repository materialization. PMAK is never used for reads.
132514
132654
  getCollection: gatewayAssets.getCollection.bind(gatewayAssets),
132515
132655
  // Mocks via the `mock` service, collection-based monitors via the `monitors`
132516
132656
  // service (jobTemplates). Both reference the collection by its full public
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postman-cs/onboarding-repo-sync",
3
- "version": "2.10.8",
3
+ "version": "2.11.0",
4
4
  "description": "Postman repo sync GitHub Action.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -25,6 +25,7 @@
25
25
  "verify:dist:assert": "npm run verify:dist:shape && npm run verify:dist:parity",
26
26
  "verify:dist": "npm run build && npm run verify:dist:assert",
27
27
  "docs:tables": "node scripts/render-action-tables.mjs",
28
+ "docs:pins": "node scripts/check-doc-pins.mjs",
28
29
  "lint": "eslint .",
29
30
  "lint:fix": "eslint . --fix",
30
31
  "test": "vitest run --exclude tests/ci-workflow-template.test.ts && vitest run tests/ci-workflow-template.test.ts && node --test .github/scripts/dispatch-e2e-monitor.test.mjs .github/scripts/prefetch-vendored-deps.test.mjs .github/scripts/verify-e2e-release.test.mjs",