@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/cli.cjs CHANGED
@@ -92334,11 +92334,11 @@ var require_valid = __commonJS({
92334
92334
  var require_clean = __commonJS({
92335
92335
  "node_modules/postman-collection/node_modules/semver/functions/clean.js"(exports2, module2) {
92336
92336
  var parse2 = require_parse5();
92337
- var clean = (version2, options) => {
92337
+ var clean2 = (version2, options) => {
92338
92338
  const s = parse2(version2.trim().replace(/^[=v]+/, ""), options);
92339
92339
  return s ? s.version : null;
92340
92340
  };
92341
- module2.exports = clean;
92341
+ module2.exports = clean2;
92342
92342
  }
92343
92343
  });
92344
92344
 
@@ -93649,7 +93649,7 @@ var require_semver2 = __commonJS({
93649
93649
  var identifiers = require_identifiers();
93650
93650
  var parse2 = require_parse5();
93651
93651
  var valid = require_valid();
93652
- var clean = require_clean();
93652
+ var clean2 = require_clean();
93653
93653
  var inc = require_inc();
93654
93654
  var diff = require_diff();
93655
93655
  var major = require_major();
@@ -93687,7 +93687,7 @@ var require_semver2 = __commonJS({
93687
93687
  module2.exports = {
93688
93688
  parse: parse2,
93689
93689
  valid,
93690
- clean,
93690
+ clean: clean2,
93691
93691
  inc,
93692
93692
  diff,
93693
93693
  major,
@@ -119798,7 +119798,7 @@ __export(cli_exports, {
119798
119798
  });
119799
119799
  module.exports = __toCommonJS(cli_exports);
119800
119800
  var import_node_child_process = require("node:child_process");
119801
- var import_node_fs4 = require("node:fs");
119801
+ var import_node_fs5 = require("node:fs");
119802
119802
  var import_promises = require("node:fs/promises");
119803
119803
  var import_node_path3 = __toESM(require("node:path"), 1);
119804
119804
  var import_node_util = require("node:util");
@@ -120166,7 +120166,7 @@ var ExitCode;
120166
120166
  })(ExitCode || (ExitCode = {}));
120167
120167
 
120168
120168
  // src/index.ts
120169
- var import_node_fs3 = require("node:fs");
120169
+ var import_node_fs4 = require("node:fs");
120170
120170
  var path3 = __toESM(require("node:path"), 1);
120171
120171
 
120172
120172
  // node_modules/js-yaml/dist/js-yaml.mjs
@@ -123217,12 +123217,14 @@ var POSTMAN_ENDPOINT_PROFILES = {
123217
123217
  prod: {
123218
123218
  apiBaseUrl: "https://api.getpostman.com",
123219
123219
  bifrostBaseUrl: "https://bifrost-premium-https-v4.gw.postman.com",
123220
+ fallbackBaseUrl: "https://go.postman.co/_api",
123220
123221
  cliInstallUrl: "https://dl-cli.pstmn.io/install/unix.sh",
123221
123222
  iapubBaseUrl: "https://iapub.postman.co"
123222
123223
  },
123223
123224
  beta: {
123224
123225
  apiBaseUrl: "https://api.getpostman-beta.com",
123225
123226
  bifrostBaseUrl: "https://bifrost-https-v4.gw.postman-beta.com",
123227
+ fallbackBaseUrl: "https://go.postman-beta.co/_api",
123226
123228
  cliInstallUrl: "https://dl-cli.pstmn-beta.io/install/unix.sh",
123227
123229
  iapubBaseUrl: "https://iapub.postman.co"
123228
123230
  }
@@ -123289,6 +123291,9 @@ function buildCiWorkflowLines(installUrl, postmanRegion) {
123289
123291
  " branches: [main]",
123290
123292
  " schedule:",
123291
123293
  ' - cron: "0 */6 * * *"',
123294
+ "concurrency:",
123295
+ " group: postman-onboard-${{ github.head_ref || github.ref_name }}",
123296
+ " cancel-in-progress: false",
123292
123297
  "jobs:",
123293
123298
  " test:",
123294
123299
  " runs-on: ubuntu-latest",
@@ -123305,7 +123310,7 @@ function buildCiWorkflowLines(installUrl, postmanRegion) {
123305
123310
  " ruby <<'RUBY'",
123306
123311
  " require 'yaml'",
123307
123312
  " config = YAML.load_file('.postman/resources.yaml') || {}",
123308
- " cloud = config.fetch('cloudResources', {})",
123313
+ " cloud = config.fetch('canonical', config.fetch('cloudResources', {}))",
123309
123314
  " collections = cloud.fetch('collections', {})",
123310
123315
  " environments = cloud.fetch('environments', {})",
123311
123316
  " smoke = collections.find { |path, _| path.include?('[Smoke]') }&.last",
@@ -123503,6 +123508,58 @@ function buildAdoCiWorkflowLines(installUrl, postmanRegion) {
123503
123508
  ""
123504
123509
  ];
123505
123510
  }
123511
+ function renderGcWorkflowTemplate() {
123512
+ return [
123513
+ "name: Postman Preview GC",
123514
+ "on:",
123515
+ " delete:",
123516
+ ' branches: ["**"]',
123517
+ " pull_request:",
123518
+ " types: [closed]",
123519
+ " schedule:",
123520
+ ' - cron: "0 2 * * *"',
123521
+ " workflow_dispatch:",
123522
+ " inputs:",
123523
+ " branch:",
123524
+ " description: Branch name to GC (optional, otherwise sweep by TTL/branch-existence)",
123525
+ " required: false",
123526
+ "concurrency:",
123527
+ " group: postman-preview-gc-${{ github.ref_name }}",
123528
+ " cancel-in-progress: false",
123529
+ "jobs:",
123530
+ " gc:",
123531
+ " runs-on: ubuntu-latest",
123532
+ " if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository",
123533
+ " steps:",
123534
+ " - uses: actions/checkout@v5",
123535
+ " with:",
123536
+ " fetch-depth: 0",
123537
+ " - name: Run Postman preview GC (provider-neutral cli.cjs gc)",
123538
+ " env:",
123539
+ " POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }}",
123540
+ " POSTMAN_ACCESS_TOKEN: ${{ secrets.POSTMAN_ACCESS_TOKEN }}",
123541
+ " POSTMAN_WORKSPACE_ID: ${{ vars.POSTMAN_WORKSPACE_ID }}",
123542
+ " REPO_URL: https://github.com/${{ github.repository }}",
123543
+ " run: |",
123544
+ " set -euo pipefail",
123545
+ " # The repo-sync action bundles a provider-neutral gc command (cli.cjs gc)",
123546
+ " # that uses the Postman access token for inventory/deletion and the",
123547
+ " # provider ambient credential (GITHUB_TOKEN via git ls-remote) for branch existence.",
123548
+ " # Daily scheduled run is the retention executor (TTL contract of last resort).",
123549
+ ' if [ -n "${{ inputs.branch }}" ]; then',
123550
+ ' npx @postman-cse/onboarding-repo-sync gc --branch "${{ inputs.branch }}" --workspace-id "$POSTMAN_WORKSPACE_ID" --repo-url "$REPO_URL"',
123551
+ " else",
123552
+ ' npx @postman-cse/onboarding-repo-sync gc --workspace-id "$POSTMAN_WORKSPACE_ID" --repo-url "$REPO_URL"',
123553
+ " fi",
123554
+ " - name: Orphan audit summary (job summary)",
123555
+ " if: always()",
123556
+ " run: |",
123557
+ ' echo "### Preview GC orphan audit" >> "$GITHUB_STEP_SUMMARY"',
123558
+ ' echo "Marker-guarded deletion only \u2014 strangers (no marker) are never deleted. See gc-summary-json for structured counts." >> "$GITHUB_STEP_SUMMARY"',
123559
+ ""
123560
+ ].join("\n");
123561
+ }
123562
+ var GC_WORKFLOW_TEMPLATE = renderGcWorkflowTemplate();
123506
123563
  function getCiWorkflowTemplate(provider, options = {}) {
123507
123564
  if (provider === "azure-devops") {
123508
123565
  const rawUrl = String(options.postmanCliInstallUrl || "").trim() || DEFAULT_POSTMAN_CLI_INSTALL_URL;
@@ -125073,6 +125130,53 @@ function createInternalIntegrationAdapter(options) {
125073
125130
  return new BifrostInternalIntegrationAdapter(options);
125074
125131
  }
125075
125132
 
125133
+ // src/lib/retry.ts
125134
+ function sleep(delayMs) {
125135
+ return new Promise((resolve2) => {
125136
+ setTimeout(resolve2, delayMs);
125137
+ });
125138
+ }
125139
+ function normalizeRetryOptions(options) {
125140
+ return {
125141
+ maxAttempts: Math.max(1, options.maxAttempts ?? 3),
125142
+ delayMs: Math.max(0, options.delayMs ?? 2e3),
125143
+ backoffMultiplier: Math.max(1, options.backoffMultiplier ?? 1),
125144
+ maxDelayMs: options.maxDelayMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, options.maxDelayMs),
125145
+ onRetry: options.onRetry ?? (async () => void 0),
125146
+ shouldRetry: options.shouldRetry ?? (() => true),
125147
+ sleep: options.sleep ?? sleep
125148
+ };
125149
+ }
125150
+ async function retry(operation, options = {}) {
125151
+ const normalized = normalizeRetryOptions(options);
125152
+ let nextDelayMs = normalized.delayMs;
125153
+ for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
125154
+ try {
125155
+ return await operation();
125156
+ } catch (error) {
125157
+ const shouldRetry = attempt < normalized.maxAttempts && normalized.shouldRetry(error, {
125158
+ attempt,
125159
+ maxAttempts: normalized.maxAttempts
125160
+ });
125161
+ if (!shouldRetry) {
125162
+ throw error;
125163
+ }
125164
+ await normalized.onRetry({
125165
+ attempt,
125166
+ maxAttempts: normalized.maxAttempts,
125167
+ delayMs: nextDelayMs,
125168
+ error
125169
+ });
125170
+ await normalized.sleep(nextDelayMs);
125171
+ nextDelayMs = Math.min(
125172
+ normalized.maxDelayMs,
125173
+ Math.round(nextDelayMs * normalized.backoffMultiplier)
125174
+ );
125175
+ }
125176
+ }
125177
+ throw new Error("Retry exhausted without returning or throwing");
125178
+ }
125179
+
125076
125180
  // src/contracts.ts
125077
125181
  var postmanRepoSyncActionContract = {
125078
125182
  name: "postman-repo-sync-action",
@@ -125242,6 +125346,25 @@ var postmanRepoSyncActionContract = {
125242
125346
  default: "warn",
125243
125347
  allowedValues: ["enforce", "warn"]
125244
125348
  },
125349
+ "branch-strategy": {
125350
+ 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.",
125351
+ required: false,
125352
+ default: "legacy",
125353
+ allowedValues: ["legacy", "preview", "publish-gate"]
125354
+ },
125355
+ "canonical-branch": {
125356
+ 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.",
125357
+ required: false
125358
+ },
125359
+ "channels": {
125360
+ 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.',
125361
+ required: false
125362
+ },
125363
+ "preview-ttl": {
125364
+ 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).",
125365
+ required: false,
125366
+ default: "30"
125367
+ },
125245
125368
  "github-token": {
125246
125369
  description: "GitHub token used for repo variable persistence and commits.",
125247
125370
  required: false
@@ -125279,6 +125402,11 @@ var postmanRepoSyncActionContract = {
125279
125402
  description: "Spec UID from bootstrap, persisted into .postman/resources.yaml cloudResources.",
125280
125403
  required: false
125281
125404
  },
125405
+ "spec-content-changed": {
125406
+ description: "Whether bootstrap changed canonical spec content; controls final native Spec Hub tag publication.",
125407
+ required: false,
125408
+ default: "true"
125409
+ },
125282
125410
  "spec-path": {
125283
125411
  description: "Optional repo-root-relative path to the local spec file for resources/workflows metadata.",
125284
125412
  required: false
@@ -125323,6 +125451,18 @@ var postmanRepoSyncActionContract = {
125323
125451
  },
125324
125452
  "commit-sha": {
125325
125453
  description: "Commit SHA produced by repo-write-mode, if any."
125454
+ },
125455
+ "sync-status": {
125456
+ description: "Branch-aware sync status: synced, skipped-branch-gate, or empty under branch-strategy legacy."
125457
+ },
125458
+ "branch-decision": {
125459
+ description: "Serialized BranchDecision JSON for downstream actions (also exported as POSTMAN_BRANCH_DECISION)."
125460
+ },
125461
+ "spec-version-tag": {
125462
+ description: "Native Spec Hub version tag created after successful canonical repo-sync finalization."
125463
+ },
125464
+ "spec-version-url": {
125465
+ description: "Read-only URL for the tagged Spec Hub snapshot."
125326
125466
  }
125327
125467
  },
125328
125468
  behavior: {
@@ -125390,53 +125530,6 @@ var PostmanAssetsClient = class {
125390
125530
  }
125391
125531
  };
125392
125532
 
125393
- // src/lib/retry.ts
125394
- function sleep(delayMs) {
125395
- return new Promise((resolve2) => {
125396
- setTimeout(resolve2, delayMs);
125397
- });
125398
- }
125399
- function normalizeRetryOptions(options) {
125400
- return {
125401
- maxAttempts: Math.max(1, options.maxAttempts ?? 3),
125402
- delayMs: Math.max(0, options.delayMs ?? 2e3),
125403
- backoffMultiplier: Math.max(1, options.backoffMultiplier ?? 1),
125404
- maxDelayMs: options.maxDelayMs === void 0 ? Number.POSITIVE_INFINITY : Math.max(0, options.maxDelayMs),
125405
- onRetry: options.onRetry ?? (async () => void 0),
125406
- shouldRetry: options.shouldRetry ?? (() => true),
125407
- sleep: options.sleep ?? sleep
125408
- };
125409
- }
125410
- async function retry(operation, options = {}) {
125411
- const normalized = normalizeRetryOptions(options);
125412
- let nextDelayMs = normalized.delayMs;
125413
- for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
125414
- try {
125415
- return await operation();
125416
- } catch (error) {
125417
- const shouldRetry = attempt < normalized.maxAttempts && normalized.shouldRetry(error, {
125418
- attempt,
125419
- maxAttempts: normalized.maxAttempts
125420
- });
125421
- if (!shouldRetry) {
125422
- throw error;
125423
- }
125424
- await normalized.onRetry({
125425
- attempt,
125426
- maxAttempts: normalized.maxAttempts,
125427
- delayMs: nextDelayMs,
125428
- error
125429
- });
125430
- await normalized.sleep(nextDelayMs);
125431
- nextDelayMs = Math.min(
125432
- normalized.maxDelayMs,
125433
- Math.round(nextDelayMs * normalized.backoffMultiplier)
125434
- );
125435
- }
125436
- }
125437
- throw new Error("Retry exhausted without returning or throwing");
125438
- }
125439
-
125440
125533
  // src/lib/postman/postman-gateway-assets-client.ts
125441
125534
  var MAX_CREATE_FLIGHTS = 256;
125442
125535
  var createFlights = /* @__PURE__ */ new Map();
@@ -125462,6 +125555,84 @@ var PostmanGatewayAssetsClient = class {
125462
125555
  if (!envelope) return null;
125463
125556
  return this.asRecord(envelope.data) ?? envelope;
125464
125557
  }
125558
+ /** Native Spec Hub tags attach to the latest changelog group. */
125559
+ async tagSpecVersion(specId, name) {
125560
+ const trimmed = name.trim().slice(0, 255);
125561
+ const response = await this.gateway.requestJson({
125562
+ service: "specification",
125563
+ method: "post",
125564
+ path: `/specifications/${specId}/tags`,
125565
+ body: { name: trimmed }
125566
+ });
125567
+ const record = this.dataOf(response) ?? {};
125568
+ return { id: String(record.id ?? "").trim(), name: String(record.name ?? trimmed).trim() };
125569
+ }
125570
+ async listSpecVersionTags(specId) {
125571
+ const response = await this.gateway.requestJson({
125572
+ service: "specification",
125573
+ method: "get",
125574
+ path: `/specifications/${specId}/tags`,
125575
+ query: { limit: "50" }
125576
+ });
125577
+ const data = Array.isArray(response?.data) ? response.data : [];
125578
+ return data.map((entry) => this.asRecord(entry)).filter((entry) => entry !== null).map((entry) => ({
125579
+ id: String(entry.id ?? "").trim(),
125580
+ // listTags returns `message`; createTag returns `name`. Accept both.
125581
+ name: String(entry.name ?? entry.message ?? "").trim()
125582
+ })).filter((entry) => entry.id || entry.name);
125583
+ }
125584
+ async deleteCollection(collectionUid) {
125585
+ const bareId = String(collectionUid).split("-").slice(-5).join("-") || collectionUid;
125586
+ await this.gateway.requestJson({
125587
+ service: "collection",
125588
+ method: "delete",
125589
+ path: `/v3/collections/${bareId}`
125590
+ });
125591
+ }
125592
+ async listSpecifications(workspaceId) {
125593
+ const response = await this.gateway.requestJson({
125594
+ service: "specification",
125595
+ method: "get",
125596
+ path: `/specifications?containerType=workspace&containerId=${workspaceId}`
125597
+ });
125598
+ const data = Array.isArray(response?.data) ? response.data : [];
125599
+ 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);
125600
+ }
125601
+ async getSpecContent(specId) {
125602
+ const files = await this.gateway.requestJson({
125603
+ service: "specification",
125604
+ method: "get",
125605
+ path: `/specifications/${specId}/files`
125606
+ });
125607
+ const entries = Array.isArray(files?.data) ? files.data : [];
125608
+ const root = entries.map((entry) => this.asRecord(entry)).find((entry) => entry?.type === "ROOT") ?? entries.map((entry) => this.asRecord(entry)).find((entry) => entry !== null);
125609
+ const fileId = String(root?.id ?? "").trim();
125610
+ if (!fileId) return void 0;
125611
+ const file = await this.gateway.requestJson({
125612
+ service: "specification",
125613
+ method: "get",
125614
+ path: `/specifications/${specId}/files/${fileId}`,
125615
+ query: { fields: "content" }
125616
+ });
125617
+ const record = this.dataOf(file);
125618
+ return typeof record?.content === "string" ? record.content : void 0;
125619
+ }
125620
+ async listSpecCollections(specId) {
125621
+ const response = await this.gateway.requestJson({
125622
+ service: "specification",
125623
+ method: "get",
125624
+ path: `/specifications/${specId}/collections`
125625
+ });
125626
+ const data = Array.isArray(response?.data) ? response.data : [];
125627
+ 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);
125628
+ }
125629
+ async deleteSpec(specId) {
125630
+ await this.gateway.requestJson({
125631
+ service: "specification",
125632
+ method: "delete",
125633
+ path: `/specifications/${specId}`
125634
+ });
125635
+ }
125465
125636
  idOf(record) {
125466
125637
  if (!record) return "";
125467
125638
  const id = record.uid ?? record.id;
@@ -125521,6 +125692,14 @@ var PostmanGatewayAssetsClient = class {
125521
125692
  createFlights.set(key, { fingerprint, promise: pending });
125522
125693
  return pending;
125523
125694
  }
125695
+ /**
125696
+ * Reconcile an ambiguous create. Returns the adopted match, `undefined`
125697
+ * when every discovery read succeeded and found nothing (the create is
125698
+ * conclusively absent, so a fallback resend cannot duplicate), or `null`
125699
+ * when the outcome is inconclusive (non-ambiguous error, or a discovery
125700
+ * read itself failed — in that case a resend could duplicate a committed
125701
+ * twin and must not fire).
125702
+ */
125524
125703
  async discoverAfterAmbiguousCreate(discover, error) {
125525
125704
  if (!this.isAmbiguousCreateOutcome(error)) {
125526
125705
  return null;
@@ -125529,12 +125708,31 @@ var PostmanGatewayAssetsClient = class {
125529
125708
  if (attempt > 0) {
125530
125709
  await this.sleep(this.reconcileDelayMs);
125531
125710
  }
125532
- const found = await discover();
125711
+ let found;
125712
+ try {
125713
+ found = await discover();
125714
+ } catch {
125715
+ return null;
125716
+ }
125533
125717
  if (found) {
125534
125718
  return found;
125535
125719
  }
125536
125720
  }
125537
- return null;
125721
+ return void 0;
125722
+ }
125723
+ /**
125724
+ * After an ambiguous create failed AND discovery proved the asset absent,
125725
+ * re-attempt the create once with the cold `/_api` fallback enabled: the
125726
+ * reconcile above guarantees no committed twin, so the resend cannot
125727
+ * duplicate. Returns null when the resend also fails (caller rethrows the
125728
+ * original error).
125729
+ */
125730
+ async resendAbsentCreate(operation) {
125731
+ try {
125732
+ return await operation();
125733
+ } catch {
125734
+ return null;
125735
+ }
125538
125736
  }
125539
125737
  publicEnvironmentUid(data, bareId) {
125540
125738
  const owner = String(data?.owner ?? "").trim();
@@ -125582,17 +125780,18 @@ var PostmanGatewayAssetsClient = class {
125582
125780
  name: envName,
125583
125781
  values: normalizedValues
125584
125782
  };
125783
+ const send2 = (fallback) => this.gateway.requestJson(
125784
+ {
125785
+ service: "sync",
125786
+ method: "post",
125787
+ path: `/environment/import?workspace=${ws}`,
125788
+ body,
125789
+ ...fallback ? { fallback } : {}
125790
+ },
125791
+ { retryTransient: false }
125792
+ );
125585
125793
  try {
125586
- const response = await this.gateway.requestJson(
125587
- {
125588
- service: "sync",
125589
- method: "post",
125590
- path: `/environment/import?workspace=${ws}`,
125591
- body
125592
- },
125593
- { retryTransient: false }
125594
- );
125595
- const data = this.dataOf(response);
125794
+ const data = this.dataOf(await send2());
125596
125795
  const bareId = this.idOf(data);
125597
125796
  if (!bareId) {
125598
125797
  throw new Error("Environment import did not return a UID");
@@ -125607,6 +125806,15 @@ var PostmanGatewayAssetsClient = class {
125607
125806
  await this.updateEnvironment(adopted, envName, values);
125608
125807
  return adopted;
125609
125808
  }
125809
+ if (adopted === void 0) {
125810
+ const retried = await this.resendAbsentCreate(async () => {
125811
+ const data = this.dataOf(await send2("auto"));
125812
+ const bareId = this.idOf(data);
125813
+ if (!bareId) throw new Error("Environment import did not return a UID");
125814
+ return this.publicEnvironmentUid(data, bareId);
125815
+ });
125816
+ if (retried) return retried;
125817
+ }
125610
125818
  throw error;
125611
125819
  }
125612
125820
  });
@@ -125738,16 +125946,20 @@ var PostmanGatewayAssetsClient = class {
125738
125946
  private: false,
125739
125947
  ...environment ? { environment } : {}
125740
125948
  };
125741
- try {
125742
- const response = await this.gateway.requestJson(
125743
- {
125744
- service: "mock",
125745
- method: "post",
125746
- path: `/mocks?workspace=${ws}`,
125747
- body
125748
- },
125749
- { retryTransient: false }
125750
- );
125949
+ const send2 = (fallback) => this.gateway.requestJson(
125950
+ {
125951
+ service: "mock",
125952
+ method: "post",
125953
+ path: `/mocks?workspace=${ws}`,
125954
+ body,
125955
+ ...fallback ? { fallback } : {}
125956
+ },
125957
+ // Unsafe create: never blind re-POST. An ESOCKETTIMEDOUT after accept
125958
+ // may still have created the mock; reconcile via discovery below and
125959
+ // let the orchestrator retry the whole create-or-adopt cycle.
125960
+ { retryTransient: false }
125961
+ );
125962
+ const parseMock = (response) => {
125751
125963
  const record = this.dataOf(response);
125752
125964
  const uid = this.idOf(record);
125753
125965
  if (!uid) {
@@ -125757,6 +125969,9 @@ var PostmanGatewayAssetsClient = class {
125757
125969
  uid,
125758
125970
  url: String(record?.url ?? record?.mockUrl ?? "").trim()
125759
125971
  };
125972
+ };
125973
+ try {
125974
+ return parseMock(await send2());
125760
125975
  } catch (error) {
125761
125976
  const adopted = await this.discoverAfterAmbiguousCreate(
125762
125977
  () => this.findMockByCollection(collection, environment, mockName),
@@ -125765,6 +125980,10 @@ var PostmanGatewayAssetsClient = class {
125765
125980
  if (adopted) {
125766
125981
  return { uid: adopted.uid, url: adopted.mockUrl };
125767
125982
  }
125983
+ if (adopted === void 0) {
125984
+ const retried = await this.resendAbsentCreate(async () => parseMock(await send2("auto")));
125985
+ if (retried) return retried;
125986
+ }
125768
125987
  throw error;
125769
125988
  }
125770
125989
  });
@@ -125784,6 +126003,33 @@ var PostmanGatewayAssetsClient = class {
125784
126003
  environment: String(m.environment ?? "")
125785
126004
  }));
125786
126005
  }
126006
+ /**
126007
+ * Delete an environment through the sync service (GC path). The path id is
126008
+ * the bare model id (public uid tail), mirroring updateEnvironment.
126009
+ */
126010
+ async deleteEnvironment(uid) {
126011
+ await this.gateway.requestJson({
126012
+ service: "sync",
126013
+ method: "delete",
126014
+ path: `/environment/${this.toModelId(uid)}`
126015
+ });
126016
+ }
126017
+ /** Delete a mock server (GC path). */
126018
+ async deleteMock(uid) {
126019
+ await this.gateway.requestJson({
126020
+ service: "mock",
126021
+ method: "delete",
126022
+ path: `/mocks/${this.toModelId(uid)}`
126023
+ });
126024
+ }
126025
+ /** Delete a collection-based monitor (jobTemplate) (GC path). */
126026
+ async deleteMonitor(uid) {
126027
+ await this.gateway.requestJson({
126028
+ service: "monitors",
126029
+ method: "delete",
126030
+ path: `/jobTemplates/${this.toModelId(uid)}`
126031
+ });
126032
+ }
125787
126033
  async mockExists(uid) {
125788
126034
  try {
125789
126035
  await this.gateway.requestJson({
@@ -125844,21 +126090,25 @@ var PostmanGatewayAssetsClient = class {
125844
126090
  distribution: null,
125845
126091
  ...environment ? { environment } : {}
125846
126092
  };
125847
- try {
125848
- const response = await this.gateway.requestJson(
125849
- {
125850
- service: "monitors",
125851
- method: "post",
125852
- path: `/jobTemplates?workspace=${ws}`,
125853
- body
125854
- },
125855
- { retryTransient: false }
125856
- );
126093
+ const send2 = (fallback) => this.gateway.requestJson(
126094
+ {
126095
+ service: "monitors",
126096
+ method: "post",
126097
+ path: `/jobTemplates?workspace=${ws}`,
126098
+ body,
126099
+ ...fallback ? { fallback } : {}
126100
+ },
126101
+ { retryTransient: false }
126102
+ );
126103
+ const parseMonitor = (response) => {
125857
126104
  const uid = this.idOf(this.dataOf(response));
125858
126105
  if (!uid) {
125859
126106
  throw new Error("Monitor create did not return a UID");
125860
126107
  }
125861
126108
  return uid;
126109
+ };
126110
+ try {
126111
+ return parseMonitor(await send2());
125862
126112
  } catch (error) {
125863
126113
  const adopted = await this.discoverAfterAmbiguousCreate(
125864
126114
  () => this.findMonitorByCollection(collection, environment, monitorName),
@@ -125867,6 +126117,10 @@ var PostmanGatewayAssetsClient = class {
125867
126117
  if (adopted?.uid) {
125868
126118
  return adopted.uid;
125869
126119
  }
126120
+ if (adopted === void 0) {
126121
+ const retried = await this.resendAbsentCreate(async () => parseMonitor(await send2("auto")));
126122
+ if (retried) return retried;
126123
+ }
125870
126124
  throw error;
125871
126125
  }
125872
126126
  });
@@ -126095,6 +126349,7 @@ var AccessTokenGatewayClient = class {
126095
126349
  fetchImpl;
126096
126350
  secretMasker;
126097
126351
  maxRetries;
126352
+ fallbackBaseUrl;
126098
126353
  retryBaseDelayMs;
126099
126354
  sleepImpl;
126100
126355
  constructor(options) {
@@ -126107,6 +126362,8 @@ var AccessTokenGatewayClient = class {
126107
126362
  this.fetchImpl = options.fetchImpl ?? fetch;
126108
126363
  this.secretMasker = options.secretMasker ?? createSecretMasker([this.tokenProvider.current()]);
126109
126364
  this.maxRetries = options.maxRetries ?? 3;
126365
+ const fallbackEnv = typeof process !== "undefined" ? process.env?.POSTMAN_ITEM_CREATE_FALLBACK : void 0;
126366
+ this.fallbackBaseUrl = fallbackEnv === "off" ? void 0 : options.fallbackBaseUrl?.replace(/\/+$/, "");
126110
126367
  this.retryBaseDelayMs = options.retryBaseDelayMs ?? 400;
126111
126368
  this.sleepImpl = options.sleepImpl ?? defaultSleep;
126112
126369
  }
@@ -126125,8 +126382,8 @@ var AccessTokenGatewayClient = class {
126125
126382
  }
126126
126383
  return headers;
126127
126384
  }
126128
- async send(request) {
126129
- const url = `${this.bifrostBaseUrl}/ws/proxy`;
126385
+ async send(request, baseUrl) {
126386
+ const url = `${baseUrl ?? this.bifrostBaseUrl}/ws/proxy`;
126130
126387
  return this.fetchImpl(url, {
126131
126388
  method: "POST",
126132
126389
  headers: this.buildHeaders(request.headers),
@@ -126139,6 +126396,43 @@ var AccessTokenGatewayClient = class {
126139
126396
  })
126140
126397
  });
126141
126398
  }
126399
+ /**
126400
+ * One cold, serial attempt against the fallback base URL after the primary
126401
+ * budget is exhausted on a transient failure. Never hedged in parallel with
126402
+ * the primary; only fires when the request would otherwise throw. Callers
126403
+ * with `retryTransient: false` still reconcile first — the fallback attempt
126404
+ * here is the resend, so it is only used for requests whose mutation is
126405
+ * known idempotent or already reconciled by the caller's adopt-on-ambiguous
126406
+ * loop.
126407
+ */
126408
+ async tryFallback(request) {
126409
+ if (!this.fallbackBaseUrl) return null;
126410
+ try {
126411
+ return await this.send(request, this.fallbackBaseUrl);
126412
+ } catch {
126413
+ return null;
126414
+ }
126415
+ }
126416
+ /**
126417
+ * Run the fallback attempt and classify its response the same way the
126418
+ * primary path would. Returns the success response, or null when the
126419
+ * fallback also failed transiently (caller then throws the original error).
126420
+ * Non-transient fallback failures (4xx) surface as their own HttpError since
126421
+ * they are the freshest authoritative answer.
126422
+ */
126423
+ fallbackEligible(request, retryTransient) {
126424
+ if (!this.fallbackBaseUrl) return false;
126425
+ return retryTransient || request.fallback === "auto";
126426
+ }
126427
+ async attemptFallback(request, retryTransient) {
126428
+ if (!this.fallbackEligible(request, retryTransient)) return null;
126429
+ const response = await this.tryFallback(request);
126430
+ if (!response) return null;
126431
+ if (response.ok) return response;
126432
+ const body = await response.text().catch(() => "");
126433
+ if (isRetryableSafeReadResponse(response.status)) return null;
126434
+ throw this.toHttpError(request, response, body);
126435
+ }
126142
126436
  /**
126143
126437
  * Send a gateway request, refreshing the token once on an auth failure and
126144
126438
  * optionally retrying transient downstream failures (5xx / Bifrost read
@@ -126161,6 +126455,8 @@ var AccessTokenGatewayClient = class {
126161
126455
  await this.sleepImpl(delay);
126162
126456
  continue;
126163
126457
  }
126458
+ const fallbackResponse2 = await this.attemptFallback(request, retryTransient);
126459
+ if (fallbackResponse2) return fallbackResponse2;
126164
126460
  throw error;
126165
126461
  }
126166
126462
  if (response.ok) {
@@ -126182,6 +126478,8 @@ var AccessTokenGatewayClient = class {
126182
126478
  await this.sleepImpl(delay);
126183
126479
  continue;
126184
126480
  }
126481
+ const fallbackResponse = await this.attemptFallback(request, retryTransient);
126482
+ if (fallbackResponse) return fallbackResponse;
126185
126483
  throw this.toHttpError(request, response, body);
126186
126484
  }
126187
126485
  }
@@ -126238,6 +126536,331 @@ var AccessTokenGatewayClient = class {
126238
126536
  }
126239
126537
  };
126240
126538
 
126539
+ // src/lib/repo/branch-decision.ts
126540
+ var import_node_fs3 = require("node:fs");
126541
+ var import_node_crypto2 = require("node:crypto");
126542
+ var ContractError = class extends Error {
126543
+ code;
126544
+ constructor(code, message) {
126545
+ super(`${code}: ${message}`);
126546
+ this.code = code;
126547
+ this.name = "ContractError";
126548
+ }
126549
+ };
126550
+ function clean(value) {
126551
+ const trimmed = (value ?? "").trim();
126552
+ return trimmed.length > 0 ? trimmed : void 0;
126553
+ }
126554
+ function stripRefPrefix(ref) {
126555
+ const raw = clean(ref);
126556
+ if (!raw) {
126557
+ return { kind: "unknown" };
126558
+ }
126559
+ if (raw.startsWith("refs/heads/")) {
126560
+ return { name: raw.slice("refs/heads/".length), kind: "branch" };
126561
+ }
126562
+ if (raw.startsWith("refs/tags/")) {
126563
+ return { name: raw.slice("refs/tags/".length), kind: "tag" };
126564
+ }
126565
+ if (raw.startsWith("refs/pull/") || raw.startsWith("refs/merge")) {
126566
+ return { kind: "unknown" };
126567
+ }
126568
+ return { name: raw, kind: "branch" };
126569
+ }
126570
+ function detectProvider(env) {
126571
+ if (clean(env.GITHUB_ACTIONS) || clean(env.GITHUB_REPOSITORY)) return "github";
126572
+ if (clean(env.GITLAB_CI) || clean(env.CI_PROJECT_PATH)) return "gitlab";
126573
+ if (clean(env.BITBUCKET_REPO_SLUG) || clean(env.BITBUCKET_BRANCH)) return "bitbucket";
126574
+ if (clean(env.TF_BUILD) || clean(env.BUILD_REPOSITORY_URI)) return "azure-devops";
126575
+ return "unknown";
126576
+ }
126577
+ function readGithubEvent(env) {
126578
+ const path5 = clean(env.GITHUB_EVENT_PATH);
126579
+ if (!path5) return void 0;
126580
+ try {
126581
+ return JSON.parse((0, import_node_fs3.readFileSync)(path5, "utf8"));
126582
+ } catch {
126583
+ return void 0;
126584
+ }
126585
+ }
126586
+ function resolveBranchIdentity(env = process.env, overrides = {}) {
126587
+ const provider = detectProvider(env);
126588
+ const explicitDefault = clean(overrides.defaultBranch);
126589
+ let headBranch;
126590
+ let rawRef;
126591
+ let refKind;
126592
+ let isPrContext = false;
126593
+ let isForkPr = false;
126594
+ let defaultBranch = explicitDefault;
126595
+ let headSha;
126596
+ switch (provider) {
126597
+ case "github": {
126598
+ const event = readGithubEvent(env);
126599
+ headSha = clean(env.GITHUB_SHA);
126600
+ defaultBranch ??= clean(event?.repository?.default_branch);
126601
+ const headRef = clean(env.GITHUB_HEAD_REF);
126602
+ if (headRef) {
126603
+ isPrContext = true;
126604
+ headBranch = headRef;
126605
+ rawRef = clean(env.GITHUB_REF) ?? headRef;
126606
+ refKind = "branch";
126607
+ const headRepo = event?.pull_request?.head?.repo?.full_name;
126608
+ const baseRepo = event?.pull_request?.base?.repo?.full_name ?? event?.repository?.full_name;
126609
+ isForkPr = Boolean(headRepo && baseRepo && headRepo !== baseRepo);
126610
+ headSha = clean(event?.pull_request?.head?.sha) ?? headSha;
126611
+ } else {
126612
+ rawRef = clean(env.GITHUB_REF) ?? clean(env.GITHUB_REF_NAME);
126613
+ const parsed = stripRefPrefix(rawRef);
126614
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
126615
+ refKind = parsed.kind;
126616
+ }
126617
+ break;
126618
+ }
126619
+ case "gitlab": {
126620
+ headSha = clean(env.CI_COMMIT_SHA);
126621
+ defaultBranch ??= clean(env.CI_DEFAULT_BRANCH);
126622
+ const mrSource = clean(env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME);
126623
+ if (mrSource) {
126624
+ isPrContext = true;
126625
+ headBranch = mrSource;
126626
+ rawRef = mrSource;
126627
+ refKind = "branch";
126628
+ const sourceProject = clean(env.CI_MERGE_REQUEST_SOURCE_PROJECT_ID);
126629
+ const targetProject = clean(env.CI_MERGE_REQUEST_PROJECT_ID);
126630
+ isForkPr = Boolean(sourceProject && targetProject && sourceProject !== targetProject);
126631
+ } else if (clean(env.CI_COMMIT_TAG)) {
126632
+ rawRef = clean(env.CI_COMMIT_TAG);
126633
+ refKind = "tag";
126634
+ } else {
126635
+ headBranch = clean(env.CI_COMMIT_BRANCH) ?? clean(env.CI_COMMIT_REF_NAME);
126636
+ rawRef = headBranch;
126637
+ refKind = headBranch ? "branch" : "unknown";
126638
+ }
126639
+ break;
126640
+ }
126641
+ case "bitbucket": {
126642
+ headSha = clean(env.BITBUCKET_COMMIT);
126643
+ if (clean(env.BITBUCKET_TAG)) {
126644
+ rawRef = clean(env.BITBUCKET_TAG);
126645
+ refKind = "tag";
126646
+ } else {
126647
+ headBranch = clean(env.BITBUCKET_BRANCH);
126648
+ rawRef = headBranch;
126649
+ refKind = headBranch ? "branch" : "unknown";
126650
+ isPrContext = Boolean(clean(env.BITBUCKET_PR_ID));
126651
+ }
126652
+ break;
126653
+ }
126654
+ case "azure-devops": {
126655
+ headSha = clean(env.BUILD_SOURCEVERSION);
126656
+ const prSource = clean(env.SYSTEM_PULLREQUEST_SOURCEBRANCH);
126657
+ if (prSource) {
126658
+ isPrContext = true;
126659
+ const parsed = stripRefPrefix(prSource);
126660
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
126661
+ rawRef = prSource;
126662
+ refKind = parsed.kind;
126663
+ const forkFlag = clean(env.SYSTEM_PULLREQUEST_ISFORK);
126664
+ isForkPr = forkFlag?.toLowerCase() === "true";
126665
+ } else {
126666
+ rawRef = clean(env.BUILD_SOURCEBRANCH);
126667
+ const parsed = stripRefPrefix(rawRef);
126668
+ headBranch = parsed.kind === "branch" ? parsed.name : void 0;
126669
+ refKind = parsed.kind;
126670
+ }
126671
+ break;
126672
+ }
126673
+ default: {
126674
+ refKind = "unknown";
126675
+ }
126676
+ }
126677
+ if (refKind === "branch" && headBranch && defaultBranch && headBranch === defaultBranch) {
126678
+ refKind = "default-branch";
126679
+ }
126680
+ return { provider, headBranch, rawRef, defaultBranch, refKind, isPrContext, isForkPr, headSha };
126681
+ }
126682
+ function parseChannelRules(input) {
126683
+ const raw = clean(input);
126684
+ const rules = [];
126685
+ for (const part of raw ? raw.split(",") : []) {
126686
+ const entry = part.trim();
126687
+ if (!entry) continue;
126688
+ const eq = entry.indexOf("=");
126689
+ if (eq <= 0 || eq === entry.length - 1) {
126690
+ throw new ContractError(
126691
+ "CONTRACT_CHANNELS_INPUT_INVALID",
126692
+ `channels entry "${entry}" must be <branch-or-glob>=<CODE>`
126693
+ );
126694
+ }
126695
+ const pattern = entry.slice(0, eq).trim();
126696
+ const code = entry.slice(eq + 1).trim().toUpperCase();
126697
+ if (!/^[A-Z][A-Z0-9_-]{0,15}$/.test(code)) {
126698
+ throw new ContractError(
126699
+ "CONTRACT_CHANNELS_INPUT_INVALID",
126700
+ `channel code "${code}" must be 1-16 chars, A-Z 0-9 _ -, starting with a letter`
126701
+ );
126702
+ }
126703
+ rules.push({ pattern, code });
126704
+ }
126705
+ if (!rules.some((rule) => rule.pattern === "release/*")) {
126706
+ rules.push({ pattern: "release/*", code: "RC" });
126707
+ }
126708
+ return rules;
126709
+ }
126710
+ function matchChannel(branch, rules) {
126711
+ for (const rule of rules) {
126712
+ if (rule.pattern.endsWith("*")) {
126713
+ const prefix = rule.pattern.slice(0, -1);
126714
+ if (branch.startsWith(prefix)) return rule;
126715
+ } else if (branch === rule.pattern) {
126716
+ return rule;
126717
+ }
126718
+ }
126719
+ return void 0;
126720
+ }
126721
+ function resolveBranchDecision(options) {
126722
+ const { strategy, identity } = options;
126723
+ const channels = options.channels ?? [];
126724
+ if (strategy === "legacy") {
126725
+ return {
126726
+ tier: "legacy",
126727
+ strategy,
126728
+ identity,
126729
+ canonicalBranch: clean(options.canonicalBranch) ?? identity.defaultBranch,
126730
+ reason: "branch-strategy legacy: branch-blind pre-v2 behavior"
126731
+ };
126732
+ }
126733
+ const canonicalBranch = clean(options.canonicalBranch) ?? identity.defaultBranch;
126734
+ if (!canonicalBranch) {
126735
+ throw new ContractError(
126736
+ "CONTRACT_DEFAULT_BRANCH_UNRESOLVED",
126737
+ `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.`
126738
+ );
126739
+ }
126740
+ if (identity.refKind === "tag" || identity.refKind === "unknown" || !identity.headBranch) {
126741
+ return {
126742
+ tier: "gated",
126743
+ strategy,
126744
+ identity,
126745
+ canonicalBranch,
126746
+ reason: `ref kind ${identity.refKind}: never canonical/preview-eligible; no-op with annotation`
126747
+ };
126748
+ }
126749
+ if (identity.headBranch === canonicalBranch) {
126750
+ return {
126751
+ tier: "canonical",
126752
+ strategy,
126753
+ identity,
126754
+ canonicalBranch,
126755
+ reason: `head branch equals canonical branch ${canonicalBranch}`
126756
+ };
126757
+ }
126758
+ const channel = matchChannel(identity.headBranch, channels);
126759
+ if (channel) {
126760
+ return {
126761
+ tier: "channel",
126762
+ strategy,
126763
+ identity,
126764
+ canonicalBranch,
126765
+ channel,
126766
+ reason: `branch ${identity.headBranch} matches channel ${channel.pattern}=${channel.code}`
126767
+ };
126768
+ }
126769
+ if (strategy === "preview") {
126770
+ if (identity.isForkPr) {
126771
+ return {
126772
+ tier: "gated",
126773
+ strategy,
126774
+ identity,
126775
+ canonicalBranch,
126776
+ reason: "fork PR: preview-ineligible (same-repo gate), gated instead"
126777
+ };
126778
+ }
126779
+ return {
126780
+ tier: "preview",
126781
+ strategy,
126782
+ identity,
126783
+ canonicalBranch,
126784
+ reason: `branch ${identity.headBranch} under branch-strategy preview`
126785
+ };
126786
+ }
126787
+ return {
126788
+ tier: "gated",
126789
+ strategy,
126790
+ identity,
126791
+ canonicalBranch,
126792
+ reason: `branch ${identity.headBranch} under branch-strategy publish-gate`
126793
+ };
126794
+ }
126795
+ var BRANCH_DECISION_ENV = "POSTMAN_BRANCH_DECISION";
126796
+ function serializeBranchDecision(decision) {
126797
+ return JSON.stringify(decision);
126798
+ }
126799
+ function parseBranchDecision(raw) {
126800
+ const value = clean(raw);
126801
+ if (!value) return void 0;
126802
+ let parsed;
126803
+ try {
126804
+ parsed = JSON.parse(value);
126805
+ } catch {
126806
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION is not valid JSON");
126807
+ }
126808
+ const candidate = parsed;
126809
+ const tiers = ["canonical", "channel", "preview", "gated", "legacy"];
126810
+ if (!candidate || typeof candidate !== "object" || !tiers.includes(candidate.tier) || !candidate.identity) {
126811
+ throw new ContractError("CONTRACT_BRANCH_DECISION_INVALID", "POSTMAN_BRANCH_DECISION does not carry a valid BranchDecision");
126812
+ }
126813
+ return candidate;
126814
+ }
126815
+ function resolveEffectiveBranchDecision(options, env = process.env) {
126816
+ const inherited = parseBranchDecision(env[BRANCH_DECISION_ENV]);
126817
+ if (inherited) return inherited;
126818
+ return resolveBranchDecision(options);
126819
+ }
126820
+ var PREVIEW_SLUG_MAX = 30;
126821
+ function buildBranchSlug(rawBranch) {
126822
+ const sanitized = rawBranch.replace(/^refs\/heads\//, "").replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
126823
+ const truncated = sanitized.slice(0, PREVIEW_SLUG_MAX);
126824
+ const lossy = truncated !== rawBranch.replace(/^refs\/heads\//, "");
126825
+ if (!lossy) {
126826
+ return { suffix: truncated, slug: truncated, lossy };
126827
+ }
126828
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(rawBranch).digest("hex").slice(0, 6);
126829
+ return { suffix: `${truncated}-${hash}`, slug: truncated, lossy };
126830
+ }
126831
+ function previewAssetName(baseName, rawBranch) {
126832
+ return `${baseName} @${buildBranchSlug(rawBranch).suffix}`;
126833
+ }
126834
+ function channelAssetName(baseName, code) {
126835
+ return `[${code}] ${baseName}`;
126836
+ }
126837
+ var MARKER_KEY = "x-pm-onboarding";
126838
+ function parseAssetMarker(description) {
126839
+ if (!description) return void 0;
126840
+ const index = description.indexOf(`${MARKER_KEY}:`);
126841
+ if (index === -1) return void 0;
126842
+ const jsonStart = description.indexOf("{", index);
126843
+ if (jsonStart === -1) return void 0;
126844
+ let depth = 0;
126845
+ for (let i = jsonStart; i < description.length; i += 1) {
126846
+ const ch = description[i];
126847
+ if (ch === "{") depth += 1;
126848
+ else if (ch === "}") {
126849
+ depth -= 1;
126850
+ if (depth === 0) {
126851
+ try {
126852
+ const parsed = JSON.parse(description.slice(jsonStart, i + 1));
126853
+ if (parsed && typeof parsed === "object" && parsed.repo && parsed.role) return parsed;
126854
+ } catch {
126855
+ return void 0;
126856
+ }
126857
+ return void 0;
126858
+ }
126859
+ }
126860
+ }
126861
+ return void 0;
126862
+ }
126863
+
126241
126864
  // src/index.ts
126242
126865
  function parseBooleanInput(value, defaultValue) {
126243
126866
  const normalized = String(value || "").trim().toLowerCase();
@@ -126256,16 +126879,16 @@ function getInput(name, env = process.env) {
126256
126879
  const runnerRaw = runnerName === normalizedName ? void 0 : env[runnerName];
126257
126880
  const hasNormalized = normalizedRaw !== void 0;
126258
126881
  const hasRunner = runnerRaw !== void 0;
126882
+ const normalizedValue = normalizeInputValue(normalizedRaw);
126883
+ const runnerValue = normalizeInputValue(runnerRaw);
126259
126884
  if (hasNormalized && hasRunner) {
126260
- const normalizedValue = normalizeInputValue(normalizedRaw);
126261
- const runnerValue = normalizeInputValue(runnerRaw);
126262
- if (normalizedValue !== runnerValue) {
126885
+ if (normalizedValue && runnerValue && normalizedValue !== runnerValue) {
126263
126886
  throw new Error(
126264
126887
  `Conflicting values for ${name}: ${normalizedName}=${JSON.stringify(normalizedValue)} vs ${runnerName}=${JSON.stringify(runnerValue)}`
126265
126888
  );
126266
126889
  }
126267
126890
  }
126268
- return normalizeInputValue(hasNormalized ? normalizedRaw : runnerRaw);
126891
+ return normalizedValue || runnerValue;
126269
126892
  }
126270
126893
  function hasInput(name, env = process.env) {
126271
126894
  const normalizedName = `INPUT_${name.replace(/-/g, "_").toUpperCase()}`;
@@ -126324,6 +126947,17 @@ function parseCredentialPreflight(value) {
126324
126947
  `Unsupported credential-preflight "${normalized}". Supported values: ${allowed.join(", ")}`
126325
126948
  );
126326
126949
  }
126950
+ function parseBranchStrategy(value) {
126951
+ const definition = postmanRepoSyncActionContract.inputs["branch-strategy"];
126952
+ const allowed = definition.allowedValues ?? [];
126953
+ const normalized = String(value || "").trim() || (definition.default ?? "legacy");
126954
+ if (allowed.includes(normalized)) {
126955
+ return normalized;
126956
+ }
126957
+ throw new Error(
126958
+ `Unsupported branch-strategy "${normalized}". Supported values: ${allowed.join(", ")}`
126959
+ );
126960
+ }
126327
126961
  function normalizeReleaseLabel(value) {
126328
126962
  const cleaned = normalizeInputValue(value).replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "").replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
126329
126963
  return cleaned;
@@ -126368,6 +127002,7 @@ function resolveInputs(env = process.env) {
126368
127002
  smokeCollectionId: getInput("smoke-collection-id", env),
126369
127003
  contractCollectionId: getInput("contract-collection-id", env),
126370
127004
  specId: getInput("spec-id", env),
127005
+ specContentChanged: parseBooleanInput(getInput("spec-content-changed", env), true),
126371
127006
  specPath: getInput("spec-path", env),
126372
127007
  collectionSyncMode: normalizeCollectionSyncMode(getInput("collection-sync-mode", env) || "refresh"),
126373
127008
  specSyncMode: normalizeSpecSyncMode(getInput("spec-sync-mode", env) || "update"),
@@ -126390,6 +127025,10 @@ function resolveInputs(env = process.env) {
126390
127025
  postmanApiKey: getInput("postman-api-key", env),
126391
127026
  postmanAccessToken: getInput("postman-access-token", env),
126392
127027
  credentialPreflight: parseCredentialPreflight(getInput("credential-preflight", env)),
127028
+ branchStrategy: parseBranchStrategy(getInput("branch-strategy", env)),
127029
+ canonicalBranch: getInput("canonical-branch", env) || void 0,
127030
+ channels: getInput("channels", env) || void 0,
127031
+ previewTtlDays: Math.max(1, Number.parseInt(getInput("preview-ttl", env) || "30", 10) || 30),
126393
127032
  adoToken: getInput("ado-token", env) || normalizeInputValue(env.SYSTEM_ACCESSTOKEN),
126394
127033
  githubToken: getInput("github-token", env),
126395
127034
  ghFallbackToken: getInput("gh-fallback-token", env),
@@ -126412,10 +127051,45 @@ function resolveInputs(env = process.env) {
126412
127051
  postmanStack,
126413
127052
  postmanApiBase: endpointProfile.apiBaseUrl,
126414
127053
  postmanBifrostBase: endpointProfile.bifrostBaseUrl,
127054
+ postmanFallbackBase: endpointProfile.fallbackBaseUrl,
126415
127055
  postmanCliInstallUrl: endpointProfile.cliInstallUrl,
126416
127056
  postmanIapubBase: endpointProfile.iapubBaseUrl
126417
127057
  };
126418
127058
  }
127059
+ function buildBranchAssetMarker(decision, inputs, now = /* @__PURE__ */ new Date()) {
127060
+ if (decision.tier !== "preview" && decision.tier !== "channel") {
127061
+ return void 0;
127062
+ }
127063
+ const rawBranch = decision.identity.headBranch;
127064
+ const repo = inputs.repoUrl || inputs.repository;
127065
+ if (!rawBranch || !repo) {
127066
+ return void 0;
127067
+ }
127068
+ const ttlMs = inputs.previewTtlDays * 24 * 60 * 60 * 1e3;
127069
+ return {
127070
+ repo,
127071
+ rawBranch,
127072
+ sanitizedBranch: buildBranchSlug(rawBranch).suffix,
127073
+ role: decision.tier,
127074
+ headSha: decision.identity.headSha,
127075
+ createdAt: now.toISOString(),
127076
+ lastSyncedAt: now.toISOString(),
127077
+ ...decision.tier === "preview" ? { expiresAt: new Date(now.getTime() + ttlMs).toISOString() } : {}
127078
+ };
127079
+ }
127080
+ function assertBranchAssetIds(inputs, decision, branchOwnedIds = process.env.POSTMAN_BRANCH_ASSET_IDS === "owned") {
127081
+ if (decision.tier === "legacy" || decision.tier === "canonical" || branchOwnedIds) return;
127082
+ const provided = [
127083
+ ["baseline-collection-id", inputs.baselineCollectionId],
127084
+ ["smoke-collection-id", inputs.smokeCollectionId],
127085
+ ["contract-collection-id", inputs.contractCollectionId]
127086
+ ].filter(([, value]) => Boolean(value));
127087
+ if (provided.length > 0) {
127088
+ throw new Error(
127089
+ `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.`
127090
+ );
127091
+ }
127092
+ }
126419
127093
  function buildEnvironmentValues(envName, baseUrl) {
126420
127094
  return [
126421
127095
  { key: "baseUrl", value: baseUrl, type: "default" },
@@ -126428,12 +127102,48 @@ function buildEnvironmentValues(envName, baseUrl) {
126428
127102
  ];
126429
127103
  }
126430
127104
  var LEGACY_BASELINE_COLLECTION_PREFIX = "[Baseline]";
127105
+ var RESOURCES_STATE_VERSION = 2;
127106
+ var SUPPORTED_STATE_VERSIONS = /* @__PURE__ */ new Set([1, RESOURCES_STATE_VERSION]);
127107
+ var StateUnreadableError = class extends Error {
127108
+ code = "CONTRACT_STATE_UNREADABLE";
127109
+ constructor(message) {
127110
+ super(`CONTRACT_STATE_UNREADABLE: ${message}`);
127111
+ this.name = "StateUnreadableError";
127112
+ }
127113
+ };
126431
127114
  function readResourcesState() {
127115
+ let raw;
126432
127116
  try {
126433
- return load((0, import_node_fs3.readFileSync)(".postman/resources.yaml", "utf8"));
127117
+ raw = (0, import_node_fs4.readFileSync)(".postman/resources.yaml", "utf8");
126434
127118
  } catch {
126435
127119
  return null;
126436
127120
  }
127121
+ let parsed;
127122
+ try {
127123
+ parsed = load(raw);
127124
+ } catch (error) {
127125
+ throw new StateUnreadableError(
127126
+ `.postman/resources.yaml exists but is not parseable YAML (${error instanceof Error ? error.message : String(error)}). Fix or delete the file; refusing to treat tracked state as absent.`
127127
+ );
127128
+ }
127129
+ if (parsed === null || parsed === void 0) {
127130
+ return null;
127131
+ }
127132
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
127133
+ throw new StateUnreadableError(
127134
+ ".postman/resources.yaml exists but does not contain a YAML mapping. Fix or delete the file; refusing to treat tracked state as absent."
127135
+ );
127136
+ }
127137
+ const state = parsed;
127138
+ if (state.version !== void 0 && !SUPPORTED_STATE_VERSIONS.has(Number(state.version))) {
127139
+ throw new StateUnreadableError(
127140
+ `.postman/resources.yaml declares unsupported state version ${String(state.version)} (supported: 1, ${RESOURCES_STATE_VERSION}). Upgrade the action or fix the file.`
127141
+ );
127142
+ }
127143
+ if (state.canonical && !state.cloudResources) {
127144
+ state.cloudResources = { ...state.canonical };
127145
+ }
127146
+ return state;
126437
127147
  }
126438
127148
  function findCloudResourceId(map, matcher) {
126439
127149
  if (!map) {
@@ -126468,7 +127178,7 @@ function isOpenApiSpecFile(filePath) {
126468
127178
  return false;
126469
127179
  }
126470
127180
  try {
126471
- const raw = (0, import_node_fs3.readFileSync)(filePath, "utf8");
127181
+ const raw = (0, import_node_fs4.readFileSync)(filePath, "utf8");
126472
127182
  const parsed = filePath.endsWith(".json") ? JSON.parse(raw) : load(raw);
126473
127183
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
126474
127184
  return false;
@@ -126497,7 +127207,7 @@ function scanLocalSpecReferences(baseDir = ".") {
126497
127207
  const found = /* @__PURE__ */ new Set();
126498
127208
  const refs = [];
126499
127209
  const visit2 = (currentDir) => {
126500
- for (const entry of (0, import_node_fs3.readdirSync)(currentDir, { withFileTypes: true })) {
127210
+ for (const entry of (0, import_node_fs4.readdirSync)(currentDir, { withFileTypes: true })) {
126501
127211
  if (ignoredDirs.has(entry.name)) {
126502
127212
  continue;
126503
127213
  }
@@ -126527,7 +127237,7 @@ function resolveMappedSpecReference(explicitSpecPath, discoveredSpecs) {
126527
127237
  const normalizedExplicitPath = normalizeToPosix(explicitSpecPath.trim());
126528
127238
  if (normalizedExplicitPath) {
126529
127239
  const explicitFullPath = path3.resolve(normalizedExplicitPath);
126530
- if ((0, import_node_fs3.existsSync)(explicitFullPath) && (0, import_node_fs3.statSync)(explicitFullPath).isFile()) {
127240
+ if ((0, import_node_fs4.existsSync)(explicitFullPath) && (0, import_node_fs4.statSync)(explicitFullPath).isFile()) {
126531
127241
  return {
126532
127242
  repoRelativePath: normalizedExplicitPath,
126533
127243
  configRelativePath: normalizeToPosix(path3.join("..", normalizedExplicitPath))
@@ -126554,7 +127264,11 @@ function createOutputs(inputs) {
126554
127264
  "mock-url": "",
126555
127265
  "monitor-id": "",
126556
127266
  "repo-sync-summary-json": "{}",
126557
- "commit-sha": ""
127267
+ "commit-sha": "",
127268
+ "sync-status": "",
127269
+ "branch-decision": "",
127270
+ "spec-version-tag": "",
127271
+ "spec-version-url": ""
126558
127272
  };
126559
127273
  }
126560
127274
  function buildGhCliEnv(env, token) {
@@ -126579,7 +127293,7 @@ function buildGhCliEnv(env, token) {
126579
127293
  }
126580
127294
  return filtered;
126581
127295
  }
126582
- async function upsertEnvironments(inputs, dependencies, resourcesState) {
127296
+ async function upsertEnvironments(inputs, dependencies, resourcesState, assetMarker) {
126583
127297
  const envUids = {
126584
127298
  ...getEnvironmentUidsFromResources(resourcesState),
126585
127299
  ...inputs.environmentUids
@@ -126589,7 +127303,6 @@ async function upsertEnvironments(inputs, dependencies, resourcesState) {
126589
127303
  }
126590
127304
  for (const envName of inputs.environments) {
126591
127305
  const runtimeUrl = String(inputs.envRuntimeUrls[envName] || "").trim();
126592
- const values = buildEnvironmentValues(envName, runtimeUrl);
126593
127306
  const displayName = `${inputs.projectName} - ${envName}`;
126594
127307
  let existingUid = String(envUids[envName] || "").trim();
126595
127308
  if (!existingUid) {
@@ -126605,10 +127318,27 @@ async function upsertEnvironments(inputs, dependencies, resourcesState) {
126605
127318
  }
126606
127319
  }
126607
127320
  if (existingUid) {
126608
- await dependencies.postman.updateEnvironment(existingUid, displayName, values);
127321
+ let marker = assetMarker;
127322
+ if (marker) {
127323
+ try {
127324
+ const existing = await dependencies.postman.getEnvironment(existingUid);
127325
+ const values3 = existing.data?.values ?? existing.values ?? [];
127326
+ const prior = values3.find((value) => value.key === "x-pm-onboarding")?.value;
127327
+ const priorMarker = parseAssetMarker(prior ? `x-pm-onboarding: ${prior}` : void 0);
127328
+ if (priorMarker?.repo === marker.repo && priorMarker.rawBranch === marker.rawBranch) {
127329
+ marker = { ...marker, createdAt: priorMarker.createdAt };
127330
+ }
127331
+ } catch {
127332
+ }
127333
+ }
127334
+ const values2 = buildEnvironmentValues(envName, runtimeUrl);
127335
+ if (marker) values2.push({ key: "x-pm-onboarding", value: JSON.stringify(marker), type: "default" });
127336
+ await dependencies.postman.updateEnvironment(existingUid, displayName, values2);
126609
127337
  envUids[envName] = existingUid;
126610
127338
  continue;
126611
127339
  }
127340
+ const values = buildEnvironmentValues(envName, runtimeUrl);
127341
+ if (assetMarker) values.push({ key: "x-pm-onboarding", value: JSON.stringify(assetMarker), type: "default" });
126612
127342
  envUids[envName] = await dependencies.postman.createEnvironment(
126613
127343
  inputs.workspaceId,
126614
127344
  displayName,
@@ -126618,7 +127348,7 @@ async function upsertEnvironments(inputs, dependencies, resourcesState) {
126618
127348
  return envUids;
126619
127349
  }
126620
127350
  function ensureDir(path5) {
126621
- (0, import_node_fs3.mkdirSync)(path5, { recursive: true });
127351
+ (0, import_node_fs4.mkdirSync)(path5, { recursive: true });
126622
127352
  }
126623
127353
  function getCollectionDirectoryName(kind, projectName) {
126624
127354
  if (kind === "Baseline") {
@@ -126655,7 +127385,7 @@ function stripVolatileFields(obj) {
126655
127385
  }
126656
127386
  function writeJsonFile(path5, content, normalize3 = false) {
126657
127387
  const data = normalize3 ? stripVolatileFields(content) : content;
126658
- (0, import_node_fs3.writeFileSync)(path5, JSON.stringify(data, null, 2));
127388
+ (0, import_node_fs4.writeFileSync)(path5, JSON.stringify(data, null, 2));
126659
127389
  }
126660
127390
  function buildMappedSpecCloudKey(mappedSource, specSyncMode, releaseLabel) {
126661
127391
  if (specSyncMode !== "version") {
@@ -126679,31 +127409,30 @@ function resolveDurableWorkspaceId(options) {
126679
127409
  }
126680
127410
  return prior === candidate ? prior : void 0;
126681
127411
  }
126682
- function buildResourcesManifest(workspaceId, collectionMap, envMap, artifactDir, localSpecRefs, mappedSpecRef, specId, existingSpecs) {
126683
- const manifest = {};
127412
+ function buildResourcesManifest(workspaceId, collectionMap, envMap, artifactDir, localSpecRefs, mappedSpecRef, specId, existingSpecs, priorState) {
127413
+ const manifest = { ...priorState ?? {} };
127414
+ delete manifest.version;
127415
+ delete manifest.workspace;
127416
+ delete manifest.localResources;
127417
+ delete manifest.cloudResources;
127418
+ delete manifest.canonical;
127419
+ manifest.version = RESOURCES_STATE_VERSION;
126684
127420
  if (workspaceId) {
126685
127421
  manifest.workspace = { id: workspaceId };
126686
127422
  }
126687
- const localResources = {};
126688
127423
  const cloudResources = {};
126689
127424
  const collectionKeys = Object.keys(collectionMap);
126690
127425
  if (collectionKeys.length > 0) {
126691
- localResources.collections = collectionKeys;
126692
127426
  cloudResources.collections = collectionMap;
126693
127427
  }
126694
127428
  const envEntries = Object.entries(envMap);
126695
127429
  if (envEntries.length > 0) {
126696
- localResources.environments = envEntries.map(
126697
- ([envName]) => `../${artifactDir}/environments/${envName}.postman_environment.json`
126698
- );
126699
127430
  cloudResources.environments = {};
126700
127431
  for (const [envName, envUid] of envEntries) {
126701
127432
  cloudResources.environments[`../${artifactDir}/environments/${envName}.postman_environment.json`] = envUid;
126702
127433
  }
126703
127434
  }
126704
- if (localSpecRefs.length > 0) {
126705
- localResources.specs = localSpecRefs;
126706
- }
127435
+ void localSpecRefs;
126707
127436
  const specs = { ...existingSpecs || {} };
126708
127437
  if (mappedSpecRef && specId) {
126709
127438
  specs[mappedSpecRef] = specId;
@@ -126711,11 +127440,8 @@ function buildResourcesManifest(workspaceId, collectionMap, envMap, artifactDir,
126711
127440
  if (Object.keys(specs).length > 0) {
126712
127441
  cloudResources.specs = specs;
126713
127442
  }
126714
- if (Object.keys(localResources).length > 0) {
126715
- manifest.localResources = localResources;
126716
- }
126717
127443
  if (Object.keys(cloudResources).length > 0) {
126718
- manifest.cloudResources = cloudResources;
127444
+ manifest.canonical = cloudResources;
126719
127445
  }
126720
127446
  return dump(manifest, {
126721
127447
  lineWidth: -1,
@@ -126756,21 +127482,21 @@ function assertPathWithinCwd(targetPath, fieldName) {
126756
127482
  if (!rawPath || hasControlCharacter2(originalPath) || path3.isAbsolute(rawPath) || path3.win32.isAbsolute(rawPath) || segments.includes("..") || rawPath.startsWith(":") || hasControlCharacter2(rawPath)) {
126757
127483
  throw new Error(`${fieldName} must stay within the repository root; received ${targetPath}`);
126758
127484
  }
126759
- const base = (0, import_node_fs3.realpathSync)(process.cwd());
127485
+ const base = (0, import_node_fs4.realpathSync)(process.cwd());
126760
127486
  const resolved = path3.resolve(base, rawPath);
126761
127487
  const relative3 = path3.relative(base, resolved);
126762
127488
  if (relative3.startsWith("..") || path3.isAbsolute(relative3)) {
126763
127489
  throw new Error(`${fieldName} must stay within the repository root; received ${targetPath}`);
126764
127490
  }
126765
127491
  let existingPath = resolved;
126766
- while (!(0, import_node_fs3.existsSync)(existingPath)) {
127492
+ while (!(0, import_node_fs4.existsSync)(existingPath)) {
126767
127493
  const parent = path3.dirname(existingPath);
126768
127494
  if (parent === existingPath) {
126769
127495
  break;
126770
127496
  }
126771
127497
  existingPath = parent;
126772
127498
  }
126773
- const realExistingPath = (0, import_node_fs3.realpathSync)(existingPath);
127499
+ const realExistingPath = (0, import_node_fs4.realpathSync)(existingPath);
126774
127500
  const realRelative = path3.relative(base, realExistingPath);
126775
127501
  if (realRelative.startsWith("..") || path3.isAbsolute(realRelative)) {
126776
127502
  throw new Error(`${fieldName} must stay within the repository root; received ${targetPath}`);
@@ -126798,8 +127524,8 @@ async function exportArtifacts(inputs, dependencies, envUids, assetProjectName,
126798
127524
  ensureDir(specsDir);
126799
127525
  ensureDir(".postman");
126800
127526
  const globalsFilePath = `${globalsDir}/workspace.globals.yaml`;
126801
- if (!(0, import_node_fs3.existsSync)(globalsFilePath)) {
126802
- (0, import_node_fs3.writeFileSync)(globalsFilePath, "name: Globals\nvalues: []\n");
127527
+ if (!(0, import_node_fs4.existsSync)(globalsFilePath)) {
127528
+ (0, import_node_fs4.writeFileSync)(globalsFilePath, "name: Globals\nvalues: []\n");
126803
127529
  }
126804
127530
  if (inputs.generateCiWorkflow) {
126805
127531
  const ciDir = inputs.ciWorkflowPath.split("/").slice(0, -1).join("/");
@@ -126846,7 +127572,7 @@ async function exportArtifacts(inputs, dependencies, envUids, assetProjectName,
126846
127572
  workspaceLinkEnabled: inputs.workspaceLinkEnabled,
126847
127573
  workspaceLinkStatus: options.workspaceLinkStatus
126848
127574
  });
126849
- (0, import_node_fs3.writeFileSync)(".postman/resources.yaml", buildResourcesManifest(
127575
+ (0, import_node_fs4.writeFileSync)(".postman/resources.yaml", buildResourcesManifest(
126850
127576
  durableWorkspaceId,
126851
127577
  manifestCollections,
126852
127578
  envUids,
@@ -126854,10 +127580,11 @@ async function exportArtifacts(inputs, dependencies, envUids, assetProjectName,
126854
127580
  discoveredSpecs.map((spec) => spec.configRelativePath),
126855
127581
  mappedSpecCloudKey,
126856
127582
  inputs.specId || void 0,
126857
- options.existingSpecs
127583
+ options.existingSpecs,
127584
+ options.priorState
126858
127585
  ));
126859
127586
  if (mappedSpec && Object.keys(manifestCollections).length > 0) {
126860
- (0, import_node_fs3.writeFileSync)(
127587
+ (0, import_node_fs4.writeFileSync)(
126861
127588
  ".postman/workflows.yaml",
126862
127589
  buildSpecCollectionWorkflowManifest(
126863
127590
  mappedSpec.configRelativePath,
@@ -126902,19 +127629,29 @@ async function commitAndPushGeneratedFiles(inputs, dependencies) {
126902
127629
  const dir = parts.slice(0, -1).join("/");
126903
127630
  ensureDir(dir);
126904
127631
  }
126905
- (0, import_node_fs3.writeFileSync)(inputs.ciWorkflowPath, ciWorkflow);
127632
+ (0, import_node_fs4.writeFileSync)(inputs.ciWorkflowPath, ciWorkflow);
127633
+ if (inputs.provider === "github" || inputs.provider === "unknown") {
127634
+ const gcPath = ".github/workflows/postman-preview-gc.yml";
127635
+ if (inputs.ciWorkflowPath.endsWith(".github/workflows/ci.yml") || inputs.ciWorkflowPath === ".github/workflows/ci.yml") {
127636
+ ensureDir(".github/workflows");
127637
+ (0, import_node_fs4.writeFileSync)(gcPath, renderGcWorkflowTemplate());
127638
+ }
127639
+ }
126906
127640
  }
126907
127641
  if (!dependencies.repoMutation || inputs.repoWriteMode === "none") {
126908
127642
  return { commitSha: "", resolvedCurrentRef: "", pushed: false };
126909
127643
  }
126910
127644
  const provisionPath = ".github/workflows/provision.yml";
126911
- const provisionExists = inputs.provider === "github" && (0, import_node_fs3.existsSync)(provisionPath);
127645
+ const provisionExists = inputs.provider === "github" && (0, import_node_fs4.existsSync)(provisionPath);
127646
+ const gcWorkflowPath = ".github/workflows/postman-preview-gc.yml";
127647
+ const gcExists = inputs.generateCiWorkflow && (0, import_node_fs4.existsSync)(gcWorkflowPath);
126912
127648
  const stagePaths = [
126913
127649
  inputs.artifactDir,
126914
127650
  ".postman",
126915
127651
  inputs.generateCiWorkflow ? inputs.ciWorkflowPath : null,
127652
+ gcExists ? gcWorkflowPath : null,
126916
127653
  provisionExists ? provisionPath : null
126917
- ].filter((entry) => typeof entry === "string" && ((0, import_node_fs3.existsSync)(entry) || entry === provisionPath));
127654
+ ].filter((entry) => typeof entry === "string" && ((0, import_node_fs4.existsSync)(entry) || entry === provisionPath));
126918
127655
  if (stagePaths.length === 0) {
126919
127656
  dependencies.core.info("No generated repository paths were found; skipping repo mutation.");
126920
127657
  return {
@@ -126962,6 +127699,31 @@ async function runRepoSync(inputs, dependencies) {
126962
127699
  }
126963
127700
  }
126964
127701
  async function runRepoSyncInner(inputs, dependencies) {
127702
+ const branchDecision = decideBranchTier(inputs);
127703
+ assertBranchAssetIds(inputs, branchDecision);
127704
+ const isCanonicalWriter = branchDecision.tier === "legacy" || branchDecision.tier === "canonical";
127705
+ if (!isCanonicalWriter) {
127706
+ if (branchDecision.tier === "preview" && branchDecision.identity.headBranch) {
127707
+ inputs = {
127708
+ ...inputs,
127709
+ projectName: previewAssetName(inputs.projectName, branchDecision.identity.headBranch),
127710
+ workspaceLinkEnabled: false,
127711
+ environmentSyncEnabled: false,
127712
+ repoWriteMode: "none"
127713
+ };
127714
+ } else if (branchDecision.tier === "channel" && branchDecision.channel) {
127715
+ inputs = {
127716
+ ...inputs,
127717
+ projectName: channelAssetName(inputs.projectName, branchDecision.channel.code),
127718
+ workspaceLinkEnabled: false,
127719
+ environmentSyncEnabled: false,
127720
+ repoWriteMode: "none"
127721
+ };
127722
+ }
127723
+ dependencies.core.info(
127724
+ `branch-aware sync: ${branchDecision.tier} run \u2014 branch-scoped asset set "${inputs.projectName}", no workspace repo-link mutation, no state write-back`
127725
+ );
127726
+ }
126965
127727
  const outputs = createOutputs(inputs);
126966
127728
  const versionRequested = inputs.collectionSyncMode === "version" || inputs.specSyncMode === "version";
126967
127729
  const releaseLabel = deriveReleaseLabel(inputs);
@@ -126969,7 +127731,8 @@ async function runRepoSyncInner(inputs, dependencies) {
126969
127731
  throw new Error("release-label is required when collection-sync-mode or spec-sync-mode is version");
126970
127732
  }
126971
127733
  const assetProjectName = createAssetProjectName(inputs, releaseLabel);
126972
- const resourcesState = readResourcesState();
127734
+ const trackedState = readResourcesState();
127735
+ const resourcesState = isCanonicalWriter ? trackedState : trackedState?.workspace ? { workspace: trackedState.workspace } : null;
126973
127736
  if (resourcesState) {
126974
127737
  if (!inputs.workspaceId && resourcesState.workspace?.id) {
126975
127738
  inputs.workspaceId = resourcesState.workspace.id;
@@ -126995,7 +127758,8 @@ async function runRepoSyncInner(inputs, dependencies) {
126995
127758
  }
126996
127759
  }
126997
127760
  }
126998
- const envUids = await upsertEnvironments(inputs, dependencies, resourcesState);
127761
+ const branchAssetMarker = buildBranchAssetMarker(branchDecision, inputs);
127762
+ const envUids = await upsertEnvironments(inputs, dependencies, resourcesState, branchAssetMarker);
126999
127763
  outputs["environment-uids-json"] = JSON.stringify(envUids);
127000
127764
  if (inputs.environmentSyncEnabled && inputs.workspaceId && dependencies.internalIntegration) {
127001
127765
  const associations = Object.entries(envUids).map(([envName, envUid]) => ({
@@ -127051,11 +127815,23 @@ async function runRepoSyncInner(inputs, dependencies) {
127051
127815
  }
127052
127816
  }
127053
127817
  if (!resolvedMockUrl) {
127054
- const mock = await dependencies.postman.createMock(
127055
- inputs.workspaceId,
127056
- mockName,
127057
- inputs.baselineCollectionId,
127058
- mockEnvUid
127818
+ const mock = await retry(
127819
+ () => dependencies.postman.createMock(
127820
+ inputs.workspaceId,
127821
+ mockName,
127822
+ inputs.baselineCollectionId,
127823
+ mockEnvUid
127824
+ ),
127825
+ {
127826
+ maxAttempts: 3,
127827
+ delayMs: 2e3,
127828
+ backoffMultiplier: 2,
127829
+ onRetry: ({ attempt, error }) => {
127830
+ dependencies.core.warning(
127831
+ `Mock create attempt ${attempt} failed (${error instanceof Error ? error.message : String(error)}); retrying`
127832
+ );
127833
+ }
127834
+ }
127059
127835
  );
127060
127836
  resolvedMockUrl = mock.url;
127061
127837
  dependencies.core.info(`Created new mock: ${resolvedMockUrl}`);
@@ -127124,16 +127900,19 @@ async function runRepoSyncInner(inputs, dependencies) {
127124
127900
  );
127125
127901
  } catch (error) {
127126
127902
  outputs["workspace-link-status"] = "failed";
127127
- dependencies.core.warning(
127128
- `Workspace link failed: ${error instanceof Error ? error.message : String(error)}`
127129
- );
127903
+ const message = `Workspace link failed: ${error instanceof Error ? error.message : String(error)}`;
127904
+ if (branchDecision.tier === "canonical") {
127905
+ throw new Error(message, { cause: error });
127906
+ }
127907
+ dependencies.core.warning(message);
127130
127908
  }
127131
127909
  }
127132
127910
  await exportArtifacts(inputs, dependencies, envUids, assetProjectName, {
127133
127911
  workspaceLinkStatus: outputs["workspace-link-status"],
127134
127912
  priorWorkspaceId: resourcesState?.workspace?.id,
127135
127913
  existingSpecs: resourcesState?.cloudResources?.specs,
127136
- releaseLabel
127914
+ releaseLabel,
127915
+ priorState: resourcesState
127137
127916
  });
127138
127917
  const commit = await commitAndPushGeneratedFiles(inputs, dependencies);
127139
127918
  outputs["commit-sha"] = commit.commitSha;
@@ -127141,6 +127920,38 @@ async function runRepoSyncInner(inputs, dependencies) {
127141
127920
  outputs["resolved-current-ref"] = commit.resolvedCurrentRef;
127142
127921
  }
127143
127922
  outputs["repo-sync-summary-json"] = createRepoSummary(outputs, envUids, commit.pushed);
127923
+ if (branchDecision.tier === "canonical" && inputs.specId && inputs.specContentChanged !== false && dependencies.postman.tagSpecVersion) {
127924
+ const shortSha = (branchDecision.identity.headSha ?? "").slice(0, 7);
127925
+ const tagName = inputs.releaseLabel ? `${inputs.releaseLabel}${shortSha ? ` (${shortSha})` : ""}` : shortSha || `sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
127926
+ try {
127927
+ const tag = await dependencies.postman.tagSpecVersion(inputs.specId, tagName);
127928
+ outputs["spec-version-tag"] = tag.name || tagName;
127929
+ if (tag.id) {
127930
+ 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)}`;
127931
+ }
127932
+ dependencies.core.info(`Tagged spec version at finalize: ${tag.name || tagName}`);
127933
+ } catch (error) {
127934
+ const status = error.status;
127935
+ if (status === 409) {
127936
+ const tags = await dependencies.postman.listSpecVersionTags?.(inputs.specId).catch(() => []) ?? [];
127937
+ const latest = tags[0];
127938
+ if (latest && (latest.name === tagName || /\([0-9a-f]{7}\)$/.test(latest.name) || /^[0-9a-f]{7}$/.test(latest.name))) {
127939
+ outputs["spec-version-tag"] = latest.name;
127940
+ outputs["spec-version-url"] = `https://web.postman.co/workspace/${encodeURIComponent(inputs.workspaceId)}/specification/${encodeURIComponent(inputs.specId)}?tagId=${encodeURIComponent(latest.id)}&versionLabel=${encodeURIComponent(latest.name)}`;
127941
+ dependencies.core.info(`Latest changelog group already tagged as "${latest.name}"; adopting.`);
127942
+ } else {
127943
+ dependencies.core.warning("Latest changelog group already carries a hand-applied tag; leaving it in place.");
127944
+ }
127945
+ } else {
127946
+ dependencies.core.warning(`Spec version tagging failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`);
127947
+ }
127948
+ }
127949
+ }
127950
+ if (inputs.branchStrategy !== "legacy" || process.env[BRANCH_DECISION_ENV]) {
127951
+ const decision = decideBranchTier(inputs);
127952
+ outputs["sync-status"] = "synced";
127953
+ outputs["branch-decision"] = serializeBranchDecision(decision);
127954
+ }
127144
127955
  for (const [name, value] of Object.entries(outputs)) {
127145
127956
  dependencies.core.setOutput(name, value);
127146
127957
  }
@@ -127244,6 +128055,8 @@ async function resolvePostmanApiKeyAndTeamId(inputs, actionCore, actionExec, mas
127244
128055
  const gateway = new AccessTokenGatewayClient({
127245
128056
  tokenProvider,
127246
128057
  bifrostBaseUrl: inputs.postmanBifrostBase,
128058
+ // No fallback on the org-mode probe: the expected non-org 400 must
128059
+ // surface verbatim, not be re-fired against the /_api alias.
127247
128060
  secretMasker: masker
127248
128061
  });
127249
128062
  const squads = await gateway.getSquads(teamId);
@@ -127299,6 +128112,7 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
127299
128112
  const gateway = new AccessTokenGatewayClient({
127300
128113
  tokenProvider,
127301
128114
  bifrostBaseUrl: inputs.postmanBifrostBase,
128115
+ fallbackBaseUrl: inputs.postmanFallbackBase,
127302
128116
  teamId: resolved.teamId,
127303
128117
  orgMode: inputs.orgMode,
127304
128118
  secretMasker: masker
@@ -127330,7 +128144,19 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
127330
128144
  listMonitors: gatewayAssets.listMonitors.bind(gatewayAssets),
127331
128145
  monitorExists: gatewayAssets.monitorExists.bind(gatewayAssets),
127332
128146
  findMonitorByCollection: gatewayAssets.findMonitorByCollection.bind(gatewayAssets),
127333
- runMonitor: gatewayAssets.runMonitor.bind(gatewayAssets)
128147
+ runMonitor: gatewayAssets.runMonitor.bind(gatewayAssets),
128148
+ listEnvironments: gatewayAssets.listEnvironments.bind(gatewayAssets),
128149
+ // GC path (preview/channel retention machine — lib/repo/preview-gc.ts).
128150
+ deleteEnvironment: gatewayAssets.deleteEnvironment.bind(gatewayAssets),
128151
+ deleteMock: gatewayAssets.deleteMock.bind(gatewayAssets),
128152
+ deleteMonitor: gatewayAssets.deleteMonitor.bind(gatewayAssets),
128153
+ deleteCollection: gatewayAssets.deleteCollection.bind(gatewayAssets),
128154
+ listSpecifications: gatewayAssets.listSpecifications.bind(gatewayAssets),
128155
+ getSpecContent: gatewayAssets.getSpecContent.bind(gatewayAssets),
128156
+ listSpecCollections: gatewayAssets.listSpecCollections.bind(gatewayAssets),
128157
+ deleteSpec: gatewayAssets.deleteSpec.bind(gatewayAssets),
128158
+ tagSpecVersion: gatewayAssets.tagSpecVersion.bind(gatewayAssets),
128159
+ listSpecVersionTags: gatewayAssets.listSpecVersionTags.bind(gatewayAssets)
127334
128160
  };
127335
128161
  const repoMutation = repository && (inputs.repoWriteMode === "commit-only" || inputs.repoWriteMode === "commit-and-push") ? new RepoMutationService({
127336
128162
  provider: inputs.provider,
@@ -127367,6 +128193,400 @@ function createRepoSyncDependencies(inputs, resolved, factories, options = {}) {
127367
128193
  repoMutation
127368
128194
  };
127369
128195
  }
128196
+ function decideBranchTier(inputs, env = process.env) {
128197
+ return resolveEffectiveBranchDecision(
128198
+ {
128199
+ strategy: inputs.branchStrategy,
128200
+ identity: resolveBranchIdentity(env, { defaultBranch: inputs.canonicalBranch }),
128201
+ canonicalBranch: inputs.canonicalBranch,
128202
+ channels: parseChannelRules(inputs.channels)
128203
+ },
128204
+ env
128205
+ );
128206
+ }
128207
+ function runGatedSkip(inputs, decision, actionCore) {
128208
+ actionCore.info(
128209
+ `branch-aware sync: gated run (${decision.reason}) \u2014 repo-sync skipped, zero workspace writes`
128210
+ );
128211
+ const outputs = createOutputs(inputs);
128212
+ outputs["sync-status"] = "skipped-branch-gate";
128213
+ outputs["branch-decision"] = serializeBranchDecision(decision);
128214
+ outputs["repo-sync-summary-json"] = JSON.stringify({
128215
+ status: "skipped-branch-gate",
128216
+ reason: decision.reason
128217
+ });
128218
+ for (const [name, value] of Object.entries(outputs)) {
128219
+ actionCore.setOutput(name, value);
128220
+ }
128221
+ return outputs;
128222
+ }
128223
+
128224
+ // src/lib/repo/preview-gc.ts
128225
+ function looksGenerated(name) {
128226
+ return / @[A-Za-z0-9._-]+/.test(name) || /^\[[A-Z][A-Z0-9]*\] /.test(name);
128227
+ }
128228
+ function resolveMarker(candidate) {
128229
+ return candidate.marker ?? parseAssetMarker(candidate.description);
128230
+ }
128231
+ function stampChannelRetirement(marker, reason, now, previewTtlDays) {
128232
+ const deleteAfter = new Date(now.getTime() + previewTtlDays * 24 * 60 * 60 * 1e3);
128233
+ return {
128234
+ ...marker,
128235
+ retirementDetectedAt: now.toISOString(),
128236
+ retirementReason: reason,
128237
+ deleteAfter: deleteAfter.toISOString()
128238
+ };
128239
+ }
128240
+ function clearChannelRetirement(marker) {
128241
+ const next = { ...marker };
128242
+ delete next.retirementDetectedAt;
128243
+ delete next.retirementReason;
128244
+ delete next.deleteAfter;
128245
+ return next;
128246
+ }
128247
+ function decideRetention(candidate, context) {
128248
+ const marker = resolveMarker(candidate);
128249
+ if (!marker) {
128250
+ if (looksGenerated(candidate.name)) {
128251
+ return { action: "orphan-audit", reason: "generated-looking name with missing or invalid marker" };
128252
+ }
128253
+ return { action: "stranger", reason: "no marker; not our asset" };
128254
+ }
128255
+ if (marker.repo !== context.repo) {
128256
+ return { action: "stranger", reason: `marker repo ${marker.repo} does not match ${context.repo}` };
128257
+ }
128258
+ if (context.triggerGeneration && marker.createdAt) {
128259
+ const created = new Date(marker.createdAt);
128260
+ if (!Number.isNaN(created.getTime()) && created.getTime() > context.triggerGeneration.getTime()) {
128261
+ return { action: "retain", reason: "marker generation is newer than the GC trigger (name reuse)" };
128262
+ }
128263
+ }
128264
+ if (marker.role === "channel") {
128265
+ if (marker.retirementReason && marker.deleteAfter) {
128266
+ const existence3 = context.branchExists(marker.rawBranch);
128267
+ const mapped = context.channelMapped?.(marker.rawBranch);
128268
+ const restored = marker.retirementReason === "branch-deleted" ? existence3 === "exists" && mapped !== false : existence3 === "exists" && mapped === true;
128269
+ if (restored) {
128270
+ return { action: "restore", reason: "retired channel branch and mapping are active again", branchProbe: "exists" };
128271
+ }
128272
+ const deleteAfter = new Date(marker.deleteAfter);
128273
+ if (!Number.isNaN(deleteAfter.getTime()) && context.now.getTime() >= deleteAfter.getTime()) {
128274
+ return { action: "delete", reason: `retired channel set past deleteAfter (${marker.retirementReason})` };
128275
+ }
128276
+ return { action: "retain", reason: "retired channel set inside deleteAfter window" };
128277
+ }
128278
+ const existence2 = context.branchExists(marker.rawBranch);
128279
+ if (existence2 === "deleted") {
128280
+ return { action: "retire", reason: "channel branch deleted; start retirement window", branchProbe: "deleted", retirementReason: "branch-deleted" };
128281
+ }
128282
+ if (context.channelMapped?.(marker.rawBranch) === false) {
128283
+ return { action: "retire", reason: "channel mapping removed; start retirement window", branchProbe: existence2, retirementReason: "mapping-removed" };
128284
+ }
128285
+ return { action: "retain", reason: "active channel set (channels are never TTL-swept)", branchProbe: existence2 };
128286
+ }
128287
+ if (context.onlyBranch !== void 0) {
128288
+ if (marker.rawBranch !== context.onlyBranch && marker.sanitizedBranch !== context.onlyBranch) {
128289
+ return { action: "retain", reason: "outside manual --branch scope" };
128290
+ }
128291
+ return { action: "delete", reason: `manual gc --branch ${context.onlyBranch}` };
128292
+ }
128293
+ if (context.allPreviews) {
128294
+ return { action: "delete", reason: "manual gc --all-previews" };
128295
+ }
128296
+ const existence = context.branchExists(marker.rawBranch);
128297
+ if (existence === "exists") {
128298
+ return { action: "retain", reason: "branch still exists", branchProbe: "exists" };
128299
+ }
128300
+ if (existence === "deleted") {
128301
+ return { action: "delete", reason: "branch deleted", branchProbe: "deleted" };
128302
+ }
128303
+ if (marker.expiresAt) {
128304
+ const expires = new Date(marker.expiresAt);
128305
+ if (!Number.isNaN(expires.getTime()) && context.now.getTime() >= expires.getTime()) {
128306
+ return { action: "delete", reason: "sliding TTL expired", branchProbe: "skipped" };
128307
+ }
128308
+ }
128309
+ return { action: "retain", reason: "branch existence unknown and TTL not expired", branchProbe: "skipped" };
128310
+ }
128311
+ async function runPreviewGc(options) {
128312
+ const { context, candidates, deleters, dryRun } = options;
128313
+ const log = options.log ?? (() => void 0);
128314
+ const summary2 = {
128315
+ repo: context.repo,
128316
+ scannedAt: context.now.toISOString(),
128317
+ degraded: Boolean(options.degraded),
128318
+ counts: { delete: 0, retire: 0, restore: 0, retain: 0, stranger: 0, "orphan-audit": 0, errors: 0 },
128319
+ entries: []
128320
+ };
128321
+ for (const candidate of candidates) {
128322
+ const decision = decideRetention(candidate, context);
128323
+ const entry = {
128324
+ kind: candidate.kind,
128325
+ uid: candidate.uid,
128326
+ name: candidate.name,
128327
+ action: decision.action,
128328
+ reason: decision.reason
128329
+ };
128330
+ summary2.counts[decision.action] += 1;
128331
+ if (decision.action === "retire" && !dryRun && candidate.kind === "environment") {
128332
+ const retire = options.retirers?.environment;
128333
+ if (!retire) {
128334
+ entry.error = "no channel retirement writer wired for environment";
128335
+ summary2.counts.errors += 1;
128336
+ } else {
128337
+ try {
128338
+ await retire(candidate, decision.retirementReason ?? "branch-deleted");
128339
+ log(`gc: retired channel ${candidate.name} (${candidate.uid}) \u2014 ${decision.reason}`);
128340
+ } catch (error) {
128341
+ entry.error = error instanceof Error ? error.message : String(error);
128342
+ summary2.counts.errors += 1;
128343
+ }
128344
+ }
128345
+ } else if (decision.action === "restore" && !dryRun && candidate.kind === "environment") {
128346
+ const restore = options.retirers?.restoreEnvironment;
128347
+ if (!restore) {
128348
+ entry.error = "no channel retirement restore writer wired for environment";
128349
+ summary2.counts.errors += 1;
128350
+ } else {
128351
+ try {
128352
+ await restore(candidate);
128353
+ log(`gc: restored channel ${candidate.name} (${candidate.uid})`);
128354
+ } catch (error) {
128355
+ entry.error = error instanceof Error ? error.message : String(error);
128356
+ summary2.counts.errors += 1;
128357
+ }
128358
+ }
128359
+ } else if (decision.action === "delete" && !dryRun) {
128360
+ const remove = deleters[candidate.kind];
128361
+ if (!remove) {
128362
+ entry.error = `no deleter wired for kind ${candidate.kind}`;
128363
+ summary2.counts.errors += 1;
128364
+ } else {
128365
+ try {
128366
+ await remove(candidate.uid);
128367
+ entry.deleted = true;
128368
+ log(`gc: deleted ${candidate.kind} ${candidate.name} (${candidate.uid}) \u2014 ${decision.reason}`);
128369
+ } catch (error) {
128370
+ entry.error = error instanceof Error ? error.message : String(error);
128371
+ summary2.counts.errors += 1;
128372
+ log(`gc: FAILED deleting ${candidate.kind} ${candidate.name} (${candidate.uid}): ${entry.error}`);
128373
+ }
128374
+ }
128375
+ } else if (decision.action === "delete" && dryRun) {
128376
+ log(`gc (dry-run): would delete ${candidate.kind} ${candidate.name} (${candidate.uid}) \u2014 ${decision.reason}`);
128377
+ }
128378
+ summary2.entries.push(entry);
128379
+ }
128380
+ return summary2;
128381
+ }
128382
+ function renderGcSummary(summary2) {
128383
+ const lines = [];
128384
+ lines.push(`Preview GC summary for ${summary2.repo} at ${summary2.scannedAt}${summary2.degraded ? " (degraded: branch probes skipped)" : ""}`);
128385
+ lines.push(
128386
+ ` deleted=${summary2.counts.delete} retired=${summary2.counts.retire} restored=${summary2.counts.restore} retained=${summary2.counts.retain} strangers=${summary2.counts.stranger} orphans=${summary2.counts["orphan-audit"]} errors=${summary2.counts.errors}`
128387
+ );
128388
+ for (const entry of summary2.entries) {
128389
+ if (entry.action === "retain" || entry.action === "stranger") continue;
128390
+ const status = entry.error ? `ERROR: ${entry.error}` : entry.deleted ? "deleted" : entry.action;
128391
+ lines.push(` [${entry.kind}] ${entry.name} (${entry.uid}): ${status} \u2014 ${entry.reason}`);
128392
+ }
128393
+ return lines.join("\n");
128394
+ }
128395
+
128396
+ // src/lib/repo/gc-runner.ts
128397
+ var GC_MARKER_ENV_KEY = "x-pm-onboarding";
128398
+ async function inventoryRemoteBranches(exec) {
128399
+ try {
128400
+ const result = await exec.getExecOutput("git", ["ls-remote", "--heads", "origin"], {
128401
+ ignoreReturnCode: true
128402
+ });
128403
+ if (result.exitCode !== 0) {
128404
+ return void 0;
128405
+ }
128406
+ const branches = /* @__PURE__ */ new Set();
128407
+ for (const line of result.stdout.split("\n")) {
128408
+ const match = line.match(/\trefs\/heads\/(.+)$/);
128409
+ if (match) branches.add(match[1].trim());
128410
+ }
128411
+ return branches;
128412
+ } catch {
128413
+ return void 0;
128414
+ }
128415
+ }
128416
+ function markerFromEnvironment(envelope) {
128417
+ const record = envelope && typeof envelope === "object" ? envelope : null;
128418
+ const data = record && typeof record.data === "object" && record.data !== null ? record.data : record;
128419
+ const values = data && Array.isArray(data.values) ? data.values : [];
128420
+ for (const value of values) {
128421
+ if (String(value?.key ?? "") === GC_MARKER_ENV_KEY) {
128422
+ return parseAssetMarker(`${GC_MARKER_ENV_KEY}: ${String(value?.value ?? "")}`);
128423
+ }
128424
+ }
128425
+ return void 0;
128426
+ }
128427
+ function environmentValues(envelope) {
128428
+ const record = envelope && typeof envelope === "object" ? envelope : null;
128429
+ const data = record && typeof record.data === "object" && record.data !== null ? record.data : record;
128430
+ const values = data && Array.isArray(data.values) ? data.values : [];
128431
+ return values.map((raw) => {
128432
+ const value = raw && typeof raw === "object" ? raw : {};
128433
+ return {
128434
+ key: String(value.key ?? ""),
128435
+ value: String(value.value ?? ""),
128436
+ enabled: value.enabled !== false,
128437
+ type: String(value.type ?? "default")
128438
+ };
128439
+ });
128440
+ }
128441
+ function markerFromSpecContent(content) {
128442
+ if (!content) return void 0;
128443
+ try {
128444
+ const parsed = load(content);
128445
+ const marker = parsed?.["x-postman-onboarding"];
128446
+ return marker && typeof marker === "object" ? parseAssetMarker(`${GC_MARKER_ENV_KEY}: ${JSON.stringify(marker)}`) : void 0;
128447
+ } catch {
128448
+ return void 0;
128449
+ }
128450
+ }
128451
+ function isGcCandidateName(name) {
128452
+ return / @[A-Za-z0-9._-]+/.test(name) || /^\[[A-Z][A-Z0-9]*\] /.test(name);
128453
+ }
128454
+ async function collectGcCandidates(postman, workspaceId) {
128455
+ const candidates = [];
128456
+ const environments = await postman.listEnvironments(workspaceId);
128457
+ for (const env of environments) {
128458
+ if (!isGcCandidateName(env.name)) continue;
128459
+ let marker;
128460
+ try {
128461
+ marker = markerFromEnvironment(await postman.getEnvironment(env.uid));
128462
+ } catch {
128463
+ marker = void 0;
128464
+ }
128465
+ candidates.push({ kind: "environment", uid: env.uid, name: env.name, marker });
128466
+ }
128467
+ const mocks = await postman.listMocks();
128468
+ for (const mock of mocks) {
128469
+ if (!isGcCandidateName(mock.name)) continue;
128470
+ const envMatch = candidates.find(
128471
+ (entry) => entry.kind === "environment" && entry.uid === mock.environment
128472
+ );
128473
+ candidates.push({ kind: "mock", uid: mock.uid, name: mock.name, marker: envMatch?.marker });
128474
+ }
128475
+ const monitors = await postman.listMonitors();
128476
+ for (const monitor of monitors) {
128477
+ if (!isGcCandidateName(monitor.name)) continue;
128478
+ const envMatch = candidates.find(
128479
+ (entry) => entry.kind === "environment" && entry.uid === monitor.environmentUid
128480
+ );
128481
+ candidates.push({ kind: "monitor", uid: monitor.uid, name: monitor.name, marker: envMatch?.marker });
128482
+ }
128483
+ const ownedCollections = /* @__PURE__ */ new Map();
128484
+ for (const mock of mocks) {
128485
+ const marker = candidates.find((entry) => entry.kind === "environment" && entry.uid === mock.environment)?.marker;
128486
+ if (marker && mock.collection) ownedCollections.set(mock.collection, { name: `${mock.name} collection`, marker });
128487
+ }
128488
+ for (const monitor of monitors) {
128489
+ const marker = candidates.find((entry) => entry.kind === "environment" && entry.uid === monitor.environmentUid)?.marker;
128490
+ if (marker && monitor.collectionUid) ownedCollections.set(monitor.collectionUid, { name: `${monitor.name} collection`, marker });
128491
+ }
128492
+ for (const [uid, collection] of ownedCollections) {
128493
+ candidates.push({ kind: "collection", uid, name: collection.name, marker: collection.marker });
128494
+ }
128495
+ const specifications = await postman.listSpecifications(workspaceId);
128496
+ for (const spec of specifications) {
128497
+ if (!isGcCandidateName(spec.name)) continue;
128498
+ let marker;
128499
+ try {
128500
+ marker = markerFromSpecContent(await postman.getSpecContent(spec.uid));
128501
+ } catch {
128502
+ marker = void 0;
128503
+ }
128504
+ candidates.push({ kind: "spec", uid: spec.uid, name: spec.name, marker });
128505
+ if (marker) {
128506
+ try {
128507
+ for (const collection of await postman.listSpecCollections(spec.uid)) {
128508
+ if (!ownedCollections.has(collection.uid)) {
128509
+ candidates.push({ kind: "collection", uid: collection.uid, name: collection.name || `${spec.name} collection`, marker });
128510
+ }
128511
+ }
128512
+ } catch {
128513
+ }
128514
+ }
128515
+ }
128516
+ return candidates;
128517
+ }
128518
+ async function runGc(options) {
128519
+ const now = options.now ?? /* @__PURE__ */ new Date();
128520
+ const log = options.log ?? (() => void 0);
128521
+ const remoteBranches = options.onlyBranch || options.allPreviews ? void 0 : await inventoryRemoteBranches(options.exec);
128522
+ const degraded = remoteBranches === void 0 && !options.onlyBranch && !options.allPreviews;
128523
+ if (degraded) {
128524
+ log("gc: branch inventory unavailable (credential absent/denied?) \u2014 degraded sweep, TTL only");
128525
+ }
128526
+ const branchExists = (rawBranch) => {
128527
+ if (!remoteBranches) return "unknown";
128528
+ return remoteBranches.has(rawBranch) ? "exists" : "deleted";
128529
+ };
128530
+ const channelRules = options.channels === void 0 ? void 0 : parseChannelRules(options.channels);
128531
+ const channelMapped = channelRules ? (rawBranch) => channelRules.some(
128532
+ (rule) => rule.pattern.endsWith("*") ? rawBranch.startsWith(rule.pattern.slice(0, -1)) : rawBranch === rule.pattern
128533
+ ) : void 0;
128534
+ const candidates = await collectGcCandidates(options.postman, options.workspaceId);
128535
+ const retiredChannels = candidates.filter((candidate) => candidate.kind === "environment" && candidate.marker?.role === "channel" && candidate.marker.retirementReason).map((candidate) => candidate.marker);
128536
+ for (const candidate of candidates) {
128537
+ const marker = candidate.marker;
128538
+ if (!marker || marker.role !== "channel" || marker.retirementReason) continue;
128539
+ const retired = retiredChannels.find((entry) => entry.repo === marker.repo && entry.rawBranch === marker.rawBranch);
128540
+ if (retired) candidate.marker = retired;
128541
+ }
128542
+ return runPreviewGc({
128543
+ context: {
128544
+ repo: options.repo,
128545
+ now,
128546
+ branchExists,
128547
+ channelMapped,
128548
+ onlyBranch: options.onlyBranch,
128549
+ allPreviews: options.allPreviews
128550
+ },
128551
+ candidates,
128552
+ deleters: {
128553
+ environment: (uid) => options.postman.deleteEnvironment(uid),
128554
+ mock: (uid) => options.postman.deleteMock(uid),
128555
+ monitor: (uid) => options.postman.deleteMonitor(uid),
128556
+ collection: (uid) => options.postman.deleteCollection(uid),
128557
+ spec: (uid) => options.postman.deleteSpec(uid)
128558
+ },
128559
+ retirers: {
128560
+ environment: async (candidate, reason) => {
128561
+ if (!candidate.marker) return;
128562
+ const envelope = await options.postman.getEnvironment(candidate.uid);
128563
+ const values = environmentValues(envelope);
128564
+ const retired = stampChannelRetirement(
128565
+ candidate.marker,
128566
+ reason,
128567
+ now,
128568
+ options.previewTtlDays ?? 30
128569
+ );
128570
+ const markerValue = values.find((value) => value.key === GC_MARKER_ENV_KEY);
128571
+ if (markerValue) markerValue.value = JSON.stringify(retired);
128572
+ else values.push({ key: GC_MARKER_ENV_KEY, value: JSON.stringify(retired), enabled: true, type: "default" });
128573
+ await options.postman.updateEnvironment(candidate.uid, candidate.name, values);
128574
+ },
128575
+ restoreEnvironment: async (candidate) => {
128576
+ if (!candidate.marker) return;
128577
+ const envelope = await options.postman.getEnvironment(candidate.uid);
128578
+ const values = environmentValues(envelope);
128579
+ const restored = clearChannelRetirement(candidate.marker);
128580
+ const markerValue = values.find((value) => value.key === GC_MARKER_ENV_KEY);
128581
+ if (markerValue) markerValue.value = JSON.stringify(restored);
128582
+ await options.postman.updateEnvironment(candidate.uid, candidate.name, values);
128583
+ }
128584
+ },
128585
+ degraded,
128586
+ dryRun: options.dryRun,
128587
+ log
128588
+ });
128589
+ }
127370
128590
 
127371
128591
  // src/cli.ts
127372
128592
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
@@ -127415,15 +128635,25 @@ var CLI_INPUT_NAMES = [
127415
128635
  "ssl-client-passphrase",
127416
128636
  "ssl-extra-ca-certs",
127417
128637
  "spec-id",
128638
+ "spec-content-changed",
127418
128639
  "spec-path",
127419
128640
  "team-id",
127420
128641
  "postman-region",
127421
- "postman-stack"
128642
+ "postman-stack",
128643
+ "branch-strategy",
128644
+ "canonical-branch",
128645
+ "channels",
128646
+ "preview-ttl"
127422
128647
  ];
127423
128648
  var HELP_TEXT = `Usage: postman-repo-sync [options]
127424
128649
 
127425
128650
  Sync Postman artifacts into a git repository.
127426
128651
 
128652
+ Subcommands:
128653
+ gc [--branch <name> | --all-previews] [--dry-run]
128654
+ Garbage-collect preview/channel asset sets
128655
+ (marker-guarded; strangers are never deleted)
128656
+
127427
128657
  Options:
127428
128658
  --help Show this help and exit
127429
128659
  --version Show version and exit
@@ -127517,7 +128747,7 @@ function resolvePackageVersion() {
127517
128747
  candidates.push(import_node_path3.default.join(process.cwd(), "package.json"));
127518
128748
  for (const packageJsonPath of candidates) {
127519
128749
  try {
127520
- const packageJson = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
128750
+ const packageJson = JSON.parse((0, import_node_fs5.readFileSync)(packageJsonPath, "utf8"));
127521
128751
  if (packageJson.name === "@postman-cse/onboarding-repo-sync" && packageJson.version) {
127522
128752
  return String(packageJson.version).trim();
127523
128753
  }
@@ -127682,9 +128912,22 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
127682
128912
  return;
127683
128913
  }
127684
128914
  const env = runtime.env ?? process.env;
128915
+ if (argv[0] === "gc") {
128916
+ await runGcCommand(argv.slice(1), env, writeStdout);
128917
+ return;
128918
+ }
127685
128919
  const config = parseCliArgs(argv, env);
127686
128920
  const inputs = resolveInputs(config.inputEnv);
127687
128921
  const reporter = new ConsoleReporter();
128922
+ const branchDecision = decideBranchTier(inputs, config.inputEnv);
128923
+ if (branchDecision.tier === "gated") {
128924
+ const result2 = runGatedSkip(inputs, branchDecision, reporter);
128925
+ await writeOptionalFile(config.resultJsonPath, JSON.stringify(result2, null, 2));
128926
+ await writeOptionalFile(config.dotenvPath, toDotenv(result2));
128927
+ writeStdout(`${JSON.stringify(result2, null, 2)}
128928
+ `);
128929
+ return;
128930
+ }
127688
128931
  await mintAccessTokenIfNeeded(inputs, reporter);
127689
128932
  const initialMasker = createSecretMasker([
127690
128933
  inputs.postmanApiKey,
@@ -127735,6 +128978,80 @@ async function runCli(argv = process.argv.slice(2), runtime = {}) {
127735
128978
  writeStdout(`${JSON.stringify(result, null, 2)}
127736
128979
  `);
127737
128980
  }
128981
+ async function runGcCommand(argv, env, writeStdout) {
128982
+ let onlyBranch;
128983
+ let allPreviews = false;
128984
+ let dryRun = false;
128985
+ const passthrough = [];
128986
+ for (let index = 0; index < argv.length; index += 1) {
128987
+ const arg = argv[index];
128988
+ if (arg === "--branch") {
128989
+ onlyBranch = argv[index + 1];
128990
+ if (!onlyBranch || onlyBranch.startsWith("--")) {
128991
+ throw new Error("Missing value for --branch");
128992
+ }
128993
+ index += 1;
128994
+ } else if (arg === "--all-previews") {
128995
+ allPreviews = true;
128996
+ } else if (arg === "--dry-run") {
128997
+ dryRun = true;
128998
+ } else {
128999
+ passthrough.push(arg);
129000
+ }
129001
+ }
129002
+ if (onlyBranch && allPreviews) {
129003
+ throw new Error("Use either --branch <name> or --all-previews, not both");
129004
+ }
129005
+ const config = parseCliArgs(passthrough, env);
129006
+ const inputs = resolveInputs(config.inputEnv);
129007
+ if (!inputs.workspaceId) {
129008
+ throw new Error("gc requires --workspace-id (the workspace holding the preview/channel sets)");
129009
+ }
129010
+ const repo = inputs.repoUrl || inputs.repository;
129011
+ if (!repo) {
129012
+ throw new Error("gc requires --repo-url or --repository to scope marker ownership");
129013
+ }
129014
+ const reporter = new ConsoleReporter();
129015
+ await mintAccessTokenIfNeeded(inputs, reporter);
129016
+ const initialMasker = createSecretMasker([
129017
+ inputs.postmanApiKey,
129018
+ inputs.postmanAccessToken,
129019
+ inputs.githubToken,
129020
+ inputs.ghFallbackToken
129021
+ ]);
129022
+ const resolved = await resolvePostmanApiKeyAndTeamId(
129023
+ inputs,
129024
+ reporter,
129025
+ createCliExec(initialMasker),
129026
+ initialMasker,
129027
+ { persistGeneratedApiKeySecret: false, env }
129028
+ );
129029
+ const dependencies = createCliDependencies(inputs, resolved);
129030
+ if (!dependencies.postman.deleteCollection || !dependencies.postman.listSpecifications || !dependencies.postman.getSpecContent || !dependencies.postman.listSpecCollections || !dependencies.postman.deleteSpec) {
129031
+ throw new Error("gc requires the full branch-aware inventory client; this runtime is missing a spec or collection GC capability.");
129032
+ }
129033
+ const summary2 = await runGc({
129034
+ workspaceId: inputs.workspaceId,
129035
+ repo,
129036
+ postman: {
129037
+ ...dependencies.postman,
129038
+ deleteCollection: dependencies.postman.deleteCollection,
129039
+ listSpecifications: dependencies.postman.listSpecifications,
129040
+ getSpecContent: dependencies.postman.getSpecContent,
129041
+ listSpecCollections: dependencies.postman.listSpecCollections,
129042
+ deleteSpec: dependencies.postman.deleteSpec
129043
+ },
129044
+ exec: createCliExec(initialMasker),
129045
+ onlyBranch,
129046
+ allPreviews,
129047
+ dryRun,
129048
+ previewTtlDays: inputs.previewTtlDays,
129049
+ channels: inputs.channels,
129050
+ log: (message) => reporter.info(message)
129051
+ });
129052
+ reporter.info(renderGcSummary(summary2));
129053
+ writeStdout(JSON.stringify(summary2, null, 2) + "\n");
129054
+ }
127738
129055
  var currentModulePath = typeof __filename === "string" ? __filename : "";
127739
129056
  var entrypoint = process.argv[1];
127740
129057
  function isEntrypoint(currentPath, entrypointPath) {
@@ -127742,7 +129059,7 @@ function isEntrypoint(currentPath, entrypointPath) {
127742
129059
  return false;
127743
129060
  }
127744
129061
  try {
127745
- return (0, import_node_fs4.realpathSync)(currentPath) === (0, import_node_fs4.realpathSync)(entrypointPath);
129062
+ return (0, import_node_fs5.realpathSync)(currentPath) === (0, import_node_fs5.realpathSync)(entrypointPath);
127746
129063
  } catch {
127747
129064
  return import_node_path3.default.resolve(currentPath) === import_node_path3.default.resolve(entrypointPath);
127748
129065
  }