@postman-cse/onboarding-repo-sync 2.1.5 → 2.1.7

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
@@ -92333,11 +92333,11 @@ var require_valid = __commonJS({
92333
92333
  var require_clean = __commonJS({
92334
92334
  "node_modules/postman-collection/node_modules/semver/functions/clean.js"(exports2, module2) {
92335
92335
  var parse2 = require_parse5();
92336
- var clean = (version2, options) => {
92336
+ var clean2 = (version2, options) => {
92337
92337
  const s = parse2(version2.trim().replace(/^[=v]+/, ""), options);
92338
92338
  return s ? s.version : null;
92339
92339
  };
92340
- module2.exports = clean;
92340
+ module2.exports = clean2;
92341
92341
  }
92342
92342
  });
92343
92343
 
@@ -93648,7 +93648,7 @@ var require_semver2 = __commonJS({
93648
93648
  var identifiers = require_identifiers();
93649
93649
  var parse2 = require_parse5();
93650
93650
  var valid = require_valid();
93651
- var clean = require_clean();
93651
+ var clean2 = require_clean();
93652
93652
  var inc = require_inc();
93653
93653
  var diff = require_diff();
93654
93654
  var major = require_major();
@@ -93686,7 +93686,7 @@ var require_semver2 = __commonJS({
93686
93686
  module2.exports = {
93687
93687
  parse: parse2,
93688
93688
  valid,
93689
- clean,
93689
+ clean: clean2,
93690
93690
  inc,
93691
93691
  diff,
93692
93692
  major,
@@ -122063,7 +122063,7 @@ function getIDToken(aud) {
122063
122063
  }
122064
122064
 
122065
122065
  // src/index.ts
122066
- var import_node_fs3 = require("node:fs");
122066
+ var import_node_fs4 = require("node:fs");
122067
122067
  var path8 = __toESM(require("node:path"), 1);
122068
122068
 
122069
122069
  // node_modules/js-yaml/dist/js-yaml.mjs
@@ -125114,12 +125114,14 @@ var POSTMAN_ENDPOINT_PROFILES = {
125114
125114
  prod: {
125115
125115
  apiBaseUrl: "https://api.getpostman.com",
125116
125116
  bifrostBaseUrl: "https://bifrost-premium-https-v4.gw.postman.com",
125117
+ fallbackBaseUrl: "https://go.postman.co/_api",
125117
125118
  cliInstallUrl: "https://dl-cli.pstmn.io/install/unix.sh",
125118
125119
  iapubBaseUrl: "https://iapub.postman.co"
125119
125120
  },
125120
125121
  beta: {
125121
125122
  apiBaseUrl: "https://api.getpostman-beta.com",
125122
125123
  bifrostBaseUrl: "https://bifrost-https-v4.gw.postman-beta.com",
125124
+ fallbackBaseUrl: "https://go.postman-beta.co/_api",
125123
125125
  cliInstallUrl: "https://dl-cli.pstmn-beta.io/install/unix.sh",
125124
125126
  iapubBaseUrl: "https://iapub.postman.co"
125125
125127
  }
@@ -125186,6 +125188,9 @@ function buildCiWorkflowLines(installUrl, postmanRegion) {
125186
125188
  " branches: [main]",
125187
125189
  " schedule:",
125188
125190
  ' - cron: "0 */6 * * *"',
125191
+ "concurrency:",
125192
+ " group: postman-onboard-${{ github.head_ref || github.ref_name }}",
125193
+ " cancel-in-progress: false",
125189
125194
  "jobs:",
125190
125195
  " test:",
125191
125196
  " runs-on: ubuntu-latest",
@@ -125202,7 +125207,7 @@ function buildCiWorkflowLines(installUrl, postmanRegion) {
125202
125207
  " ruby <<'RUBY'",
125203
125208
  " require 'yaml'",
125204
125209
  " config = YAML.load_file('.postman/resources.yaml') || {}",
125205
- " cloud = config.fetch('cloudResources', {})",
125210
+ " cloud = config.fetch('canonical', config.fetch('cloudResources', {}))",
125206
125211
  " collections = cloud.fetch('collections', {})",
125207
125212
  " environments = cloud.fetch('environments', {})",
125208
125213
  " smoke = collections.find { |path, _| path.include?('[Smoke]') }&.last",
@@ -125400,6 +125405,58 @@ function buildAdoCiWorkflowLines(installUrl, postmanRegion) {
125400
125405
  ""
125401
125406
  ];
125402
125407
  }
125408
+ function renderGcWorkflowTemplate() {
125409
+ return [
125410
+ "name: Postman Preview GC",
125411
+ "on:",
125412
+ " delete:",
125413
+ ' branches: ["**"]',
125414
+ " pull_request:",
125415
+ " types: [closed]",
125416
+ " schedule:",
125417
+ ' - cron: "0 2 * * *"',
125418
+ " workflow_dispatch:",
125419
+ " inputs:",
125420
+ " branch:",
125421
+ " description: Branch name to GC (optional, otherwise sweep by TTL/branch-existence)",
125422
+ " required: false",
125423
+ "concurrency:",
125424
+ " group: postman-preview-gc-${{ github.ref_name }}",
125425
+ " cancel-in-progress: false",
125426
+ "jobs:",
125427
+ " gc:",
125428
+ " runs-on: ubuntu-latest",
125429
+ " if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository",
125430
+ " steps:",
125431
+ " - uses: actions/checkout@v5",
125432
+ " with:",
125433
+ " fetch-depth: 0",
125434
+ " - name: Run Postman preview GC (provider-neutral cli.cjs gc)",
125435
+ " env:",
125436
+ " POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }}",
125437
+ " POSTMAN_ACCESS_TOKEN: ${{ secrets.POSTMAN_ACCESS_TOKEN }}",
125438
+ " POSTMAN_WORKSPACE_ID: ${{ vars.POSTMAN_WORKSPACE_ID }}",
125439
+ " REPO_URL: https://github.com/${{ github.repository }}",
125440
+ " run: |",
125441
+ " set -euo pipefail",
125442
+ " # The repo-sync action bundles a provider-neutral gc command (cli.cjs gc)",
125443
+ " # that uses the Postman access token for inventory/deletion and the",
125444
+ " # provider ambient credential (GITHUB_TOKEN via git ls-remote) for branch existence.",
125445
+ " # Daily scheduled run is the retention executor (TTL contract of last resort).",
125446
+ ' if [ -n "${{ inputs.branch }}" ]; then',
125447
+ ' npx @postman-cse/onboarding-repo-sync gc --branch "${{ inputs.branch }}" --workspace-id "$POSTMAN_WORKSPACE_ID" --repo-url "$REPO_URL"',
125448
+ " else",
125449
+ ' npx @postman-cse/onboarding-repo-sync gc --workspace-id "$POSTMAN_WORKSPACE_ID" --repo-url "$REPO_URL"',
125450
+ " fi",
125451
+ " - name: Orphan audit summary (job summary)",
125452
+ " if: always()",
125453
+ " run: |",
125454
+ ' echo "### Preview GC orphan audit" >> "$GITHUB_STEP_SUMMARY"',
125455
+ ' echo "Marker-guarded deletion only \u2014 strangers (no marker) are never deleted. See gc-summary-json for structured counts." >> "$GITHUB_STEP_SUMMARY"',
125456
+ ""
125457
+ ].join("\n");
125458
+ }
125459
+ var GC_WORKFLOW_TEMPLATE = renderGcWorkflowTemplate();
125403
125460
  function getCiWorkflowTemplate(provider, options = {}) {
125404
125461
  if (provider === "azure-devops") {
125405
125462
  const rawUrl = String(options.postmanCliInstallUrl || "").trim() || DEFAULT_POSTMAN_CLI_INSTALL_URL;
@@ -126970,6 +127027,53 @@ function createInternalIntegrationAdapter(options) {
126970
127027
  return new BifrostInternalIntegrationAdapter(options);
126971
127028
  }
126972
127029
 
127030
+ // src/lib/retry.ts
127031
+ function sleep(delayMs) {
127032
+ return new Promise((resolve3) => {
127033
+ setTimeout(resolve3, delayMs);
127034
+ });
127035
+ }
127036
+ function normalizeRetryOptions(options) {
127037
+ return {
127038
+ maxAttempts: Math.max(1, options.maxAttempts ?? 3),
127039
+ delayMs: Math.max(0, options.delayMs ?? 2e3),
127040
+ backoffMultiplier: Math.max(1, options.backoffMultiplier ?? 1),
127041
+ maxDelayMs: options.maxDelayMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, options.maxDelayMs),
127042
+ onRetry: options.onRetry ?? (async () => void 0),
127043
+ shouldRetry: options.shouldRetry ?? (() => true),
127044
+ sleep: options.sleep ?? sleep
127045
+ };
127046
+ }
127047
+ async function retry(operation, options = {}) {
127048
+ const normalized = normalizeRetryOptions(options);
127049
+ let nextDelayMs = normalized.delayMs;
127050
+ for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
127051
+ try {
127052
+ return await operation();
127053
+ } catch (error2) {
127054
+ const shouldRetry = attempt < normalized.maxAttempts && normalized.shouldRetry(error2, {
127055
+ attempt,
127056
+ maxAttempts: normalized.maxAttempts
127057
+ });
127058
+ if (!shouldRetry) {
127059
+ throw error2;
127060
+ }
127061
+ await normalized.onRetry({
127062
+ attempt,
127063
+ maxAttempts: normalized.maxAttempts,
127064
+ delayMs: nextDelayMs,
127065
+ error: error2
127066
+ });
127067
+ await normalized.sleep(nextDelayMs);
127068
+ nextDelayMs = Math.min(
127069
+ normalized.maxDelayMs,
127070
+ Math.round(nextDelayMs * normalized.backoffMultiplier)
127071
+ );
127072
+ }
127073
+ }
127074
+ throw new Error("Retry exhausted without returning or throwing");
127075
+ }
127076
+
126973
127077
  // src/contracts.ts
126974
127078
  var postmanRepoSyncActionContract = {
126975
127079
  name: "postman-repo-sync-action",
@@ -127139,6 +127243,25 @@ var postmanRepoSyncActionContract = {
127139
127243
  default: "warn",
127140
127244
  allowedValues: ["enforce", "warn"]
127141
127245
  },
127246
+ "branch-strategy": {
127247
+ description: "Branch-aware sync strategy. legacy (default) keeps branch-blind behavior; publish-gate restricts canonical writes to the canonical branch and skips repo-sync on other branches; preview additionally maintains suffixed per-branch preview asset sets.",
127248
+ required: false,
127249
+ default: "legacy",
127250
+ allowedValues: ["legacy", "preview", "publish-gate"]
127251
+ },
127252
+ "canonical-branch": {
127253
+ description: "Explicit canonical branch (the sole writer of canonical assets and tracked state). Defaults to the provider-resolved default branch; required on providers without a default-branch variable (Bitbucket, Azure DevOps) when branch-strategy is not legacy.",
127254
+ required: false
127255
+ },
127256
+ "channels": {
127257
+ 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.',
127258
+ required: false
127259
+ },
127260
+ "preview-ttl": {
127261
+ description: "Sliding TTL in days for preview asset sets (refreshed on every successful preview sync; the retention contract of last resort when no provider credential is available for branch-existence checks).",
127262
+ required: false,
127263
+ default: "30"
127264
+ },
127142
127265
  "github-token": {
127143
127266
  description: "GitHub token used for repo variable persistence and commits.",
127144
127267
  required: false
@@ -127176,6 +127299,11 @@ var postmanRepoSyncActionContract = {
127176
127299
  description: "Spec UID from bootstrap, persisted into .postman/resources.yaml cloudResources.",
127177
127300
  required: false
127178
127301
  },
127302
+ "spec-content-changed": {
127303
+ description: "Whether bootstrap changed canonical spec content; controls final native Spec Hub tag publication.",
127304
+ required: false,
127305
+ default: "true"
127306
+ },
127179
127307
  "spec-path": {
127180
127308
  description: "Optional repo-root-relative path to the local spec file for resources/workflows metadata.",
127181
127309
  required: false
@@ -127220,6 +127348,18 @@ var postmanRepoSyncActionContract = {
127220
127348
  },
127221
127349
  "commit-sha": {
127222
127350
  description: "Commit SHA produced by repo-write-mode, if any."
127351
+ },
127352
+ "sync-status": {
127353
+ description: "Branch-aware sync status: synced, skipped-branch-gate, or empty under branch-strategy legacy."
127354
+ },
127355
+ "branch-decision": {
127356
+ description: "Serialized BranchDecision JSON for downstream actions (also exported as POSTMAN_BRANCH_DECISION)."
127357
+ },
127358
+ "spec-version-tag": {
127359
+ description: "Native Spec Hub version tag created after successful canonical repo-sync finalization."
127360
+ },
127361
+ "spec-version-url": {
127362
+ description: "Read-only URL for the tagged Spec Hub snapshot."
127223
127363
  }
127224
127364
  },
127225
127365
  behavior: {
@@ -127287,53 +127427,6 @@ var PostmanAssetsClient = class {
127287
127427
  }
127288
127428
  };
127289
127429
 
127290
- // src/lib/retry.ts
127291
- function sleep(delayMs) {
127292
- return new Promise((resolve3) => {
127293
- setTimeout(resolve3, delayMs);
127294
- });
127295
- }
127296
- function normalizeRetryOptions(options) {
127297
- return {
127298
- maxAttempts: Math.max(1, options.maxAttempts ?? 3),
127299
- delayMs: Math.max(0, options.delayMs ?? 2e3),
127300
- backoffMultiplier: Math.max(1, options.backoffMultiplier ?? 1),
127301
- maxDelayMs: options.maxDelayMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, options.maxDelayMs),
127302
- onRetry: options.onRetry ?? (async () => void 0),
127303
- shouldRetry: options.shouldRetry ?? (() => true),
127304
- sleep: options.sleep ?? sleep
127305
- };
127306
- }
127307
- async function retry(operation, options = {}) {
127308
- const normalized = normalizeRetryOptions(options);
127309
- let nextDelayMs = normalized.delayMs;
127310
- for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
127311
- try {
127312
- return await operation();
127313
- } catch (error2) {
127314
- const shouldRetry = attempt < normalized.maxAttempts && normalized.shouldRetry(error2, {
127315
- attempt,
127316
- maxAttempts: normalized.maxAttempts
127317
- });
127318
- if (!shouldRetry) {
127319
- throw error2;
127320
- }
127321
- await normalized.onRetry({
127322
- attempt,
127323
- maxAttempts: normalized.maxAttempts,
127324
- delayMs: nextDelayMs,
127325
- error: error2
127326
- });
127327
- await normalized.sleep(nextDelayMs);
127328
- nextDelayMs = Math.min(
127329
- normalized.maxDelayMs,
127330
- Math.round(nextDelayMs * normalized.backoffMultiplier)
127331
- );
127332
- }
127333
- }
127334
- throw new Error("Retry exhausted without returning or throwing");
127335
- }
127336
-
127337
127430
  // src/lib/postman/postman-gateway-assets-client.ts
127338
127431
  var MAX_CREATE_FLIGHTS = 256;
127339
127432
  var createFlights = /* @__PURE__ */ new Map();
@@ -127359,6 +127452,84 @@ var PostmanGatewayAssetsClient = class {
127359
127452
  if (!envelope) return null;
127360
127453
  return this.asRecord(envelope.data) ?? envelope;
127361
127454
  }
127455
+ /** Native Spec Hub tags attach to the latest changelog group. */
127456
+ async tagSpecVersion(specId, name) {
127457
+ const trimmed = name.trim().slice(0, 255);
127458
+ const response = await this.gateway.requestJson({
127459
+ service: "specification",
127460
+ method: "post",
127461
+ path: `/specifications/${specId}/tags`,
127462
+ body: { name: trimmed }
127463
+ });
127464
+ const record = this.dataOf(response) ?? {};
127465
+ return { id: String(record.id ?? "").trim(), name: String(record.name ?? trimmed).trim() };
127466
+ }
127467
+ async listSpecVersionTags(specId) {
127468
+ const response = await this.gateway.requestJson({
127469
+ service: "specification",
127470
+ method: "get",
127471
+ path: `/specifications/${specId}/tags`,
127472
+ query: { limit: "50" }
127473
+ });
127474
+ const data = Array.isArray(response?.data) ? response.data : [];
127475
+ return data.map((entry) => this.asRecord(entry)).filter((entry) => entry !== null).map((entry) => ({
127476
+ id: String(entry.id ?? "").trim(),
127477
+ // listTags returns `message`; createTag returns `name`. Accept both.
127478
+ name: String(entry.name ?? entry.message ?? "").trim()
127479
+ })).filter((entry) => entry.id || entry.name);
127480
+ }
127481
+ async deleteCollection(collectionUid) {
127482
+ const bareId = String(collectionUid).split("-").slice(-5).join("-") || collectionUid;
127483
+ await this.gateway.requestJson({
127484
+ service: "collection",
127485
+ method: "delete",
127486
+ path: `/v3/collections/${bareId}`
127487
+ });
127488
+ }
127489
+ async listSpecifications(workspaceId) {
127490
+ const response = await this.gateway.requestJson({
127491
+ service: "specification",
127492
+ method: "get",
127493
+ path: `/specifications?containerType=workspace&containerId=${workspaceId}`
127494
+ });
127495
+ const data = Array.isArray(response?.data) ? response.data : [];
127496
+ return data.map((entry) => this.asRecord(entry)).filter((entry) => entry !== null).map((entry) => ({ uid: String(entry.id ?? entry.uid ?? "").trim(), name: String(entry.name ?? "").trim() })).filter((entry) => entry.uid && entry.name);
127497
+ }
127498
+ async getSpecContent(specId) {
127499
+ const files = await this.gateway.requestJson({
127500
+ service: "specification",
127501
+ method: "get",
127502
+ path: `/specifications/${specId}/files`
127503
+ });
127504
+ const entries = Array.isArray(files?.data) ? files.data : [];
127505
+ const root = entries.map((entry) => this.asRecord(entry)).find((entry) => entry?.type === "ROOT") ?? entries.map((entry) => this.asRecord(entry)).find((entry) => entry !== null);
127506
+ const fileId = String(root?.id ?? "").trim();
127507
+ if (!fileId) return void 0;
127508
+ const file = await this.gateway.requestJson({
127509
+ service: "specification",
127510
+ method: "get",
127511
+ path: `/specifications/${specId}/files/${fileId}`,
127512
+ query: { fields: "content" }
127513
+ });
127514
+ const record = this.dataOf(file);
127515
+ return typeof record?.content === "string" ? record.content : void 0;
127516
+ }
127517
+ async listSpecCollections(specId) {
127518
+ const response = await this.gateway.requestJson({
127519
+ service: "specification",
127520
+ method: "get",
127521
+ path: `/specifications/${specId}/collections`
127522
+ });
127523
+ const data = Array.isArray(response?.data) ? response.data : [];
127524
+ return data.map((entry) => this.asRecord(entry)).filter((entry) => entry !== null).map((entry) => ({ uid: String(entry.collection ?? entry.collectionId ?? entry.id ?? "").trim(), name: String(entry.name ?? "").trim() })).filter((entry) => entry.uid);
127525
+ }
127526
+ async deleteSpec(specId) {
127527
+ await this.gateway.requestJson({
127528
+ service: "specification",
127529
+ method: "delete",
127530
+ path: `/specifications/${specId}`
127531
+ });
127532
+ }
127362
127533
  idOf(record) {
127363
127534
  if (!record) return "";
127364
127535
  const id = record.uid ?? record.id;
@@ -127418,6 +127589,14 @@ var PostmanGatewayAssetsClient = class {
127418
127589
  createFlights.set(key, { fingerprint, promise: pending });
127419
127590
  return pending;
127420
127591
  }
127592
+ /**
127593
+ * Reconcile an ambiguous create. Returns the adopted match, `undefined`
127594
+ * when every discovery read succeeded and found nothing (the create is
127595
+ * conclusively absent, so a fallback resend cannot duplicate), or `null`
127596
+ * when the outcome is inconclusive (non-ambiguous error, or a discovery
127597
+ * read itself failed — in that case a resend could duplicate a committed
127598
+ * twin and must not fire).
127599
+ */
127421
127600
  async discoverAfterAmbiguousCreate(discover, error2) {
127422
127601
  if (!this.isAmbiguousCreateOutcome(error2)) {
127423
127602
  return null;
@@ -127426,12 +127605,31 @@ var PostmanGatewayAssetsClient = class {
127426
127605
  if (attempt > 0) {
127427
127606
  await this.sleep(this.reconcileDelayMs);
127428
127607
  }
127429
- const found = await discover();
127608
+ let found;
127609
+ try {
127610
+ found = await discover();
127611
+ } catch {
127612
+ return null;
127613
+ }
127430
127614
  if (found) {
127431
127615
  return found;
127432
127616
  }
127433
127617
  }
127434
- return null;
127618
+ return void 0;
127619
+ }
127620
+ /**
127621
+ * After an ambiguous create failed AND discovery proved the asset absent,
127622
+ * re-attempt the create once with the cold `/_api` fallback enabled: the
127623
+ * reconcile above guarantees no committed twin, so the resend cannot
127624
+ * duplicate. Returns null when the resend also fails (caller rethrows the
127625
+ * original error).
127626
+ */
127627
+ async resendAbsentCreate(operation) {
127628
+ try {
127629
+ return await operation();
127630
+ } catch {
127631
+ return null;
127632
+ }
127435
127633
  }
127436
127634
  publicEnvironmentUid(data, bareId) {
127437
127635
  const owner = String(data?.owner ?? "").trim();
@@ -127479,17 +127677,18 @@ var PostmanGatewayAssetsClient = class {
127479
127677
  name: envName,
127480
127678
  values: normalizedValues
127481
127679
  };
127680
+ const send2 = (fallback) => this.gateway.requestJson(
127681
+ {
127682
+ service: "sync",
127683
+ method: "post",
127684
+ path: `/environment/import?workspace=${ws}`,
127685
+ body,
127686
+ ...fallback ? { fallback } : {}
127687
+ },
127688
+ { retryTransient: false }
127689
+ );
127482
127690
  try {
127483
- const response = await this.gateway.requestJson(
127484
- {
127485
- service: "sync",
127486
- method: "post",
127487
- path: `/environment/import?workspace=${ws}`,
127488
- body
127489
- },
127490
- { retryTransient: false }
127491
- );
127492
- const data = this.dataOf(response);
127691
+ const data = this.dataOf(await send2());
127493
127692
  const bareId = this.idOf(data);
127494
127693
  if (!bareId) {
127495
127694
  throw new Error("Environment import did not return a UID");
@@ -127504,6 +127703,15 @@ var PostmanGatewayAssetsClient = class {
127504
127703
  await this.updateEnvironment(adopted, envName, values);
127505
127704
  return adopted;
127506
127705
  }
127706
+ if (adopted === void 0) {
127707
+ const retried = await this.resendAbsentCreate(async () => {
127708
+ const data = this.dataOf(await send2("auto"));
127709
+ const bareId = this.idOf(data);
127710
+ if (!bareId) throw new Error("Environment import did not return a UID");
127711
+ return this.publicEnvironmentUid(data, bareId);
127712
+ });
127713
+ if (retried) return retried;
127714
+ }
127507
127715
  throw error2;
127508
127716
  }
127509
127717
  });
@@ -127635,16 +127843,20 @@ var PostmanGatewayAssetsClient = class {
127635
127843
  private: false,
127636
127844
  ...environment ? { environment } : {}
127637
127845
  };
127638
- try {
127639
- const response = await this.gateway.requestJson(
127640
- {
127641
- service: "mock",
127642
- method: "post",
127643
- path: `/mocks?workspace=${ws}`,
127644
- body
127645
- },
127646
- { retryTransient: false }
127647
- );
127846
+ const send2 = (fallback) => this.gateway.requestJson(
127847
+ {
127848
+ service: "mock",
127849
+ method: "post",
127850
+ path: `/mocks?workspace=${ws}`,
127851
+ body,
127852
+ ...fallback ? { fallback } : {}
127853
+ },
127854
+ // Unsafe create: never blind re-POST. An ESOCKETTIMEDOUT after accept
127855
+ // may still have created the mock; reconcile via discovery below and
127856
+ // let the orchestrator retry the whole create-or-adopt cycle.
127857
+ { retryTransient: false }
127858
+ );
127859
+ const parseMock = (response) => {
127648
127860
  const record = this.dataOf(response);
127649
127861
  const uid = this.idOf(record);
127650
127862
  if (!uid) {
@@ -127654,6 +127866,9 @@ var PostmanGatewayAssetsClient = class {
127654
127866
  uid,
127655
127867
  url: String(record?.url ?? record?.mockUrl ?? "").trim()
127656
127868
  };
127869
+ };
127870
+ try {
127871
+ return parseMock(await send2());
127657
127872
  } catch (error2) {
127658
127873
  const adopted = await this.discoverAfterAmbiguousCreate(
127659
127874
  () => this.findMockByCollection(collection, environment, mockName),
@@ -127662,6 +127877,10 @@ var PostmanGatewayAssetsClient = class {
127662
127877
  if (adopted) {
127663
127878
  return { uid: adopted.uid, url: adopted.mockUrl };
127664
127879
  }
127880
+ if (adopted === void 0) {
127881
+ const retried = await this.resendAbsentCreate(async () => parseMock(await send2("auto")));
127882
+ if (retried) return retried;
127883
+ }
127665
127884
  throw error2;
127666
127885
  }
127667
127886
  });
@@ -127681,6 +127900,33 @@ var PostmanGatewayAssetsClient = class {
127681
127900
  environment: String(m.environment ?? "")
127682
127901
  }));
127683
127902
  }
127903
+ /**
127904
+ * Delete an environment through the sync service (GC path). The path id is
127905
+ * the bare model id (public uid tail), mirroring updateEnvironment.
127906
+ */
127907
+ async deleteEnvironment(uid) {
127908
+ await this.gateway.requestJson({
127909
+ service: "sync",
127910
+ method: "delete",
127911
+ path: `/environment/${this.toModelId(uid)}`
127912
+ });
127913
+ }
127914
+ /** Delete a mock server (GC path). */
127915
+ async deleteMock(uid) {
127916
+ await this.gateway.requestJson({
127917
+ service: "mock",
127918
+ method: "delete",
127919
+ path: `/mocks/${this.toModelId(uid)}`
127920
+ });
127921
+ }
127922
+ /** Delete a collection-based monitor (jobTemplate) (GC path). */
127923
+ async deleteMonitor(uid) {
127924
+ await this.gateway.requestJson({
127925
+ service: "monitors",
127926
+ method: "delete",
127927
+ path: `/jobTemplates/${this.toModelId(uid)}`
127928
+ });
127929
+ }
127684
127930
  async mockExists(uid) {
127685
127931
  try {
127686
127932
  await this.gateway.requestJson({
@@ -127741,21 +127987,25 @@ var PostmanGatewayAssetsClient = class {
127741
127987
  distribution: null,
127742
127988
  ...environment ? { environment } : {}
127743
127989
  };
127744
- try {
127745
- const response = await this.gateway.requestJson(
127746
- {
127747
- service: "monitors",
127748
- method: "post",
127749
- path: `/jobTemplates?workspace=${ws}`,
127750
- body
127751
- },
127752
- { retryTransient: false }
127753
- );
127990
+ const send2 = (fallback) => this.gateway.requestJson(
127991
+ {
127992
+ service: "monitors",
127993
+ method: "post",
127994
+ path: `/jobTemplates?workspace=${ws}`,
127995
+ body,
127996
+ ...fallback ? { fallback } : {}
127997
+ },
127998
+ { retryTransient: false }
127999
+ );
128000
+ const parseMonitor = (response) => {
127754
128001
  const uid = this.idOf(this.dataOf(response));
127755
128002
  if (!uid) {
127756
128003
  throw new Error("Monitor create did not return a UID");
127757
128004
  }
127758
128005
  return uid;
128006
+ };
128007
+ try {
128008
+ return parseMonitor(await send2());
127759
128009
  } catch (error2) {
127760
128010
  const adopted = await this.discoverAfterAmbiguousCreate(
127761
128011
  () => this.findMonitorByCollection(collection, environment, monitorName),
@@ -127764,6 +128014,10 @@ var PostmanGatewayAssetsClient = class {
127764
128014
  if (adopted?.uid) {
127765
128015
  return adopted.uid;
127766
128016
  }
128017
+ if (adopted === void 0) {
128018
+ const retried = await this.resendAbsentCreate(async () => parseMonitor(await send2("auto")));
128019
+ if (retried) return retried;
128020
+ }
127767
128021
  throw error2;
127768
128022
  }
127769
128023
  });
@@ -127992,6 +128246,7 @@ var AccessTokenGatewayClient = class {
127992
128246
  fetchImpl;
127993
128247
  secretMasker;
127994
128248
  maxRetries;
128249
+ fallbackBaseUrl;
127995
128250
  retryBaseDelayMs;
127996
128251
  sleepImpl;
127997
128252
  constructor(options) {
@@ -128004,6 +128259,8 @@ var AccessTokenGatewayClient = class {
128004
128259
  this.fetchImpl = options.fetchImpl ?? fetch;
128005
128260
  this.secretMasker = options.secretMasker ?? createSecretMasker([this.tokenProvider.current()]);
128006
128261
  this.maxRetries = options.maxRetries ?? 3;
128262
+ const fallbackEnv = typeof process !== "undefined" ? process.env?.POSTMAN_ITEM_CREATE_FALLBACK : void 0;
128263
+ this.fallbackBaseUrl = fallbackEnv === "off" ? void 0 : options.fallbackBaseUrl?.replace(/\/+$/, "");
128007
128264
  this.retryBaseDelayMs = options.retryBaseDelayMs ?? 400;
128008
128265
  this.sleepImpl = options.sleepImpl ?? defaultSleep;
128009
128266
  }
@@ -128022,8 +128279,8 @@ var AccessTokenGatewayClient = class {
128022
128279
  }
128023
128280
  return headers;
128024
128281
  }
128025
- async send(request) {
128026
- const url = `${this.bifrostBaseUrl}/ws/proxy`;
128282
+ async send(request, baseUrl) {
128283
+ const url = `${baseUrl ?? this.bifrostBaseUrl}/ws/proxy`;
128027
128284
  return this.fetchImpl(url, {
128028
128285
  method: "POST",
128029
128286
  headers: this.buildHeaders(request.headers),
@@ -128036,6 +128293,43 @@ var AccessTokenGatewayClient = class {
128036
128293
  })
128037
128294
  });
128038
128295
  }
128296
+ /**
128297
+ * One cold, serial attempt against the fallback base URL after the primary
128298
+ * budget is exhausted on a transient failure. Never hedged in parallel with
128299
+ * the primary; only fires when the request would otherwise throw. Callers
128300
+ * with `retryTransient: false` still reconcile first — the fallback attempt
128301
+ * here is the resend, so it is only used for requests whose mutation is
128302
+ * known idempotent or already reconciled by the caller's adopt-on-ambiguous
128303
+ * loop.
128304
+ */
128305
+ async tryFallback(request) {
128306
+ if (!this.fallbackBaseUrl) return null;
128307
+ try {
128308
+ return await this.send(request, this.fallbackBaseUrl);
128309
+ } catch {
128310
+ return null;
128311
+ }
128312
+ }
128313
+ /**
128314
+ * Run the fallback attempt and classify its response the same way the
128315
+ * primary path would. Returns the success response, or null when the
128316
+ * fallback also failed transiently (caller then throws the original error).
128317
+ * Non-transient fallback failures (4xx) surface as their own HttpError since
128318
+ * they are the freshest authoritative answer.
128319
+ */
128320
+ fallbackEligible(request, retryTransient) {
128321
+ if (!this.fallbackBaseUrl) return false;
128322
+ return retryTransient || request.fallback === "auto";
128323
+ }
128324
+ async attemptFallback(request, retryTransient) {
128325
+ if (!this.fallbackEligible(request, retryTransient)) return null;
128326
+ const response = await this.tryFallback(request);
128327
+ if (!response) return null;
128328
+ if (response.ok) return response;
128329
+ const body = await response.text().catch(() => "");
128330
+ if (isRetryableSafeReadResponse(response.status)) return null;
128331
+ throw this.toHttpError(request, response, body);
128332
+ }
128039
128333
  /**
128040
128334
  * Send a gateway request, refreshing the token once on an auth failure and
128041
128335
  * optionally retrying transient downstream failures (5xx / Bifrost read
@@ -128058,6 +128352,8 @@ var AccessTokenGatewayClient = class {
128058
128352
  await this.sleepImpl(delay);
128059
128353
  continue;
128060
128354
  }
128355
+ const fallbackResponse2 = await this.attemptFallback(request, retryTransient);
128356
+ if (fallbackResponse2) return fallbackResponse2;
128061
128357
  throw error2;
128062
128358
  }
128063
128359
  if (response.ok) {
@@ -128079,6 +128375,8 @@ var AccessTokenGatewayClient = class {
128079
128375
  await this.sleepImpl(delay);
128080
128376
  continue;
128081
128377
  }
128378
+ const fallbackResponse = await this.attemptFallback(request, retryTransient);
128379
+ if (fallbackResponse) return fallbackResponse;
128082
128380
  throw this.toHttpError(request, response, body);
128083
128381
  }
128084
128382
  }
@@ -128187,6 +128485,331 @@ function validateCertMaterial(certBase64, keyBase64, passphrase) {
128187
128485
  }
128188
128486
  }
128189
128487
 
128488
+ // src/lib/repo/branch-decision.ts
128489
+ var import_node_fs3 = require("node:fs");
128490
+ var import_node_crypto3 = require("node:crypto");
128491
+ var ContractError = class extends Error {
128492
+ code;
128493
+ constructor(code, message) {
128494
+ super(`${code}: ${message}`);
128495
+ this.code = code;
128496
+ this.name = "ContractError";
128497
+ }
128498
+ };
128499
+ function clean(value) {
128500
+ const trimmed = (value ?? "").trim();
128501
+ return trimmed.length > 0 ? trimmed : void 0;
128502
+ }
128503
+ function stripRefPrefix(ref) {
128504
+ const raw = clean(ref);
128505
+ if (!raw) {
128506
+ return { kind: "unknown" };
128507
+ }
128508
+ if (raw.startsWith("refs/heads/")) {
128509
+ return { name: raw.slice("refs/heads/".length), kind: "branch" };
128510
+ }
128511
+ if (raw.startsWith("refs/tags/")) {
128512
+ return { name: raw.slice("refs/tags/".length), kind: "tag" };
128513
+ }
128514
+ if (raw.startsWith("refs/pull/") || raw.startsWith("refs/merge")) {
128515
+ return { kind: "unknown" };
128516
+ }
128517
+ return { name: raw, kind: "branch" };
128518
+ }
128519
+ function detectProvider(env) {
128520
+ if (clean(env.GITHUB_ACTIONS) || clean(env.GITHUB_REPOSITORY)) return "github";
128521
+ if (clean(env.GITLAB_CI) || clean(env.CI_PROJECT_PATH)) return "gitlab";
128522
+ if (clean(env.BITBUCKET_REPO_SLUG) || clean(env.BITBUCKET_BRANCH)) return "bitbucket";
128523
+ if (clean(env.TF_BUILD) || clean(env.BUILD_REPOSITORY_URI)) return "azure-devops";
128524
+ return "unknown";
128525
+ }
128526
+ function readGithubEvent(env) {
128527
+ const path9 = clean(env.GITHUB_EVENT_PATH);
128528
+ if (!path9) return void 0;
128529
+ try {
128530
+ return JSON.parse((0, import_node_fs3.readFileSync)(path9, "utf8"));
128531
+ } catch {
128532
+ return void 0;
128533
+ }
128534
+ }
128535
+ function resolveBranchIdentity(env = process.env, overrides = {}) {
128536
+ const provider = detectProvider(env);
128537
+ const explicitDefault = clean(overrides.defaultBranch);
128538
+ let headBranch;
128539
+ let rawRef;
128540
+ let refKind;
128541
+ let isPrContext = false;
128542
+ let isForkPr = false;
128543
+ let defaultBranch = explicitDefault;
128544
+ let headSha;
128545
+ switch (provider) {
128546
+ case "github": {
128547
+ const event = readGithubEvent(env);
128548
+ headSha = clean(env.GITHUB_SHA);
128549
+ defaultBranch ??= clean(event?.repository?.default_branch);
128550
+ const headRef = clean(env.GITHUB_HEAD_REF);
128551
+ if (headRef) {
128552
+ isPrContext = true;
128553
+ headBranch = headRef;
128554
+ rawRef = clean(env.GITHUB_REF) ?? headRef;
128555
+ refKind = "branch";
128556
+ const headRepo = event?.pull_request?.head?.repo?.full_name;
128557
+ const baseRepo = event?.pull_request?.base?.repo?.full_name ?? event?.repository?.full_name;
128558
+ isForkPr = Boolean(headRepo && baseRepo && headRepo !== baseRepo);
128559
+ headSha = clean(event?.pull_request?.head?.sha) ?? headSha;
128560
+ } else {
128561
+ rawRef = clean(env.GITHUB_REF) ?? clean(env.GITHUB_REF_NAME);
128562
+ const parsed = stripRefPrefix(rawRef);
128563
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
128564
+ refKind = parsed.kind;
128565
+ }
128566
+ break;
128567
+ }
128568
+ case "gitlab": {
128569
+ headSha = clean(env.CI_COMMIT_SHA);
128570
+ defaultBranch ??= clean(env.CI_DEFAULT_BRANCH);
128571
+ const mrSource = clean(env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME);
128572
+ if (mrSource) {
128573
+ isPrContext = true;
128574
+ headBranch = mrSource;
128575
+ rawRef = mrSource;
128576
+ refKind = "branch";
128577
+ const sourceProject = clean(env.CI_MERGE_REQUEST_SOURCE_PROJECT_ID);
128578
+ const targetProject = clean(env.CI_MERGE_REQUEST_PROJECT_ID);
128579
+ isForkPr = Boolean(sourceProject && targetProject && sourceProject !== targetProject);
128580
+ } else if (clean(env.CI_COMMIT_TAG)) {
128581
+ rawRef = clean(env.CI_COMMIT_TAG);
128582
+ refKind = "tag";
128583
+ } else {
128584
+ headBranch = clean(env.CI_COMMIT_BRANCH) ?? clean(env.CI_COMMIT_REF_NAME);
128585
+ rawRef = headBranch;
128586
+ refKind = headBranch ? "branch" : "unknown";
128587
+ }
128588
+ break;
128589
+ }
128590
+ case "bitbucket": {
128591
+ headSha = clean(env.BITBUCKET_COMMIT);
128592
+ if (clean(env.BITBUCKET_TAG)) {
128593
+ rawRef = clean(env.BITBUCKET_TAG);
128594
+ refKind = "tag";
128595
+ } else {
128596
+ headBranch = clean(env.BITBUCKET_BRANCH);
128597
+ rawRef = headBranch;
128598
+ refKind = headBranch ? "branch" : "unknown";
128599
+ isPrContext = Boolean(clean(env.BITBUCKET_PR_ID));
128600
+ }
128601
+ break;
128602
+ }
128603
+ case "azure-devops": {
128604
+ headSha = clean(env.BUILD_SOURCEVERSION);
128605
+ const prSource = clean(env.SYSTEM_PULLREQUEST_SOURCEBRANCH);
128606
+ if (prSource) {
128607
+ isPrContext = true;
128608
+ const parsed = stripRefPrefix(prSource);
128609
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
128610
+ rawRef = prSource;
128611
+ refKind = parsed.kind;
128612
+ const forkFlag = clean(env.SYSTEM_PULLREQUEST_ISFORK);
128613
+ isForkPr = forkFlag?.toLowerCase() === "true";
128614
+ } else {
128615
+ rawRef = clean(env.BUILD_SOURCEBRANCH);
128616
+ const parsed = stripRefPrefix(rawRef);
128617
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
128618
+ refKind = parsed.kind;
128619
+ }
128620
+ break;
128621
+ }
128622
+ default: {
128623
+ refKind = "unknown";
128624
+ }
128625
+ }
128626
+ if (refKind === "branch" && headBranch && defaultBranch && headBranch === defaultBranch) {
128627
+ refKind = "default-branch";
128628
+ }
128629
+ return { provider, headBranch, rawRef, defaultBranch, refKind, isPrContext, isForkPr, headSha };
128630
+ }
128631
+ function parseChannelRules(input) {
128632
+ const raw = clean(input);
128633
+ const rules = [];
128634
+ for (const part of raw ? raw.split(",") : []) {
128635
+ const entry = part.trim();
128636
+ if (!entry) continue;
128637
+ const eq = entry.indexOf("=");
128638
+ if (eq <= 0 || eq === entry.length - 1) {
128639
+ throw new ContractError(
128640
+ "CONTRACT_CHANNELS_INPUT_INVALID",
128641
+ `channels entry "${entry}" must be <branch-or-glob>=<CODE>`
128642
+ );
128643
+ }
128644
+ const pattern = entry.slice(0, eq).trim();
128645
+ const code = entry.slice(eq + 1).trim().toUpperCase();
128646
+ if (!/^[A-Z][A-Z0-9_-]{0,15}$/.test(code)) {
128647
+ throw new ContractError(
128648
+ "CONTRACT_CHANNELS_INPUT_INVALID",
128649
+ `channel code "${code}" must be 1-16 chars, A-Z 0-9 _ -, starting with a letter`
128650
+ );
128651
+ }
128652
+ rules.push({ pattern, code });
128653
+ }
128654
+ if (!rules.some((rule) => rule.pattern === "release/*")) {
128655
+ rules.push({ pattern: "release/*", code: "RC" });
128656
+ }
128657
+ return rules;
128658
+ }
128659
+ function matchChannel(branch, rules) {
128660
+ for (const rule of rules) {
128661
+ if (rule.pattern.endsWith("*")) {
128662
+ const prefix = rule.pattern.slice(0, -1);
128663
+ if (branch.startsWith(prefix)) return rule;
128664
+ } else if (branch === rule.pattern) {
128665
+ return rule;
128666
+ }
128667
+ }
128668
+ return void 0;
128669
+ }
128670
+ function resolveBranchDecision(options) {
128671
+ const { strategy, identity } = options;
128672
+ const channels = options.channels ?? [];
128673
+ if (strategy === "legacy") {
128674
+ return {
128675
+ tier: "legacy",
128676
+ strategy,
128677
+ identity,
128678
+ canonicalBranch: clean(options.canonicalBranch) ?? identity.defaultBranch,
128679
+ reason: "branch-strategy legacy: branch-blind pre-v2 behavior"
128680
+ };
128681
+ }
128682
+ const canonicalBranch = clean(options.canonicalBranch) ?? identity.defaultBranch;
128683
+ if (!canonicalBranch) {
128684
+ throw new ContractError(
128685
+ "CONTRACT_DEFAULT_BRANCH_UNRESOLVED",
128686
+ `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.`
128687
+ );
128688
+ }
128689
+ if (identity.refKind === "tag" || identity.refKind === "unknown" || !identity.headBranch) {
128690
+ return {
128691
+ tier: "gated",
128692
+ strategy,
128693
+ identity,
128694
+ canonicalBranch,
128695
+ reason: `ref kind ${identity.refKind}: never canonical/preview-eligible; no-op with annotation`
128696
+ };
128697
+ }
128698
+ if (identity.headBranch === canonicalBranch) {
128699
+ return {
128700
+ tier: "canonical",
128701
+ strategy,
128702
+ identity,
128703
+ canonicalBranch,
128704
+ reason: `head branch equals canonical branch ${canonicalBranch}`
128705
+ };
128706
+ }
128707
+ const channel = matchChannel(identity.headBranch, channels);
128708
+ if (channel) {
128709
+ return {
128710
+ tier: "channel",
128711
+ strategy,
128712
+ identity,
128713
+ canonicalBranch,
128714
+ channel,
128715
+ reason: `branch ${identity.headBranch} matches channel ${channel.pattern}=${channel.code}`
128716
+ };
128717
+ }
128718
+ if (strategy === "preview") {
128719
+ if (identity.isForkPr) {
128720
+ return {
128721
+ tier: "gated",
128722
+ strategy,
128723
+ identity,
128724
+ canonicalBranch,
128725
+ reason: "fork PR: preview-ineligible (same-repo gate), gated instead"
128726
+ };
128727
+ }
128728
+ return {
128729
+ tier: "preview",
128730
+ strategy,
128731
+ identity,
128732
+ canonicalBranch,
128733
+ reason: `branch ${identity.headBranch} under branch-strategy preview`
128734
+ };
128735
+ }
128736
+ return {
128737
+ tier: "gated",
128738
+ strategy,
128739
+ identity,
128740
+ canonicalBranch,
128741
+ reason: `branch ${identity.headBranch} under branch-strategy publish-gate`
128742
+ };
128743
+ }
128744
+ var BRANCH_DECISION_ENV = "POSTMAN_BRANCH_DECISION";
128745
+ function serializeBranchDecision(decision) {
128746
+ return JSON.stringify(decision);
128747
+ }
128748
+ function parseBranchDecision(raw) {
128749
+ const value = clean(raw);
128750
+ if (!value) return void 0;
128751
+ let parsed;
128752
+ try {
128753
+ parsed = JSON.parse(value);
128754
+ } catch {
128755
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION is not valid JSON");
128756
+ }
128757
+ const candidate = parsed;
128758
+ const tiers = ["canonical", "channel", "preview", "gated", "legacy"];
128759
+ if (!candidate || typeof candidate !== "object" || !tiers.includes(candidate.tier) || !candidate.identity) {
128760
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION does not carry a valid BranchDecision");
128761
+ }
128762
+ return candidate;
128763
+ }
128764
+ function resolveEffectiveBranchDecision(options, env = process.env) {
128765
+ const inherited = parseBranchDecision(env[BRANCH_DECISION_ENV]);
128766
+ if (inherited) return inherited;
128767
+ return resolveBranchDecision(options);
128768
+ }
128769
+ var PREVIEW_SLUG_MAX = 30;
128770
+ function buildBranchSlug(rawBranch) {
128771
+ const sanitized = rawBranch.replace(/^refs\/heads\//, "").replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
128772
+ const truncated = sanitized.slice(0, PREVIEW_SLUG_MAX);
128773
+ const lossy = truncated !== rawBranch.replace(/^refs\/heads\//, "");
128774
+ if (!lossy) {
128775
+ return { suffix: truncated, slug: truncated, lossy };
128776
+ }
128777
+ const hash = (0, import_node_crypto3.createHash)("sha256").update(rawBranch).digest("hex").slice(0, 6);
128778
+ return { suffix: `${truncated}-${hash}`, slug: truncated, lossy };
128779
+ }
128780
+ function previewAssetName(baseName, rawBranch) {
128781
+ return `${baseName} @${buildBranchSlug(rawBranch).suffix}`;
128782
+ }
128783
+ function channelAssetName(baseName, code) {
128784
+ return `[${code}] ${baseName}`;
128785
+ }
128786
+ var MARKER_KEY = "x-pm-onboarding";
128787
+ function parseAssetMarker(description) {
128788
+ if (!description) return void 0;
128789
+ const index = description.indexOf(`${MARKER_KEY}:`);
128790
+ if (index === -1) return void 0;
128791
+ const jsonStart = description.indexOf("{", index);
128792
+ if (jsonStart === -1) return void 0;
128793
+ let depth = 0;
128794
+ for (let i = jsonStart; i < description.length; i += 1) {
128795
+ const ch = description[i];
128796
+ if (ch === "{") depth += 1;
128797
+ else if (ch === "}") {
128798
+ depth -= 1;
128799
+ if (depth === 0) {
128800
+ try {
128801
+ const parsed = JSON.parse(description.slice(jsonStart, i + 1));
128802
+ if (parsed && typeof parsed === "object" && parsed.repo && parsed.role) return parsed;
128803
+ } catch {
128804
+ return void 0;
128805
+ }
128806
+ return void 0;
128807
+ }
128808
+ }
128809
+ }
128810
+ return void 0;
128811
+ }
128812
+
128190
128813
  // src/index.ts
128191
128814
  function parseBooleanInput(value, defaultValue) {
128192
128815
  const normalized = String(value || "").trim().toLowerCase();
@@ -128205,16 +128828,16 @@ function getInput2(name, env = process.env) {
128205
128828
  const runnerRaw = runnerName === normalizedName ? void 0 : env[runnerName];
128206
128829
  const hasNormalized = normalizedRaw !== void 0;
128207
128830
  const hasRunner = runnerRaw !== void 0;
128831
+ const normalizedValue = normalizeInputValue(normalizedRaw);
128832
+ const runnerValue = normalizeInputValue(runnerRaw);
128208
128833
  if (hasNormalized && hasRunner) {
128209
- const normalizedValue = normalizeInputValue(normalizedRaw);
128210
- const runnerValue = normalizeInputValue(runnerRaw);
128211
- if (normalizedValue !== runnerValue) {
128834
+ if (normalizedValue && runnerValue && normalizedValue !== runnerValue) {
128212
128835
  throw new Error(
128213
128836
  `Conflicting values for ${name}: ${normalizedName}=${JSON.stringify(normalizedValue)} vs ${runnerName}=${JSON.stringify(runnerValue)}`
128214
128837
  );
128215
128838
  }
128216
128839
  }
128217
- return normalizeInputValue(hasNormalized ? normalizedRaw : runnerRaw);
128840
+ return normalizedValue || runnerValue;
128218
128841
  }
128219
128842
  function hasInput(name, env = process.env) {
128220
128843
  const normalizedName = `INPUT_${name.replace(/-/g, "_").toUpperCase()}`;
@@ -128276,6 +128899,17 @@ function parseCredentialPreflight(value) {
128276
128899
  `Unsupported credential-preflight "${normalized}". Supported values: ${allowed.join(", ")}`
128277
128900
  );
128278
128901
  }
128902
+ function parseBranchStrategy(value) {
128903
+ const definition = postmanRepoSyncActionContract.inputs["branch-strategy"];
128904
+ const allowed = definition.allowedValues ?? [];
128905
+ const normalized = String(value || "").trim() || (definition.default ?? "legacy");
128906
+ if (allowed.includes(normalized)) {
128907
+ return normalized;
128908
+ }
128909
+ throw new Error(
128910
+ `Unsupported branch-strategy "${normalized}". Supported values: ${allowed.join(", ")}`
128911
+ );
128912
+ }
128279
128913
  function normalizeReleaseLabel(value) {
128280
128914
  const cleaned = normalizeInputValue(value).replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
128281
128915
  return cleaned;
@@ -128320,6 +128954,7 @@ function resolveInputs(env = process.env) {
128320
128954
  smokeCollectionId: getInput2("smoke-collection-id", env),
128321
128955
  contractCollectionId: getInput2("contract-collection-id", env),
128322
128956
  specId: getInput2("spec-id", env),
128957
+ specContentChanged: parseBooleanInput(getInput2("spec-content-changed", env), true),
128323
128958
  specPath: getInput2("spec-path", env),
128324
128959
  collectionSyncMode: normalizeCollectionSyncMode(getInput2("collection-sync-mode", env) || "refresh"),
128325
128960
  specSyncMode: normalizeSpecSyncMode(getInput2("spec-sync-mode", env) || "update"),
@@ -128342,6 +128977,10 @@ function resolveInputs(env = process.env) {
128342
128977
  postmanApiKey: getInput2("postman-api-key", env),
128343
128978
  postmanAccessToken: getInput2("postman-access-token", env),
128344
128979
  credentialPreflight: parseCredentialPreflight(getInput2("credential-preflight", env)),
128980
+ branchStrategy: parseBranchStrategy(getInput2("branch-strategy", env)),
128981
+ canonicalBranch: getInput2("canonical-branch", env) || void 0,
128982
+ channels: getInput2("channels", env) || void 0,
128983
+ previewTtlDays: Math.max(1, Number.parseInt(getInput2("preview-ttl", env) || "30", 10) || 30),
128345
128984
  adoToken: getInput2("ado-token", env) || normalizeInputValue(env.SYSTEM_ACCESSTOKEN),
128346
128985
  githubToken: getInput2("github-token", env),
128347
128986
  ghFallbackToken: getInput2("gh-fallback-token", env),
@@ -128364,10 +129003,45 @@ function resolveInputs(env = process.env) {
128364
129003
  postmanStack,
128365
129004
  postmanApiBase: endpointProfile.apiBaseUrl,
128366
129005
  postmanBifrostBase: endpointProfile.bifrostBaseUrl,
129006
+ postmanFallbackBase: endpointProfile.fallbackBaseUrl,
128367
129007
  postmanCliInstallUrl: endpointProfile.cliInstallUrl,
128368
129008
  postmanIapubBase: endpointProfile.iapubBaseUrl
128369
129009
  };
128370
129010
  }
129011
+ function buildBranchAssetMarker(decision, inputs, now = /* @__PURE__ */ new Date()) {
129012
+ if (decision.tier !== "preview" && decision.tier !== "channel") {
129013
+ return void 0;
129014
+ }
129015
+ const rawBranch = decision.identity.headBranch;
129016
+ const repo = inputs.repoUrl || inputs.repository;
129017
+ if (!rawBranch || !repo) {
129018
+ return void 0;
129019
+ }
129020
+ const ttlMs = inputs.previewTtlDays * 24 * 60 * 60 * 1e3;
129021
+ return {
129022
+ repo,
129023
+ rawBranch,
129024
+ sanitizedBranch: buildBranchSlug(rawBranch).suffix,
129025
+ role: decision.tier,
129026
+ headSha: decision.identity.headSha,
129027
+ createdAt: now.toISOString(),
129028
+ lastSyncedAt: now.toISOString(),
129029
+ ...decision.tier === "preview" ? { expiresAt: new Date(now.getTime() + ttlMs).toISOString() } : {}
129030
+ };
129031
+ }
129032
+ function assertBranchAssetIds(inputs, decision, branchOwnedIds = process.env.POSTMAN_BRANCH_ASSET_IDS === "owned") {
129033
+ if (decision.tier === "legacy" || decision.tier === "canonical" || branchOwnedIds) return;
129034
+ const provided = [
129035
+ ["baseline-collection-id", inputs.baselineCollectionId],
129036
+ ["smoke-collection-id", inputs.smokeCollectionId],
129037
+ ["contract-collection-id", inputs.contractCollectionId]
129038
+ ].filter(([, value]) => Boolean(value));
129039
+ if (provided.length > 0) {
129040
+ throw new Error(
129041
+ `CONTRACT_BRANCH_CANONICAL_WRITE: a ${decision.tier} repo-sync run cannot accept explicit collection IDs (${provided.map(([name]) => name).join(", ")}). Run bootstrap in the same branch-aware pipeline so it can produce branch-owned IDs.`
129042
+ );
129043
+ }
129044
+ }
128371
129045
  function buildEnvironmentValues(envName, baseUrl) {
128372
129046
  return [
128373
129047
  { key: "baseUrl", value: baseUrl, type: "default" },
@@ -128380,12 +129054,48 @@ function buildEnvironmentValues(envName, baseUrl) {
128380
129054
  ];
128381
129055
  }
128382
129056
  var LEGACY_BASELINE_COLLECTION_PREFIX = "[Baseline]";
129057
+ var RESOURCES_STATE_VERSION = 2;
129058
+ var SUPPORTED_STATE_VERSIONS = /* @__PURE__ */ new Set([1, RESOURCES_STATE_VERSION]);
129059
+ var StateUnreadableError = class extends Error {
129060
+ code = "CONTRACT_STATE_UNREADABLE";
129061
+ constructor(message) {
129062
+ super(`CONTRACT_STATE_UNREADABLE: ${message}`);
129063
+ this.name = "StateUnreadableError";
129064
+ }
129065
+ };
128383
129066
  function readResourcesState() {
129067
+ let raw;
128384
129068
  try {
128385
- return load((0, import_node_fs3.readFileSync)(".postman/resources.yaml", "utf8"));
129069
+ raw = (0, import_node_fs4.readFileSync)(".postman/resources.yaml", "utf8");
128386
129070
  } catch {
128387
129071
  return null;
128388
129072
  }
129073
+ let parsed;
129074
+ try {
129075
+ parsed = load(raw);
129076
+ } catch (error2) {
129077
+ throw new StateUnreadableError(
129078
+ `.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.`
129079
+ );
129080
+ }
129081
+ if (parsed === null || parsed === void 0) {
129082
+ return null;
129083
+ }
129084
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
129085
+ throw new StateUnreadableError(
129086
+ ".postman/resources.yaml exists but does not contain a YAML mapping. Fix or delete the file; refusing to treat tracked state as absent."
129087
+ );
129088
+ }
129089
+ const state = parsed;
129090
+ if (state.version !== void 0 && !SUPPORTED_STATE_VERSIONS.has(Number(state.version))) {
129091
+ throw new StateUnreadableError(
129092
+ `.postman/resources.yaml declares unsupported state version ${String(state.version)} (supported: 1, ${RESOURCES_STATE_VERSION}). Upgrade the action or fix the file.`
129093
+ );
129094
+ }
129095
+ if (state.canonical && !state.cloudResources) {
129096
+ state.cloudResources = { ...state.canonical };
129097
+ }
129098
+ return state;
128389
129099
  }
128390
129100
  function findCloudResourceId(map, matcher) {
128391
129101
  if (!map) {
@@ -128420,7 +129130,7 @@ function isOpenApiSpecFile(filePath) {
128420
129130
  return false;
128421
129131
  }
128422
129132
  try {
128423
- const raw = (0, import_node_fs3.readFileSync)(filePath, "utf8");
129133
+ const raw = (0, import_node_fs4.readFileSync)(filePath, "utf8");
128424
129134
  const parsed = filePath.endsWith(".json") ? JSON.parse(raw) : load(raw);
128425
129135
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
128426
129136
  return false;
@@ -128449,7 +129159,7 @@ function scanLocalSpecReferences(baseDir = ".") {
128449
129159
  const found = /* @__PURE__ */ new Set();
128450
129160
  const refs = [];
128451
129161
  const visit2 = (currentDir) => {
128452
- for (const entry of (0, import_node_fs3.readdirSync)(currentDir, { withFileTypes: true })) {
129162
+ for (const entry of (0, import_node_fs4.readdirSync)(currentDir, { withFileTypes: true })) {
128453
129163
  if (ignoredDirs.has(entry.name)) {
128454
129164
  continue;
128455
129165
  }
@@ -128479,7 +129189,7 @@ function resolveMappedSpecReference(explicitSpecPath, discoveredSpecs) {
128479
129189
  const normalizedExplicitPath = normalizeToPosix(explicitSpecPath.trim());
128480
129190
  if (normalizedExplicitPath) {
128481
129191
  const explicitFullPath = path8.resolve(normalizedExplicitPath);
128482
- if ((0, import_node_fs3.existsSync)(explicitFullPath) && (0, import_node_fs3.statSync)(explicitFullPath).isFile()) {
129192
+ if ((0, import_node_fs4.existsSync)(explicitFullPath) && (0, import_node_fs4.statSync)(explicitFullPath).isFile()) {
128483
129193
  return {
128484
129194
  repoRelativePath: normalizedExplicitPath,
128485
129195
  configRelativePath: normalizeToPosix(path8.join("..", normalizedExplicitPath))
@@ -128506,7 +129216,11 @@ function createOutputs(inputs) {
128506
129216
  "mock-url": "",
128507
129217
  "monitor-id": "",
128508
129218
  "repo-sync-summary-json": "{}",
128509
- "commit-sha": ""
129219
+ "commit-sha": "",
129220
+ "sync-status": "",
129221
+ "branch-decision": "",
129222
+ "spec-version-tag": "",
129223
+ "spec-version-url": ""
128510
129224
  };
128511
129225
  }
128512
129226
  function readActionInputs(actionCore) {
@@ -128551,6 +129265,10 @@ function readActionInputs(actionCore) {
128551
129265
  INPUT_POSTMAN_API_KEY: postmanApiKey,
128552
129266
  INPUT_POSTMAN_ACCESS_TOKEN: postmanAccessToken,
128553
129267
  INPUT_CREDENTIAL_PREFLIGHT: readInput(actionCore, "credential-preflight") || "warn",
129268
+ INPUT_BRANCH_STRATEGY: readInput(actionCore, "branch-strategy"),
129269
+ INPUT_CANONICAL_BRANCH: readInput(actionCore, "canonical-branch"),
129270
+ INPUT_CHANNELS: readInput(actionCore, "channels"),
129271
+ INPUT_PREVIEW_TTL: readInput(actionCore, "preview-ttl"),
128554
129272
  INPUT_ADO_TOKEN: adoToken,
128555
129273
  INPUT_GITHUB_TOKEN: githubToken,
128556
129274
  INPUT_GH_FALLBACK_TOKEN: ghFallbackToken,
@@ -128665,7 +129383,7 @@ async function persistSslSecrets(inputs, actionCore, actionExec, repository, env
128665
129383
  );
128666
129384
  }
128667
129385
  }
128668
- async function upsertEnvironments(inputs, dependencies, resourcesState) {
129386
+ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMarker) {
128669
129387
  const envUids = {
128670
129388
  ...getEnvironmentUidsFromResources(resourcesState),
128671
129389
  ...inputs.environmentUids
@@ -128675,7 +129393,6 @@ async function upsertEnvironments(inputs, dependencies, resourcesState) {
128675
129393
  }
128676
129394
  for (const envName of inputs.environments) {
128677
129395
  const runtimeUrl = String(inputs.envRuntimeUrls[envName] || "").trim();
128678
- const values = buildEnvironmentValues(envName, runtimeUrl);
128679
129396
  const displayName = `${inputs.projectName} - ${envName}`;
128680
129397
  let existingUid = String(envUids[envName] || "").trim();
128681
129398
  if (!existingUid) {
@@ -128691,10 +129408,27 @@ async function upsertEnvironments(inputs, dependencies, resourcesState) {
128691
129408
  }
128692
129409
  }
128693
129410
  if (existingUid) {
128694
- await dependencies.postman.updateEnvironment(existingUid, displayName, values);
129411
+ let marker = assetMarker;
129412
+ if (marker) {
129413
+ try {
129414
+ const existing = await dependencies.postman.getEnvironment(existingUid);
129415
+ const values3 = existing.data?.values ?? existing.values ?? [];
129416
+ const prior = values3.find((value) => value.key === "x-pm-onboarding")?.value;
129417
+ const priorMarker = parseAssetMarker(prior ? `x-pm-onboarding: ${prior}` : void 0);
129418
+ if (priorMarker?.repo === marker.repo && priorMarker.rawBranch === marker.rawBranch) {
129419
+ marker = { ...marker, createdAt: priorMarker.createdAt };
129420
+ }
129421
+ } catch {
129422
+ }
129423
+ }
129424
+ const values2 = buildEnvironmentValues(envName, runtimeUrl);
129425
+ if (marker) values2.push({ key: "x-pm-onboarding", value: JSON.stringify(marker), type: "default" });
129426
+ await dependencies.postman.updateEnvironment(existingUid, displayName, values2);
128695
129427
  envUids[envName] = existingUid;
128696
129428
  continue;
128697
129429
  }
129430
+ const values = buildEnvironmentValues(envName, runtimeUrl);
129431
+ if (assetMarker) values.push({ key: "x-pm-onboarding", value: JSON.stringify(assetMarker), type: "default" });
128698
129432
  envUids[envName] = await dependencies.postman.createEnvironment(
128699
129433
  inputs.workspaceId,
128700
129434
  displayName,
@@ -128704,7 +129438,7 @@ async function upsertEnvironments(inputs, dependencies, resourcesState) {
128704
129438
  return envUids;
128705
129439
  }
128706
129440
  function ensureDir(path9) {
128707
- (0, import_node_fs3.mkdirSync)(path9, { recursive: true });
129441
+ (0, import_node_fs4.mkdirSync)(path9, { recursive: true });
128708
129442
  }
128709
129443
  function getCollectionDirectoryName(kind, projectName) {
128710
129444
  if (kind === "Baseline") {
@@ -128741,7 +129475,7 @@ function stripVolatileFields(obj) {
128741
129475
  }
128742
129476
  function writeJsonFile(path9, content, normalize3 = false) {
128743
129477
  const data = normalize3 ? stripVolatileFields(content) : content;
128744
- (0, import_node_fs3.writeFileSync)(path9, JSON.stringify(data, null, 2));
129478
+ (0, import_node_fs4.writeFileSync)(path9, JSON.stringify(data, null, 2));
128745
129479
  }
128746
129480
  function buildMappedSpecCloudKey(mappedSource, specSyncMode, releaseLabel) {
128747
129481
  if (specSyncMode !== "version") {
@@ -128765,31 +129499,30 @@ function resolveDurableWorkspaceId(options) {
128765
129499
  }
128766
129500
  return prior === candidate ? prior : void 0;
128767
129501
  }
128768
- function buildResourcesManifest(workspaceId, collectionMap, envMap, artifactDir, localSpecRefs, mappedSpecRef, specId, existingSpecs) {
128769
- const manifest = {};
129502
+ function buildResourcesManifest(workspaceId, collectionMap, envMap, artifactDir, localSpecRefs, mappedSpecRef, specId, existingSpecs, priorState) {
129503
+ const manifest = { ...priorState ?? {} };
129504
+ delete manifest.version;
129505
+ delete manifest.workspace;
129506
+ delete manifest.localResources;
129507
+ delete manifest.cloudResources;
129508
+ delete manifest.canonical;
129509
+ manifest.version = RESOURCES_STATE_VERSION;
128770
129510
  if (workspaceId) {
128771
129511
  manifest.workspace = { id: workspaceId };
128772
129512
  }
128773
- const localResources = {};
128774
129513
  const cloudResources = {};
128775
129514
  const collectionKeys = Object.keys(collectionMap);
128776
129515
  if (collectionKeys.length > 0) {
128777
- localResources.collections = collectionKeys;
128778
129516
  cloudResources.collections = collectionMap;
128779
129517
  }
128780
129518
  const envEntries = Object.entries(envMap);
128781
129519
  if (envEntries.length > 0) {
128782
- localResources.environments = envEntries.map(
128783
- ([envName]) => `../${artifactDir}/environments/${envName}.postman_environment.json`
128784
- );
128785
129520
  cloudResources.environments = {};
128786
129521
  for (const [envName, envUid] of envEntries) {
128787
129522
  cloudResources.environments[`../${artifactDir}/environments/${envName}.postman_environment.json`] = envUid;
128788
129523
  }
128789
129524
  }
128790
- if (localSpecRefs.length > 0) {
128791
- localResources.specs = localSpecRefs;
128792
- }
129525
+ void localSpecRefs;
128793
129526
  const specs = { ...existingSpecs || {} };
128794
129527
  if (mappedSpecRef && specId) {
128795
129528
  specs[mappedSpecRef] = specId;
@@ -128797,11 +129530,8 @@ function buildResourcesManifest(workspaceId, collectionMap, envMap, artifactDir,
128797
129530
  if (Object.keys(specs).length > 0) {
128798
129531
  cloudResources.specs = specs;
128799
129532
  }
128800
- if (Object.keys(localResources).length > 0) {
128801
- manifest.localResources = localResources;
128802
- }
128803
129533
  if (Object.keys(cloudResources).length > 0) {
128804
- manifest.cloudResources = cloudResources;
129534
+ manifest.canonical = cloudResources;
128805
129535
  }
128806
129536
  return dump(manifest, {
128807
129537
  lineWidth: -1,
@@ -128842,21 +129572,21 @@ function assertPathWithinCwd(targetPath, fieldName) {
128842
129572
  if (!rawPath || hasControlCharacter2(originalPath) || path8.isAbsolute(rawPath) || path8.win32.isAbsolute(rawPath) || segments.includes("..") || rawPath.startsWith(":") || hasControlCharacter2(rawPath)) {
128843
129573
  throw new Error(`${fieldName} must stay within the repository root; received ${targetPath}`);
128844
129574
  }
128845
- const base = (0, import_node_fs3.realpathSync)(process.cwd());
129575
+ const base = (0, import_node_fs4.realpathSync)(process.cwd());
128846
129576
  const resolved = path8.resolve(base, rawPath);
128847
129577
  const relative4 = path8.relative(base, resolved);
128848
129578
  if (relative4.startsWith("..") || path8.isAbsolute(relative4)) {
128849
129579
  throw new Error(`${fieldName} must stay within the repository root; received ${targetPath}`);
128850
129580
  }
128851
129581
  let existingPath = resolved;
128852
- while (!(0, import_node_fs3.existsSync)(existingPath)) {
129582
+ while (!(0, import_node_fs4.existsSync)(existingPath)) {
128853
129583
  const parent = path8.dirname(existingPath);
128854
129584
  if (parent === existingPath) {
128855
129585
  break;
128856
129586
  }
128857
129587
  existingPath = parent;
128858
129588
  }
128859
- const realExistingPath = (0, import_node_fs3.realpathSync)(existingPath);
129589
+ const realExistingPath = (0, import_node_fs4.realpathSync)(existingPath);
128860
129590
  const realRelative = path8.relative(base, realExistingPath);
128861
129591
  if (realRelative.startsWith("..") || path8.isAbsolute(realRelative)) {
128862
129592
  throw new Error(`${fieldName} must stay within the repository root; received ${targetPath}`);
@@ -128884,8 +129614,8 @@ async function exportArtifacts(inputs, dependencies, envUids, assetProjectName,
128884
129614
  ensureDir(specsDir);
128885
129615
  ensureDir(".postman");
128886
129616
  const globalsFilePath = `${globalsDir}/workspace.globals.yaml`;
128887
- if (!(0, import_node_fs3.existsSync)(globalsFilePath)) {
128888
- (0, import_node_fs3.writeFileSync)(globalsFilePath, "name: Globals\nvalues: []\n");
129617
+ if (!(0, import_node_fs4.existsSync)(globalsFilePath)) {
129618
+ (0, import_node_fs4.writeFileSync)(globalsFilePath, "name: Globals\nvalues: []\n");
128889
129619
  }
128890
129620
  if (inputs.generateCiWorkflow) {
128891
129621
  const ciDir = inputs.ciWorkflowPath.split("/").slice(0, -1).join("/");
@@ -128932,7 +129662,7 @@ async function exportArtifacts(inputs, dependencies, envUids, assetProjectName,
128932
129662
  workspaceLinkEnabled: inputs.workspaceLinkEnabled,
128933
129663
  workspaceLinkStatus: options.workspaceLinkStatus
128934
129664
  });
128935
- (0, import_node_fs3.writeFileSync)(".postman/resources.yaml", buildResourcesManifest(
129665
+ (0, import_node_fs4.writeFileSync)(".postman/resources.yaml", buildResourcesManifest(
128936
129666
  durableWorkspaceId,
128937
129667
  manifestCollections,
128938
129668
  envUids,
@@ -128940,10 +129670,11 @@ async function exportArtifacts(inputs, dependencies, envUids, assetProjectName,
128940
129670
  discoveredSpecs.map((spec) => spec.configRelativePath),
128941
129671
  mappedSpecCloudKey,
128942
129672
  inputs.specId || void 0,
128943
- options.existingSpecs
129673
+ options.existingSpecs,
129674
+ options.priorState
128944
129675
  ));
128945
129676
  if (mappedSpec && Object.keys(manifestCollections).length > 0) {
128946
- (0, import_node_fs3.writeFileSync)(
129677
+ (0, import_node_fs4.writeFileSync)(
128947
129678
  ".postman/workflows.yaml",
128948
129679
  buildSpecCollectionWorkflowManifest(
128949
129680
  mappedSpec.configRelativePath,
@@ -128988,19 +129719,29 @@ async function commitAndPushGeneratedFiles(inputs, dependencies) {
128988
129719
  const dir = parts.slice(0, -1).join("/");
128989
129720
  ensureDir(dir);
128990
129721
  }
128991
- (0, import_node_fs3.writeFileSync)(inputs.ciWorkflowPath, ciWorkflow);
129722
+ (0, import_node_fs4.writeFileSync)(inputs.ciWorkflowPath, ciWorkflow);
129723
+ if (inputs.provider === "github" || inputs.provider === "unknown") {
129724
+ const gcPath = ".github/workflows/postman-preview-gc.yml";
129725
+ if (inputs.ciWorkflowPath.endsWith(".github/workflows/ci.yml") || inputs.ciWorkflowPath === ".github/workflows/ci.yml") {
129726
+ ensureDir(".github/workflows");
129727
+ (0, import_node_fs4.writeFileSync)(gcPath, renderGcWorkflowTemplate());
129728
+ }
129729
+ }
128992
129730
  }
128993
129731
  if (!dependencies.repoMutation || inputs.repoWriteMode === "none") {
128994
129732
  return { commitSha: "", resolvedCurrentRef: "", pushed: false };
128995
129733
  }
128996
129734
  const provisionPath = ".github/workflows/provision.yml";
128997
- const provisionExists = inputs.provider === "github" && (0, import_node_fs3.existsSync)(provisionPath);
129735
+ const provisionExists = inputs.provider === "github" && (0, import_node_fs4.existsSync)(provisionPath);
129736
+ const gcWorkflowPath = ".github/workflows/postman-preview-gc.yml";
129737
+ const gcExists = inputs.generateCiWorkflow && (0, import_node_fs4.existsSync)(gcWorkflowPath);
128998
129738
  const stagePaths = [
128999
129739
  inputs.artifactDir,
129000
129740
  ".postman",
129001
129741
  inputs.generateCiWorkflow ? inputs.ciWorkflowPath : null,
129742
+ gcExists ? gcWorkflowPath : null,
129002
129743
  provisionExists ? provisionPath : null
129003
- ].filter((entry) => typeof entry === "string" && ((0, import_node_fs3.existsSync)(entry) || entry === provisionPath));
129744
+ ].filter((entry) => typeof entry === "string" && ((0, import_node_fs4.existsSync)(entry) || entry === provisionPath));
129004
129745
  if (stagePaths.length === 0) {
129005
129746
  dependencies.core.info("No generated repository paths were found; skipping repo mutation.");
129006
129747
  return {
@@ -129048,6 +129789,31 @@ async function runRepoSync(inputs, dependencies) {
129048
129789
  }
129049
129790
  }
129050
129791
  async function runRepoSyncInner(inputs, dependencies) {
129792
+ const branchDecision = decideBranchTier(inputs);
129793
+ assertBranchAssetIds(inputs, branchDecision);
129794
+ const isCanonicalWriter = branchDecision.tier === "legacy" || branchDecision.tier === "canonical";
129795
+ if (!isCanonicalWriter) {
129796
+ if (branchDecision.tier === "preview" && branchDecision.identity.headBranch) {
129797
+ inputs = {
129798
+ ...inputs,
129799
+ projectName: previewAssetName(inputs.projectName, branchDecision.identity.headBranch),
129800
+ workspaceLinkEnabled: false,
129801
+ environmentSyncEnabled: false,
129802
+ repoWriteMode: "none"
129803
+ };
129804
+ } else if (branchDecision.tier === "channel" && branchDecision.channel) {
129805
+ inputs = {
129806
+ ...inputs,
129807
+ projectName: channelAssetName(inputs.projectName, branchDecision.channel.code),
129808
+ workspaceLinkEnabled: false,
129809
+ environmentSyncEnabled: false,
129810
+ repoWriteMode: "none"
129811
+ };
129812
+ }
129813
+ dependencies.core.info(
129814
+ `branch-aware sync: ${branchDecision.tier} run \u2014 branch-scoped asset set "${inputs.projectName}", no workspace repo-link mutation, no state write-back`
129815
+ );
129816
+ }
129051
129817
  const outputs = createOutputs(inputs);
129052
129818
  const versionRequested = inputs.collectionSyncMode === "version" || inputs.specSyncMode === "version";
129053
129819
  const releaseLabel = deriveReleaseLabel(inputs);
@@ -129055,7 +129821,8 @@ async function runRepoSyncInner(inputs, dependencies) {
129055
129821
  throw new Error("release-label is required when collection-sync-mode or spec-sync-mode is version");
129056
129822
  }
129057
129823
  const assetProjectName = createAssetProjectName(inputs, releaseLabel);
129058
- const resourcesState = readResourcesState();
129824
+ const trackedState = readResourcesState();
129825
+ const resourcesState = isCanonicalWriter ? trackedState : trackedState?.workspace ? { workspace: trackedState.workspace } : null;
129059
129826
  if (resourcesState) {
129060
129827
  if (!inputs.workspaceId && resourcesState.workspace?.id) {
129061
129828
  inputs.workspaceId = resourcesState.workspace.id;
@@ -129081,7 +129848,8 @@ async function runRepoSyncInner(inputs, dependencies) {
129081
129848
  }
129082
129849
  }
129083
129850
  }
129084
- const envUids = await upsertEnvironments(inputs, dependencies, resourcesState);
129851
+ const branchAssetMarker = buildBranchAssetMarker(branchDecision, inputs);
129852
+ const envUids = await upsertEnvironments(inputs, dependencies, resourcesState, branchAssetMarker);
129085
129853
  outputs["environment-uids-json"] = JSON.stringify(envUids);
129086
129854
  if (inputs.environmentSyncEnabled && inputs.workspaceId && dependencies.internalIntegration) {
129087
129855
  const associations = Object.entries(envUids).map(([envName, envUid]) => ({
@@ -129137,11 +129905,23 @@ async function runRepoSyncInner(inputs, dependencies) {
129137
129905
  }
129138
129906
  }
129139
129907
  if (!resolvedMockUrl) {
129140
- const mock = await dependencies.postman.createMock(
129141
- inputs.workspaceId,
129142
- mockName,
129143
- inputs.baselineCollectionId,
129144
- mockEnvUid
129908
+ const mock = await retry(
129909
+ () => dependencies.postman.createMock(
129910
+ inputs.workspaceId,
129911
+ mockName,
129912
+ inputs.baselineCollectionId,
129913
+ mockEnvUid
129914
+ ),
129915
+ {
129916
+ maxAttempts: 3,
129917
+ delayMs: 2e3,
129918
+ backoffMultiplier: 2,
129919
+ onRetry: ({ attempt, error: error2 }) => {
129920
+ dependencies.core.warning(
129921
+ `Mock create attempt ${attempt} failed (${error2 instanceof Error ? error2.message : String(error2)}); retrying`
129922
+ );
129923
+ }
129924
+ }
129145
129925
  );
129146
129926
  resolvedMockUrl = mock.url;
129147
129927
  dependencies.core.info(`Created new mock: ${resolvedMockUrl}`);
@@ -129210,16 +129990,19 @@ async function runRepoSyncInner(inputs, dependencies) {
129210
129990
  );
129211
129991
  } catch (error2) {
129212
129992
  outputs["workspace-link-status"] = "failed";
129213
- dependencies.core.warning(
129214
- `Workspace link failed: ${error2 instanceof Error ? error2.message : String(error2)}`
129215
- );
129993
+ const message = `Workspace link failed: ${error2 instanceof Error ? error2.message : String(error2)}`;
129994
+ if (branchDecision.tier === "canonical") {
129995
+ throw new Error(message, { cause: error2 });
129996
+ }
129997
+ dependencies.core.warning(message);
129216
129998
  }
129217
129999
  }
129218
130000
  await exportArtifacts(inputs, dependencies, envUids, assetProjectName, {
129219
130001
  workspaceLinkStatus: outputs["workspace-link-status"],
129220
130002
  priorWorkspaceId: resourcesState?.workspace?.id,
129221
130003
  existingSpecs: resourcesState?.cloudResources?.specs,
129222
- releaseLabel
130004
+ releaseLabel,
130005
+ priorState: resourcesState
129223
130006
  });
129224
130007
  const commit = await commitAndPushGeneratedFiles(inputs, dependencies);
129225
130008
  outputs["commit-sha"] = commit.commitSha;
@@ -129227,6 +130010,38 @@ async function runRepoSyncInner(inputs, dependencies) {
129227
130010
  outputs["resolved-current-ref"] = commit.resolvedCurrentRef;
129228
130011
  }
129229
130012
  outputs["repo-sync-summary-json"] = createRepoSummary(outputs, envUids, commit.pushed);
130013
+ if (branchDecision.tier === "canonical" && inputs.specId && inputs.specContentChanged !== false && dependencies.postman.tagSpecVersion) {
130014
+ const shortSha = (branchDecision.identity.headSha ?? "").slice(0, 7);
130015
+ const tagName = inputs.releaseLabel ? `${inputs.releaseLabel}${shortSha ? ` (${shortSha})` : ""}` : shortSha || `sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
130016
+ try {
130017
+ const tag = await dependencies.postman.tagSpecVersion(inputs.specId, tagName);
130018
+ outputs["spec-version-tag"] = tag.name || tagName;
130019
+ if (tag.id) {
130020
+ outputs["spec-version-url"] = `https://web.postman.co/workspace/${encodeURIComponent(inputs.workspaceId)}/specification/${encodeURIComponent(inputs.specId)}?tagId=${encodeURIComponent(tag.id)}&versionLabel=${encodeURIComponent(tag.name || tagName)}`;
130021
+ }
130022
+ dependencies.core.info(`Tagged spec version at finalize: ${tag.name || tagName}`);
130023
+ } catch (error2) {
130024
+ const status = error2.status;
130025
+ if (status === 409) {
130026
+ const tags = await dependencies.postman.listSpecVersionTags?.(inputs.specId).catch(() => []) ?? [];
130027
+ const latest = tags[0];
130028
+ if (latest && (latest.name === tagName || /\([0-9a-f]{7}\)$/.test(latest.name) || /^[0-9a-f]{7}$/.test(latest.name))) {
130029
+ outputs["spec-version-tag"] = latest.name;
130030
+ outputs["spec-version-url"] = `https://web.postman.co/workspace/${encodeURIComponent(inputs.workspaceId)}/specification/${encodeURIComponent(inputs.specId)}?tagId=${encodeURIComponent(latest.id)}&versionLabel=${encodeURIComponent(latest.name)}`;
130031
+ dependencies.core.info(`Latest changelog group already tagged as "${latest.name}"; adopting.`);
130032
+ } else {
130033
+ dependencies.core.warning("Latest changelog group already carries a hand-applied tag; leaving it in place.");
130034
+ }
130035
+ } else {
130036
+ dependencies.core.warning(`Spec version tagging failed (non-fatal): ${error2 instanceof Error ? error2.message : String(error2)}`);
130037
+ }
130038
+ }
130039
+ }
130040
+ if (inputs.branchStrategy !== "legacy" || process.env[BRANCH_DECISION_ENV]) {
130041
+ const decision = decideBranchTier(inputs);
130042
+ outputs["sync-status"] = "synced";
130043
+ outputs["branch-decision"] = serializeBranchDecision(decision);
130044
+ }
129230
130045
  for (const [name, value] of Object.entries(outputs)) {
129231
130046
  dependencies.core.setOutput(name, value);
129232
130047
  }
@@ -129330,6 +130145,8 @@ async function resolvePostmanApiKeyAndTeamId(inputs, actionCore, actionExec, mas
129330
130145
  const gateway = new AccessTokenGatewayClient({
129331
130146
  tokenProvider,
129332
130147
  bifrostBaseUrl: inputs.postmanBifrostBase,
130148
+ // No fallback on the org-mode probe: the expected non-org 400 must
130149
+ // surface verbatim, not be re-fired against the /_api alias.
129333
130150
  secretMasker: masker
129334
130151
  });
129335
130152
  const squads = await gateway.getSquads(teamId);
@@ -129385,6 +130202,7 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
129385
130202
  const gateway = new AccessTokenGatewayClient({
129386
130203
  tokenProvider,
129387
130204
  bifrostBaseUrl: inputs.postmanBifrostBase,
130205
+ fallbackBaseUrl: inputs.postmanFallbackBase,
129388
130206
  teamId: resolved.teamId,
129389
130207
  orgMode: inputs.orgMode,
129390
130208
  secretMasker: masker
@@ -129416,7 +130234,19 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
129416
130234
  listMonitors: gatewayAssets.listMonitors.bind(gatewayAssets),
129417
130235
  monitorExists: gatewayAssets.monitorExists.bind(gatewayAssets),
129418
130236
  findMonitorByCollection: gatewayAssets.findMonitorByCollection.bind(gatewayAssets),
129419
- runMonitor: gatewayAssets.runMonitor.bind(gatewayAssets)
130237
+ runMonitor: gatewayAssets.runMonitor.bind(gatewayAssets),
130238
+ listEnvironments: gatewayAssets.listEnvironments.bind(gatewayAssets),
130239
+ // GC path (preview/channel retention machine — lib/repo/preview-gc.ts).
130240
+ deleteEnvironment: gatewayAssets.deleteEnvironment.bind(gatewayAssets),
130241
+ deleteMock: gatewayAssets.deleteMock.bind(gatewayAssets),
130242
+ deleteMonitor: gatewayAssets.deleteMonitor.bind(gatewayAssets),
130243
+ deleteCollection: gatewayAssets.deleteCollection.bind(gatewayAssets),
130244
+ listSpecifications: gatewayAssets.listSpecifications.bind(gatewayAssets),
130245
+ getSpecContent: gatewayAssets.getSpecContent.bind(gatewayAssets),
130246
+ listSpecCollections: gatewayAssets.listSpecCollections.bind(gatewayAssets),
130247
+ deleteSpec: gatewayAssets.deleteSpec.bind(gatewayAssets),
130248
+ tagSpecVersion: gatewayAssets.tagSpecVersion.bind(gatewayAssets),
130249
+ listSpecVersionTags: gatewayAssets.listSpecVersionTags.bind(gatewayAssets)
129420
130250
  };
129421
130251
  const repoMutation = repository && (inputs.repoWriteMode === "commit-only" || inputs.repoWriteMode === "commit-and-push") ? new RepoMutationService({
129422
130252
  provider: inputs.provider,
@@ -129453,8 +130283,43 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
129453
130283
  repoMutation
129454
130284
  };
129455
130285
  }
130286
+ function decideBranchTier(inputs, env = process.env) {
130287
+ return resolveEffectiveBranchDecision(
130288
+ {
130289
+ strategy: inputs.branchStrategy,
130290
+ identity: resolveBranchIdentity(env, { defaultBranch: inputs.canonicalBranch }),
130291
+ canonicalBranch: inputs.canonicalBranch,
130292
+ channels: parseChannelRules(inputs.channels)
130293
+ },
130294
+ env
130295
+ );
130296
+ }
130297
+ function runGatedSkip(inputs, decision, actionCore) {
130298
+ actionCore.info(
130299
+ `branch-aware sync: gated run (${decision.reason}) \u2014 repo-sync skipped, zero workspace writes`
130300
+ );
130301
+ const outputs = createOutputs(inputs);
130302
+ outputs["sync-status"] = "skipped-branch-gate";
130303
+ outputs["branch-decision"] = serializeBranchDecision(decision);
130304
+ outputs["repo-sync-summary-json"] = JSON.stringify({
130305
+ status: "skipped-branch-gate",
130306
+ reason: decision.reason
130307
+ });
130308
+ for (const [name, value] of Object.entries(outputs)) {
130309
+ actionCore.setOutput(name, value);
130310
+ }
130311
+ return outputs;
130312
+ }
129456
130313
  async function runAction(actionCore = core_exports, actionExec = exec_exports) {
129457
130314
  const inputs = readActionInputs(actionCore);
130315
+ const branchDecision = decideBranchTier(inputs);
130316
+ if (branchDecision.tier === "gated") {
130317
+ return runGatedSkip(inputs, branchDecision, actionCore);
130318
+ }
130319
+ if (branchDecision.tier !== "legacy") {
130320
+ actionCore.info(`branch-aware sync: tier=${branchDecision.tier} (${branchDecision.reason})`);
130321
+ process.env[BRANCH_DECISION_ENV] = serializeBranchDecision(branchDecision);
130322
+ }
129458
130323
  await mintAccessTokenIfNeeded(inputs, {
129459
130324
  info: (message) => actionCore.info(message),
129460
130325
  warning: (message) => actionCore.warning(message)