@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/action.cjs CHANGED
@@ -99989,11 +99989,11 @@ var require_valid = __commonJS({
99989
99989
  var require_clean = __commonJS({
99990
99990
  "node_modules/postman-collection/node_modules/semver/functions/clean.js"(exports2, module2) {
99991
99991
  var parse10 = require_parse6();
99992
- var clean = (version, options) => {
99992
+ var clean2 = (version, options) => {
99993
99993
  const s = parse10(version.trim().replace(/^[=v]+/, ""), options);
99994
99994
  return s ? s.version : null;
99995
99995
  };
99996
- module2.exports = clean;
99996
+ module2.exports = clean2;
99997
99997
  }
99998
99998
  });
99999
99999
 
@@ -101304,7 +101304,7 @@ var require_semver2 = __commonJS({
101304
101304
  var identifiers = require_identifiers();
101305
101305
  var parse10 = require_parse6();
101306
101306
  var valid = require_valid();
101307
- var clean = require_clean();
101307
+ var clean2 = require_clean();
101308
101308
  var inc = require_inc();
101309
101309
  var diff = require_diff();
101310
101310
  var major = require_major();
@@ -101342,7 +101342,7 @@ var require_semver2 = __commonJS({
101342
101342
  module2.exports = {
101343
101343
  parse: parse10,
101344
101344
  valid,
101345
- clean,
101345
+ clean: clean2,
101346
101346
  inc,
101347
101347
  diff,
101348
101348
  major,
@@ -256197,8 +256197,8 @@ function getIDToken(aud) {
256197
256197
  }
256198
256198
 
256199
256199
  // src/index.ts
256200
- var import_node_crypto5 = require("node:crypto");
256201
- var import_node_fs5 = require("node:fs");
256200
+ var import_node_crypto6 = require("node:crypto");
256201
+ var import_node_fs6 = require("node:fs");
256202
256202
  var path9 = __toESM(require("node:path"), 1);
256203
256203
  var import_yaml4 = __toESM(require_dist(), 1);
256204
256204
 
@@ -256306,6 +256306,12 @@ var bootstrapActionContract = {
256306
256306
  default: "",
256307
256307
  allowedValues: ["3.0", "3.1"]
256308
256308
  },
256309
+ "preserve-oas30-type-null": {
256310
+ 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.",
256311
+ required: false,
256312
+ default: "false",
256313
+ allowedValues: ["true", "false"]
256314
+ },
256309
256315
  "breaking-change-mode": {
256310
256316
  description: "OpenAPI breaking-change comparison mode.",
256311
256317
  required: false,
@@ -256362,6 +256368,20 @@ var bootstrapActionContract = {
256362
256368
  default: "warn",
256363
256369
  allowedValues: ["enforce", "warn"]
256364
256370
  },
256371
+ "branch-strategy": {
256372
+ 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.",
256373
+ required: false,
256374
+ default: "legacy",
256375
+ allowedValues: ["legacy", "preview", "publish-gate"]
256376
+ },
256377
+ "canonical-branch": {
256378
+ 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.",
256379
+ required: false
256380
+ },
256381
+ "channels": {
256382
+ 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.',
256383
+ required: false
256384
+ },
256365
256385
  "integration-backend": {
256366
256386
  description: "Integration backend for downstream workspace connectivity.",
256367
256387
  required: false,
@@ -256431,6 +256451,21 @@ var bootstrapActionContract = {
256431
256451
  },
256432
256452
  "breaking-change-summary-json": {
256433
256453
  description: "JSON summary of the OpenAPI breaking-change check."
256454
+ },
256455
+ "sync-status": {
256456
+ 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."
256457
+ },
256458
+ "branch-decision": {
256459
+ description: "Serialized BranchDecision JSON for downstream actions (also exported as POSTMAN_BRANCH_DECISION)."
256460
+ },
256461
+ "spec-version-tag": {
256462
+ 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)."
256463
+ },
256464
+ "spec-version-url": {
256465
+ description: "Reserved for the repo-sync finalizer; bootstrap does not tag before complete onboarding."
256466
+ },
256467
+ "spec-content-changed": {
256468
+ description: "Whether bootstrap changed canonical spec content; repo-sync uses this to skip native version tags on no-op syncs."
256434
256469
  }
256435
256470
  },
256436
256471
  retainedBehavior: [
@@ -257557,6 +257592,15 @@ function assertSupportedLocalViewContract(node, options) {
257557
257592
  }
257558
257593
 
257559
257594
  // src/lib/postman/additional-collections.ts
257595
+ var RESOURCES_STATE_VERSION = 2;
257596
+ var SUPPORTED_STATE_VERSIONS = /* @__PURE__ */ new Set([1, RESOURCES_STATE_VERSION]);
257597
+ var StateUnreadableError = class extends Error {
257598
+ code = "CONTRACT_STATE_UNREADABLE";
257599
+ constructor(message) {
257600
+ super(`CONTRACT_STATE_UNREADABLE: ${message}`);
257601
+ this.name = "StateUnreadableError";
257602
+ }
257603
+ };
257560
257604
  var ADDITIONAL_COLLECTION_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".yaml", ".yml"]);
257561
257605
  var POSTMAN_COLLECTION_V21_SCHEMA_FRAGMENT = "/collection/v2.1.0/collection.json";
257562
257606
  var V3_DEFINITION_PATH = import_node_path2.default.join(".resources", "definition.yaml");
@@ -257612,16 +257656,46 @@ function resourcesStatePath() {
257612
257656
  return import_node_path2.default.join(workspaceRootForLocalInputs(), RESOURCES_PATH);
257613
257657
  }
257614
257658
  function readResourcesState() {
257659
+ let raw;
257615
257660
  try {
257616
- return (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(resourcesStatePath(), "utf8"));
257661
+ raw = (0, import_node_fs2.readFileSync)(resourcesStatePath(), "utf8");
257617
257662
  } catch {
257618
257663
  return null;
257619
257664
  }
257665
+ let parsed;
257666
+ try {
257667
+ parsed = (0, import_yaml.parse)(raw);
257668
+ } catch (error2) {
257669
+ throw new StateUnreadableError(
257670
+ `.postman/resources.yaml exists but is not parseable YAML (${error2 instanceof Error ? error2.message : String(error2)}). Fix or delete the file; refusing to treat tracked state as absent.`
257671
+ );
257672
+ }
257673
+ if (parsed === null || parsed === void 0) {
257674
+ return null;
257675
+ }
257676
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
257677
+ throw new StateUnreadableError(
257678
+ ".postman/resources.yaml exists but does not contain a YAML mapping. Fix or delete the file; refusing to treat tracked state as absent."
257679
+ );
257680
+ }
257681
+ const state = parsed;
257682
+ if (state.version !== void 0 && !SUPPORTED_STATE_VERSIONS.has(Number(state.version))) {
257683
+ throw new StateUnreadableError(
257684
+ `.postman/resources.yaml declares unsupported state version ${String(state.version)} (supported: 1, ${RESOURCES_STATE_VERSION}). Upgrade the action or fix the file.`
257685
+ );
257686
+ }
257687
+ if (state.canonical && !state.cloudResources) {
257688
+ state.cloudResources = { ...state.canonical };
257689
+ }
257690
+ return state;
257620
257691
  }
257621
257692
  function writeResourcesState(state) {
257622
257693
  const target = resourcesStatePath();
257623
257694
  (0, import_node_fs2.mkdirSync)(import_node_path2.default.dirname(target), { recursive: true });
257624
- (0, import_node_fs2.writeFileSync)(target, (0, import_yaml.stringify)(state), "utf8");
257695
+ const canonical = state.cloudResources ?? state.canonical ?? {};
257696
+ const persisted = { ...state, version: RESOURCES_STATE_VERSION, canonical };
257697
+ delete persisted.cloudResources;
257698
+ (0, import_node_fs2.writeFileSync)(target, (0, import_yaml.stringify)(persisted), "utf8");
257625
257699
  }
257626
257700
  function findCloudResourceId(map, matcher) {
257627
257701
  if (!map) {
@@ -257631,7 +257705,7 @@ function findCloudResourceId(map, matcher) {
257631
257705
  return match?.[1];
257632
257706
  }
257633
257707
  function findExistingAdditionalCollectionId(resourcesState, resourcePath) {
257634
- return resourcesState?.cloudResources?.additionalCollections?.[resourcePath] ?? resourcesState?.cloudResources?.collections?.[resourcePath];
257708
+ return resourcesState?.canonical?.additionalCollections?.[resourcePath] ?? resourcesState?.canonical?.collections?.[resourcePath] ?? resourcesState?.cloudResources?.additionalCollections?.[resourcePath] ?? resourcesState?.cloudResources?.collections?.[resourcePath];
257635
257709
  }
257636
257710
  function resolveAdditionalCollectionsDir(directoryInput) {
257637
257711
  const workspaceRoot = workspaceRootForLocalInputs();
@@ -258633,6 +258707,8 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
258633
258707
  * endpoint aborts rather than blocking the run forever. */
258634
258708
  static REQUEST_TIMEOUT_MS = 3e4;
258635
258709
  static POSTMAN_APP_VERSION_URL = `https://dl.pstmn.io/update/status?currentVersion=${_BifrostInternalIntegrationAdapter.MINIMUM_POSTMAN_APP_VERSION}&platform=osx_arm64`;
258710
+ /** Concurrent dual-trigger previews share one spec; peer sync holds a 423 lock. */
258711
+ static SYNC_LOCKED_MAX_RETRIES = 6;
258636
258712
  accessToken;
258637
258713
  tokenProvider;
258638
258714
  appVersionPromise;
@@ -258641,6 +258717,7 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
258641
258717
  gatewayBaseUrl;
258642
258718
  orgMode;
258643
258719
  secretMasker;
258720
+ sleep;
258644
258721
  teamId;
258645
258722
  constructor(options) {
258646
258723
  this.accessToken = String(options.accessToken || "").trim();
@@ -258654,6 +258731,7 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
258654
258731
  ).replace(/\/+$/, "");
258655
258732
  this.orgMode = options.orgMode ?? false;
258656
258733
  this.secretMasker = options.secretMasker ?? createSecretMasker([this.accessToken]);
258734
+ this.sleep = options.sleep ?? ((delayMs) => new Promise((resolve5) => setTimeout(resolve5, delayMs)));
258657
258735
  this.teamId = String(options.teamId || "").trim();
258658
258736
  }
258659
258737
  configureTeamContext(teamId, orgMode) {
@@ -258860,55 +258938,72 @@ var BifrostInternalIntegrationAdapter = class _BifrostInternalIntegrationAdapter
258860
258938
  if (collections.length === 0) {
258861
258939
  return;
258862
258940
  }
258863
- const response = await this.proxyRequest(
258864
- "specification",
258865
- "put",
258866
- `/specifications/${specificationId}/collections`,
258867
- collections.map((collection) => ({
258868
- collectionId: collection.collectionId,
258869
- ...collection.syncOptions ? { syncOptions: collection.syncOptions } : {}
258870
- }))
258871
- );
258872
- if (response.ok) {
258873
- return;
258941
+ const body2 = collections.map((collection) => ({
258942
+ collectionId: collection.collectionId,
258943
+ ...collection.syncOptions ? { syncOptions: collection.syncOptions } : {}
258944
+ }));
258945
+ for (let lockedAttempt = 0; ; lockedAttempt += 1) {
258946
+ const response = await this.proxyRequest(
258947
+ "specification",
258948
+ "put",
258949
+ `/specifications/${specificationId}/collections`,
258950
+ body2
258951
+ );
258952
+ if (response.ok) {
258953
+ return;
258954
+ }
258955
+ const httpErr = await HttpError.fromResponse(response, {
258956
+ method: "POST",
258957
+ requestHeaders: {
258958
+ "Content-Type": "application/json",
258959
+ "x-access-token": this.currentToken(),
258960
+ ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
258961
+ },
258962
+ secretValues: [this.currentToken()],
258963
+ url: `${this.bifrostBaseUrl}/ws/proxy`
258964
+ });
258965
+ if (httpErr.status === 423 && lockedAttempt < _BifrostInternalIntegrationAdapter.SYNC_LOCKED_MAX_RETRIES) {
258966
+ await this.sleep(2e3 * Math.pow(2, lockedAttempt));
258967
+ continue;
258968
+ }
258969
+ const advised = adviseFromHttpError(
258970
+ httpErr,
258971
+ this.adviceContext("collection-to-specification linking")
258972
+ );
258973
+ throw advised ?? httpErr;
258874
258974
  }
258875
- const httpErr = await HttpError.fromResponse(response, {
258876
- method: "POST",
258877
- requestHeaders: {
258878
- "Content-Type": "application/json",
258879
- "x-access-token": this.currentToken(),
258880
- ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
258881
- },
258882
- secretValues: [this.currentToken()],
258883
- url: `${this.bifrostBaseUrl}/ws/proxy`
258884
- });
258885
- const advised = adviseFromHttpError(
258886
- httpErr,
258887
- this.adviceContext("collection-to-specification linking")
258888
- );
258889
- throw advised ?? httpErr;
258890
258975
  }
258891
258976
  async syncCollection(specificationId, collectionId) {
258892
- const response = await this.proxyRequest(
258893
- "specification",
258894
- "post",
258895
- `/specifications/${specificationId}/collections/${collectionId}/sync`
258896
- );
258897
- if (response.ok) {
258898
- return;
258977
+ for (let lockedAttempt = 0; ; lockedAttempt += 1) {
258978
+ const response = await this.proxyRequest(
258979
+ "specification",
258980
+ "post",
258981
+ `/specifications/${specificationId}/collections/${collectionId}/sync`
258982
+ );
258983
+ if (response.ok) {
258984
+ return;
258985
+ }
258986
+ const bodyText = await response.clone().text().catch(() => "");
258987
+ if (response.status === 400 && /already in sync/i.test(bodyText)) {
258988
+ return;
258989
+ }
258990
+ const httpErr = await HttpError.fromResponse(response, {
258991
+ method: "POST",
258992
+ requestHeaders: {
258993
+ "Content-Type": "application/json",
258994
+ "x-access-token": this.currentToken(),
258995
+ ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
258996
+ },
258997
+ secretValues: [this.currentToken()],
258998
+ url: `${this.bifrostBaseUrl}/ws/proxy`
258999
+ });
259000
+ if (httpErr.status === 423 && lockedAttempt < _BifrostInternalIntegrationAdapter.SYNC_LOCKED_MAX_RETRIES) {
259001
+ await this.sleep(2e3 * Math.pow(2, lockedAttempt));
259002
+ continue;
259003
+ }
259004
+ const advised = adviseFromHttpError(httpErr, this.adviceContext("collection sync"));
259005
+ throw advised ?? httpErr;
258899
259006
  }
258900
- const httpErr = await HttpError.fromResponse(response, {
258901
- method: "POST",
258902
- requestHeaders: {
258903
- "Content-Type": "application/json",
258904
- "x-access-token": this.currentToken(),
258905
- ...this.teamId && this.orgMode ? { "x-entity-team-id": this.teamId } : {}
258906
- },
258907
- secretValues: [this.currentToken()],
258908
- url: `${this.bifrostBaseUrl}/ws/proxy`
258909
- });
258910
- const advised = adviseFromHttpError(httpErr, this.adviceContext("collection sync"));
258911
- throw advised ?? httpErr;
258912
259007
  }
258913
259008
  async getWorkspaceGitRepoUrl(workspaceId) {
258914
259009
  const response = await this.proxyRequest(
@@ -264053,6 +264148,16 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264053
264148
  if (openapiVersion !== "3.0" && openapiVersion !== "3.1") {
264054
264149
  throw new Error(`uploadSpec: unsupported openapiVersion "${openapiVersion}". Expected '3.0' or '3.1'.`);
264055
264150
  }
264151
+ const before = await this.findSpecificationsByExactName(workspaceId, projectName);
264152
+ const existing = adoptExactMatch(
264153
+ `specification:${workspaceId}:${projectName}`,
264154
+ before,
264155
+ (entry) => entry.id
264156
+ );
264157
+ if (existing) {
264158
+ await this.updateSpec(existing.id, specContent, workspaceId);
264159
+ return existing.id;
264160
+ }
264056
264161
  const specType = openapiVersion === "3.1" ? "OPENAPI:3.1" : "OPENAPI:3.0";
264057
264162
  let created;
264058
264163
  try {
@@ -264077,10 +264182,49 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264077
264182
  if (!match) throw error2;
264078
264183
  created = { data: { id: match.id } };
264079
264184
  }
264080
- const specId = String(asRecord8(created?.data)?.id ?? created?.id ?? "").trim();
264185
+ let specId = String(asRecord8(created?.data)?.id ?? created?.id ?? "").trim();
264081
264186
  if (!specId) {
264082
264187
  throw new Error("Spec upload did not return an ID");
264083
264188
  }
264189
+ let matches = await this.findSpecificationsByExactName(workspaceId, projectName);
264190
+ if (before.length === 0 && matches.length <= 1) {
264191
+ await this.sleep(1e3);
264192
+ const settled = await this.findSpecificationsByExactName(workspaceId, projectName);
264193
+ if (settled.length > 0) matches = settled;
264194
+ }
264195
+ if (matches.length > 1 && before.length === 0) {
264196
+ for (const duplicate of matches.slice(1)) {
264197
+ try {
264198
+ await this.deleteSpecification(duplicate.id);
264199
+ } catch (error2) {
264200
+ if (!(error2 instanceof HttpError && error2.status === 404)) throw error2;
264201
+ }
264202
+ }
264203
+ for (let attempt = 0; attempt < 5; attempt += 1) {
264204
+ matches = await this.findSpecificationsByExactName(workspaceId, projectName);
264205
+ if (matches.length <= 1) break;
264206
+ await this.sleep(250 * (attempt + 1));
264207
+ }
264208
+ const converged = adoptExactMatch(
264209
+ `specification:${workspaceId}:${projectName}`,
264210
+ matches,
264211
+ (entry) => entry.id
264212
+ );
264213
+ if (!converged) {
264214
+ throw new Error(`Concurrent specification create for ${projectName} did not converge`);
264215
+ }
264216
+ specId = converged.id;
264217
+ if (specId !== String(asRecord8(created?.data)?.id ?? created?.id ?? "").trim()) {
264218
+ await this.updateSpec(specId, specContent, workspaceId);
264219
+ }
264220
+ } else {
264221
+ const verified = adoptExactMatch(
264222
+ `specification:${workspaceId}:${projectName}`,
264223
+ matches,
264224
+ (entry) => entry.id
264225
+ );
264226
+ if (verified) specId = verified.id;
264227
+ }
264084
264228
  await this.gateway.requestJson({
264085
264229
  service: "specification",
264086
264230
  method: "get",
@@ -264088,6 +264232,14 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264088
264232
  });
264089
264233
  return specId;
264090
264234
  }
264235
+ async deleteSpecification(specId) {
264236
+ await this.gateway.requestJson({
264237
+ service: "specification",
264238
+ method: "delete",
264239
+ path: `/specifications/${specId}`,
264240
+ retry: "none"
264241
+ });
264242
+ }
264091
264243
  async findSpecificationsByExactName(workspaceId, name) {
264092
264244
  const response = await this.gateway.requestJson({
264093
264245
  service: "specification",
@@ -264151,6 +264303,41 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264151
264303
  return void 0;
264152
264304
  }
264153
264305
  }
264306
+ /**
264307
+ * Native Spec Hub version tags (branch-aware sync P3.5). Tags attach to the
264308
+ * LATEST changelog group; the backend 409s when that group is already tagged
264309
+ * (VersionControlService). Callers handle 409 as idempotent-by-group.
264310
+ */
264311
+ async tagSpecVersion(specId, name) {
264312
+ const trimmed = name.trim().slice(0, 255);
264313
+ const created = await this.gateway.requestJson({
264314
+ service: "specification",
264315
+ method: "post",
264316
+ path: `/specifications/${specId}/tags`,
264317
+ retry: "none",
264318
+ body: { name: trimmed }
264319
+ });
264320
+ const record = asRecord8(created?.data) ?? created ?? {};
264321
+ return {
264322
+ id: String(record.id ?? "").trim(),
264323
+ name: String(record.name ?? trimmed).trim()
264324
+ };
264325
+ }
264326
+ /** List a spec's native version tags (newest first per backend ordering). */
264327
+ async listSpecVersionTags(specId) {
264328
+ const response = await this.gateway.requestJson({
264329
+ service: "specification",
264330
+ method: "get",
264331
+ path: `/specifications/${specId}/tags`,
264332
+ query: { limit: "50" }
264333
+ });
264334
+ const entries = Array.isArray(response?.data) ? response.data : [];
264335
+ return entries.map((value) => asRecord8(value)).filter((value) => value !== null).map((value) => ({
264336
+ id: String(value.id ?? "").trim(),
264337
+ // listTags returns `message`; createTag returns `name`. Accept both.
264338
+ name: String(value.name ?? value.message ?? "").trim()
264339
+ })).filter((value) => value.id || value.name);
264340
+ }
264154
264341
  /** Resolve a specification's ROOT file uuid via the files list. */
264155
264342
  async resolveRootFileId(specId) {
264156
264343
  const files = await this.gateway.requestJson({
@@ -264170,79 +264357,182 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264170
264357
  */
264171
264358
  async generateCollection(specId, projectName, prefix, folderStrategy, nestedFolderHierarchy, requestNameSource) {
264172
264359
  const name = [prefix.trim(), projectName.trim()].filter(Boolean).join(" ");
264173
- const submittedName = `${name} [bootstrap:${this.createIdentity()}]`;
264174
- const body2 = {
264175
- name: submittedName,
264176
- options: {
264177
- requestNameSource,
264178
- folderStrategy,
264179
- ...folderStrategy === "Tags" ? { nestedFolderHierarchy } : {}
264180
- }
264181
- };
264182
264360
  const before = await this.listGeneratedCollectionRefs(specId);
264183
- let taskId;
264184
- try {
264185
- taskId = await this.postGenerationWithLockRetry(specId, body2);
264186
- } catch (error2) {
264187
- if (!isAmbiguousTransportError(error2)) throw error2;
264188
- const beforeIds2 = new Set(before.map((entry) => entry.id));
264189
- const appeared2 = (await this.listGeneratedCollectionRefs(specId)).filter(
264190
- (entry) => !beforeIds2.has(entry.id)
264191
- );
264192
- const exactAppeared = await this.filterGeneratedCollectionsByExactName(
264193
- appeared2,
264361
+ const existing = adoptExactMatch(
264362
+ `generated-collection:${specId}:${name}`,
264363
+ await this.filterGeneratedCollectionsByExactName(before, name),
264364
+ (entry) => entry.id
264365
+ );
264366
+ if (existing) return this.convergeGeneratedCollections(specId, name, existing.id);
264367
+ let lastError;
264368
+ for (let taskAttempt = 0; taskAttempt < 2; taskAttempt += 1) {
264369
+ const submittedName = `${name} [bootstrap:${this.createIdentity()}]`;
264370
+ const body2 = {
264371
+ name: submittedName,
264372
+ options: {
264373
+ requestNameSource,
264374
+ folderStrategy,
264375
+ ...folderStrategy === "Tags" ? { nestedFolderHierarchy } : {}
264376
+ }
264377
+ };
264378
+ let taskId;
264379
+ try {
264380
+ const generation = await this.postGenerationWithLockRetry(specId, body2, name);
264381
+ if (generation.adoptedId) {
264382
+ return this.convergeGeneratedCollections(specId, name, generation.adoptedId);
264383
+ }
264384
+ taskId = generation.taskId ?? "";
264385
+ } catch (error2) {
264386
+ if (!isAmbiguousTransportError(error2)) throw error2;
264387
+ const beforeIds2 = new Set(before.map((entry) => entry.id));
264388
+ const appeared2 = (await this.listGeneratedCollectionRefs(specId)).filter(
264389
+ (entry) => !beforeIds2.has(entry.id)
264390
+ );
264391
+ const exactAppeared = await this.filterGeneratedCollectionsByExactName(
264392
+ appeared2,
264393
+ submittedName
264394
+ );
264395
+ const match = adoptExactMatch(
264396
+ `generated-collection:${specId}:${submittedName}`,
264397
+ exactAppeared,
264398
+ (entry) => entry.id
264399
+ );
264400
+ if (!match) throw error2;
264401
+ await this.renameGeneratedCollection(match.id, name);
264402
+ return this.convergeGeneratedCollections(specId, name, match.id);
264403
+ }
264404
+ let taskFailed = false;
264405
+ if (taskId) {
264406
+ for (let attempt = 0; attempt < this.generationPollAttempts; attempt += 1) {
264407
+ await this.sleep(this.generationPollDelayMs);
264408
+ const task = await this.gateway.requestJson({
264409
+ service: "specification",
264410
+ method: "get",
264411
+ path: "/tasks",
264412
+ query: { entityId: specId, entityType: "specification", type: "collection-generation" }
264413
+ });
264414
+ const status = String(asRecord8(task?.data)?.[taskId] ?? "").toLowerCase();
264415
+ if (status === "failed" || status === "error") {
264416
+ taskFailed = true;
264417
+ lastError = new Error(`Collection generation task failed for ${prefix}`);
264418
+ break;
264419
+ }
264420
+ if (status && status !== "in-progress" && status !== "pending" && status !== "queued") {
264421
+ break;
264422
+ }
264423
+ if (attempt === this.generationPollAttempts - 1) {
264424
+ throw new Error(`Collection generation timed out for ${prefix}`);
264425
+ }
264426
+ }
264427
+ }
264428
+ if (taskFailed) {
264429
+ await this.sleep(1e3 * (taskAttempt + 1));
264430
+ continue;
264431
+ }
264432
+ const beforeIds = new Set(before.map((entry) => entry.id));
264433
+ const after = await this.listGeneratedCollectionRefs(specId);
264434
+ const appeared = after.filter((entry) => !beforeIds.has(entry.id));
264435
+ const candidates = await this.filterGeneratedCollectionsByExactName(
264436
+ appeared,
264194
264437
  submittedName
264195
264438
  );
264196
- const match = adoptExactMatch(
264439
+ const uid = adoptExactMatch(
264197
264440
  `generated-collection:${specId}:${submittedName}`,
264198
- exactAppeared,
264441
+ candidates,
264199
264442
  (entry) => entry.id
264200
- );
264201
- if (!match) throw error2;
264202
- await this.renameGeneratedCollection(match.id, name);
264203
- return match.id;
264443
+ )?.id;
264444
+ if (!uid) {
264445
+ throw new Error(`Collection generation did not yield a collection uid for ${prefix}`);
264446
+ }
264447
+ await this.renameGeneratedCollection(uid, name);
264448
+ return this.convergeGeneratedCollections(specId, name, uid);
264204
264449
  }
264205
- if (taskId) {
264206
- for (let attempt = 0; attempt < this.generationPollAttempts; attempt += 1) {
264207
- await this.sleep(this.generationPollDelayMs);
264208
- const task = await this.gateway.requestJson({
264209
- service: "specification",
264210
- method: "get",
264211
- path: "/tasks",
264212
- query: { entityId: specId, entityType: "specification", type: "collection-generation" }
264213
- });
264214
- const status = String(asRecord8(task?.data)?.[taskId] ?? "").toLowerCase();
264215
- if (status === "failed" || status === "error") {
264216
- throw new Error(`Collection generation task failed for ${prefix}`);
264217
- }
264218
- if (status && status !== "in-progress" && status !== "pending" && status !== "queued") {
264219
- break;
264220
- }
264221
- if (attempt === this.generationPollAttempts - 1) {
264222
- throw new Error(`Collection generation timed out for ${prefix}`);
264450
+ throw lastError ?? new Error(`Collection generation task failed for ${prefix}`);
264451
+ }
264452
+ /**
264453
+ * Re-elect the durable generated collection for a final name. Call after
264454
+ * concurrent dual-trigger generates and before description/inject/tag so a
264455
+ * peer orphan-sweep cannot leave this runner holding a deleted id.
264456
+ */
264457
+ async adoptGeneratedCollection(specId, projectName, prefix, preferredId = "") {
264458
+ const name = [prefix.trim(), projectName.trim()].filter(Boolean).join(" ");
264459
+ return this.convergeGeneratedCollections(specId, name, preferredId);
264460
+ }
264461
+ /**
264462
+ * Concurrent dual-trigger previews can each generate+rename the same final
264463
+ * collection identity. Elect the stable lowest-id winner.
264464
+ *
264465
+ * Losers only delete *their own* preferred collection (never a peer's still-
264466
+ * in-use id). Winners wait briefly for peers to self-delete, then clean any
264467
+ * leftover same-identity orphans (temps + extra finals).
264468
+ */
264469
+ async convergeGeneratedCollections(specId, finalName, preferredId) {
264470
+ const tempPrefix = `${finalName} [bootstrap:`;
264471
+ const hydrate = async () => {
264472
+ const linked = await this.listGeneratedCollectionRefs(specId);
264473
+ return Promise.all(
264474
+ linked.map(async (entry) => {
264475
+ if (entry.name) return { id: entry.id, name: entry.name };
264476
+ try {
264477
+ const collection = await this.gateway.requestJson({
264478
+ service: "collection",
264479
+ method: "get",
264480
+ path: `/v3/collections/${this.bareModelId(entry.id)}`
264481
+ });
264482
+ return {
264483
+ id: entry.id,
264484
+ name: String(asRecord8(collection?.data)?.name ?? "").trim()
264485
+ };
264486
+ } catch (error2) {
264487
+ if (error2 instanceof HttpError && error2.status === 404) {
264488
+ return { id: entry.id, name: "" };
264489
+ }
264490
+ throw error2;
264491
+ }
264492
+ })
264493
+ );
264494
+ };
264495
+ const selectSameIdentity = (entries) => entries.filter(
264496
+ (entry) => entry.name === finalName || entry.name.startsWith(tempPrefix)
264497
+ ).sort((a, b) => a.id.localeCompare(b.id));
264498
+ let sameIdentity = selectSameIdentity(await hydrate());
264499
+ if (sameIdentity.length === 0) return preferredId;
264500
+ if (sameIdentity.length === 1) {
264501
+ await this.sleep(1e3);
264502
+ sameIdentity = selectSameIdentity(await hydrate());
264503
+ if (sameIdentity.length === 0) return preferredId;
264504
+ }
264505
+ const winner = sameIdentity[0];
264506
+ if (winner.name !== finalName) {
264507
+ await this.renameGeneratedCollection(winner.id, finalName);
264508
+ }
264509
+ if (preferredId && preferredId !== winner.id) {
264510
+ const own = sameIdentity.find((entry) => entry.id === preferredId);
264511
+ if (own) {
264512
+ try {
264513
+ await this.deleteCollection(preferredId);
264514
+ } catch (error2) {
264515
+ if (!(error2 instanceof HttpError && error2.status === 404)) throw error2;
264223
264516
  }
264224
264517
  }
264518
+ return winner.id;
264225
264519
  }
264226
- const beforeIds = new Set(before.map((entry) => entry.id));
264227
- const after = await this.listGeneratedCollectionRefs(specId);
264228
- const appeared = after.filter((entry) => !beforeIds.has(entry.id));
264229
- const candidates = await this.filterGeneratedCollectionsByExactName(
264230
- appeared,
264231
- submittedName
264232
- );
264233
- const uid = adoptExactMatch(
264234
- `generated-collection:${specId}:${submittedName}`,
264235
- candidates,
264236
- (entry) => entry.id
264237
- )?.id;
264238
- if (!uid) {
264239
- throw new Error(`Collection generation did not yield a collection uid for ${prefix}`);
264520
+ if (sameIdentity.length > 1) {
264521
+ await this.sleep(1500);
264522
+ sameIdentity = selectSameIdentity(await hydrate());
264523
+ for (const duplicate of sameIdentity) {
264524
+ if (duplicate.id === winner.id) continue;
264525
+ try {
264526
+ await this.deleteCollection(duplicate.id);
264527
+ } catch (error2) {
264528
+ if (!(error2 instanceof HttpError && error2.status === 404)) throw error2;
264529
+ }
264530
+ }
264240
264531
  }
264241
- await this.renameGeneratedCollection(uid, name);
264242
- return uid;
264532
+ return winner.id;
264243
264533
  }
264244
264534
  /** POST the generation request, retrying a 423-locked spec; returns the task id. */
264245
- async postGenerationWithLockRetry(specId, body2) {
264535
+ async postGenerationWithLockRetry(specId, body2, finalName) {
264246
264536
  for (let lockedAttempt = 0; ; lockedAttempt += 1) {
264247
264537
  try {
264248
264538
  const created = await this.gateway.requestJson({
@@ -264252,13 +264542,22 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264252
264542
  retry: "none",
264253
264543
  body: body2
264254
264544
  });
264255
- return String(asRecord8(created?.data)?.taskId ?? "").trim();
264545
+ return { taskId: String(asRecord8(created?.data)?.taskId ?? "").trim() };
264256
264546
  } catch (error2) {
264257
264547
  const locked = error2 instanceof HttpError && error2.status === 423;
264258
264548
  if (!locked || lockedAttempt >= _PostmanGatewayAssetsClient.GENERATION_LOCKED_MAX_RETRIES) {
264259
264549
  throw error2;
264260
264550
  }
264261
264551
  await this.sleep(5e3 * Math.pow(2, lockedAttempt));
264552
+ const adopted = adoptExactMatch(
264553
+ `generated-collection:${specId}:${finalName}`,
264554
+ await this.filterGeneratedCollectionsByExactName(
264555
+ await this.listGeneratedCollectionRefs(specId),
264556
+ finalName
264557
+ ),
264558
+ (entry) => entry.id
264559
+ );
264560
+ if (adopted) return { adoptedId: adopted.id };
264262
264561
  }
264263
264562
  }
264264
264563
  }
@@ -264295,14 +264594,24 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264295
264594
  return hydrated.filter((entry) => entry.name === expectedName);
264296
264595
  }
264297
264596
  async renameGeneratedCollection(collectionId, name) {
264298
- await this.gateway.requestJson({
264299
- service: "collection",
264300
- method: "patch",
264301
- path: `/v3/collections/${this.bareModelId(collectionId)}`,
264302
- // Replacing a generated collection's name with the same value is idempotent.
264303
- retry: "safe",
264304
- body: [{ op: "replace", path: "/name", value: name }]
264305
- });
264597
+ try {
264598
+ await this.gateway.requestJson({
264599
+ service: "collection",
264600
+ method: "patch",
264601
+ path: `/v3/collections/${this.bareModelId(collectionId)}`,
264602
+ // Replacing a generated collection's name with the same value is idempotent.
264603
+ retry: "safe",
264604
+ body: [{ op: "replace", path: "/name", value: name }]
264605
+ });
264606
+ } catch (error2) {
264607
+ if (error2 instanceof HttpError && error2.status === 400 && /must update at least one|REJECTED_PATCH/i.test(
264608
+ `${error2.message}
264609
+ ${error2.responseBody ?? ""}`
264610
+ )) {
264611
+ return;
264612
+ }
264613
+ throw error2;
264614
+ }
264306
264615
  }
264307
264616
  /**
264308
264617
  * Create a team-visible workspace through the gateway workspaces service.
@@ -264577,6 +264886,7 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
264577
264886
  service: "tagging",
264578
264887
  method: "put",
264579
264888
  path: `/v1/tags/collections/${collectionUid}`,
264889
+ retry: "safe",
264580
264890
  body: { tags: normalized.map((slug) => ({ slug })) }
264581
264891
  });
264582
264892
  }
@@ -265203,6 +265513,15 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
265203
265513
  async createCollection(workspaceId, collection, options = {}) {
265204
265514
  const v3 = this.normalizeCollectionForWrite(collection);
265205
265515
  const desiredName = String(v3.name ?? "Untitled Collection");
265516
+ const existing = adoptExactMatch(
265517
+ `collection:${workspaceId}:${desiredName}`,
265518
+ await this.findCollectionsByExactName(workspaceId, desiredName),
265519
+ (entry) => entry.id
265520
+ );
265521
+ if (existing) {
265522
+ await this.updateCollection(existing.id, collection);
265523
+ return existing.id;
265524
+ }
265206
265525
  const submittedName = `${desiredName} [bootstrap:${this.createIdentity()}]`;
265207
265526
  const rootBody = { name: submittedName };
265208
265527
  let created;
@@ -265253,6 +265572,10 @@ var PostmanGatewayAssetsClient = class _PostmanGatewayAssetsClient {
265253
265572
  }
265254
265573
  return rawId;
265255
265574
  }
265575
+ /** Patch only the durable collection description without reconciling its item tree. */
265576
+ async updateCollectionDescription(collectionUid, description) {
265577
+ await this.applyCollectionLevelSettings(this.bareModelId(collectionUid), { description });
265578
+ }
265256
265579
  /**
265257
265580
  * Full-replace reconcile of a curated local v2.1.0 or collection v3 payload: delete every
265258
265581
  * root-level item (deleting a folder cascades its children server-side —
@@ -265477,7 +265800,8 @@ var AccessTokenGatewayClient = class {
265477
265800
  * `requestJson` can still parse it.
265478
265801
  */
265479
265802
  rebuildResponse(response, body2) {
265480
- return new Response(body2, {
265803
+ const nullBody = response.status === 204 || response.status === 205 || response.status === 304;
265804
+ return new Response(nullBody ? null : body2, {
265481
265805
  status: response.status,
265482
265806
  statusText: response.statusText,
265483
265807
  headers: response.headers
@@ -281929,6 +282253,61 @@ function serializeOpenApiDocument(document2) {
281929
282253
  return `${JSON.stringify(document2, null, 2)}
281930
282254
  `;
281931
282255
  }
282256
+ function typeNullPath(pathSegments) {
282257
+ return pathSegments.map(String).join(".");
282258
+ }
282259
+ function isNullOnlySchema(value) {
282260
+ const record = asRecord10(value);
282261
+ if (!record || record.type !== "null") return false;
282262
+ return Object.keys(record).every((key) => key === "type" || key.startsWith("x-"));
282263
+ }
282264
+ function createOas30TypeNullCompatibilityDocument(sourceDocument) {
282265
+ const document2 = structuredClone(sourceDocument);
282266
+ const sourceTypeNullPaths = [];
282267
+ const visit4 = (value, pathSegments) => {
282268
+ if (Array.isArray(value)) {
282269
+ value.forEach((entry, index) => visit4(entry, [...pathSegments, index]));
282270
+ return;
282271
+ }
282272
+ const record = asRecord10(value);
282273
+ if (!record) return;
282274
+ const oneOf = Array.isArray(record.oneOf) ? record.oneOf : void 0;
282275
+ const nullIndexes = oneOf ? oneOf.flatMap((entry, index) => isNullOnlySchema(entry) ? [index] : []) : [];
282276
+ if (nullIndexes.length > 0) {
282277
+ if (oneOf?.length !== 2 || nullIndexes.length !== 1) {
282278
+ throw new Error(
282279
+ `CONTRACT_OAS30_TYPE_NULL_UNSUPPORTED: ${typeNullPath(pathSegments)} must use oneOf with exactly one null-only member and one non-null schema`
282280
+ );
282281
+ }
282282
+ const nullIndex = nullIndexes[0];
282283
+ const nonNullIndex = nullIndex === 0 ? 1 : 0;
282284
+ const nonNullSchema = asRecord10(oneOf[nonNullIndex]);
282285
+ if (!nonNullSchema || nonNullSchema.type === "null") {
282286
+ throw new Error(
282287
+ `CONTRACT_OAS30_TYPE_NULL_UNSUPPORTED: ${typeNullPath(pathSegments)} must pair the null-only member with one non-null schema`
282288
+ );
282289
+ }
282290
+ const siblings = Object.fromEntries(
282291
+ Object.entries(record).filter(([key]) => key !== "oneOf")
282292
+ );
282293
+ for (const key of Object.keys(record)) delete record[key];
282294
+ Object.assign(record, structuredClone(nonNullSchema), siblings, { nullable: true });
282295
+ sourceTypeNullPaths.push(typeNullPath([...pathSegments, "oneOf", nullIndex, "type"]));
282296
+ visit4(record, pathSegments);
282297
+ return;
282298
+ }
282299
+ if (record.type === "null") {
282300
+ throw new Error(
282301
+ `CONTRACT_OAS30_TYPE_NULL_UNSUPPORTED: ${typeNullPath([...pathSegments, "type"])} is not a supported nullable oneOf member`
282302
+ );
282303
+ }
282304
+ for (const [key, child4] of Object.entries(record)) {
282305
+ visit4(child4, [...pathSegments, key]);
282306
+ }
282307
+ };
282308
+ visit4(document2, []);
282309
+ return { document: document2, sourceTypeNullPaths };
282310
+ }
281932
282311
  async function bundleSpec(baseUrl, document2, options) {
281933
282312
  const budget = options.budget ?? { refs: 0, totalBytes: Buffer.byteLength(JSON.stringify(document2), "utf8") };
281934
282313
  const fetchText = options.fetchText ?? safeFetchText;
@@ -281970,7 +282349,14 @@ function createCachedFetchText(options) {
281970
282349
  async function buildLoadedSpec(content, baseRef, options, fetchText, budget) {
281971
282350
  const document2 = parseOpenApiDocument(content);
281972
282351
  const version = detectOpenApiVersion2(document2);
281973
- const bundledDocument = await bundleSpec(baseRef, document2, { ...options, budget, fetchText });
282352
+ let contractDocument = document2;
282353
+ let sourceTypeNullPaths = [];
282354
+ if (options.preserveOas30TypeNull && version === "3.0") {
282355
+ const compatibility = createOas30TypeNullCompatibilityDocument(document2);
282356
+ contractDocument = compatibility.document;
282357
+ sourceTypeNullPaths = compatibility.sourceTypeNullPaths;
282358
+ }
282359
+ const bundledDocument = await bundleSpec(baseRef, contractDocument, { ...options, budget, fetchText });
281974
282360
  const validation = await validate(bundledDocument, {
281975
282361
  resolve: { external: false, file: false },
281976
282362
  dereference: { circular: "ignore" },
@@ -281984,6 +282370,7 @@ async function buildLoadedSpec(content, baseRef, options, fetchText, budget) {
281984
282370
  bundledDocument,
281985
282371
  contractIndex: buildContractIndex(bundledDocument),
281986
282372
  content,
282373
+ sourceTypeNullPaths,
281987
282374
  version
281988
282375
  };
281989
282376
  }
@@ -282109,6 +282496,309 @@ function looksLikeIntrospection(record) {
282109
282496
  return Boolean(data && typeof data === "object" && data.__schema);
282110
282497
  }
282111
282498
 
282499
+ // src/lib/repo/branch-decision.ts
282500
+ var import_node_fs5 = require("node:fs");
282501
+ var import_node_crypto4 = require("node:crypto");
282502
+ var ContractError = class extends Error {
282503
+ code;
282504
+ constructor(code, message) {
282505
+ super(`${code}: ${message}`);
282506
+ this.code = code;
282507
+ this.name = "ContractError";
282508
+ }
282509
+ };
282510
+ function clean(value) {
282511
+ const trimmed = (value ?? "").trim();
282512
+ return trimmed.length > 0 ? trimmed : void 0;
282513
+ }
282514
+ function stripRefPrefix(ref) {
282515
+ const raw = clean(ref);
282516
+ if (!raw) {
282517
+ return { kind: "unknown" };
282518
+ }
282519
+ if (raw.startsWith("refs/heads/")) {
282520
+ return { name: raw.slice("refs/heads/".length), kind: "branch" };
282521
+ }
282522
+ if (raw.startsWith("refs/tags/")) {
282523
+ return { name: raw.slice("refs/tags/".length), kind: "tag" };
282524
+ }
282525
+ if (raw.startsWith("refs/pull/") || raw.startsWith("refs/merge")) {
282526
+ return { kind: "unknown" };
282527
+ }
282528
+ return { name: raw, kind: "branch" };
282529
+ }
282530
+ function detectProvider(env) {
282531
+ if (clean(env.GITHUB_ACTIONS) || clean(env.GITHUB_REPOSITORY)) return "github";
282532
+ if (clean(env.GITLAB_CI) || clean(env.CI_PROJECT_PATH)) return "gitlab";
282533
+ if (clean(env.BITBUCKET_REPO_SLUG) || clean(env.BITBUCKET_BRANCH)) return "bitbucket";
282534
+ if (clean(env.TF_BUILD) || clean(env.BUILD_REPOSITORY_URI)) return "azure-devops";
282535
+ return "unknown";
282536
+ }
282537
+ function readGithubEvent(env) {
282538
+ const path10 = clean(env.GITHUB_EVENT_PATH);
282539
+ if (!path10) return void 0;
282540
+ try {
282541
+ return JSON.parse((0, import_node_fs5.readFileSync)(path10, "utf8"));
282542
+ } catch {
282543
+ return void 0;
282544
+ }
282545
+ }
282546
+ function resolveBranchIdentity(env = process.env, overrides = {}) {
282547
+ const provider = detectProvider(env);
282548
+ const explicitDefault = clean(overrides.defaultBranch);
282549
+ let headBranch;
282550
+ let rawRef;
282551
+ let refKind;
282552
+ let isPrContext = false;
282553
+ let isForkPr = false;
282554
+ let defaultBranch = explicitDefault;
282555
+ let headSha;
282556
+ switch (provider) {
282557
+ case "github": {
282558
+ const event2 = readGithubEvent(env);
282559
+ headSha = clean(env.GITHUB_SHA);
282560
+ defaultBranch ??= clean(event2?.repository?.default_branch);
282561
+ const headRef = clean(env.GITHUB_HEAD_REF);
282562
+ if (headRef) {
282563
+ isPrContext = true;
282564
+ headBranch = headRef;
282565
+ rawRef = clean(env.GITHUB_REF) ?? headRef;
282566
+ refKind = "branch";
282567
+ const headRepo = event2?.pull_request?.head?.repo?.full_name;
282568
+ const baseRepo = event2?.pull_request?.base?.repo?.full_name ?? event2?.repository?.full_name;
282569
+ isForkPr = Boolean(headRepo && baseRepo && headRepo !== baseRepo);
282570
+ headSha = clean(event2?.pull_request?.head?.sha) ?? headSha;
282571
+ } else {
282572
+ rawRef = clean(env.GITHUB_REF) ?? clean(env.GITHUB_REF_NAME);
282573
+ const parsed = stripRefPrefix(rawRef);
282574
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
282575
+ refKind = parsed.kind;
282576
+ }
282577
+ break;
282578
+ }
282579
+ case "gitlab": {
282580
+ headSha = clean(env.CI_COMMIT_SHA);
282581
+ defaultBranch ??= clean(env.CI_DEFAULT_BRANCH);
282582
+ const mrSource = clean(env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME);
282583
+ if (mrSource) {
282584
+ isPrContext = true;
282585
+ headBranch = mrSource;
282586
+ rawRef = mrSource;
282587
+ refKind = "branch";
282588
+ const sourceProject = clean(env.CI_MERGE_REQUEST_SOURCE_PROJECT_ID);
282589
+ const targetProject = clean(env.CI_MERGE_REQUEST_PROJECT_ID);
282590
+ isForkPr = Boolean(sourceProject && targetProject && sourceProject !== targetProject);
282591
+ } else if (clean(env.CI_COMMIT_TAG)) {
282592
+ rawRef = clean(env.CI_COMMIT_TAG);
282593
+ refKind = "tag";
282594
+ } else {
282595
+ headBranch = clean(env.CI_COMMIT_BRANCH) ?? clean(env.CI_COMMIT_REF_NAME);
282596
+ rawRef = headBranch;
282597
+ refKind = headBranch ? "branch" : "unknown";
282598
+ }
282599
+ break;
282600
+ }
282601
+ case "bitbucket": {
282602
+ headSha = clean(env.BITBUCKET_COMMIT);
282603
+ if (clean(env.BITBUCKET_TAG)) {
282604
+ rawRef = clean(env.BITBUCKET_TAG);
282605
+ refKind = "tag";
282606
+ } else {
282607
+ headBranch = clean(env.BITBUCKET_BRANCH);
282608
+ rawRef = headBranch;
282609
+ refKind = headBranch ? "branch" : "unknown";
282610
+ isPrContext = Boolean(clean(env.BITBUCKET_PR_ID));
282611
+ }
282612
+ break;
282613
+ }
282614
+ case "azure-devops": {
282615
+ headSha = clean(env.BUILD_SOURCEVERSION);
282616
+ const prSource = clean(env.SYSTEM_PULLREQUEST_SOURCEBRANCH);
282617
+ if (prSource) {
282618
+ isPrContext = true;
282619
+ const parsed = stripRefPrefix(prSource);
282620
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
282621
+ rawRef = prSource;
282622
+ refKind = parsed.kind;
282623
+ const forkFlag = clean(env.SYSTEM_PULLREQUEST_ISFORK);
282624
+ isForkPr = forkFlag?.toLowerCase() === "true";
282625
+ } else {
282626
+ rawRef = clean(env.BUILD_SOURCEBRANCH);
282627
+ const parsed = stripRefPrefix(rawRef);
282628
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
282629
+ refKind = parsed.kind;
282630
+ }
282631
+ break;
282632
+ }
282633
+ default: {
282634
+ refKind = "unknown";
282635
+ }
282636
+ }
282637
+ if (refKind === "branch" && headBranch && defaultBranch && headBranch === defaultBranch) {
282638
+ refKind = "default-branch";
282639
+ }
282640
+ return { provider, headBranch, rawRef, defaultBranch, refKind, isPrContext, isForkPr, headSha };
282641
+ }
282642
+ function parseChannelRules(input) {
282643
+ const raw = clean(input);
282644
+ const rules = [];
282645
+ for (const part of raw ? raw.split(",") : []) {
282646
+ const entry = part.trim();
282647
+ if (!entry) continue;
282648
+ const eq = entry.indexOf("=");
282649
+ if (eq <= 0 || eq === entry.length - 1) {
282650
+ throw new ContractError(
282651
+ "CONTRACT_CHANNELS_INPUT_INVALID",
282652
+ `channels entry "${entry}" must be <branch-or-glob>=<CODE>`
282653
+ );
282654
+ }
282655
+ const pattern = entry.slice(0, eq).trim();
282656
+ const code = entry.slice(eq + 1).trim().toUpperCase();
282657
+ if (!/^[A-Z][A-Z0-9_-]{0,15}$/.test(code)) {
282658
+ throw new ContractError(
282659
+ "CONTRACT_CHANNELS_INPUT_INVALID",
282660
+ `channel code "${code}" must be 1-16 chars, A-Z 0-9 _ -, starting with a letter`
282661
+ );
282662
+ }
282663
+ rules.push({ pattern, code });
282664
+ }
282665
+ if (!rules.some((rule) => rule.pattern === "release/*")) {
282666
+ rules.push({ pattern: "release/*", code: "RC" });
282667
+ }
282668
+ return rules;
282669
+ }
282670
+ function matchChannel(branch, rules) {
282671
+ for (const rule of rules) {
282672
+ if (rule.pattern.endsWith("*")) {
282673
+ const prefix = rule.pattern.slice(0, -1);
282674
+ if (branch.startsWith(prefix)) return rule;
282675
+ } else if (branch === rule.pattern) {
282676
+ return rule;
282677
+ }
282678
+ }
282679
+ return void 0;
282680
+ }
282681
+ function resolveBranchDecision(options) {
282682
+ const { strategy, identity } = options;
282683
+ const channels = options.channels ?? [];
282684
+ if (strategy === "legacy") {
282685
+ return {
282686
+ tier: "legacy",
282687
+ strategy,
282688
+ identity,
282689
+ canonicalBranch: clean(options.canonicalBranch) ?? identity.defaultBranch,
282690
+ reason: "branch-strategy legacy: branch-blind pre-v2 behavior"
282691
+ };
282692
+ }
282693
+ const canonicalBranch = clean(options.canonicalBranch) ?? identity.defaultBranch;
282694
+ if (!canonicalBranch) {
282695
+ throw new ContractError(
282696
+ "CONTRACT_DEFAULT_BRANCH_UNRESOLVED",
282697
+ `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.`
282698
+ );
282699
+ }
282700
+ if (identity.refKind === "tag" || identity.refKind === "unknown" || !identity.headBranch) {
282701
+ return {
282702
+ tier: "gated",
282703
+ strategy,
282704
+ identity,
282705
+ canonicalBranch,
282706
+ reason: `ref kind ${identity.refKind}: never canonical/preview-eligible; no-op with annotation`
282707
+ };
282708
+ }
282709
+ if (identity.headBranch === canonicalBranch) {
282710
+ return {
282711
+ tier: "canonical",
282712
+ strategy,
282713
+ identity,
282714
+ canonicalBranch,
282715
+ reason: `head branch equals canonical branch ${canonicalBranch}`
282716
+ };
282717
+ }
282718
+ const channel = matchChannel(identity.headBranch, channels);
282719
+ if (channel) {
282720
+ return {
282721
+ tier: "channel",
282722
+ strategy,
282723
+ identity,
282724
+ canonicalBranch,
282725
+ channel,
282726
+ reason: `branch ${identity.headBranch} matches channel ${channel.pattern}=${channel.code}`
282727
+ };
282728
+ }
282729
+ if (strategy === "preview") {
282730
+ if (identity.isForkPr) {
282731
+ return {
282732
+ tier: "gated",
282733
+ strategy,
282734
+ identity,
282735
+ canonicalBranch,
282736
+ reason: "fork PR: preview-ineligible (same-repo gate), gated instead"
282737
+ };
282738
+ }
282739
+ return {
282740
+ tier: "preview",
282741
+ strategy,
282742
+ identity,
282743
+ canonicalBranch,
282744
+ reason: `branch ${identity.headBranch} under branch-strategy preview`
282745
+ };
282746
+ }
282747
+ return {
282748
+ tier: "gated",
282749
+ strategy,
282750
+ identity,
282751
+ canonicalBranch,
282752
+ reason: `branch ${identity.headBranch} under branch-strategy publish-gate`
282753
+ };
282754
+ }
282755
+ var BRANCH_DECISION_ENV = "POSTMAN_BRANCH_DECISION";
282756
+ function serializeBranchDecision(decision) {
282757
+ return JSON.stringify(decision);
282758
+ }
282759
+ function parseBranchDecision(raw) {
282760
+ const value = clean(raw);
282761
+ if (!value) return void 0;
282762
+ let parsed;
282763
+ try {
282764
+ parsed = JSON.parse(value);
282765
+ } catch {
282766
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION is not valid JSON");
282767
+ }
282768
+ const candidate = parsed;
282769
+ const tiers = ["canonical", "channel", "preview", "gated", "legacy"];
282770
+ if (!candidate || typeof candidate !== "object" || !tiers.includes(candidate.tier) || !candidate.identity) {
282771
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION does not carry a valid BranchDecision");
282772
+ }
282773
+ return candidate;
282774
+ }
282775
+ function resolveEffectiveBranchDecision(options, env = process.env) {
282776
+ const inherited = parseBranchDecision(env[BRANCH_DECISION_ENV]);
282777
+ if (inherited) return inherited;
282778
+ return resolveBranchDecision(options);
282779
+ }
282780
+ var PREVIEW_SLUG_MAX = 30;
282781
+ function buildBranchSlug(rawBranch) {
282782
+ const sanitized = rawBranch.replace(/^refs\/heads\//, "").replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
282783
+ const truncated = sanitized.slice(0, PREVIEW_SLUG_MAX);
282784
+ const lossy = truncated !== rawBranch.replace(/^refs\/heads\//, "");
282785
+ if (!lossy) {
282786
+ return { suffix: truncated, slug: truncated, lossy };
282787
+ }
282788
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(rawBranch).digest("hex").slice(0, 6);
282789
+ return { suffix: `${truncated}-${hash}`, slug: truncated, lossy };
282790
+ }
282791
+ function previewAssetName(baseName, rawBranch) {
282792
+ return `${baseName} @${buildBranchSlug(rawBranch).suffix}`;
282793
+ }
282794
+ function channelAssetName(baseName, code) {
282795
+ return `[${code}] ${baseName}`;
282796
+ }
282797
+ var MARKER_KEY = "x-pm-onboarding";
282798
+ function renderAssetMarker(marker) {
282799
+ return `${MARKER_KEY}: ${JSON.stringify(marker)}`;
282800
+ }
282801
+
282112
282802
  // node_modules/graphql/jsutils/inspect.mjs
282113
282803
  var MAX_ARRAY_LENGTH = 10;
282114
282804
  var MAX_RECURSIVE_DEPTH = 2;
@@ -305440,7 +306130,7 @@ function parseWsdl(content, opts) {
305440
306130
  }
305441
306131
 
305442
306132
  // src/lib/protocols/soap/builder.ts
305443
- var import_node_crypto4 = require("node:crypto");
306133
+ var import_node_crypto5 = require("node:crypto");
305444
306134
  var COLLECTION_V210_SCHEMA2 = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json";
305445
306135
  var SOAP11_ENVELOPE_NS = "http://schemas.xmlsoap.org/soap/envelope/";
305446
306136
  var SOAP12_ENVELOPE_NS = "http://www.w3.org/2003/05/soap-envelope";
@@ -305448,7 +306138,7 @@ var SOAP11_CONTENT_TYPE = "text/xml; charset=UTF-8";
305448
306138
  var SOAP12_CONTENT_TYPE = "application/soap+xml; charset=UTF-8";
305449
306139
  var WSA_NS = "http://www.w3.org/2005/08/addressing";
305450
306140
  function stableId2(seed) {
305451
- return (0, import_node_crypto4.createHash)("sha256").update(seed).digest("hex").slice(0, 32);
306141
+ return (0, import_node_crypto5.createHash)("sha256").update(seed).digest("hex").slice(0, 32);
305452
306142
  }
305453
306143
  function envelopeNamespace(version) {
305454
306144
  return version === "1.2" ? SOAP12_ENVELOPE_NS : SOAP11_ENVELOPE_NS;
@@ -312211,6 +312901,11 @@ function resolveInputs(env = process.env) {
312211
312901
  ),
312212
312902
  protocolEndpointUrl: getInput2("protocol-endpoint-url", env),
312213
312903
  openapiVersion: resolveOpenapiVersion(getInput2("openapi-version", env)),
312904
+ preserveOas30TypeNull: parseBooleanInput(
312905
+ "preserve-oas30-type-null",
312906
+ getInput2("preserve-oas30-type-null", env),
312907
+ false
312908
+ ),
312214
312909
  breakingChangeMode: parseBreakingChangeMode(getInput2("breaking-change-mode", env)),
312215
312910
  breakingBaselineSpecPath: getInput2("breaking-baseline-spec-path", env),
312216
312911
  breakingRulesPath: getInput2("breaking-rules-path", env) ?? bootstrapActionContract.inputs["breaking-rules-path"].default,
@@ -312241,8 +312936,71 @@ function resolveInputs(env = process.env) {
312241
312936
  githubRef: env.GITHUB_REF,
312242
312937
  githubSha: env.GITHUB_SHA,
312243
312938
  githubToken: getInput2("github-token", env) || env.GITHUB_TOKEN || "",
312244
- ghFallbackToken: getInput2("gh-fallback-token", env) || env.GH_FALLBACK_TOKEN || ""
312939
+ ghFallbackToken: getInput2("gh-fallback-token", env) || env.GH_FALLBACK_TOKEN || "",
312940
+ branchStrategy: parseEnumInput(
312941
+ "branch-strategy",
312942
+ getInput2("branch-strategy", env),
312943
+ "legacy"
312944
+ ),
312945
+ canonicalBranch: getInput2("canonical-branch", env),
312946
+ channels: getInput2("channels", env)
312947
+ };
312948
+ }
312949
+ function decideBranchTier(inputs, env = process.env) {
312950
+ return resolveEffectiveBranchDecision(
312951
+ {
312952
+ // Tolerate hand-built inputs objects (tests, embedders) that omit the
312953
+ // field: absent means legacy, exactly like the action default.
312954
+ strategy: inputs.branchStrategy ?? "legacy",
312955
+ identity: resolveBranchIdentity(env, { defaultBranch: inputs.canonicalBranch }),
312956
+ canonicalBranch: inputs.canonicalBranch,
312957
+ channels: parseChannelRules(inputs.channels)
312958
+ },
312959
+ env
312960
+ );
312961
+ }
312962
+ function embedSpecBranchMarker(content, decision, repo) {
312963
+ if (decision.tier !== "preview" && decision.tier !== "channel" || !decision.identity.headBranch || !repo) {
312964
+ return content;
312965
+ }
312966
+ const rawBranch = decision.identity.headBranch;
312967
+ const now = (/* @__PURE__ */ new Date()).toISOString();
312968
+ const marker = {
312969
+ repo,
312970
+ rawBranch,
312971
+ sanitizedBranch: rawBranch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").slice(0, 30),
312972
+ role: decision.tier,
312973
+ headSha: decision.identity.headSha,
312974
+ createdAt: now,
312975
+ lastSyncedAt: now
312245
312976
  };
312977
+ try {
312978
+ if (content.trim().startsWith("{")) {
312979
+ return `${JSON.stringify({ ...JSON.parse(content), "x-postman-onboarding": marker }, null, 2)}
312980
+ `;
312981
+ }
312982
+ const document2 = (0, import_yaml4.parse)(content);
312983
+ if (!document2 || typeof document2 !== "object" || Array.isArray(document2)) return content;
312984
+ return (0, import_yaml4.stringify)({ ...document2, "x-postman-onboarding": marker });
312985
+ } catch {
312986
+ return content;
312987
+ }
312988
+ }
312989
+ function renderCollectionBranchMarker(decision, repo, now = /* @__PURE__ */ new Date()) {
312990
+ if (decision.tier !== "preview" && decision.tier !== "channel" || !decision.identity.headBranch || !repo) {
312991
+ return void 0;
312992
+ }
312993
+ const rawBranch = decision.identity.headBranch;
312994
+ const timestamp = now.toISOString();
312995
+ return renderAssetMarker({
312996
+ repo,
312997
+ rawBranch,
312998
+ sanitizedBranch: rawBranch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").slice(0, 30),
312999
+ role: decision.tier,
313000
+ headSha: decision.identity.headSha,
313001
+ createdAt: timestamp,
313002
+ lastSyncedAt: timestamp
313003
+ });
312246
313004
  }
312247
313005
  function createPlannedOutputs(inputs) {
312248
313006
  const workspaceName = inputs.domainCode ? `[${inputs.domainCode}] ${inputs.projectName}` : inputs.projectName;
@@ -312275,7 +313033,12 @@ function createPlannedOutputs(inputs) {
312275
313033
  mode: inputs.breakingChangeMode,
312276
313034
  status: "skipped",
312277
313035
  summaryPath: ""
312278
- })
313036
+ }),
313037
+ "sync-status": "",
313038
+ "branch-decision": "",
313039
+ "spec-version-tag": "",
313040
+ "spec-version-url": "",
313041
+ "spec-content-changed": ""
312279
313042
  };
312280
313043
  }
312281
313044
  function warnIfDeprecatedAccessToken(actionCore, inputs) {
@@ -312312,9 +313075,6 @@ function readActionInputs(actionCore) {
312312
313075
  }
312313
313076
  const postmanApiKey = optionalInput(actionCore, "postman-api-key") ?? "";
312314
313077
  const postmanAccessToken = optionalInput(actionCore, "postman-access-token");
312315
- if (!postmanApiKey && !postmanAccessToken) {
312316
- throw new Error("One of postman-api-key or postman-access-token is required.");
312317
- }
312318
313078
  const githubToken = optionalInput(actionCore, "github-token") || process.env.GITHUB_TOKEN;
312319
313079
  const ghFallbackToken = optionalInput(actionCore, "gh-fallback-token") || process.env.GH_FALLBACK_TOKEN;
312320
313080
  if (postmanApiKey) actionCore.setSecret(postmanApiKey);
@@ -312358,6 +313118,7 @@ function readActionInputs(actionCore) {
312358
313118
  INPUT_NESTED_FOLDER_HIERARCHY: optionalInput(actionCore, "nested-folder-hierarchy") ?? bootstrapActionContract.inputs["nested-folder-hierarchy"].default,
312359
313119
  INPUT_REQUEST_NAME_SOURCE: optionalInput(actionCore, "request-name-source") ?? bootstrapActionContract.inputs["request-name-source"].default,
312360
313120
  INPUT_OPENAPI_VERSION: optionalInput(actionCore, "openapi-version") ?? "",
313121
+ INPUT_PRESERVE_OAS30_TYPE_NULL: optionalInput(actionCore, "preserve-oas30-type-null") ?? bootstrapActionContract.inputs["preserve-oas30-type-null"].default,
312361
313122
  INPUT_BREAKING_CHANGE_MODE: optionalInput(actionCore, "breaking-change-mode") ?? bootstrapActionContract.inputs["breaking-change-mode"].default,
312362
313123
  INPUT_BREAKING_BASELINE_SPEC_PATH: optionalInput(actionCore, "breaking-baseline-spec-path"),
312363
313124
  INPUT_BREAKING_RULES_PATH: optionalInput(actionCore, "breaking-rules-path") ?? bootstrapActionContract.inputs["breaking-rules-path"].default,
@@ -312468,6 +313229,27 @@ async function lintSpecViaCli(dependencies, workspaceId, specId) {
312468
313229
  warnings
312469
313230
  };
312470
313231
  }
313232
+ function normalizeLintPath(value) {
313233
+ return value.trim().replace(/^\$\.?/, "").replace(/\[(\d+)\]/g, ".$1").replace(/^\./, "");
313234
+ }
313235
+ function applyOas30TypeNullLintCompatibility(summary2, sourceTypeNullPaths) {
313236
+ const acceptedPaths = new Set(sourceTypeNullPaths.map(normalizeLintPath));
313237
+ if (acceptedPaths.size === 0) return summary2;
313238
+ const violations = summary2.violations.map((violation) => {
313239
+ const path10 = normalizeLintPath(violation.path ?? "");
313240
+ const issue2 = violation.issue ?? "";
313241
+ const isTypeEnumFinding = /["']?type["']? property/i.test(issue2) && /allowed values|must be equal to one of/i.test(issue2);
313242
+ if (violation.severity === "ERROR" && acceptedPaths.has(path10) && isTypeEnumFinding) {
313243
+ return { ...violation, severity: "WARNING" };
313244
+ }
313245
+ return violation;
313246
+ });
313247
+ return {
313248
+ errors: violations.filter((entry) => entry.severity === "ERROR").length,
313249
+ violations,
313250
+ warnings: violations.filter((entry) => entry.severity === "WARNING").length
313251
+ };
313252
+ }
312471
313253
  function shouldRetrySpecFetch(error2) {
312472
313254
  const retryability = classifySafeFetchRetryability(error2);
312473
313255
  return retryability === "retryable" || retryability === "unknown";
@@ -312948,7 +313730,7 @@ async function runProtocolBootstrap(specType, rawSpecContent, inputs, dependenci
312948
313730
  const workspaceRoot = path9.resolve(process.env.GITHUB_WORKSPACE ?? process.cwd());
312949
313731
  const serviceConfigPath = path9.join(path9.dirname(path9.resolve(workspaceRoot, inputs.specPath)), "grpc_service_config.json");
312950
313732
  try {
312951
- grpcServiceConfigJson = (0, import_node_fs5.readFileSync)(serviceConfigPath, "utf8");
313733
+ grpcServiceConfigJson = (0, import_node_fs6.readFileSync)(serviceConfigPath, "utf8");
312952
313734
  dependencies.core.info(`Found gRPC service config at ${serviceConfigPath}; validating it against the proto contract`);
312953
313735
  } catch {
312954
313736
  }
@@ -312962,7 +313744,7 @@ async function runProtocolBootstrap(specType, rawSpecContent, inputs, dependenci
312962
313744
  const resolved = path9.resolve(specDir, location2);
312963
313745
  if (!resolved.startsWith(specDir + path9.sep)) return void 0;
312964
313746
  try {
312965
- return (0, import_node_fs5.readFileSync)(resolved, "utf8");
313747
+ return (0, import_node_fs6.readFileSync)(resolved, "utf8");
312966
313748
  } catch {
312967
313749
  return void 0;
312968
313750
  }
@@ -313103,6 +313885,42 @@ async function createExtensibleContractCollection(workspaceId, built, inputs, de
313103
313885
  }
313104
313886
  async function runBootstrapInner(inputs, dependencies, telemetry) {
313105
313887
  const outputs = createPlannedOutputs(inputs);
313888
+ const branchDecision = decideBranchTier(inputs);
313889
+ const isCanonicalWriter = branchDecision.tier === "legacy" || branchDecision.tier === "canonical";
313890
+ const canonicalProjectName = inputs.projectName;
313891
+ const workspaceName = createWorkspaceName(inputs);
313892
+ const aboutText = `Auto-provisioned by Postman for ${inputs.projectName}`;
313893
+ if (branchDecision.tier === "preview" && branchDecision.identity.headBranch) {
313894
+ inputs = {
313895
+ ...inputs,
313896
+ projectName: previewAssetName(inputs.projectName, branchDecision.identity.headBranch)
313897
+ };
313898
+ dependencies.core.info(`branch-aware sync: preview asset set "${inputs.projectName}"`);
313899
+ } else if (branchDecision.tier === "channel" && branchDecision.channel) {
313900
+ inputs = {
313901
+ ...inputs,
313902
+ projectName: channelAssetName(inputs.projectName, branchDecision.channel.code)
313903
+ };
313904
+ dependencies.core.info(`branch-aware sync: channel asset set "${inputs.projectName}"`);
313905
+ }
313906
+ const collectionBranchMarker = renderCollectionBranchMarker(branchDecision, inputs.repoUrl);
313907
+ if (branchDecision.tier !== "legacy") {
313908
+ outputs["sync-status"] = "synced";
313909
+ outputs["branch-decision"] = serializeBranchDecision(branchDecision);
313910
+ }
313911
+ if (!isCanonicalWriter) {
313912
+ const explicitCanonicalIds = [
313913
+ ["spec-id", inputs.specId],
313914
+ ["baseline-collection-id", inputs.baselineCollectionId],
313915
+ ["smoke-collection-id", inputs.smokeCollectionId],
313916
+ ["contract-collection-id", inputs.contractCollectionId]
313917
+ ].filter(([, value]) => Boolean(value));
313918
+ if (explicitCanonicalIds.length > 0) {
313919
+ throw new Error(
313920
+ `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).`
313921
+ );
313922
+ }
313923
+ }
313106
313924
  const requiresReleaseLabel = inputs.collectionSyncMode === "version" || inputs.specSyncMode === "version";
313107
313925
  const releaseLabel = requiresReleaseLabel ? deriveReleaseLabel(inputs) : void 0;
313108
313926
  if (requiresReleaseLabel && !releaseLabel) {
@@ -313110,11 +313928,23 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313110
313928
  "Versioned spec or collection sync requires a release-label or derivable GitHub ref metadata"
313111
313929
  );
313112
313930
  }
313113
- const collectionAssetProjectName = inputs.collectionSyncMode === "version" ? createAssetProjectName(inputs, releaseLabel) : inputs.projectName;
313114
- const workspaceName = createWorkspaceName(inputs);
313115
- const aboutText = `Auto-provisioned by Postman for ${inputs.projectName}`;
313116
- const stateStore = resolveResourcesStateStore(dependencies);
313117
- const resourcesState = stateStore.read();
313931
+ const collectionAssetProjectName = branchDecision.tier === "channel" ? canonicalProjectName : inputs.collectionSyncMode === "version" ? createAssetProjectName(inputs, releaseLabel) : inputs.projectName;
313932
+ const rawStateStore = resolveResourcesStateStore(dependencies);
313933
+ const trackedState = rawStateStore.read();
313934
+ const stateStore = isCanonicalWriter ? rawStateStore : {
313935
+ read: rawStateStore.read,
313936
+ write: () => {
313937
+ dependencies.core.info(
313938
+ `branch-aware sync: skipping .postman/resources.yaml write on ${branchDecision.tier} run (canonical-only tracked state)`
313939
+ );
313940
+ }
313941
+ };
313942
+ const resourcesState = isCanonicalWriter ? trackedState : trackedState?.workspace ? { workspace: trackedState.workspace } : null;
313943
+ if (!isCanonicalWriter && trackedState?.cloudResources) {
313944
+ dependencies.core.info(
313945
+ "branch-aware sync: canonical asset ids in .postman/resources.yaml are not resolved on a non-canonical run"
313946
+ );
313947
+ }
313118
313948
  const writableResourcesState = resourcesState ?? {};
313119
313949
  const additionalCollections = loadAdditionalCollectionFiles(
313120
313950
  inputs.additionalCollectionsDir,
@@ -313134,16 +313964,19 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313134
313964
  }
313135
313965
  let previousSpecContent;
313136
313966
  let previousSpecRollbackHash;
313967
+ let specContentUnchanged = false;
313137
313968
  let detectedOpenapiVersion = "3.0";
313138
313969
  let contractIndex;
313139
313970
  let sourceSpecContent = "";
313971
+ let sourceTypeNullPaths = [];
313972
+ let preserveSourceSpecBytes = false;
313140
313973
  const rawSpecContent = await runGroup(
313141
313974
  dependencies.core,
313142
313975
  "Read API Spec",
313143
313976
  async () => {
313144
313977
  if (inputs.specPath) {
313145
313978
  const workspaceRoot = path9.resolve(process.env.GITHUB_WORKSPACE ?? process.cwd());
313146
- return (0, import_node_fs5.readFileSync)(path9.resolve(workspaceRoot, inputs.specPath), "utf8");
313979
+ return (0, import_node_fs6.readFileSync)(path9.resolve(workspaceRoot, inputs.specPath), "utf8");
313147
313980
  }
313148
313981
  if (dependencies.specFetcher === fetch) {
313149
313982
  return safeFetchText(inputs.specUrl, { depth: 0 });
@@ -313179,6 +314012,7 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313179
314012
  };
313180
314013
  const rootKey = inputs.specPath ? void 0 : normalizeRef(inputs.specUrl);
313181
314014
  const loaderOptions = {
314015
+ preserveOas30TypeNull: Boolean(inputs.preserveOas30TypeNull),
313182
314016
  fetchText: async (url, fetchOptions) => {
313183
314017
  if (rootKey && normalizeRef(url) === rootKey) return rawSpecContent;
313184
314018
  if (dependencies.specFetcher === fetch) return safeFetchText(url, fetchOptions);
@@ -313187,6 +314021,8 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313187
314021
  };
313188
314022
  const loaded = inputs.specPath ? await loadOpenApiContractSpecFromPath(inputs.specPath, loaderOptions) : await loadOpenApiContractSpec(inputs.specUrl, loaderOptions);
313189
314023
  sourceSpecContent = loaded.content;
314024
+ sourceTypeNullPaths = loaded.sourceTypeNullPaths;
314025
+ preserveSourceSpecBytes = sourceTypeNullPaths.length > 0;
313190
314026
  const document2 = normalizeSpecDocument(
313191
314027
  loaded.bundledContent,
313192
314028
  (msg) => dependencies.core.warning(msg)
@@ -313209,11 +314045,11 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313209
314045
  `Unable to verify existing Spec Hub OpenAPI version for spec-id ${specId}; clear spec-id to create a fresh spec`
313210
314046
  );
313211
314047
  }
313212
- previousSpecContent = normalizeSpecDocument(
314048
+ previousSpecContent = preserveSourceSpecBytes ? previousRaw : normalizeSpecDocument(
313213
314049
  previousRaw,
313214
314050
  (msg) => dependencies.core.warning(`Previous spec normalization: ${msg}`)
313215
314051
  );
313216
- previousSpecRollbackHash = (0, import_node_crypto5.createHash)("sha256").update(previousSpecContent).digest("hex");
314052
+ previousSpecRollbackHash = (0, import_node_crypto6.createHash)("sha256").update(previousSpecContent).digest("hex");
313217
314053
  const existingSpecType = normalizeSpecTypeFromContent(previousSpecContent);
313218
314054
  if (existingSpecType !== incomingSpecType) {
313219
314055
  throw new Error(
@@ -313224,6 +314060,12 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313224
314060
  dependencies.core.info(
313225
314061
  `Auto-detected OpenAPI version from spec content: ${detectedOpenapiVersion}`
313226
314062
  );
314063
+ if (preserveSourceSpecBytes) {
314064
+ dependencies.core.info(
314065
+ `Preserving original OpenAPI source bytes; accepted ${sourceTypeNullPaths.length} legacy type: null member(s) for internal validation`
314066
+ );
314067
+ return loaded.content;
314068
+ }
313227
314069
  return document2;
313228
314070
  }
313229
314071
  );
@@ -313259,6 +314101,11 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313259
314101
  `OpenAPI breaking-change check failed: ${breakingChangeResult.message || "breaking changes detected"}`
313260
314102
  );
313261
314103
  }
314104
+ const uploadSpecContent = embedSpecBranchMarker(
314105
+ specContent,
314106
+ branchDecision,
314107
+ inputs.repoUrl
314108
+ );
313262
314109
  const provisioned = await provisionWorkspace(
313263
314110
  inputs,
313264
314111
  dependencies,
@@ -313383,10 +314230,17 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313383
314230
  specId ? "Update Spec in Spec Hub" : "Upload Spec to Spec Hub",
313384
314231
  async () => {
313385
314232
  if (specId) {
313386
- dependencies.core.info(
313387
- `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.`
313388
- );
313389
- await dependencies.postman.updateSpec(specId, specContent, workspaceId);
314233
+ if (previousSpecContent !== void 0 && (0, import_node_crypto6.createHash)("sha256").update(uploadSpecContent).digest("hex") === (0, import_node_crypto6.createHash)("sha256").update(previousSpecContent).digest("hex")) {
314234
+ specContentUnchanged = true;
314235
+ dependencies.core.info(
314236
+ `Spec content unchanged (sha256 match); skipping Spec Hub update and version tag for ${specId}.`
314237
+ );
314238
+ } else {
314239
+ dependencies.core.info(
314240
+ `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.`
314241
+ );
314242
+ await dependencies.postman.updateSpec(specId, uploadSpecContent, workspaceId);
314243
+ }
313390
314244
  } else {
313391
314245
  specId = await dependencies.postman.uploadSpec(
313392
314246
  workspaceId || "",
@@ -313394,7 +314248,7 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313394
314248
  inputs,
313395
314249
  inputs.specSyncMode === "version" ? releaseLabel : void 0
313396
314250
  ),
313397
- specContent,
314251
+ uploadSpecContent,
313398
314252
  detectedOpenapiVersion
313399
314253
  );
313400
314254
  }
@@ -313411,8 +314265,9 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313411
314265
  }
313412
314266
  )
313413
314267
  );
314268
+ outputs["spec-content-changed"] = isCanonicalWriter && !specContentUnchanged ? "true" : "false";
313414
314269
  if (lintEnabled) {
313415
- const lintSummary = await runRollbackStage(
314270
+ let lintSummary = await runRollbackStage(
313416
314271
  "Lint Spec via Postman CLI",
313417
314272
  async () => runGroup(
313418
314273
  dependencies.core,
@@ -313420,6 +314275,16 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313420
314275
  async () => lintSpecViaCli(dependencies, workspaceId || "", outputs["spec-id"])
313421
314276
  )
313422
314277
  );
314278
+ if (preserveSourceSpecBytes) {
314279
+ const originalErrorCount = lintSummary.errors;
314280
+ lintSummary = applyOas30TypeNullLintCompatibility(lintSummary, sourceTypeNullPaths);
314281
+ const acceptedCount = originalErrorCount - lintSummary.errors;
314282
+ if (acceptedCount > 0) {
314283
+ dependencies.core.info(
314284
+ `Accepted ${acceptedCount} Postman CLI type: null finding(s) covered by preserve-oas30-type-null`
314285
+ );
314286
+ }
314287
+ }
313423
314288
  outputs["lint-summary-json"] = JSON.stringify({
313424
314289
  errors: lintSummary.errors,
313425
314290
  total: lintSummary.violations.length,
@@ -313444,7 +314309,12 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313444
314309
  });
313445
314310
  dependencies.core.warning("lint skipped: governance errors not enforced (no postman-api-key)");
313446
314311
  }
313447
- await runRollbackStage(
314312
+ if (specContentUnchanged) {
314313
+ outputs["baseline-collection-id"] = baselineCollectionId || "";
314314
+ outputs["smoke-collection-id"] = smokeCollectionId || "";
314315
+ outputs["contract-collection-id"] = contractCollectionId || "";
314316
+ dependencies.core.info("Spec content unchanged; skipping collection regeneration and version finalization.");
314317
+ } else await runRollbackStage(
313448
314318
  "Generate Collections from Spec",
313449
314319
  async () => runGroup(
313450
314320
  dependencies.core,
@@ -313483,10 +314353,11 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313483
314353
  return;
313484
314354
  }
313485
314355
  }
314356
+ const effectivePrefix = branchDecision.tier === "channel" && branchDecision.channel ? channelAssetName(prefix, branchDecision.channel.code).trim() : prefix;
313486
314357
  outputs[outputKey] = await dependencies.postman.generateCollection(
313487
314358
  specId2,
313488
314359
  assetProjectName,
313489
- prefix,
314360
+ effectivePrefix,
313490
314361
  inputs.folderStrategy,
313491
314362
  inputs.nestedFolderHierarchy,
313492
314363
  inputs.requestNameSource
@@ -313507,6 +314378,32 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313507
314378
  await ensureCollection(BASELINE_COLLECTION_PREFIX, baselineCollectionId, "baseline-collection-id");
313508
314379
  await ensureCollection(SMOKE_COLLECTION_PREFIX, smokeCollectionId, "smoke-collection-id");
313509
314380
  await ensureCollection(CONTRACT_COLLECTION_PREFIX, contractCollectionId, "contract-collection-id");
314381
+ if (dependencies.postman.adoptGeneratedCollection) {
314382
+ const reelect = async (prefix, outputKey) => {
314383
+ const effectivePrefix = branchDecision.tier === "channel" && branchDecision.channel ? channelAssetName(prefix, branchDecision.channel.code).trim() : prefix;
314384
+ const preferred = outputs[outputKey];
314385
+ if (!preferred) return;
314386
+ outputs[outputKey] = await dependencies.postman.adoptGeneratedCollection(
314387
+ specId2,
314388
+ assetProjectName,
314389
+ effectivePrefix,
314390
+ preferred
314391
+ );
314392
+ };
314393
+ await reelect(BASELINE_COLLECTION_PREFIX, "baseline-collection-id");
314394
+ await reelect(SMOKE_COLLECTION_PREFIX, "smoke-collection-id");
314395
+ await reelect(CONTRACT_COLLECTION_PREFIX, "contract-collection-id");
314396
+ }
314397
+ if (collectionBranchMarker) {
314398
+ if (!dependencies.postman.updateCollectionDescription) {
314399
+ throw new Error("Branch-scoped collections require updateCollectionDescription support");
314400
+ }
314401
+ await Promise.all([
314402
+ outputs["baseline-collection-id"],
314403
+ outputs["smoke-collection-id"],
314404
+ outputs["contract-collection-id"]
314405
+ ].filter(Boolean).map((id) => dependencies.postman.updateCollectionDescription(id, collectionBranchMarker)));
314406
+ }
313510
314407
  if (!dependencies.postman.injectContractTests) {
313511
314408
  throw new Error(
313512
314409
  "Dynamic contract tests require injectContractTests support from the access-token gateway client"
@@ -313609,6 +314506,12 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313609
314506
  workspaceId: workspaceId || ""
313610
314507
  });
313611
314508
  for (const result of additionalResults) {
314509
+ if (collectionBranchMarker) {
314510
+ if (!dependencies.postman.updateCollectionDescription) {
314511
+ throw new Error("Branch-scoped collections require updateCollectionDescription support");
314512
+ }
314513
+ await dependencies.postman.updateCollectionDescription(result.collectionId, collectionBranchMarker);
314514
+ }
313612
314515
  completedExternalSideEffects.push(
313613
314516
  `${result.operation}AdditionalCollection(${result.collectionId} from ${result.displayPath})`
313614
314517
  );
@@ -313684,8 +314587,66 @@ async function runBootstrapInner(inputs, dependencies, telemetry) {
313684
314587
  }
313685
314588
  return outputs;
313686
314589
  }
314590
+ async function runGatedValidation(inputs, decision, actionCore) {
314591
+ actionCore.info(`branch-aware sync: gated run (${decision.reason}) \u2014 credential-free static validation, zero workspace writes`);
314592
+ const outputs = createPlannedOutputs(inputs);
314593
+ outputs["sync-status"] = "skipped-branch-gate";
314594
+ outputs["branch-decision"] = serializeBranchDecision(decision);
314595
+ let violations = [];
314596
+ let validated = false;
314597
+ try {
314598
+ let content;
314599
+ if (inputs.specPath) {
314600
+ content = (0, import_node_fs6.readFileSync)(inputs.specPath, "utf8");
314601
+ } else if (inputs.specUrl) {
314602
+ content = await safeFetchText(inputs.specUrl, { depth: 0 });
314603
+ }
314604
+ if (content) {
314605
+ const specType = detectSpecType(content, inputs.specPath);
314606
+ if (specType === "openapi") {
314607
+ const document2 = parseOpenApiDocument(content);
314608
+ const index = buildContractIndex(document2);
314609
+ violations = index.warnings;
314610
+ validated = true;
314611
+ } else {
314612
+ actionCore.info(`branch gate: static lint for spec type ${specType} runs through its protocol builder on publish; parse-only gate applied`);
314613
+ validated = true;
314614
+ }
314615
+ } else {
314616
+ actionCore.info("branch gate: no spec-url/spec-path provided; nothing to validate");
314617
+ }
314618
+ } catch (error2) {
314619
+ throw new Error(
314620
+ `branch gate: static validation failed: ${error2 instanceof Error ? error2.message : String(error2)}`,
314621
+ { cause: error2 }
314622
+ );
314623
+ }
314624
+ outputs["lint-summary-json"] = JSON.stringify({
314625
+ status: validated ? "static-only" : "skipped",
314626
+ reason: "branch-gated run: governance lint requires upload and runs on publish/preview syncs",
314627
+ errors: 0,
314628
+ warnings: violations.length,
314629
+ total: violations.length,
314630
+ violations: violations.map((issue2) => ({ issue: issue2, severity: "WARNING" }))
314631
+ });
314632
+ for (const [name, value] of Object.entries(outputs)) {
314633
+ actionCore.setOutput(name, value);
314634
+ }
314635
+ return outputs;
314636
+ }
313687
314637
  async function runAction(actionCore = core_exports, actionExec = exec_exports, actionIo = io_exports) {
313688
314638
  const inputs = readActionInputs(actionCore);
314639
+ const branchDecision = decideBranchTier(inputs);
314640
+ if (branchDecision.tier === "gated") {
314641
+ return runGatedValidation(inputs, branchDecision, actionCore);
314642
+ }
314643
+ if (!inputs.postmanApiKey && !inputs.postmanAccessToken) {
314644
+ throw new Error("One of postman-api-key or postman-access-token is required for a writing sync.");
314645
+ }
314646
+ if (branchDecision.tier !== "legacy") {
314647
+ actionCore.info(`branch-aware sync: tier=${branchDecision.tier} (${branchDecision.reason})`);
314648
+ process.env[BRANCH_DECISION_ENV] = serializeBranchDecision(branchDecision);
314649
+ }
313689
314650
  await mintAccessTokenIfNeeded2(inputs, {
313690
314651
  info: (message) => actionCore.info(message),
313691
314652
  warning: (message) => actionCore.warning(message)
@@ -313762,6 +314723,7 @@ function createRoutingPostmanClient(options) {
313762
314723
  updateSpec: requireAccessToken("updateSpec"),
313763
314724
  getSpecContent: requireAccessToken("getSpecContent"),
313764
314725
  generateCollection: requireAccessToken("generateCollection"),
314726
+ adoptGeneratedCollection: requireAccessToken("adoptGeneratedCollection"),
313765
314727
  createWorkspace: requireAccessToken("createWorkspace"),
313766
314728
  getWorkspaceVisibility: requireAccessToken("getWorkspaceVisibility"),
313767
314729
  getWorkspaceGitRepoUrl: requireAccessToken("getWorkspaceGitRepoUrl"),
@@ -313771,10 +314733,13 @@ function createRoutingPostmanClient(options) {
313771
314733
  inviteRequesterToWorkspace: requireAccessToken("inviteRequesterToWorkspace"),
313772
314734
  injectTests: requireAccessToken("injectTests"),
313773
314735
  tagCollection: requireAccessToken("tagCollection"),
314736
+ tagSpecVersion: requireAccessToken("tagSpecVersion"),
314737
+ listSpecVersionTags: requireAccessToken("listSpecVersionTags"),
313774
314738
  deleteCollection: requireAccessToken("deleteCollection"),
313775
314739
  injectContractTests: requireAccessToken("injectContractTests"),
313776
314740
  createCollection: requireAccessToken("createCollection"),
313777
- updateCollection: requireAccessToken("updateCollection")
314741
+ updateCollection: requireAccessToken("updateCollection"),
314742
+ updateCollectionDescription: requireAccessToken("updateCollectionDescription")
313778
314743
  };
313779
314744
  }
313780
314745
  return {
@@ -313786,6 +314751,7 @@ function createRoutingPostmanClient(options) {
313786
314751
  // not a reason to reach for the API key.
313787
314752
  uploadSpec: (workspaceId, projectName, specContent, openapiVersion) => gateway.uploadSpec(workspaceId, projectName, specContent, openapiVersion ?? "3.0"),
313788
314753
  generateCollection: (specId, projectName, prefix, folderStrategy, nestedFolderHierarchy, requestNameSource) => gateway.generateCollection(specId, projectName, prefix, folderStrategy, nestedFolderHierarchy, requestNameSource),
314754
+ adoptGeneratedCollection: (specId, projectName, prefix, preferredId) => gateway.adoptGeneratedCollection(specId, projectName, prefix, preferredId),
313789
314755
  updateSpec: (specId, specContent, workspaceId) => gateway.updateSpec(specId, specContent, workspaceId),
313790
314756
  getSpecContent: (specId) => gateway.getSpecContent(specId),
313791
314757
  createWorkspace: (name, about, targetTeamId) => gateway.createWorkspace(name, about, targetTeamId),
@@ -313797,6 +314763,8 @@ function createRoutingPostmanClient(options) {
313797
314763
  // + tagging surfaces (live-proven). PMAK is reserved for token minting, so
313798
314764
  // these never fall back to the API key even when one is present.
313799
314765
  injectTests: (collectionId, type) => gateway.injectTests(collectionId, type),
314766
+ tagSpecVersion: (specId, name) => gateway.tagSpecVersion(specId, name),
314767
+ listSpecVersionTags: (specId) => gateway.listSpecVersionTags(specId),
313800
314768
  tagCollection: (collectionId, tags) => gateway.tagCollection(collectionId, tags),
313801
314769
  // Sub-team (squad) enumeration over the gateway `ums` service. Access-token
313802
314770
  // only — never PMAK — so org-mode detection no longer needs a PMAK GET /teams.
@@ -313814,7 +314782,8 @@ function createRoutingPostmanClient(options) {
313814
314782
  // so no asset op ever reaches for the API key.
313815
314783
  injectContractTests: (collectionUid, index) => gateway.injectContractTests(collectionUid, index),
313816
314784
  createCollection: (workspaceId, collection, options2) => gateway.createCollection(workspaceId, collection, options2),
313817
- updateCollection: (collectionUid, collection) => gateway.updateCollection(collectionUid, collection)
314785
+ updateCollection: (collectionUid, collection) => gateway.updateCollection(collectionUid, collection),
314786
+ updateCollectionDescription: (collectionUid, description) => gateway.updateCollectionDescription(collectionUid, description)
313818
314787
  };
313819
314788
  }
313820
314789
  function createBootstrapDependencies(inputs, factories, orgMode = false) {