@postman-cse/onboarding-bootstrap 2.9.4 → 2.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -99990,11 +99990,11 @@ var require_valid = __commonJS({
99990
99990
  var require_clean = __commonJS({
99991
99991
  "node_modules/postman-collection/node_modules/semver/functions/clean.js"(exports2, module2) {
99992
99992
  var parse10 = require_parse6();
99993
- var clean = (version, options) => {
99993
+ var clean2 = (version, options) => {
99994
99994
  const s = parse10(version.trim().replace(/^[=v]+/, ""), options);
99995
99995
  return s ? s.version : null;
99996
99996
  };
99997
- module2.exports = clean;
99997
+ module2.exports = clean2;
99998
99998
  }
99999
99999
  });
100000
100000
 
@@ -101305,7 +101305,7 @@ var require_semver2 = __commonJS({
101305
101305
  var identifiers = require_identifiers();
101306
101306
  var parse10 = require_parse6();
101307
101307
  var valid = require_valid();
101308
- var clean = require_clean();
101308
+ var clean2 = require_clean();
101309
101309
  var inc = require_inc();
101310
101310
  var diff = require_diff();
101311
101311
  var major = require_major();
@@ -101343,7 +101343,7 @@ var require_semver2 = __commonJS({
101343
101343
  module2.exports = {
101344
101344
  parse: parse10,
101345
101345
  valid,
101346
- clean,
101346
+ clean: clean2,
101347
101347
  inc,
101348
101348
  diff,
101349
101349
  major,
@@ -253789,7 +253789,7 @@ __export(cli_exports, {
253789
253789
  toDotenv: () => toDotenv
253790
253790
  });
253791
253791
  module.exports = __toCommonJS(cli_exports);
253792
- var import_node_fs6 = require("node:fs");
253792
+ var import_node_fs7 = require("node:fs");
253793
253793
  var import_promises4 = require("node:fs/promises");
253794
253794
  var import_node_child_process = require("node:child_process");
253795
253795
  var import_node_path5 = __toESM(require("node:path"), 1);
@@ -254515,8 +254515,8 @@ var ExitCode;
254515
254515
  })(ExitCode || (ExitCode = {}));
254516
254516
 
254517
254517
  // src/index.ts
254518
- var import_node_crypto5 = require("node:crypto");
254519
- var import_node_fs5 = require("node:fs");
254518
+ var import_node_crypto6 = require("node:crypto");
254519
+ var import_node_fs6 = require("node:fs");
254520
254520
  var path6 = __toESM(require("node:path"), 1);
254521
254521
  var import_yaml4 = __toESM(require_dist(), 1);
254522
254522
 
@@ -254624,6 +254624,12 @@ var bootstrapActionContract = {
254624
254624
  default: "",
254625
254625
  allowedValues: ["3.0", "3.1"]
254626
254626
  },
254627
+ "preserve-oas30-type-null": {
254628
+ description: "Opt-in compatibility mode for legacy OpenAPI 3.0 oneOf schemas that pair a normal schema with type: null. The original source bytes are uploaded unchanged while an internal nullable view is used for validation and generated artifacts.",
254629
+ required: false,
254630
+ default: "false",
254631
+ allowedValues: ["true", "false"]
254632
+ },
254627
254633
  "breaking-change-mode": {
254628
254634
  description: "OpenAPI breaking-change comparison mode.",
254629
254635
  required: false,
@@ -254680,6 +254686,20 @@ var bootstrapActionContract = {
254680
254686
  default: "warn",
254681
254687
  allowedValues: ["enforce", "warn"]
254682
254688
  },
254689
+ "branch-strategy": {
254690
+ description: "Branch-aware sync strategy. legacy (default) keeps branch-blind behavior; publish-gate restricts canonical writes to the canonical branch and runs credential-free static validation on other branches; preview additionally maintains suffixed per-branch preview asset sets.",
254691
+ required: false,
254692
+ default: "legacy",
254693
+ allowedValues: ["legacy", "preview", "publish-gate"]
254694
+ },
254695
+ "canonical-branch": {
254696
+ description: "Explicit canonical branch (the sole writer of canonical assets). Defaults to the provider-resolved default branch; required on providers without a default-branch variable (Bitbucket, Azure DevOps) when branch-strategy is not legacy.",
254697
+ required: false
254698
+ },
254699
+ "channels": {
254700
+ description: 'Comma-separated channel map for long-lived promotion branches, e.g. "develop=DEV, staging=STAGE, release/*=RC". Channel branches maintain prefix-named parallel asset sets and never mutate canonical assets.',
254701
+ required: false
254702
+ },
254683
254703
  "integration-backend": {
254684
254704
  description: "Integration backend for downstream workspace connectivity.",
254685
254705
  required: false,
@@ -254749,6 +254769,21 @@ var bootstrapActionContract = {
254749
254769
  },
254750
254770
  "breaking-change-summary-json": {
254751
254771
  description: "JSON summary of the OpenAPI breaking-change check."
254772
+ },
254773
+ "sync-status": {
254774
+ description: "Branch-aware sync status: synced (canonical/channel/preview write ran), skipped-branch-gate (gated run, credential-free validation only), or empty under branch-strategy legacy."
254775
+ },
254776
+ "branch-decision": {
254777
+ description: "Serialized BranchDecision JSON for downstream actions (also exported as POSTMAN_BRANCH_DECISION)."
254778
+ },
254779
+ "spec-version-tag": {
254780
+ description: "Native Spec Hub version tag applied on this canonical publish (tag-per-publish), empty when tagging was skipped (no-op sync, non-canonical run, or legacy client)."
254781
+ },
254782
+ "spec-version-url": {
254783
+ description: "Reserved for the repo-sync finalizer; bootstrap does not tag before complete onboarding."
254784
+ },
254785
+ "spec-content-changed": {
254786
+ description: "Whether bootstrap changed canonical spec content; repo-sync uses this to skip native version tags on no-op syncs."
254752
254787
  }
254753
254788
  },
254754
254789
  retainedBehavior: [
@@ -255875,6 +255910,15 @@ function assertSupportedLocalViewContract(node, options) {
255875
255910
  }
255876
255911
 
255877
255912
  // src/lib/postman/additional-collections.ts
255913
+ var RESOURCES_STATE_VERSION = 2;
255914
+ var SUPPORTED_STATE_VERSIONS = /* @__PURE__ */ new Set([1, RESOURCES_STATE_VERSION]);
255915
+ var StateUnreadableError = class extends Error {
255916
+ code = "CONTRACT_STATE_UNREADABLE";
255917
+ constructor(message) {
255918
+ super(`CONTRACT_STATE_UNREADABLE: ${message}`);
255919
+ this.name = "StateUnreadableError";
255920
+ }
255921
+ };
255878
255922
  var ADDITIONAL_COLLECTION_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".yaml", ".yml"]);
255879
255923
  var POSTMAN_COLLECTION_V21_SCHEMA_FRAGMENT = "/collection/v2.1.0/collection.json";
255880
255924
  var V3_DEFINITION_PATH = import_node_path2.default.join(".resources", "definition.yaml");
@@ -255930,16 +255974,46 @@ function resourcesStatePath() {
255930
255974
  return import_node_path2.default.join(workspaceRootForLocalInputs(), RESOURCES_PATH);
255931
255975
  }
255932
255976
  function readResourcesState() {
255977
+ let raw;
255933
255978
  try {
255934
- return (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(resourcesStatePath(), "utf8"));
255979
+ raw = (0, import_node_fs2.readFileSync)(resourcesStatePath(), "utf8");
255935
255980
  } catch {
255936
255981
  return null;
255937
255982
  }
255983
+ let parsed;
255984
+ try {
255985
+ parsed = (0, import_yaml.parse)(raw);
255986
+ } catch (error) {
255987
+ throw new StateUnreadableError(
255988
+ `.postman/resources.yaml exists but is not parseable YAML (${error instanceof Error ? error.message : String(error)}). Fix or delete the file; refusing to treat tracked state as absent.`
255989
+ );
255990
+ }
255991
+ if (parsed === null || parsed === void 0) {
255992
+ return null;
255993
+ }
255994
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
255995
+ throw new StateUnreadableError(
255996
+ ".postman/resources.yaml exists but does not contain a YAML mapping. Fix or delete the file; refusing to treat tracked state as absent."
255997
+ );
255998
+ }
255999
+ const state = parsed;
256000
+ if (state.version !== void 0 && !SUPPORTED_STATE_VERSIONS.has(Number(state.version))) {
256001
+ throw new StateUnreadableError(
256002
+ `.postman/resources.yaml declares unsupported state version ${String(state.version)} (supported: 1, ${RESOURCES_STATE_VERSION}). Upgrade the action or fix the file.`
256003
+ );
256004
+ }
256005
+ if (state.canonical && !state.cloudResources) {
256006
+ state.cloudResources = { ...state.canonical };
256007
+ }
256008
+ return state;
255938
256009
  }
255939
256010
  function writeResourcesState(state) {
255940
256011
  const target = resourcesStatePath();
255941
256012
  (0, import_node_fs2.mkdirSync)(import_node_path2.default.dirname(target), { recursive: true });
255942
- (0, import_node_fs2.writeFileSync)(target, (0, import_yaml.stringify)(state), "utf8");
256013
+ const canonical = state.cloudResources ?? state.canonical ?? {};
256014
+ const persisted = { ...state, version: RESOURCES_STATE_VERSION, canonical };
256015
+ delete persisted.cloudResources;
256016
+ (0, import_node_fs2.writeFileSync)(target, (0, import_yaml.stringify)(persisted), "utf8");
255943
256017
  }
255944
256018
  function findCloudResourceId(map, matcher) {
255945
256019
  if (!map) {
@@ -255949,7 +256023,7 @@ function findCloudResourceId(map, matcher) {
255949
256023
  return match?.[1];
255950
256024
  }
255951
256025
  function findExistingAdditionalCollectionId(resourcesState, resourcePath) {
255952
- return resourcesState?.cloudResources?.additionalCollections?.[resourcePath] ?? resourcesState?.cloudResources?.collections?.[resourcePath];
256026
+ return resourcesState?.canonical?.additionalCollections?.[resourcePath] ?? resourcesState?.canonical?.collections?.[resourcePath] ?? resourcesState?.cloudResources?.additionalCollections?.[resourcePath] ?? resourcesState?.cloudResources?.collections?.[resourcePath];
255953
256027
  }
255954
256028
  function resolveAdditionalCollectionsDir(directoryInput) {
255955
256029
  const workspaceRoot = workspaceRootForLocalInputs();
@@ -256951,6 +257025,8 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
256951
257025
  * endpoint aborts rather than blocking the run forever. */
256952
257026
  static REQUEST_TIMEOUT_MS = 3e4;
256953
257027
  static POSTMAN_APP_VERSION_URL = `https://dl.pstmn.io/update/status?currentVersion=${_BifrostInternalIntegrationAdapter.MINIMUM_POSTMAN_APP_VERSION}&platform=osx_arm64`;
257028
+ /** Concurrent dual-trigger previews share one spec; peer sync holds a 423 lock. */
257029
+ static SYNC_LOCKED_MAX_RETRIES = 6;
256954
257030
  accessToken;
256955
257031
  tokenProvider;
256956
257032
  appVersionPromise;
@@ -256959,6 +257035,7 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
256959
257035
  gatewayBaseUrl;
256960
257036
  orgMode;
256961
257037
  secretMasker;
257038
+ sleep;
256962
257039
  teamId;
256963
257040
  constructor(options) {
256964
257041
  this.accessToken = String(options.accessToken || "").trim();
@@ -256972,6 +257049,7 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
256972
257049
  ).replace(/\/+$/, "");
256973
257050
  this.orgMode = options.orgMode ?? false;
256974
257051
  this.secretMasker = options.secretMasker ?? createSecretMasker([this.accessToken]);
257052
+ this.sleep = options.sleep ?? ((delayMs) => new Promise((resolve4) => setTimeout(resolve4, delayMs)));
256975
257053
  this.teamId = String(options.teamId || "").trim();
256976
257054
  }
256977
257055
  configureTeamContext(teamId, orgMode) {
@@ -257178,55 +257256,72 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
257178
257256
  if (collections.length === 0) {
257179
257257
  return;
257180
257258
  }
257181
- const response = await this.proxyRequest(
257182
- "specification",
257183
- "put",
257184
- `/specifications/${specificationId}/collections`,
257185
- collections.map((collection) => ({
257186
- collectionId: collection.collectionId,
257187
- ...collection.syncOptions ? { syncOptions: collection.syncOptions } : {}
257188
- }))
257189
- );
257190
- if (response.ok) {
257191
- return;
257259
+ const body2 = collections.map((collection) => ({
257260
+ collectionId: collection.collectionId,
257261
+ ...collection.syncOptions ? { syncOptions: collection.syncOptions } : {}
257262
+ }));
257263
+ for (let lockedAttempt = 0; ; lockedAttempt += 1) {
257264
+ const response = await this.proxyRequest(
257265
+ "specification",
257266
+ "put",
257267
+ `/specifications/${specificationId}/collections`,
257268
+ body2
257269
+ );
257270
+ if (response.ok) {
257271
+ return;
257272
+ }
257273
+ const httpErr = await HttpError.fromResponse(response, {
257274
+ method: "POST",
257275
+ requestHeaders: {
257276
+ "Content-Type": "application/json",
257277
+ "x-access-token": this.currentToken(),
257278
+ ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
257279
+ },
257280
+ secretValues: [this.currentToken()],
257281
+ url: `${this.bifrostBaseUrl}/ws/proxy`
257282
+ });
257283
+ if (httpErr.status === 423 && lockedAttempt < _BifrostInternalIntegrationAdapter.SYNC_LOCKED_MAX_RETRIES) {
257284
+ await this.sleep(2e3 * Math.pow(2, lockedAttempt));
257285
+ continue;
257286
+ }
257287
+ const advised = adviseFromHttpError(
257288
+ httpErr,
257289
+ this.adviceContext("collection-to-specification linking")
257290
+ );
257291
+ throw advised ?? httpErr;
257192
257292
  }
257193
- const httpErr = await HttpError.fromResponse(response, {
257194
- method: "POST",
257195
- requestHeaders: {
257196
- "Content-Type": "application/json",
257197
- "x-access-token": this.currentToken(),
257198
- ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
257199
- },
257200
- secretValues: [this.currentToken()],
257201
- url: `${this.bifrostBaseUrl}/ws/proxy`
257202
- });
257203
- const advised = adviseFromHttpError(
257204
- httpErr,
257205
- this.adviceContext("collection-to-specification linking")
257206
- );
257207
- throw advised ?? httpErr;
257208
257293
  }
257209
257294
  async syncCollection(specificationId, collectionId) {
257210
- const response = await this.proxyRequest(
257211
- "specification",
257212
- "post",
257213
- `/specifications/${specificationId}/collections/${collectionId}/sync`
257214
- );
257215
- if (response.ok) {
257216
- return;
257295
+ for (let lockedAttempt = 0; ; lockedAttempt += 1) {
257296
+ const response = await this.proxyRequest(
257297
+ "specification",
257298
+ "post",
257299
+ `/specifications/${specificationId}/collections/${collectionId}/sync`
257300
+ );
257301
+ if (response.ok) {
257302
+ return;
257303
+ }
257304
+ const bodyText = await response.clone().text().catch(() => "");
257305
+ if (response.status === 400 && /already in sync/i.test(bodyText)) {
257306
+ return;
257307
+ }
257308
+ const httpErr = await HttpError.fromResponse(response, {
257309
+ method: "POST",
257310
+ requestHeaders: {
257311
+ "Content-Type": "application/json",
257312
+ "x-access-token": this.currentToken(),
257313
+ ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
257314
+ },
257315
+ secretValues: [this.currentToken()],
257316
+ url: `${this.bifrostBaseUrl}/ws/proxy`
257317
+ });
257318
+ if (httpErr.status === 423 && lockedAttempt < _BifrostInternalIntegrationAdapter.SYNC_LOCKED_MAX_RETRIES) {
257319
+ await this.sleep(2e3 * Math.pow(2, lockedAttempt));
257320
+ continue;
257321
+ }
257322
+ const advised = adviseFromHttpError(httpErr, this.adviceContext("collection sync"));
257323
+ throw advised ?? httpErr;
257217
257324
  }
257218
- const httpErr = await HttpError.fromResponse(response, {
257219
- method: "POST",
257220
- requestHeaders: {
257221
- "Content-Type": "application/json",
257222
- "x-access-token": this.currentToken(),
257223
- ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
257224
- },
257225
- secretValues: [this.currentToken()],
257226
- url: `${this.bifrostBaseUrl}/ws/proxy`
257227
- });
257228
- const advised = adviseFromHttpError(httpErr, this.adviceContext("collection sync"));
257229
- throw advised ?? httpErr;
257230
257325
  }
257231
257326
  async getWorkspaceGitRepoUrl(workspaceId) {
257232
257327
  const response = await this.proxyRequest(
@@ -262371,6 +262466,16 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262371
262466
  if (openapiVersion !== "3.0" && openapiVersion !== "3.1") {
262372
262467
  throw new Error(`uploadSpec: unsupported openapiVersion "${openapiVersion}". Expected '3.0' or '3.1'.`);
262373
262468
  }
262469
+ const before = await this.findSpecificationsByExactName(workspaceId, projectName);
262470
+ const existing = adoptExactMatch(
262471
+ `specification:${workspaceId}:${projectName}`,
262472
+ before,
262473
+ (entry) => entry.id
262474
+ );
262475
+ if (existing) {
262476
+ await this.updateSpec(existing.id, specContent, workspaceId);
262477
+ return existing.id;
262478
+ }
262374
262479
  const specType = openapiVersion === "3.1" ? "OPENAPI:3.1" : "OPENAPI:3.0";
262375
262480
  let created;
262376
262481
  try {
@@ -262395,10 +262500,49 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262395
262500
  if (!match) throw error;
262396
262501
  created = { data: { id: match.id } };
262397
262502
  }
262398
- const specId = String(asRecord8(created?.data)?.id ?? created?.id ?? "").trim();
262503
+ let specId = String(asRecord8(created?.data)?.id ?? created?.id ?? "").trim();
262399
262504
  if (!specId) {
262400
262505
  throw new Error("Spec upload did not return an ID");
262401
262506
  }
262507
+ let matches = await this.findSpecificationsByExactName(workspaceId, projectName);
262508
+ if (before.length === 0 && matches.length <= 1) {
262509
+ await this.sleep(1e3);
262510
+ const settled = await this.findSpecificationsByExactName(workspaceId, projectName);
262511
+ if (settled.length > 0) matches = settled;
262512
+ }
262513
+ if (matches.length > 1 && before.length === 0) {
262514
+ for (const duplicate of matches.slice(1)) {
262515
+ try {
262516
+ await this.deleteSpecification(duplicate.id);
262517
+ } catch (error) {
262518
+ if (!(error instanceof HttpError && error.status === 404)) throw error;
262519
+ }
262520
+ }
262521
+ for (let attempt = 0; attempt < 5; attempt += 1) {
262522
+ matches = await this.findSpecificationsByExactName(workspaceId, projectName);
262523
+ if (matches.length <= 1) break;
262524
+ await this.sleep(250 * (attempt + 1));
262525
+ }
262526
+ const converged = adoptExactMatch(
262527
+ `specification:${workspaceId}:${projectName}`,
262528
+ matches,
262529
+ (entry) => entry.id
262530
+ );
262531
+ if (!converged) {
262532
+ throw new Error(`Concurrent specification create for ${projectName} did not converge`);
262533
+ }
262534
+ specId = converged.id;
262535
+ if (specId !== String(asRecord8(created?.data)?.id ?? created?.id ?? "").trim()) {
262536
+ await this.updateSpec(specId, specContent, workspaceId);
262537
+ }
262538
+ } else {
262539
+ const verified = adoptExactMatch(
262540
+ `specification:${workspaceId}:${projectName}`,
262541
+ matches,
262542
+ (entry) => entry.id
262543
+ );
262544
+ if (verified) specId = verified.id;
262545
+ }
262402
262546
  await this.gateway.requestJson({
262403
262547
  service: "specification",
262404
262548
  method: "get",
@@ -262406,6 +262550,14 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262406
262550
  });
262407
262551
  return specId;
262408
262552
  }
262553
+ async deleteSpecification(specId) {
262554
+ await this.gateway.requestJson({
262555
+ service: "specification",
262556
+ method: "delete",
262557
+ path: `/specifications/${specId}`,
262558
+ retry: "none"
262559
+ });
262560
+ }
262409
262561
  async findSpecificationsByExactName(workspaceId, name) {
262410
262562
  const response = await this.gateway.requestJson({
262411
262563
  service: "specification",
@@ -262469,6 +262621,41 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262469
262621
  return void 0;
262470
262622
  }
262471
262623
  }
262624
+ /**
262625
+ * Native Spec Hub version tags (branch-aware sync P3.5). Tags attach to the
262626
+ * LATEST changelog group; the backend 409s when that group is already tagged
262627
+ * (VersionControlService). Callers handle 409 as idempotent-by-group.
262628
+ */
262629
+ async tagSpecVersion(specId, name) {
262630
+ const trimmed = name.trim().slice(0, 255);
262631
+ const created = await this.gateway.requestJson({
262632
+ service: "specification",
262633
+ method: "post",
262634
+ path: `/specifications/${specId}/tags`,
262635
+ retry: "none",
262636
+ body: { name: trimmed }
262637
+ });
262638
+ const record = asRecord8(created?.data) ?? created ?? {};
262639
+ return {
262640
+ id: String(record.id ?? "").trim(),
262641
+ name: String(record.name ?? trimmed).trim()
262642
+ };
262643
+ }
262644
+ /** List a spec's native version tags (newest first per backend ordering). */
262645
+ async listSpecVersionTags(specId) {
262646
+ const response = await this.gateway.requestJson({
262647
+ service: "specification",
262648
+ method: "get",
262649
+ path: `/specifications/${specId}/tags`,
262650
+ query: { limit: "50" }
262651
+ });
262652
+ const entries = Array.isArray(response?.data) ? response.data : [];
262653
+ return entries.map((value) => asRecord8(value)).filter((value) => value !== null).map((value) => ({
262654
+ id: String(value.id ?? "").trim(),
262655
+ // listTags returns `message`; createTag returns `name`. Accept both.
262656
+ name: String(value.name ?? value.message ?? "").trim()
262657
+ })).filter((value) => value.id || value.name);
262658
+ }
262472
262659
  /** Resolve a specification's ROOT file uuid via the files list. */
262473
262660
  async resolveRootFileId(specId) {
262474
262661
  const files = await this.gateway.requestJson({
@@ -262488,79 +262675,182 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262488
262675
  */
262489
262676
  async generateCollection(specId, projectName, prefix, folderStrategy, nestedFolderHierarchy, requestNameSource) {
262490
262677
  const name = [prefix.trim(), projectName.trim()].filter(Boolean).join(" ");
262491
- const submittedName = `${name} [bootstrap:${this.createIdentity()}]`;
262492
- const body2 = {
262493
- name: submittedName,
262494
- options: {
262495
- requestNameSource,
262496
- folderStrategy,
262497
- ...folderStrategy === "Tags" ? { nestedFolderHierarchy } : {}
262498
- }
262499
- };
262500
262678
  const before = await this.listGeneratedCollectionRefs(specId);
262501
- let taskId;
262502
- try {
262503
- taskId = await this.postGenerationWithLockRetry(specId, body2);
262504
- } catch (error) {
262505
- if (!isAmbiguousTransportError(error)) throw error;
262506
- const beforeIds2 = new Set(before.map((entry) => entry.id));
262507
- const appeared2 = (await this.listGeneratedCollectionRefs(specId)).filter(
262508
- (entry) => !beforeIds2.has(entry.id)
262509
- );
262510
- const exactAppeared = await this.filterGeneratedCollectionsByExactName(
262511
- appeared2,
262679
+ const existing = adoptExactMatch(
262680
+ `generated-collection:${specId}:${name}`,
262681
+ await this.filterGeneratedCollectionsByExactName(before, name),
262682
+ (entry) => entry.id
262683
+ );
262684
+ if (existing) return this.convergeGeneratedCollections(specId, name, existing.id);
262685
+ let lastError;
262686
+ for (let taskAttempt = 0; taskAttempt < 2; taskAttempt += 1) {
262687
+ const submittedName = `${name} [bootstrap:${this.createIdentity()}]`;
262688
+ const body2 = {
262689
+ name: submittedName,
262690
+ options: {
262691
+ requestNameSource,
262692
+ folderStrategy,
262693
+ ...folderStrategy === "Tags" ? { nestedFolderHierarchy } : {}
262694
+ }
262695
+ };
262696
+ let taskId;
262697
+ try {
262698
+ const generation = await this.postGenerationWithLockRetry(specId, body2, name);
262699
+ if (generation.adoptedId) {
262700
+ return this.convergeGeneratedCollections(specId, name, generation.adoptedId);
262701
+ }
262702
+ taskId = generation.taskId ?? "";
262703
+ } catch (error) {
262704
+ if (!isAmbiguousTransportError(error)) throw error;
262705
+ const beforeIds2 = new Set(before.map((entry) => entry.id));
262706
+ const appeared2 = (await this.listGeneratedCollectionRefs(specId)).filter(
262707
+ (entry) => !beforeIds2.has(entry.id)
262708
+ );
262709
+ const exactAppeared = await this.filterGeneratedCollectionsByExactName(
262710
+ appeared2,
262711
+ submittedName
262712
+ );
262713
+ const match = adoptExactMatch(
262714
+ `generated-collection:${specId}:${submittedName}`,
262715
+ exactAppeared,
262716
+ (entry) => entry.id
262717
+ );
262718
+ if (!match) throw error;
262719
+ await this.renameGeneratedCollection(match.id, name);
262720
+ return this.convergeGeneratedCollections(specId, name, match.id);
262721
+ }
262722
+ let taskFailed = false;
262723
+ if (taskId) {
262724
+ for (let attempt = 0; attempt < this.generationPollAttempts; attempt += 1) {
262725
+ await this.sleep(this.generationPollDelayMs);
262726
+ const task = await this.gateway.requestJson({
262727
+ service: "specification",
262728
+ method: "get",
262729
+ path: "/tasks",
262730
+ query: { entityId: specId, entityType: "specification", type: "collection-generation" }
262731
+ });
262732
+ const status = String(asRecord8(task?.data)?.[taskId] ?? "").toLowerCase();
262733
+ if (status === "failed" || status === "error") {
262734
+ taskFailed = true;
262735
+ lastError = new Error(`Collection generation task failed for ${prefix}`);
262736
+ break;
262737
+ }
262738
+ if (status && status !== "in-progress" && status !== "pending" && status !== "queued") {
262739
+ break;
262740
+ }
262741
+ if (attempt === this.generationPollAttempts - 1) {
262742
+ throw new Error(`Collection generation timed out for ${prefix}`);
262743
+ }
262744
+ }
262745
+ }
262746
+ if (taskFailed) {
262747
+ await this.sleep(1e3 * (taskAttempt + 1));
262748
+ continue;
262749
+ }
262750
+ const beforeIds = new Set(before.map((entry) => entry.id));
262751
+ const after = await this.listGeneratedCollectionRefs(specId);
262752
+ const appeared = after.filter((entry) => !beforeIds.has(entry.id));
262753
+ const candidates = await this.filterGeneratedCollectionsByExactName(
262754
+ appeared,
262512
262755
  submittedName
262513
262756
  );
262514
- const match = adoptExactMatch(
262757
+ const uid = adoptExactMatch(
262515
262758
  `generated-collection:${specId}:${submittedName}`,
262516
- exactAppeared,
262759
+ candidates,
262517
262760
  (entry) => entry.id
262518
- );
262519
- if (!match) throw error;
262520
- await this.renameGeneratedCollection(match.id, name);
262521
- return match.id;
262761
+ )?.id;
262762
+ if (!uid) {
262763
+ throw new Error(`Collection generation did not yield a collection uid for ${prefix}`);
262764
+ }
262765
+ await this.renameGeneratedCollection(uid, name);
262766
+ return this.convergeGeneratedCollections(specId, name, uid);
262522
262767
  }
262523
- if (taskId) {
262524
- for (let attempt = 0; attempt < this.generationPollAttempts; attempt += 1) {
262525
- await this.sleep(this.generationPollDelayMs);
262526
- const task = await this.gateway.requestJson({
262527
- service: "specification",
262528
- method: "get",
262529
- path: "/tasks",
262530
- query: { entityId: specId, entityType: "specification", type: "collection-generation" }
262531
- });
262532
- const status = String(asRecord8(task?.data)?.[taskId] ?? "").toLowerCase();
262533
- if (status === "failed" || status === "error") {
262534
- throw new Error(`Collection generation task failed for ${prefix}`);
262535
- }
262536
- if (status && status !== "in-progress" && status !== "pending" && status !== "queued") {
262537
- break;
262538
- }
262539
- if (attempt === this.generationPollAttempts - 1) {
262540
- throw new Error(`Collection generation timed out for ${prefix}`);
262768
+ throw lastError ?? new Error(`Collection generation task failed for ${prefix}`);
262769
+ }
262770
+ /**
262771
+ * Re-elect the durable generated collection for a final name. Call after
262772
+ * concurrent dual-trigger generates and before description/inject/tag so a
262773
+ * peer orphan-sweep cannot leave this runner holding a deleted id.
262774
+ */
262775
+ async adoptGeneratedCollection(specId, projectName, prefix, preferredId = "") {
262776
+ const name = [prefix.trim(), projectName.trim()].filter(Boolean).join(" ");
262777
+ return this.convergeGeneratedCollections(specId, name, preferredId);
262778
+ }
262779
+ /**
262780
+ * Concurrent dual-trigger previews can each generate+rename the same final
262781
+ * collection identity. Elect the stable lowest-id winner.
262782
+ *
262783
+ * Losers only delete *their own* preferred collection (never a peer's still-
262784
+ * in-use id). Winners wait briefly for peers to self-delete, then clean any
262785
+ * leftover same-identity orphans (temps + extra finals).
262786
+ */
262787
+ async convergeGeneratedCollections(specId, finalName, preferredId) {
262788
+ const tempPrefix = `${finalName} [bootstrap:`;
262789
+ const hydrate = async () => {
262790
+ const linked = await this.listGeneratedCollectionRefs(specId);
262791
+ return Promise.all(
262792
+ linked.map(async (entry) => {
262793
+ if (entry.name) return { id: entry.id, name: entry.name };
262794
+ try {
262795
+ const collection = await this.gateway.requestJson({
262796
+ service: "collection",
262797
+ method: "get",
262798
+ path: `/v3/collections/${this.bareModelId(entry.id)}`
262799
+ });
262800
+ return {
262801
+ id: entry.id,
262802
+ name: String(asRecord8(collection?.data)?.name ?? "").trim()
262803
+ };
262804
+ } catch (error) {
262805
+ if (error instanceof HttpError && error.status === 404) {
262806
+ return { id: entry.id, name: "" };
262807
+ }
262808
+ throw error;
262809
+ }
262810
+ })
262811
+ );
262812
+ };
262813
+ const selectSameIdentity = (entries) => entries.filter(
262814
+ (entry) => entry.name === finalName || entry.name.startsWith(tempPrefix)
262815
+ ).sort((a, b) => a.id.localeCompare(b.id));
262816
+ let sameIdentity = selectSameIdentity(await hydrate());
262817
+ if (sameIdentity.length === 0) return preferredId;
262818
+ if (sameIdentity.length === 1) {
262819
+ await this.sleep(1e3);
262820
+ sameIdentity = selectSameIdentity(await hydrate());
262821
+ if (sameIdentity.length === 0) return preferredId;
262822
+ }
262823
+ const winner = sameIdentity[0];
262824
+ if (winner.name !== finalName) {
262825
+ await this.renameGeneratedCollection(winner.id, finalName);
262826
+ }
262827
+ if (preferredId && preferredId !== winner.id) {
262828
+ const own = sameIdentity.find((entry) => entry.id === preferredId);
262829
+ if (own) {
262830
+ try {
262831
+ await this.deleteCollection(preferredId);
262832
+ } catch (error) {
262833
+ if (!(error instanceof HttpError && error.status === 404)) throw error;
262541
262834
  }
262542
262835
  }
262836
+ return winner.id;
262543
262837
  }
262544
- const beforeIds = new Set(before.map((entry) => entry.id));
262545
- const after = await this.listGeneratedCollectionRefs(specId);
262546
- const appeared = after.filter((entry) => !beforeIds.has(entry.id));
262547
- const candidates = await this.filterGeneratedCollectionsByExactName(
262548
- appeared,
262549
- submittedName
262550
- );
262551
- const uid = adoptExactMatch(
262552
- `generated-collection:${specId}:${submittedName}`,
262553
- candidates,
262554
- (entry) => entry.id
262555
- )?.id;
262556
- if (!uid) {
262557
- throw new Error(`Collection generation did not yield a collection uid for ${prefix}`);
262838
+ if (sameIdentity.length > 1) {
262839
+ await this.sleep(1500);
262840
+ sameIdentity = selectSameIdentity(await hydrate());
262841
+ for (const duplicate of sameIdentity) {
262842
+ if (duplicate.id === winner.id) continue;
262843
+ try {
262844
+ await this.deleteCollection(duplicate.id);
262845
+ } catch (error) {
262846
+ if (!(error instanceof HttpError && error.status === 404)) throw error;
262847
+ }
262848
+ }
262558
262849
  }
262559
- await this.renameGeneratedCollection(uid, name);
262560
- return uid;
262850
+ return winner.id;
262561
262851
  }
262562
262852
  /** POST the generation request, retrying a 423-locked spec; returns the task id. */
262563
- async postGenerationWithLockRetry(specId, body2) {
262853
+ async postGenerationWithLockRetry(specId, body2, finalName) {
262564
262854
  for (let lockedAttempt = 0; ; lockedAttempt += 1) {
262565
262855
  try {
262566
262856
  const created = await this.gateway.requestJson({
@@ -262570,13 +262860,22 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262570
262860
  retry: "none",
262571
262861
  body: body2
262572
262862
  });
262573
- return String(asRecord8(created?.data)?.taskId ?? "").trim();
262863
+ return { taskId: String(asRecord8(created?.data)?.taskId ?? "").trim() };
262574
262864
  } catch (error) {
262575
262865
  const locked = error instanceof HttpError && error.status === 423;
262576
262866
  if (!locked || lockedAttempt >= _PostmanGatewayAssetsClient.GENERATION_LOCKED_MAX_RETRIES) {
262577
262867
  throw error;
262578
262868
  }
262579
262869
  await this.sleep(5e3 * Math.pow(2, lockedAttempt));
262870
+ const adopted = adoptExactMatch(
262871
+ `generated-collection:${specId}:${finalName}`,
262872
+ await this.filterGeneratedCollectionsByExactName(
262873
+ await this.listGeneratedCollectionRefs(specId),
262874
+ finalName
262875
+ ),
262876
+ (entry) => entry.id
262877
+ );
262878
+ if (adopted) return { adoptedId: adopted.id };
262580
262879
  }
262581
262880
  }
262582
262881
  }
@@ -262613,14 +262912,24 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262613
262912
  return hydrated.filter((entry) => entry.name === expectedName);
262614
262913
  }
262615
262914
  async renameGeneratedCollection(collectionId, name) {
262616
- await this.gateway.requestJson({
262617
- service: "collection",
262618
- method: "patch",
262619
- path: `/v3/collections/${this.bareModelId(collectionId)}`,
262620
- // Replacing a generated collection's name with the same value is idempotent.
262621
- retry: "safe",
262622
- body: [{ op: "replace", path: "/name", value: name }]
262623
- });
262915
+ try {
262916
+ await this.gateway.requestJson({
262917
+ service: "collection",
262918
+ method: "patch",
262919
+ path: `/v3/collections/${this.bareModelId(collectionId)}`,
262920
+ // Replacing a generated collection's name with the same value is idempotent.
262921
+ retry: "safe",
262922
+ body: [{ op: "replace", path: "/name", value: name }]
262923
+ });
262924
+ } catch (error) {
262925
+ if (error instanceof HttpError && error.status === 400 && /must update at least one|REJECTED_PATCH/i.test(
262926
+ `${error.message}
262927
+ ${error.responseBody ?? ""}`
262928
+ )) {
262929
+ return;
262930
+ }
262931
+ throw error;
262932
+ }
262624
262933
  }
262625
262934
  /**
262626
262935
  * Create a team-visible workspace through the gateway workspaces service.
@@ -262895,6 +263204,7 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
262895
263204
  service: "tagging",
262896
263205
  method: "put",
262897
263206
  path: `/v1/tags/collections/${collectionUid}`,
263207
+ retry: "safe",
262898
263208
  body: { tags: normalized.map((slug) => ({ slug })) }
262899
263209
  });
262900
263210
  }
@@ -263521,6 +263831,15 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
263521
263831
  async createCollection(workspaceId, collection, options = {}) {
263522
263832
  const v3 = this.normalizeCollectionForWrite(collection);
263523
263833
  const desiredName = String(v3.name ?? "Untitled Collection");
263834
+ const existing = adoptExactMatch(
263835
+ `collection:${workspaceId}:${desiredName}`,
263836
+ await this.findCollectionsByExactName(workspaceId, desiredName),
263837
+ (entry) => entry.id
263838
+ );
263839
+ if (existing) {
263840
+ await this.updateCollection(existing.id, collection);
263841
+ return existing.id;
263842
+ }
263524
263843
  const submittedName = `${desiredName} [bootstrap:${this.createIdentity()}]`;
263525
263844
  const rootBody = { name: submittedName };
263526
263845
  let created;
@@ -263571,6 +263890,10 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
263571
263890
  }
263572
263891
  return rawId;
263573
263892
  }
263893
+ /** Patch only the durable collection description without reconciling its item tree. */
263894
+ async updateCollectionDescription(collectionUid, description) {
263895
+ await this.applyCollectionLevelSettings(this.bareModelId(collectionUid), { description });
263896
+ }
263574
263897
  /**
263575
263898
  * Full-replace reconcile of a curated local v2.1.0 or collection v3 payload: delete every
263576
263899
  * root-level item (deleting a folder cascades its children server-side —
@@ -263795,7 +264118,8 @@ var AccessTokenGatewayClient = class {
263795
264118
  * `requestJson` can still parse it.
263796
264119
  */
263797
264120
  rebuildResponse(response, body2) {
263798
- return new Response(body2, {
264121
+ const nullBody = response.status === 204 || response.status === 205 || response.status === 304;
264122
+ return new Response(nullBody ? null : body2, {
263799
264123
  status: response.status,
263800
264124
  statusText: response.statusText,
263801
264125
  headers: response.headers
@@ -280247,6 +280571,61 @@ function serializeOpenApiDocument(document2) {
280247
280571
  return `${JSON.stringify(document2, null, 2)}
280248
280572
  `;
280249
280573
  }
280574
+ function typeNullPath(pathSegments) {
280575
+ return pathSegments.map(String).join(".");
280576
+ }
280577
+ function isNullOnlySchema(value) {
280578
+ const record = asRecord10(value);
280579
+ if (!record || record.type !== "null") return false;
280580
+ return Object.keys(record).every((key) => key === "type" || key.startsWith("x-"));
280581
+ }
280582
+ function createOas30TypeNullCompatibilityDocument(sourceDocument) {
280583
+ const document2 = structuredClone(sourceDocument);
280584
+ const sourceTypeNullPaths = [];
280585
+ const visit4 = (value, pathSegments) => {
280586
+ if (Array.isArray(value)) {
280587
+ value.forEach((entry, index) => visit4(entry, [...pathSegments, index]));
280588
+ return;
280589
+ }
280590
+ const record = asRecord10(value);
280591
+ if (!record) return;
280592
+ const oneOf = Array.isArray(record.oneOf) ? record.oneOf : void 0;
280593
+ const nullIndexes = oneOf ? oneOf.flatMap((entry, index) => isNullOnlySchema(entry) ? [index] : []) : [];
280594
+ if (nullIndexes.length > 0) {
280595
+ if (oneOf?.length !== 2 || nullIndexes.length !== 1) {
280596
+ throw new Error(
280597
+ `CONTRACT_OAS30_TYPE_NULL_UNSUPPORTED: ${typeNullPath(pathSegments)} must use oneOf with exactly one null-only member and one non-null schema`
280598
+ );
280599
+ }
280600
+ const nullIndex = nullIndexes[0];
280601
+ const nonNullIndex = nullIndex === 0 ? 1 : 0;
280602
+ const nonNullSchema = asRecord10(oneOf[nonNullIndex]);
280603
+ if (!nonNullSchema || nonNullSchema.type === "null") {
280604
+ throw new Error(
280605
+ `CONTRACT_OAS30_TYPE_NULL_UNSUPPORTED: ${typeNullPath(pathSegments)} must pair the null-only member with one non-null schema`
280606
+ );
280607
+ }
280608
+ const siblings = Object.fromEntries(
280609
+ Object.entries(record).filter(([key]) => key !== "oneOf")
280610
+ );
280611
+ for (const key of Object.keys(record)) delete record[key];
280612
+ Object.assign(record, structuredClone(nonNullSchema), siblings, { nullable: true });
280613
+ sourceTypeNullPaths.push(typeNullPath([...pathSegments, "oneOf", nullIndex, "type"]));
280614
+ visit4(record, pathSegments);
280615
+ return;
280616
+ }
280617
+ if (record.type === "null") {
280618
+ throw new Error(
280619
+ `CONTRACT_OAS30_TYPE_NULL_UNSUPPORTED: ${typeNullPath([...pathSegments, "type"])} is not a supported nullable oneOf member`
280620
+ );
280621
+ }
280622
+ for (const [key, child3] of Object.entries(record)) {
280623
+ visit4(child3, [...pathSegments, key]);
280624
+ }
280625
+ };
280626
+ visit4(document2, []);
280627
+ return { document: document2, sourceTypeNullPaths };
280628
+ }
280250
280629
  async function bundleSpec(baseUrl, document2, options) {
280251
280630
  const budget = options.budget ?? { refs: 0, totalBytes: Buffer.byteLength(JSON.stringify(document2), "utf8") };
280252
280631
  const fetchText = options.fetchText ?? safeFetchText;
@@ -280288,7 +280667,14 @@ function createCachedFetchText(options) {
280288
280667
  async function buildLoadedSpec(content, baseRef, options, fetchText, budget) {
280289
280668
  const document2 = parseOpenApiDocument(content);
280290
280669
  const version = detectOpenApiVersion2(document2);
280291
- const bundledDocument = await bundleSpec(baseRef, document2, { ...options, budget, fetchText });
280670
+ let contractDocument = document2;
280671
+ let sourceTypeNullPaths = [];
280672
+ if (options.preserveOas30TypeNull && version === "3.0") {
280673
+ const compatibility = createOas30TypeNullCompatibilityDocument(document2);
280674
+ contractDocument = compatibility.document;
280675
+ sourceTypeNullPaths = compatibility.sourceTypeNullPaths;
280676
+ }
280677
+ const bundledDocument = await bundleSpec(baseRef, contractDocument, { ...options, budget, fetchText });
280292
280678
  const validation = await validate(bundledDocument, {
280293
280679
  resolve: { external: false, file: false },
280294
280680
  dereference: { circular: "ignore" },
@@ -280302,6 +280688,7 @@ async function buildLoadedSpec(content, baseRef, options, fetchText, budget) {
280302
280688
  bundledDocument,
280303
280689
  contractIndex: buildContractIndex(bundledDocument),
280304
280690
  content,
280691
+ sourceTypeNullPaths,
280305
280692
  version
280306
280693
  };
280307
280694
  }
@@ -280427,6 +280814,309 @@ function looksLikeIntrospection(record) {
280427
280814
  return Boolean(data && typeof data === "object" && data.__schema);
280428
280815
  }
280429
280816
 
280817
+ // src/lib/repo/branch-decision.ts
280818
+ var import_node_fs5 = require("node:fs");
280819
+ var import_node_crypto4 = require("node:crypto");
280820
+ var ContractError = class extends Error {
280821
+ code;
280822
+ constructor(code, message) {
280823
+ super(`${code}: ${message}`);
280824
+ this.code = code;
280825
+ this.name = "ContractError";
280826
+ }
280827
+ };
280828
+ function clean(value) {
280829
+ const trimmed = (value ?? "").trim();
280830
+ return trimmed.length > 0 ? trimmed : void 0;
280831
+ }
280832
+ function stripRefPrefix(ref) {
280833
+ const raw = clean(ref);
280834
+ if (!raw) {
280835
+ return { kind: "unknown" };
280836
+ }
280837
+ if (raw.startsWith("refs/heads/")) {
280838
+ return { name: raw.slice("refs/heads/".length), kind: "branch" };
280839
+ }
280840
+ if (raw.startsWith("refs/tags/")) {
280841
+ return { name: raw.slice("refs/tags/".length), kind: "tag" };
280842
+ }
280843
+ if (raw.startsWith("refs/pull/") || raw.startsWith("refs/merge")) {
280844
+ return { kind: "unknown" };
280845
+ }
280846
+ return { name: raw, kind: "branch" };
280847
+ }
280848
+ function detectProvider(env) {
280849
+ if (clean(env.GITHUB_ACTIONS) || clean(env.GITHUB_REPOSITORY)) return "github";
280850
+ if (clean(env.GITLAB_CI) || clean(env.CI_PROJECT_PATH)) return "gitlab";
280851
+ if (clean(env.BITBUCKET_REPO_SLUG) || clean(env.BITBUCKET_BRANCH)) return "bitbucket";
280852
+ if (clean(env.TF_BUILD) || clean(env.BUILD_REPOSITORY_URI)) return "azure-devops";
280853
+ return "unknown";
280854
+ }
280855
+ function readGithubEvent(env) {
280856
+ const path8 = clean(env.GITHUB_EVENT_PATH);
280857
+ if (!path8) return void 0;
280858
+ try {
280859
+ return JSON.parse((0, import_node_fs5.readFileSync)(path8, "utf8"));
280860
+ } catch {
280861
+ return void 0;
280862
+ }
280863
+ }
280864
+ function resolveBranchIdentity(env = process.env, overrides = {}) {
280865
+ const provider = detectProvider(env);
280866
+ const explicitDefault = clean(overrides.defaultBranch);
280867
+ let headBranch;
280868
+ let rawRef;
280869
+ let refKind;
280870
+ let isPrContext = false;
280871
+ let isForkPr = false;
280872
+ let defaultBranch = explicitDefault;
280873
+ let headSha;
280874
+ switch (provider) {
280875
+ case "github": {
280876
+ const event2 = readGithubEvent(env);
280877
+ headSha = clean(env.GITHUB_SHA);
280878
+ defaultBranch ??= clean(event2?.repository?.default_branch);
280879
+ const headRef = clean(env.GITHUB_HEAD_REF);
280880
+ if (headRef) {
280881
+ isPrContext = true;
280882
+ headBranch = headRef;
280883
+ rawRef = clean(env.GITHUB_REF) ?? headRef;
280884
+ refKind = "branch";
280885
+ const headRepo = event2?.pull_request?.head?.repo?.full_name;
280886
+ const baseRepo = event2?.pull_request?.base?.repo?.full_name ?? event2?.repository?.full_name;
280887
+ isForkPr = Boolean(headRepo && baseRepo && headRepo !== baseRepo);
280888
+ headSha = clean(event2?.pull_request?.head?.sha) ?? headSha;
280889
+ } else {
280890
+ rawRef = clean(env.GITHUB_REF) ?? clean(env.GITHUB_REF_NAME);
280891
+ const parsed = stripRefPrefix(rawRef);
280892
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
280893
+ refKind = parsed.kind;
280894
+ }
280895
+ break;
280896
+ }
280897
+ case "gitlab": {
280898
+ headSha = clean(env.CI_COMMIT_SHA);
280899
+ defaultBranch ??= clean(env.CI_DEFAULT_BRANCH);
280900
+ const mrSource = clean(env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME);
280901
+ if (mrSource) {
280902
+ isPrContext = true;
280903
+ headBranch = mrSource;
280904
+ rawRef = mrSource;
280905
+ refKind = "branch";
280906
+ const sourceProject = clean(env.CI_MERGE_REQUEST_SOURCE_PROJECT_ID);
280907
+ const targetProject = clean(env.CI_MERGE_REQUEST_PROJECT_ID);
280908
+ isForkPr = Boolean(sourceProject && targetProject && sourceProject !== targetProject);
280909
+ } else if (clean(env.CI_COMMIT_TAG)) {
280910
+ rawRef = clean(env.CI_COMMIT_TAG);
280911
+ refKind = "tag";
280912
+ } else {
280913
+ headBranch = clean(env.CI_COMMIT_BRANCH) ?? clean(env.CI_COMMIT_REF_NAME);
280914
+ rawRef = headBranch;
280915
+ refKind = headBranch ? "branch" : "unknown";
280916
+ }
280917
+ break;
280918
+ }
280919
+ case "bitbucket": {
280920
+ headSha = clean(env.BITBUCKET_COMMIT);
280921
+ if (clean(env.BITBUCKET_TAG)) {
280922
+ rawRef = clean(env.BITBUCKET_TAG);
280923
+ refKind = "tag";
280924
+ } else {
280925
+ headBranch = clean(env.BITBUCKET_BRANCH);
280926
+ rawRef = headBranch;
280927
+ refKind = headBranch ? "branch" : "unknown";
280928
+ isPrContext = Boolean(clean(env.BITBUCKET_PR_ID));
280929
+ }
280930
+ break;
280931
+ }
280932
+ case "azure-devops": {
280933
+ headSha = clean(env.BUILD_SOURCEVERSION);
280934
+ const prSource = clean(env.SYSTEM_PULLREQUEST_SOURCEBRANCH);
280935
+ if (prSource) {
280936
+ isPrContext = true;
280937
+ const parsed = stripRefPrefix(prSource);
280938
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
280939
+ rawRef = prSource;
280940
+ refKind = parsed.kind;
280941
+ const forkFlag = clean(env.SYSTEM_PULLREQUEST_ISFORK);
280942
+ isForkPr = forkFlag?.toLowerCase() === "true";
280943
+ } else {
280944
+ rawRef = clean(env.BUILD_SOURCEBRANCH);
280945
+ const parsed = stripRefPrefix(rawRef);
280946
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
280947
+ refKind = parsed.kind;
280948
+ }
280949
+ break;
280950
+ }
280951
+ default: {
280952
+ refKind = "unknown";
280953
+ }
280954
+ }
280955
+ if (refKind === "branch" && headBranch && defaultBranch && headBranch === defaultBranch) {
280956
+ refKind = "default-branch";
280957
+ }
280958
+ return { provider, headBranch, rawRef, defaultBranch, refKind, isPrContext, isForkPr, headSha };
280959
+ }
280960
+ function parseChannelRules(input) {
280961
+ const raw = clean(input);
280962
+ const rules = [];
280963
+ for (const part of raw ? raw.split(",") : []) {
280964
+ const entry = part.trim();
280965
+ if (!entry) continue;
280966
+ const eq = entry.indexOf("=");
280967
+ if (eq <= 0 || eq === entry.length - 1) {
280968
+ throw new ContractError(
280969
+ "CONTRACT_CHANNELS_INPUT_INVALID",
280970
+ `channels entry "${entry}" must be <branch-or-glob>=<CODE>`
280971
+ );
280972
+ }
280973
+ const pattern = entry.slice(0, eq).trim();
280974
+ const code = entry.slice(eq + 1).trim().toUpperCase();
280975
+ if (!/^[A-Z][A-Z0-9_-]{0,15}$/.test(code)) {
280976
+ throw new ContractError(
280977
+ "CONTRACT_CHANNELS_INPUT_INVALID",
280978
+ `channel code "${code}" must be 1-16 chars, A-Z 0-9 _ -, starting with a letter`
280979
+ );
280980
+ }
280981
+ rules.push({ pattern, code });
280982
+ }
280983
+ if (!rules.some((rule) => rule.pattern === "release/*")) {
280984
+ rules.push({ pattern: "release/*", code: "RC" });
280985
+ }
280986
+ return rules;
280987
+ }
280988
+ function matchChannel(branch, rules) {
280989
+ for (const rule of rules) {
280990
+ if (rule.pattern.endsWith("*")) {
280991
+ const prefix = rule.pattern.slice(0, -1);
280992
+ if (branch.startsWith(prefix)) return rule;
280993
+ } else if (branch === rule.pattern) {
280994
+ return rule;
280995
+ }
280996
+ }
280997
+ return void 0;
280998
+ }
280999
+ function resolveBranchDecision(options) {
281000
+ const { strategy, identity } = options;
281001
+ const channels = options.channels ?? [];
281002
+ if (strategy === "legacy") {
281003
+ return {
281004
+ tier: "legacy",
281005
+ strategy,
281006
+ identity,
281007
+ canonicalBranch: clean(options.canonicalBranch) ?? identity.defaultBranch,
281008
+ reason: "branch-strategy legacy: branch-blind pre-v2 behavior"
281009
+ };
281010
+ }
281011
+ const canonicalBranch = clean(options.canonicalBranch) ?? identity.defaultBranch;
281012
+ if (!canonicalBranch) {
281013
+ throw new ContractError(
281014
+ "CONTRACT_DEFAULT_BRANCH_UNRESOLVED",
281015
+ `cannot resolve the canonical branch on ${identity.provider} (no explicit canonical-branch input and the provider exposes no default-branch variable). Set the canonical-branch input.`
281016
+ );
281017
+ }
281018
+ if (identity.refKind === "tag" || identity.refKind === "unknown" || !identity.headBranch) {
281019
+ return {
281020
+ tier: "gated",
281021
+ strategy,
281022
+ identity,
281023
+ canonicalBranch,
281024
+ reason: `ref kind ${identity.refKind}: never canonical/preview-eligible; no-op with annotation`
281025
+ };
281026
+ }
281027
+ if (identity.headBranch === canonicalBranch) {
281028
+ return {
281029
+ tier: "canonical",
281030
+ strategy,
281031
+ identity,
281032
+ canonicalBranch,
281033
+ reason: `head branch equals canonical branch ${canonicalBranch}`
281034
+ };
281035
+ }
281036
+ const channel = matchChannel(identity.headBranch, channels);
281037
+ if (channel) {
281038
+ return {
281039
+ tier: "channel",
281040
+ strategy,
281041
+ identity,
281042
+ canonicalBranch,
281043
+ channel,
281044
+ reason: `branch ${identity.headBranch} matches channel ${channel.pattern}=${channel.code}`
281045
+ };
281046
+ }
281047
+ if (strategy === "preview") {
281048
+ if (identity.isForkPr) {
281049
+ return {
281050
+ tier: "gated",
281051
+ strategy,
281052
+ identity,
281053
+ canonicalBranch,
281054
+ reason: "fork PR: preview-ineligible (same-repo gate), gated instead"
281055
+ };
281056
+ }
281057
+ return {
281058
+ tier: "preview",
281059
+ strategy,
281060
+ identity,
281061
+ canonicalBranch,
281062
+ reason: `branch ${identity.headBranch} under branch-strategy preview`
281063
+ };
281064
+ }
281065
+ return {
281066
+ tier: "gated",
281067
+ strategy,
281068
+ identity,
281069
+ canonicalBranch,
281070
+ reason: `branch ${identity.headBranch} under branch-strategy publish-gate`
281071
+ };
281072
+ }
281073
+ var BRANCH_DECISION_ENV = "POSTMAN_BRANCH_DECISION";
281074
+ function serializeBranchDecision(decision) {
281075
+ return JSON.stringify(decision);
281076
+ }
281077
+ function parseBranchDecision(raw) {
281078
+ const value = clean(raw);
281079
+ if (!value) return void 0;
281080
+ let parsed;
281081
+ try {
281082
+ parsed = JSON.parse(value);
281083
+ } catch {
281084
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION is not valid JSON");
281085
+ }
281086
+ const candidate = parsed;
281087
+ const tiers = ["canonical", "channel", "preview", "gated", "legacy"];
281088
+ if (!candidate || typeof candidate !== "object" || !tiers.includes(candidate.tier) || !candidate.identity) {
281089
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION does not carry a valid BranchDecision");
281090
+ }
281091
+ return candidate;
281092
+ }
281093
+ function resolveEffectiveBranchDecision(options, env = process.env) {
281094
+ const inherited = parseBranchDecision(env[BRANCH_DECISION_ENV]);
281095
+ if (inherited) return inherited;
281096
+ return resolveBranchDecision(options);
281097
+ }
281098
+ var PREVIEW_SLUG_MAX = 30;
281099
+ function buildBranchSlug(rawBranch) {
281100
+ const sanitized = rawBranch.replace(/^refs\/heads\//, "").replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
281101
+ const truncated = sanitized.slice(0, PREVIEW_SLUG_MAX);
281102
+ const lossy = truncated !== rawBranch.replace(/^refs\/heads\//, "");
281103
+ if (!lossy) {
281104
+ return { suffix: truncated, slug: truncated, lossy };
281105
+ }
281106
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(rawBranch).digest("hex").slice(0, 6);
281107
+ return { suffix: `${truncated}-${hash}`, slug: truncated, lossy };
281108
+ }
281109
+ function previewAssetName(baseName, rawBranch) {
281110
+ return `${baseName} @${buildBranchSlug(rawBranch).suffix}`;
281111
+ }
281112
+ function channelAssetName(baseName, code) {
281113
+ return `[${code}] ${baseName}`;
281114
+ }
281115
+ var MARKER_KEY = "x-pm-onboarding";
281116
+ function renderAssetMarker(marker) {
281117
+ return `${MARKER_KEY}: ${JSON.stringify(marker)}`;
281118
+ }
281119
+
280430
281120
  // node_modules/graphql/jsutils/inspect.mjs
280431
281121
  var MAX_ARRAY_LENGTH = 10;
280432
281122
  var MAX_RECURSIVE_DEPTH = 2;
@@ -303758,7 +304448,7 @@ function parseWsdl(content, opts) {
303758
304448
  }
303759
304449
 
303760
304450
  // src/lib/protocols/soap/builder.ts
303761
- var import_node_crypto4 = require("node:crypto");
304451
+ var import_node_crypto5 = require("node:crypto");
303762
304452
  var COLLECTION_V210_SCHEMA2 = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json";
303763
304453
  var SOAP11_ENVELOPE_NS = "http://schemas.xmlsoap.org/soap/envelope/";
303764
304454
  var SOAP12_ENVELOPE_NS = "http://www.w3.org/2003/05/soap-envelope";
@@ -303766,7 +304456,7 @@ var SOAP11_CONTENT_TYPE = "text/xml; charset=UTF-8";
303766
304456
  var SOAP12_CONTENT_TYPE = "application/soap+xml; charset=UTF-8";
303767
304457
  var WSA_NS = "http://www.w3.org/2005/08/addressing";
303768
304458
  function stableId2(seed) {
303769
- return (0, import_node_crypto4.createHash)("sha256").update(seed).digest("hex").slice(0, 32);
304459
+ return (0, import_node_crypto5.createHash)("sha256").update(seed).digest("hex").slice(0, 32);
303770
304460
  }
303771
304461
  function envelopeNamespace(version) {
303772
304462
  return version === "1.2" ? SOAP12_ENVELOPE_NS : SOAP11_ENVELOPE_NS;
@@ -310523,6 +311213,11 @@ function resolveInputs(env = process.env) {
310523
311213
  ),
310524
311214
  protocolEndpointUrl: getInput("protocol-endpoint-url", env),
310525
311215
  openapiVersion: resolveOpenapiVersion(getInput("openapi-version", env)),
311216
+ preserveOas30TypeNull: parseBooleanInput(
311217
+ "preserve-oas30-type-null",
311218
+ getInput("preserve-oas30-type-null", env),
311219
+ false
311220
+ ),
310526
311221
  breakingChangeMode: parseBreakingChangeMode(getInput("breaking-change-mode", env)),
310527
311222
  breakingBaselineSpecPath: getInput("breaking-baseline-spec-path", env),
310528
311223
  breakingRulesPath: getInput("breaking-rules-path", env) ?? bootstrapActionContract.inputs["breaking-rules-path"].default,
@@ -310553,9 +311248,72 @@ function resolveInputs(env = process.env) {
310553
311248
  githubRef: env.GITHUB_REF,
310554
311249
  githubSha: env.GITHUB_SHA,
310555
311250
  githubToken: getInput("github-token", env) || env.GITHUB_TOKEN || "",
310556
- ghFallbackToken: getInput("gh-fallback-token", env) || env.GH_FALLBACK_TOKEN || ""
311251
+ ghFallbackToken: getInput("gh-fallback-token", env) || env.GH_FALLBACK_TOKEN || "",
311252
+ branchStrategy: parseEnumInput(
311253
+ "branch-strategy",
311254
+ getInput("branch-strategy", env),
311255
+ "legacy"
311256
+ ),
311257
+ canonicalBranch: getInput("canonical-branch", env),
311258
+ channels: getInput("channels", env)
310557
311259
  };
310558
311260
  }
311261
+ function decideBranchTier(inputs, env = process.env) {
311262
+ return resolveEffectiveBranchDecision(
311263
+ {
311264
+ // Tolerate hand-built inputs objects (tests, embedders) that omit the
311265
+ // field: absent means legacy, exactly like the action default.
311266
+ strategy: inputs.branchStrategy ?? "legacy",
311267
+ identity: resolveBranchIdentity(env, { defaultBranch: inputs.canonicalBranch }),
311268
+ canonicalBranch: inputs.canonicalBranch,
311269
+ channels: parseChannelRules(inputs.channels)
311270
+ },
311271
+ env
311272
+ );
311273
+ }
311274
+ function embedSpecBranchMarker(content, decision, repo) {
311275
+ if (decision.tier !== "preview" && decision.tier !== "channel" || !decision.identity.headBranch || !repo) {
311276
+ return content;
311277
+ }
311278
+ const rawBranch = decision.identity.headBranch;
311279
+ const now = (/* @__PURE__ */ new Date()).toISOString();
311280
+ const marker = {
311281
+ repo,
311282
+ rawBranch,
311283
+ sanitizedBranch: rawBranch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").slice(0, 30),
311284
+ role: decision.tier,
311285
+ headSha: decision.identity.headSha,
311286
+ createdAt: now,
311287
+ lastSyncedAt: now
311288
+ };
311289
+ try {
311290
+ if (content.trim().startsWith("{")) {
311291
+ return `${JSON.stringify({ ...JSON.parse(content), "x-postman-onboarding": marker }, null, 2)}
311292
+ `;
311293
+ }
311294
+ const document2 = (0, import_yaml4.parse)(content);
311295
+ if (!document2 || typeof document2 !== "object" || Array.isArray(document2)) return content;
311296
+ return (0, import_yaml4.stringify)({ ...document2, "x-postman-onboarding": marker });
311297
+ } catch {
311298
+ return content;
311299
+ }
311300
+ }
311301
+ function renderCollectionBranchMarker(decision, repo, now = /* @__PURE__ */ new Date()) {
311302
+ if (decision.tier !== "preview" && decision.tier !== "channel" || !decision.identity.headBranch || !repo) {
311303
+ return void 0;
311304
+ }
311305
+ const rawBranch = decision.identity.headBranch;
311306
+ const timestamp = now.toISOString();
311307
+ return renderAssetMarker({
311308
+ repo,
311309
+ rawBranch,
311310
+ sanitizedBranch: rawBranch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").slice(0, 30),
311311
+ role: decision.tier,
311312
+ headSha: decision.identity.headSha,
311313
+ createdAt: timestamp,
311314
+ lastSyncedAt: timestamp
311315
+ });
311316
+ }
310559
311317
  function createPlannedOutputs(inputs) {
310560
311318
  const workspaceName = inputs.domainCode ? `[${inputs.domainCode}] ${inputs.projectName}` : inputs.projectName;
310561
311319
  return {
@@ -310587,7 +311345,12 @@ function createPlannedOutputs(inputs) {
310587
311345
  mode: inputs.breakingChangeMode,
310588
311346
  status: "skipped",
310589
311347
  summaryPath: ""
310590
- })
311348
+ }),
311349
+ "sync-status": "",
311350
+ "branch-decision": "",
311351
+ "spec-version-tag": "",
311352
+ "spec-version-url": "",
311353
+ "spec-content-changed": ""
310591
311354
  };
310592
311355
  }
310593
311356
  async function mintAccessTokenIfNeeded2(inputs, log, setSecret2) {
@@ -310692,6 +311455,27 @@ async function lintSpecViaCli(dependencies, workspaceId, specId) {
310692
311455
  warnings
310693
311456
  };
310694
311457
  }
311458
+ function normalizeLintPath(value) {
311459
+ return value.trim().replace(/^\$\.?/, "").replace(/\[(\d+)\]/g, ".$1").replace(/^\./, "");
311460
+ }
311461
+ function applyOas30TypeNullLintCompatibility(summary2, sourceTypeNullPaths) {
311462
+ const acceptedPaths = new Set(sourceTypeNullPaths.map(normalizeLintPath));
311463
+ if (acceptedPaths.size === 0) return summary2;
311464
+ const violations = summary2.violations.map((violation) => {
311465
+ const path8 = normalizeLintPath(violation.path ?? "");
311466
+ const issue2 = violation.issue ?? "";
311467
+ const isTypeEnumFinding = /["']?type["']? property/i.test(issue2) && /allowed values|must be equal to one of/i.test(issue2);
311468
+ if (violation.severity === "ERROR" && acceptedPaths.has(path8) && isTypeEnumFinding) {
311469
+ return { ...violation, severity: "WARNING" };
311470
+ }
311471
+ return violation;
311472
+ });
311473
+ return {
311474
+ errors: violations.filter((entry) => entry.severity === "ERROR").length,
311475
+ violations,
311476
+ warnings: violations.filter((entry) => entry.severity === "WARNING").length
311477
+ };
311478
+ }
310695
311479
  function shouldRetrySpecFetch(error) {
310696
311480
  const retryability = classifySafeFetchRetryability(error);
310697
311481
  return retryability === "retryable" || retryability === "unknown";
@@ -311172,7 +311956,7 @@ async function runProtocolBootstrap(specType, rawSpecContent, inputs, dependenci
311172
311956
  const workspaceRoot = path6.resolve(process.env.GITHUB_WORKSPACE ?? process.cwd());
311173
311957
  const serviceConfigPath = path6.join(path6.dirname(path6.resolve(workspaceRoot, inputs.specPath)), "grpc_service_config.json");
311174
311958
  try {
311175
- grpcServiceConfigJson = (0, import_node_fs5.readFileSync)(serviceConfigPath, "utf8");
311959
+ grpcServiceConfigJson = (0, import_node_fs6.readFileSync)(serviceConfigPath, "utf8");
311176
311960
  dependencies.core.info(`Found gRPC service config at ${serviceConfigPath}; validating it against the proto contract`);
311177
311961
  } catch {
311178
311962
  }
@@ -311186,7 +311970,7 @@ async function runProtocolBootstrap(specType, rawSpecContent, inputs, dependenci
311186
311970
  const resolved = path6.resolve(specDir, location2);
311187
311971
  if (!resolved.startsWith(specDir + path6.sep)) return void 0;
311188
311972
  try {
311189
- return (0, import_node_fs5.readFileSync)(resolved, "utf8");
311973
+ return (0, import_node_fs6.readFileSync)(resolved, "utf8");
311190
311974
  } catch {
311191
311975
  return void 0;
311192
311976
  }
@@ -311327,6 +312111,42 @@ async function createExtensibleContractCollection(workspaceId, built, inputs, de
311327
312111
  }
311328
312112
  async function runBootstrapInner(inputs, dependencies, telemetry) {
311329
312113
  const outputs = createPlannedOutputs(inputs);
312114
+ const branchDecision = decideBranchTier(inputs);
312115
+ const isCanonicalWriter = branchDecision.tier === "legacy" || branchDecision.tier === "canonical";
312116
+ const canonicalProjectName = inputs.projectName;
312117
+ const workspaceName = createWorkspaceName(inputs);
312118
+ const aboutText = `Auto-provisioned by Postman for ${inputs.projectName}`;
312119
+ if (branchDecision.tier === "preview" && branchDecision.identity.headBranch) {
312120
+ inputs = {
312121
+ ...inputs,
312122
+ projectName: previewAssetName(inputs.projectName, branchDecision.identity.headBranch)
312123
+ };
312124
+ dependencies.core.info(`branch-aware sync: preview asset set "${inputs.projectName}"`);
312125
+ } else if (branchDecision.tier === "channel" && branchDecision.channel) {
312126
+ inputs = {
312127
+ ...inputs,
312128
+ projectName: channelAssetName(inputs.projectName, branchDecision.channel.code)
312129
+ };
312130
+ dependencies.core.info(`branch-aware sync: channel asset set "${inputs.projectName}"`);
312131
+ }
312132
+ const collectionBranchMarker = renderCollectionBranchMarker(branchDecision, inputs.repoUrl);
312133
+ if (branchDecision.tier !== "legacy") {
312134
+ outputs["sync-status"] = "synced";
312135
+ outputs["branch-decision"] = serializeBranchDecision(branchDecision);
312136
+ }
312137
+ if (!isCanonicalWriter) {
312138
+ const explicitCanonicalIds = [
312139
+ ["spec-id", inputs.specId],
312140
+ ["baseline-collection-id", inputs.baselineCollectionId],
312141
+ ["smoke-collection-id", inputs.smokeCollectionId],
312142
+ ["contract-collection-id", inputs.contractCollectionId]
312143
+ ].filter(([, value]) => Boolean(value));
312144
+ if (explicitCanonicalIds.length > 0) {
312145
+ throw new Error(
312146
+ `CONTRACT_BRANCH_CANONICAL_WRITE: a ${branchDecision.tier} run must not mutate canonical assets, but explicit asset id input(s) were provided: ${explicitCanonicalIds.map(([name]) => name).join(", ")}. Remove them (preview/channel runs discover or create their own suffixed asset sets).`
312147
+ );
312148
+ }
312149
+ }
311330
312150
  const requiresReleaseLabel = inputs.collectionSyncMode === "version" || inputs.specSyncMode === "version";
311331
312151
  const releaseLabel = requiresReleaseLabel ? deriveReleaseLabel(inputs) : void 0;
311332
312152
  if (requiresReleaseLabel && !releaseLabel) {
@@ -311334,11 +312154,23 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311334
312154
  "Versioned spec or collection sync requires a release-label or derivable GitHub ref metadata"
311335
312155
  );
311336
312156
  }
311337
- const collectionAssetProjectName = inputs.collectionSyncMode === "version" ? createAssetProjectName(inputs, releaseLabel) : inputs.projectName;
311338
- const workspaceName = createWorkspaceName(inputs);
311339
- const aboutText = `Auto-provisioned by Postman for ${inputs.projectName}`;
311340
- const stateStore = resolveResourcesStateStore(dependencies);
311341
- const resourcesState = stateStore.read();
312157
+ const collectionAssetProjectName = branchDecision.tier === "channel" ? canonicalProjectName : inputs.collectionSyncMode === "version" ? createAssetProjectName(inputs, releaseLabel) : inputs.projectName;
312158
+ const rawStateStore = resolveResourcesStateStore(dependencies);
312159
+ const trackedState = rawStateStore.read();
312160
+ const stateStore = isCanonicalWriter ? rawStateStore : {
312161
+ read: rawStateStore.read,
312162
+ write: () => {
312163
+ dependencies.core.info(
312164
+ `branch-aware sync: skipping .postman/resources.yaml write on ${branchDecision.tier} run (canonical-only tracked state)`
312165
+ );
312166
+ }
312167
+ };
312168
+ const resourcesState = isCanonicalWriter ? trackedState : trackedState?.workspace ? { workspace: trackedState.workspace } : null;
312169
+ if (!isCanonicalWriter && trackedState?.cloudResources) {
312170
+ dependencies.core.info(
312171
+ "branch-aware sync: canonical asset ids in .postman/resources.yaml are not resolved on a non-canonical run"
312172
+ );
312173
+ }
311342
312174
  const writableResourcesState = resourcesState ?? {};
311343
312175
  const additionalCollections = loadAdditionalCollectionFiles(
311344
312176
  inputs.additionalCollectionsDir,
@@ -311358,16 +312190,19 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311358
312190
  }
311359
312191
  let previousSpecContent;
311360
312192
  let previousSpecRollbackHash;
312193
+ let specContentUnchanged = false;
311361
312194
  let detectedOpenapiVersion = "3.0";
311362
312195
  let contractIndex;
311363
312196
  let sourceSpecContent = "";
312197
+ let sourceTypeNullPaths = [];
312198
+ let preserveSourceSpecBytes = false;
311364
312199
  const rawSpecContent = await runGroup(
311365
312200
  dependencies.core,
311366
312201
  "Read API Spec",
311367
312202
  async () => {
311368
312203
  if (inputs.specPath) {
311369
312204
  const workspaceRoot = path6.resolve(process.env.GITHUB_WORKSPACE ?? process.cwd());
311370
- return (0, import_node_fs5.readFileSync)(path6.resolve(workspaceRoot, inputs.specPath), "utf8");
312205
+ return (0, import_node_fs6.readFileSync)(path6.resolve(workspaceRoot, inputs.specPath), "utf8");
311371
312206
  }
311372
312207
  if (dependencies.specFetcher === fetch) {
311373
312208
  return safeFetchText(inputs.specUrl, { depth: 0 });
@@ -311403,6 +312238,7 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311403
312238
  };
311404
312239
  const rootKey = inputs.specPath ? void 0 : normalizeRef(inputs.specUrl);
311405
312240
  const loaderOptions = {
312241
+ preserveOas30TypeNull: Boolean(inputs.preserveOas30TypeNull),
311406
312242
  fetchText: async (url, fetchOptions) => {
311407
312243
  if (rootKey && normalizeRef(url) === rootKey) return rawSpecContent;
311408
312244
  if (dependencies.specFetcher === fetch) return safeFetchText(url, fetchOptions);
@@ -311411,6 +312247,8 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311411
312247
  };
311412
312248
  const loaded = inputs.specPath ? await loadOpenApiContractSpecFromPath(inputs.specPath, loaderOptions) : await loadOpenApiContractSpec(inputs.specUrl, loaderOptions);
311413
312249
  sourceSpecContent = loaded.content;
312250
+ sourceTypeNullPaths = loaded.sourceTypeNullPaths;
312251
+ preserveSourceSpecBytes = sourceTypeNullPaths.length > 0;
311414
312252
  const document2 = normalizeSpecDocument(
311415
312253
  loaded.bundledContent,
311416
312254
  (msg) => dependencies.core.warning(msg)
@@ -311433,11 +312271,11 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311433
312271
  `Unable to verify existing Spec Hub OpenAPI version for spec-id ${specId}; clear spec-id to create a fresh spec`
311434
312272
  );
311435
312273
  }
311436
- previousSpecContent = normalizeSpecDocument(
312274
+ previousSpecContent = preserveSourceSpecBytes ? previousRaw : normalizeSpecDocument(
311437
312275
  previousRaw,
311438
312276
  (msg) => dependencies.core.warning(`Previous spec normalization: ${msg}`)
311439
312277
  );
311440
- previousSpecRollbackHash = (0, import_node_crypto5.createHash)("sha256").update(previousSpecContent).digest("hex");
312278
+ previousSpecRollbackHash = (0, import_node_crypto6.createHash)("sha256").update(previousSpecContent).digest("hex");
311441
312279
  const existingSpecType = normalizeSpecTypeFromContent(previousSpecContent);
311442
312280
  if (existingSpecType !== incomingSpecType) {
311443
312281
  throw new Error(
@@ -311448,6 +312286,12 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311448
312286
  dependencies.core.info(
311449
312287
  `Auto-detected OpenAPI version from spec content: ${detectedOpenapiVersion}`
311450
312288
  );
312289
+ if (preserveSourceSpecBytes) {
312290
+ dependencies.core.info(
312291
+ `Preserving original OpenAPI source bytes; accepted ${sourceTypeNullPaths.length} legacy type: null member(s) for internal validation`
312292
+ );
312293
+ return loaded.content;
312294
+ }
311451
312295
  return document2;
311452
312296
  }
311453
312297
  );
@@ -311483,6 +312327,11 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311483
312327
  `OpenAPI breaking-change check failed: ${breakingChangeResult.message || "breaking changes detected"}`
311484
312328
  );
311485
312329
  }
312330
+ const uploadSpecContent = embedSpecBranchMarker(
312331
+ specContent,
312332
+ branchDecision,
312333
+ inputs.repoUrl
312334
+ );
311486
312335
  const provisioned = await provisionWorkspace(
311487
312336
  inputs,
311488
312337
  dependencies,
@@ -311607,10 +312456,17 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311607
312456
  specId ? "Update Spec in Spec Hub" : "Upload Spec to Spec Hub",
311608
312457
  async () => {
311609
312458
  if (specId) {
311610
- dependencies.core.info(
311611
- `Updating existing spec ${specId} (detected version: ${detectedOpenapiVersion}). Note: the spec type (OPENAPI:3.0 / OPENAPI:3.1) is set at creation and cannot be changed on update. If you changed OpenAPI versions, clear the spec-id input to create a fresh spec.`
311612
- );
311613
- await dependencies.postman.updateSpec(specId, specContent, workspaceId);
312459
+ if (previousSpecContent !== void 0 && (0, import_node_crypto6.createHash)("sha256").update(uploadSpecContent).digest("hex") === (0, import_node_crypto6.createHash)("sha256").update(previousSpecContent).digest("hex")) {
312460
+ specContentUnchanged = true;
312461
+ dependencies.core.info(
312462
+ `Spec content unchanged (sha256 match); skipping Spec Hub update and version tag for ${specId}.`
312463
+ );
312464
+ } else {
312465
+ dependencies.core.info(
312466
+ `Updating existing spec ${specId} (detected version: ${detectedOpenapiVersion}). Note: the spec type (OPENAPI:3.0 / OPENAPI:3.1) is set at creation and cannot be changed on update. If you changed OpenAPI versions, clear the spec-id input to create a fresh spec.`
312467
+ );
312468
+ await dependencies.postman.updateSpec(specId, uploadSpecContent, workspaceId);
312469
+ }
311614
312470
  } else {
311615
312471
  specId = await dependencies.postman.uploadSpec(
311616
312472
  workspaceId || "",
@@ -311618,7 +312474,7 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311618
312474
  inputs,
311619
312475
  inputs.specSyncMode === "version" ? releaseLabel : void 0
311620
312476
  ),
311621
- specContent,
312477
+ uploadSpecContent,
311622
312478
  detectedOpenapiVersion
311623
312479
  );
311624
312480
  }
@@ -311635,8 +312491,9 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311635
312491
  }
311636
312492
  )
311637
312493
  );
312494
+ outputs["spec-content-changed"] = isCanonicalWriter && !specContentUnchanged ? "true" : "false";
311638
312495
  if (lintEnabled) {
311639
- const lintSummary = await runRollbackStage(
312496
+ let lintSummary = await runRollbackStage(
311640
312497
  "Lint Spec via Postman CLI",
311641
312498
  async () => runGroup(
311642
312499
  dependencies.core,
@@ -311644,6 +312501,16 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311644
312501
  async () => lintSpecViaCli(dependencies, workspaceId || "", outputs["spec-id"])
311645
312502
  )
311646
312503
  );
312504
+ if (preserveSourceSpecBytes) {
312505
+ const originalErrorCount = lintSummary.errors;
312506
+ lintSummary = applyOas30TypeNullLintCompatibility(lintSummary, sourceTypeNullPaths);
312507
+ const acceptedCount = originalErrorCount - lintSummary.errors;
312508
+ if (acceptedCount > 0) {
312509
+ dependencies.core.info(
312510
+ `Accepted ${acceptedCount} Postman CLI type: null finding(s) covered by preserve-oas30-type-null`
312511
+ );
312512
+ }
312513
+ }
311647
312514
  outputs["lint-summary-json"] = JSON.stringify({
311648
312515
  errors: lintSummary.errors,
311649
312516
  total: lintSummary.violations.length,
@@ -311668,7 +312535,12 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311668
312535
  });
311669
312536
  dependencies.core.warning("lint skipped: governance errors not enforced (no postman-api-key)");
311670
312537
  }
311671
- await runRollbackStage(
312538
+ if (specContentUnchanged) {
312539
+ outputs["baseline-collection-id"] = baselineCollectionId || "";
312540
+ outputs["smoke-collection-id"] = smokeCollectionId || "";
312541
+ outputs["contract-collection-id"] = contractCollectionId || "";
312542
+ dependencies.core.info("Spec content unchanged; skipping collection regeneration and version finalization.");
312543
+ } else await runRollbackStage(
311672
312544
  "Generate Collections from Spec",
311673
312545
  async () => runGroup(
311674
312546
  dependencies.core,
@@ -311707,10 +312579,11 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311707
312579
  return;
311708
312580
  }
311709
312581
  }
312582
+ const effectivePrefix = branchDecision.tier === "channel" && branchDecision.channel ? channelAssetName(prefix, branchDecision.channel.code).trim() : prefix;
311710
312583
  outputs[outputKey] = await dependencies.postman.generateCollection(
311711
312584
  specId2,
311712
312585
  assetProjectName,
311713
- prefix,
312586
+ effectivePrefix,
311714
312587
  inputs.folderStrategy,
311715
312588
  inputs.nestedFolderHierarchy,
311716
312589
  inputs.requestNameSource
@@ -311731,6 +312604,32 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311731
312604
  await ensureCollection(BASELINE_COLLECTION_PREFIX, baselineCollectionId, "baseline-collection-id");
311732
312605
  await ensureCollection(SMOKE_COLLECTION_PREFIX, smokeCollectionId, "smoke-collection-id");
311733
312606
  await ensureCollection(CONTRACT_COLLECTION_PREFIX, contractCollectionId, "contract-collection-id");
312607
+ if (dependencies.postman.adoptGeneratedCollection) {
312608
+ const reelect = async (prefix, outputKey) => {
312609
+ const effectivePrefix = branchDecision.tier === "channel" && branchDecision.channel ? channelAssetName(prefix, branchDecision.channel.code).trim() : prefix;
312610
+ const preferred = outputs[outputKey];
312611
+ if (!preferred) return;
312612
+ outputs[outputKey] = await dependencies.postman.adoptGeneratedCollection(
312613
+ specId2,
312614
+ assetProjectName,
312615
+ effectivePrefix,
312616
+ preferred
312617
+ );
312618
+ };
312619
+ await reelect(BASELINE_COLLECTION_PREFIX, "baseline-collection-id");
312620
+ await reelect(SMOKE_COLLECTION_PREFIX, "smoke-collection-id");
312621
+ await reelect(CONTRACT_COLLECTION_PREFIX, "contract-collection-id");
312622
+ }
312623
+ if (collectionBranchMarker) {
312624
+ if (!dependencies.postman.updateCollectionDescription) {
312625
+ throw new Error("Branch-scoped collections require updateCollectionDescription support");
312626
+ }
312627
+ await Promise.all([
312628
+ outputs["baseline-collection-id"],
312629
+ outputs["smoke-collection-id"],
312630
+ outputs["contract-collection-id"]
312631
+ ].filter(Boolean).map((id) => dependencies.postman.updateCollectionDescription(id, collectionBranchMarker)));
312632
+ }
311734
312633
  if (!dependencies.postman.injectContractTests) {
311735
312634
  throw new Error(
311736
312635
  "Dynamic contract tests require injectContractTests support from the access-token gateway client"
@@ -311833,6 +312732,12 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311833
312732
  workspaceId: workspaceId || ""
311834
312733
  });
311835
312734
  for (const result of additionalResults) {
312735
+ if (collectionBranchMarker) {
312736
+ if (!dependencies.postman.updateCollectionDescription) {
312737
+ throw new Error("Branch-scoped collections require updateCollectionDescription support");
312738
+ }
312739
+ await dependencies.postman.updateCollectionDescription(result.collectionId, collectionBranchMarker);
312740
+ }
311836
312741
  completedExternalSideEffects.push(
311837
312742
  `${result.operation}AdditionalCollection(${result.collectionId} from ${result.displayPath})`
311838
312743
  );
@@ -311908,6 +312813,53 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
311908
312813
  }
311909
312814
  return outputs;
311910
312815
  }
312816
+ async function runGatedValidation(inputs, decision, actionCore) {
312817
+ actionCore.info(`branch-aware sync: gated run (${decision.reason}) \u2014 credential-free static validation, zero workspace writes`);
312818
+ const outputs = createPlannedOutputs(inputs);
312819
+ outputs["sync-status"] = "skipped-branch-gate";
312820
+ outputs["branch-decision"] = serializeBranchDecision(decision);
312821
+ let violations = [];
312822
+ let validated = false;
312823
+ try {
312824
+ let content;
312825
+ if (inputs.specPath) {
312826
+ content = (0, import_node_fs6.readFileSync)(inputs.specPath, "utf8");
312827
+ } else if (inputs.specUrl) {
312828
+ content = await safeFetchText(inputs.specUrl, { depth: 0 });
312829
+ }
312830
+ if (content) {
312831
+ const specType = detectSpecType(content, inputs.specPath);
312832
+ if (specType === "openapi") {
312833
+ const document2 = parseOpenApiDocument(content);
312834
+ const index = buildContractIndex(document2);
312835
+ violations = index.warnings;
312836
+ validated = true;
312837
+ } else {
312838
+ actionCore.info(`branch gate: static lint for spec type ${specType} runs through its protocol builder on publish; parse-only gate applied`);
312839
+ validated = true;
312840
+ }
312841
+ } else {
312842
+ actionCore.info("branch gate: no spec-url/spec-path provided; nothing to validate");
312843
+ }
312844
+ } catch (error) {
312845
+ throw new Error(
312846
+ `branch gate: static validation failed: ${error instanceof Error ? error.message : String(error)}`,
312847
+ { cause: error }
312848
+ );
312849
+ }
312850
+ outputs["lint-summary-json"] = JSON.stringify({
312851
+ status: validated ? "static-only" : "skipped",
312852
+ reason: "branch-gated run: governance lint requires upload and runs on publish/preview syncs",
312853
+ errors: 0,
312854
+ warnings: violations.length,
312855
+ total: violations.length,
312856
+ violations: violations.map((issue2) => ({ issue: issue2, severity: "WARNING" }))
312857
+ });
312858
+ for (const [name, value] of Object.entries(outputs)) {
312859
+ actionCore.setOutput(name, value);
312860
+ }
312861
+ return outputs;
312862
+ }
311911
312863
  function createRoutingPostmanClient(options) {
311912
312864
  const { gateway } = options;
311913
312865
  const requireAccessToken = (operation) => async () => {
@@ -311925,6 +312877,7 @@ function createRoutingPostmanClient(options) {
311925
312877
  updateSpec: requireAccessToken("updateSpec"),
311926
312878
  getSpecContent: requireAccessToken("getSpecContent"),
311927
312879
  generateCollection: requireAccessToken("generateCollection"),
312880
+ adoptGeneratedCollection: requireAccessToken("adoptGeneratedCollection"),
311928
312881
  createWorkspace: requireAccessToken("createWorkspace"),
311929
312882
  getWorkspaceVisibility: requireAccessToken("getWorkspaceVisibility"),
311930
312883
  getWorkspaceGitRepoUrl: requireAccessToken("getWorkspaceGitRepoUrl"),
@@ -311934,10 +312887,13 @@ function createRoutingPostmanClient(options) {
311934
312887
  inviteRequesterToWorkspace: requireAccessToken("inviteRequesterToWorkspace"),
311935
312888
  injectTests: requireAccessToken("injectTests"),
311936
312889
  tagCollection: requireAccessToken("tagCollection"),
312890
+ tagSpecVersion: requireAccessToken("tagSpecVersion"),
312891
+ listSpecVersionTags: requireAccessToken("listSpecVersionTags"),
311937
312892
  deleteCollection: requireAccessToken("deleteCollection"),
311938
312893
  injectContractTests: requireAccessToken("injectContractTests"),
311939
312894
  createCollection: requireAccessToken("createCollection"),
311940
- updateCollection: requireAccessToken("updateCollection")
312895
+ updateCollection: requireAccessToken("updateCollection"),
312896
+ updateCollectionDescription: requireAccessToken("updateCollectionDescription")
311941
312897
  };
311942
312898
  }
311943
312899
  return {
@@ -311949,6 +312905,7 @@ function createRoutingPostmanClient(options) {
311949
312905
  // not a reason to reach for the API key.
311950
312906
  uploadSpec: (workspaceId, projectName, specContent, openapiVersion) => gateway.uploadSpec(workspaceId, projectName, specContent, openapiVersion ?? "3.0"),
311951
312907
  generateCollection: (specId, projectName, prefix, folderStrategy, nestedFolderHierarchy, requestNameSource) => gateway.generateCollection(specId, projectName, prefix, folderStrategy, nestedFolderHierarchy, requestNameSource),
312908
+ adoptGeneratedCollection: (specId, projectName, prefix, preferredId) => gateway.adoptGeneratedCollection(specId, projectName, prefix, preferredId),
311952
312909
  updateSpec: (specId, specContent, workspaceId) => gateway.updateSpec(specId, specContent, workspaceId),
311953
312910
  getSpecContent: (specId) => gateway.getSpecContent(specId),
311954
312911
  createWorkspace: (name, about, targetTeamId) => gateway.createWorkspace(name, about, targetTeamId),
@@ -311960,6 +312917,8 @@ function createRoutingPostmanClient(options) {
311960
312917
  // + tagging surfaces (live-proven). PMAK is reserved for token minting, so
311961
312918
  // these never fall back to the API key even when one is present.
311962
312919
  injectTests: (collectionId, type) => gateway.injectTests(collectionId, type),
312920
+ tagSpecVersion: (specId, name) => gateway.tagSpecVersion(specId, name),
312921
+ listSpecVersionTags: (specId) => gateway.listSpecVersionTags(specId),
311963
312922
  tagCollection: (collectionId, tags) => gateway.tagCollection(collectionId, tags),
311964
312923
  // Sub-team (squad) enumeration over the gateway `ums` service. Access-token
311965
312924
  // only — never PMAK — so org-mode detection no longer needs a PMAK GET /teams.
@@ -311977,7 +312936,8 @@ function createRoutingPostmanClient(options) {
311977
312936
  // so no asset op ever reaches for the API key.
311978
312937
  injectContractTests: (collectionUid, index) => gateway.injectContractTests(collectionUid, index),
311979
312938
  createCollection: (workspaceId, collection, options2) => gateway.createCollection(workspaceId, collection, options2),
311980
- updateCollection: (collectionUid, collection) => gateway.updateCollection(collectionUid, collection)
312939
+ updateCollection: (collectionUid, collection) => gateway.updateCollection(collectionUid, collection),
312940
+ updateCollectionDescription: (collectionUid, description) => gateway.updateCollectionDescription(collectionUid, description)
311981
312941
  };
311982
312942
  }
311983
312943
  function createBootstrapDependencies(inputs, factories, orgMode = false) {
@@ -312080,7 +313040,7 @@ function resolvePackageVersion() {
312080
313040
  candidates.push(import_node_path5.default.join(process.cwd(), "package.json"));
312081
313041
  for (const packageJsonPath of candidates) {
312082
313042
  try {
312083
- const packageJson = JSON.parse((0, import_node_fs6.readFileSync)(packageJsonPath, "utf8"));
313043
+ const packageJson = JSON.parse((0, import_node_fs7.readFileSync)(packageJsonPath, "utf8"));
312084
313044
  if (packageJson.name === "@postman-cse/onboarding-bootstrap" && packageJson.version) {
312085
313045
  return String(packageJson.version).trim();
312086
313046
  }
@@ -312131,6 +313091,9 @@ var cliInputNames = [
312131
313091
  "postman-api-key",
312132
313092
  "postman-access-token",
312133
313093
  "credential-preflight",
313094
+ "branch-strategy",
313095
+ "canonical-branch",
313096
+ "channels",
312134
313097
  "workspace-id",
312135
313098
  "spec-id",
312136
313099
  "baseline-collection-id",
@@ -312156,6 +313119,7 @@ var cliInputNames = [
312156
313119
  "workspace-team-id",
312157
313120
  "repo-url",
312158
313121
  "openapi-version",
313122
+ "preserve-oas30-type-null",
312159
313123
  "breaking-change-mode",
312160
313124
  "breaking-baseline-spec-path",
312161
313125
  "breaking-rules-path",
@@ -312292,11 +313256,11 @@ function nearestExistingPath2(candidate) {
312292
313256
  return current;
312293
313257
  }
312294
313258
  function pathExists(candidate) {
312295
- if ((0, import_node_fs6.existsSync)(candidate)) {
313259
+ if ((0, import_node_fs7.existsSync)(candidate)) {
312296
313260
  return true;
312297
313261
  }
312298
313262
  try {
312299
- (0, import_node_fs6.lstatSync)(candidate);
313263
+ (0, import_node_fs7.lstatSync)(candidate);
312300
313264
  return true;
312301
313265
  } catch {
312302
313266
  return false;
@@ -312304,10 +313268,10 @@ function pathExists(candidate) {
312304
313268
  }
312305
313269
  function checkedRealPath(existingPath, workspaceRealPath) {
312306
313270
  try {
312307
- return (0, import_node_fs6.realpathSync)(existingPath);
313271
+ return (0, import_node_fs7.realpathSync)(existingPath);
312308
313272
  } catch (error) {
312309
- if ((0, import_node_fs6.lstatSync)(existingPath).isSymbolicLink()) {
312310
- const linkTarget = (0, import_node_fs6.readlinkSync)(existingPath);
313273
+ if ((0, import_node_fs7.lstatSync)(existingPath).isSymbolicLink()) {
313274
+ const linkTarget = (0, import_node_fs7.readlinkSync)(existingPath);
312311
313275
  const resolvedTarget = import_node_path5.default.resolve(import_node_path5.default.dirname(existingPath), linkTarget);
312312
313276
  ensureInsideWorkspace(workspaceRealPath, resolvedTarget);
312313
313277
  }
@@ -312319,7 +313283,7 @@ function assertOutputFileAllowed2(filePath) {
312319
313283
  return void 0;
312320
313284
  }
312321
313285
  const workspaceRoot = import_node_path5.default.resolve(process.cwd());
312322
- const workspaceRealPath = (0, import_node_fs6.realpathSync)(workspaceRoot);
313286
+ const workspaceRealPath = (0, import_node_fs7.realpathSync)(workspaceRoot);
312323
313287
  const resolved = import_node_path5.default.isAbsolute(filePath) ? import_node_path5.default.resolve(filePath) : import_node_path5.default.resolve(workspaceRoot, filePath);
312324
313288
  const existingPath = nearestExistingPath2(resolved);
312325
313289
  ensureInsideWorkspace(workspaceRealPath, checkedRealPath(existingPath, workspaceRealPath));
@@ -312331,7 +313295,7 @@ async function writeOptionalFile(filePath, content) {
312331
313295
  return;
312332
313296
  }
312333
313297
  await (0, import_promises4.mkdir)(import_node_path5.default.dirname(resolved), { recursive: true });
312334
- ensureInsideWorkspace((0, import_node_fs6.realpathSync)(import_node_path5.default.resolve(process.cwd())), (0, import_node_fs6.realpathSync)(import_node_path5.default.dirname(resolved)));
313298
+ ensureInsideWorkspace((0, import_node_fs7.realpathSync)(import_node_path5.default.resolve(process.cwd())), (0, import_node_fs7.realpathSync)(import_node_path5.default.dirname(resolved)));
312335
313299
  await (0, import_promises4.writeFile)(resolved, content, "utf8");
312336
313300
  }
312337
313301
  function requireCliInput(name, value) {
@@ -312339,7 +313303,7 @@ function requireCliInput(name, value) {
312339
313303
  throw new Error(`${name} is required`);
312340
313304
  }
312341
313305
  }
312342
- function validateCliInputs(inputs) {
313306
+ function validateCliInputs(inputs, options = {}) {
312343
313307
  requireCliInput("project-name", inputs.projectName);
312344
313308
  if (!inputs.specUrl && !inputs.specPath) {
312345
313309
  throw new Error("One of spec-url or spec-path is required");
@@ -312347,7 +313311,7 @@ function validateCliInputs(inputs) {
312347
313311
  if (inputs.specUrl && inputs.specPath) {
312348
313312
  throw new Error("Provide either spec-url or spec-path, not both.");
312349
313313
  }
312350
- if (!inputs.postmanApiKey && !inputs.postmanAccessToken) {
313314
+ if (options.requireCredentials !== false && !inputs.postmanApiKey && !inputs.postmanAccessToken) {
312351
313315
  throw new Error("One of postman-api-key or postman-access-token is required.");
312352
313316
  }
312353
313317
  }
@@ -312368,9 +313332,26 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
312368
313332
  const env = runtime.env ?? process.env;
312369
313333
  const config = parseCliArgs(argv, env);
312370
313334
  const inputs = resolveInputs(config.inputEnv);
312371
- validateCliInputs(inputs);
313335
+ const branchDecision = decideBranchTier(inputs, config.inputEnv);
313336
+ validateCliInputs(inputs, { requireCredentials: branchDecision.tier !== "gated" });
312372
313337
  assertOutputFileAllowed2(config.resultJsonPath);
312373
313338
  assertOutputFileAllowed2(config.dotenvPath);
313339
+ if (branchDecision.tier === "gated") {
313340
+ const gatedReporter = new ConsoleReporter();
313341
+ const gated = await runGatedValidation(inputs, branchDecision, {
313342
+ info: (m) => gatedReporter.info(m),
313343
+ warning: (m) => gatedReporter.warning(m),
313344
+ setOutput: () => void 0
313345
+ });
313346
+ await writeOptionalFile(config.resultJsonPath, JSON.stringify(gated, null, 2));
313347
+ await writeOptionalFile(config.dotenvPath, toDotenv(gated));
313348
+ writeStdout(`${JSON.stringify(gated, null, 2)}
313349
+ `);
313350
+ return;
313351
+ }
313352
+ if (branchDecision.tier !== "legacy") {
313353
+ process.env[BRANCH_DECISION_ENV] = serializeBranchDecision(branchDecision);
313354
+ }
312374
313355
  const mintReporter = new ConsoleReporter();
312375
313356
  await mintAccessTokenIfNeeded2(inputs, mintReporter);
312376
313357
  const dependencies = createCliDependencies(inputs);
@@ -312403,7 +313384,7 @@ function isEntrypoint(currentPath, entrypointPath) {
312403
313384
  return false;
312404
313385
  }
312405
313386
  try {
312406
- return (0, import_node_fs6.realpathSync)(currentPath) === (0, import_node_fs6.realpathSync)(entrypointPath);
313387
+ return (0, import_node_fs7.realpathSync)(currentPath) === (0, import_node_fs7.realpathSync)(entrypointPath);
312407
313388
  } catch {
312408
313389
  return import_node_path5.default.resolve(currentPath) === import_node_path5.default.resolve(entrypointPath);
312409
313390
  }