@postman-cse/onboarding-bootstrap 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -202,6 +202,7 @@ steps:
202
202
  | `baseline-collection-id` | | Reuse an existing baseline collection. |
203
203
  | `smoke-collection-id` | | Reuse an existing smoke collection. |
204
204
  | `contract-collection-id` | | Reuse an existing contract collection. |
205
+ | `sync-examples` | `true` | Whether linked spec/collection relations should enable example syncing during cloud linkage. |
205
206
  | `collection-sync-mode` | `refresh` | Collection lifecycle policy. `reuse` keeps existing collections, `refresh` regenerates the current collection set from the latest spec, and `version` creates or reuses release-scoped collections. |
206
207
  | `spec-sync-mode` | `update` | Spec lifecycle policy. `update` keeps one canonical spec current in Spec Hub, while `version` creates or reuses a release-scoped spec asset. |
207
208
  | `release-label` | | Optional release label used for versioned specs and collections. When omitted for versioned sync, the action derives one from GitHub tag or branch metadata. |
@@ -247,6 +248,14 @@ Current Postman asset state lives in `.postman/resources.yaml`.
247
248
  - `update` and `reuse` modes resolve current-state mappings from the checked-out ref.
248
249
  - `version` mode reuses only the checked-out ref's mappings; release history lives in git history and tags, not in a separate manifest file or repository variables.
249
250
 
251
+ ### Cloud spec-to-collection syncing
252
+
253
+ After collections exist, bootstrap links them to the cloud specification and triggers a spec-side collection sync when `postman-access-token` is available.
254
+
255
+ - `sync-examples: true` (default) enables example syncing in that relation setup.
256
+ - `sync-examples: false` keeps the relation but disables example syncing.
257
+ - If `postman-access-token` is missing, bootstrap warns and skips the cloud link/sync step.
258
+
250
259
  ### Contract smoke monitoring
251
260
 
252
261
  This repo includes `.github/workflows/contract-smoke.yml`, a scheduled live contract check for the upstream Postman APIs used by bootstrap.
package/action.yml CHANGED
@@ -17,6 +17,10 @@ inputs:
17
17
  contract-collection-id:
18
18
  description: Existing contract collection ID
19
19
  required: false
20
+ sync-examples:
21
+ description: Whether linked spec/collection relations should enable example syncing
22
+ required: false
23
+ default: 'true'
20
24
  collection-sync-mode:
21
25
  description: Collection lifecycle policy (reuse, refresh, or version)
22
26
  required: false
package/dist/cli.cjs CHANGED
@@ -28728,6 +28728,12 @@ var openAlphaActionContract = {
28728
28728
  description: "Existing contract collection ID.",
28729
28729
  required: false
28730
28730
  },
28731
+ "sync-examples": {
28732
+ description: "Whether linked spec/collection relations should enable example syncing.",
28733
+ required: false,
28734
+ default: "true",
28735
+ allowedValues: ["true", "false"]
28736
+ },
28731
28737
  "collection-sync-mode": {
28732
28738
  description: "Collection lifecycle policy: reuse existing collections, refresh them from the latest spec, or version them by release label.",
28733
28739
  required: false,
@@ -29659,6 +29665,26 @@ var BifrostInternalIntegrationAdapter = class {
29659
29665
  options.workerBaseUrl || "https://catalog-admin.postman-account2009.workers.dev"
29660
29666
  ).replace(/\/+$/, "");
29661
29667
  }
29668
+ async proxyRequest(service, method, requestPath, body) {
29669
+ const url = "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy";
29670
+ const headers = {
29671
+ "Content-Type": "application/json",
29672
+ "x-access-token": this.accessToken
29673
+ };
29674
+ if (this.teamId) {
29675
+ headers["x-entity-team-id"] = this.teamId;
29676
+ }
29677
+ return this.fetchImpl(url, {
29678
+ method: "POST",
29679
+ headers,
29680
+ body: JSON.stringify({
29681
+ service,
29682
+ method,
29683
+ path: requestPath,
29684
+ ...body !== void 0 ? { body } : {}
29685
+ })
29686
+ });
29687
+ }
29662
29688
  async assignWorkspaceToGovernanceGroup(workspaceId, domain, mappingJson) {
29663
29689
  let mapping;
29664
29690
  try {
@@ -29752,14 +29778,6 @@ var BifrostInternalIntegrationAdapter = class {
29752
29778
  }
29753
29779
  }
29754
29780
  async connectWorkspaceToRepository(workspaceId, repoUrl) {
29755
- const url = "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy";
29756
- const headers = {
29757
- "Content-Type": "application/json",
29758
- "x-access-token": this.accessToken
29759
- };
29760
- if (this.teamId) {
29761
- headers["x-entity-team-id"] = this.teamId;
29762
- }
29763
29781
  const payload = {
29764
29782
  service: "workspaces",
29765
29783
  method: "POST",
@@ -29770,11 +29788,12 @@ var BifrostInternalIntegrationAdapter = class {
29770
29788
  versionControl: true
29771
29789
  }
29772
29790
  };
29773
- const response = await this.fetchImpl(url, {
29774
- method: "POST",
29775
- headers,
29776
- body: JSON.stringify(payload)
29777
- });
29791
+ const response = await this.proxyRequest(
29792
+ payload.service,
29793
+ payload.method,
29794
+ payload.path,
29795
+ payload.body
29796
+ );
29778
29797
  if (response.ok) return;
29779
29798
  if (response.status === 400) {
29780
29799
  const body = await response.text();
@@ -29791,29 +29810,68 @@ var BifrostInternalIntegrationAdapter = class {
29791
29810
  }
29792
29811
  throw await HttpError.fromResponse(response, {
29793
29812
  method: "POST",
29794
- requestHeaders: headers,
29813
+ requestHeaders: {
29814
+ "Content-Type": "application/json",
29815
+ "x-access-token": this.accessToken,
29816
+ ...this.teamId ? { "x-entity-team-id": this.teamId } : {}
29817
+ },
29795
29818
  secretValues: [this.accessToken],
29796
- url
29819
+ url: "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy"
29797
29820
  });
29798
29821
  }
29799
- async getWorkspaceGitRepoUrl(workspaceId) {
29800
- const url = "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy";
29801
- const headers = {
29802
- "Content-Type": "application/json",
29803
- "x-access-token": this.accessToken
29804
- };
29805
- if (this.teamId) {
29806
- headers["x-entity-team-id"] = this.teamId;
29822
+ async linkCollectionsToSpecification(specificationId, collections) {
29823
+ if (collections.length === 0) {
29824
+ return;
29807
29825
  }
29808
- const response = await this.fetchImpl(url, {
29826
+ const response = await this.proxyRequest(
29827
+ "specification",
29828
+ "put",
29829
+ `/specifications/${specificationId}/collections`,
29830
+ collections.map((collection) => ({
29831
+ collectionId: collection.collectionId,
29832
+ ...collection.syncOptions ? { syncOptions: collection.syncOptions } : {}
29833
+ }))
29834
+ );
29835
+ if (response.ok) {
29836
+ return;
29837
+ }
29838
+ throw await HttpError.fromResponse(response, {
29809
29839
  method: "POST",
29810
- headers,
29811
- body: JSON.stringify({
29812
- service: "workspaces",
29813
- method: "GET",
29814
- path: `/workspaces/${workspaceId}/filesystem`
29815
- })
29840
+ requestHeaders: {
29841
+ "Content-Type": "application/json",
29842
+ "x-access-token": this.accessToken,
29843
+ ...this.teamId ? { "x-entity-team-id": this.teamId } : {}
29844
+ },
29845
+ secretValues: [this.accessToken],
29846
+ url: "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy"
29847
+ });
29848
+ }
29849
+ async syncCollection(specificationId, collectionId) {
29850
+ const response = await this.proxyRequest(
29851
+ "specification",
29852
+ "post",
29853
+ `/specifications/${specificationId}/collections/${collectionId}/sync`
29854
+ );
29855
+ if (response.ok) {
29856
+ return;
29857
+ }
29858
+ throw await HttpError.fromResponse(response, {
29859
+ method: "POST",
29860
+ requestHeaders: {
29861
+ "Content-Type": "application/json",
29862
+ "x-access-token": this.accessToken,
29863
+ ...this.teamId ? { "x-entity-team-id": this.teamId } : {}
29864
+ },
29865
+ secretValues: [this.accessToken],
29866
+ url: "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy"
29816
29867
  });
29868
+ }
29869
+ async getWorkspaceGitRepoUrl(workspaceId) {
29870
+ const response = await this.proxyRequest(
29871
+ "workspaces",
29872
+ "GET",
29873
+ `/workspaces/${workspaceId}/filesystem`
29874
+ );
29817
29875
  if (response.status === 404) return null;
29818
29876
  if (!response.ok) return null;
29819
29877
  const body = await response.text();
@@ -29996,6 +30054,13 @@ function requireInput(actionCore, name) {
29996
30054
  function optionalInput(actionCore, name) {
29997
30055
  return normalizeInputValue(actionCore.getInput(name));
29998
30056
  }
30057
+ function parseBooleanInput(value, defaultValue) {
30058
+ const normalized = String(value || "").trim().toLowerCase();
30059
+ if (!normalized) return defaultValue;
30060
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
30061
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
30062
+ return defaultValue;
30063
+ }
29999
30064
  function parseCollectionSyncMode(value) {
30000
30065
  if (value === "reuse" || value === "version") {
30001
30066
  return value;
@@ -30040,6 +30105,7 @@ function resolveInputs(env = process.env) {
30040
30105
  baselineCollectionId: getInput("baseline-collection-id", env),
30041
30106
  smokeCollectionId: getInput("smoke-collection-id", env),
30042
30107
  contractCollectionId: getInput("contract-collection-id", env),
30108
+ syncExamples: parseBooleanInput(getInput("sync-examples", env), true),
30043
30109
  collectionSyncMode: parseCollectionSyncMode(getInput("collection-sync-mode", env)),
30044
30110
  specSyncMode: parseSpecSyncMode(getInput("spec-sync-mode", env)),
30045
30111
  releaseLabel: getInput("release-label", env),
@@ -30099,6 +30165,7 @@ function readActionInputs(actionCore) {
30099
30165
  INPUT_BASELINE_COLLECTION_ID: optionalInput(actionCore, "baseline-collection-id"),
30100
30166
  INPUT_SMOKE_COLLECTION_ID: optionalInput(actionCore, "smoke-collection-id"),
30101
30167
  INPUT_CONTRACT_COLLECTION_ID: optionalInput(actionCore, "contract-collection-id"),
30168
+ INPUT_SYNC_EXAMPLES: optionalInput(actionCore, "sync-examples") ?? openAlphaActionContract.inputs["sync-examples"].default,
30102
30169
  INPUT_COLLECTION_SYNC_MODE: optionalInput(actionCore, "collection-sync-mode") ?? openAlphaActionContract.inputs["collection-sync-mode"].default,
30103
30170
  INPUT_SPEC_SYNC_MODE: optionalInput(actionCore, "spec-sync-mode") ?? openAlphaActionContract.inputs["spec-sync-mode"].default,
30104
30171
  INPUT_RELEASE_LABEL: optionalInput(actionCore, "release-label"),
@@ -30671,6 +30738,48 @@ For CLI usage, pass --workspace-team-id <id> or export POSTMAN_WORKSPACE_TEAM_ID
30671
30738
  ]);
30672
30739
  }
30673
30740
  );
30741
+ const linkedCollectionIds = [
30742
+ outputs["baseline-collection-id"],
30743
+ outputs["smoke-collection-id"],
30744
+ outputs["contract-collection-id"]
30745
+ ].filter(Boolean);
30746
+ if (linkedCollectionIds.length > 0) {
30747
+ if (dependencies.internalIntegration) {
30748
+ await runGroup(
30749
+ dependencies.core,
30750
+ "Link Collections to Specification",
30751
+ async () => {
30752
+ await dependencies.internalIntegration?.linkCollectionsToSpecification(
30753
+ outputs["spec-id"],
30754
+ linkedCollectionIds.map((collectionId) => ({
30755
+ collectionId,
30756
+ syncOptions: {
30757
+ syncExamples: inputs.syncExamples
30758
+ }
30759
+ }))
30760
+ );
30761
+ }
30762
+ );
30763
+ await runGroup(
30764
+ dependencies.core,
30765
+ "Sync Linked Collections",
30766
+ async () => {
30767
+ await Promise.all(
30768
+ linkedCollectionIds.map(
30769
+ (collectionId) => dependencies.internalIntegration.syncCollection(
30770
+ outputs["spec-id"],
30771
+ collectionId
30772
+ )
30773
+ )
30774
+ );
30775
+ }
30776
+ );
30777
+ } else {
30778
+ dependencies.core.warning(
30779
+ "Skipping cloud spec-to-collection linking and sync because postman-access-token is not configured"
30780
+ );
30781
+ }
30782
+ }
30674
30783
  for (const [name, value] of Object.entries(outputs)) {
30675
30784
  dependencies.core.setOutput(name, value);
30676
30785
  }
@@ -30858,6 +30967,7 @@ function parseCliArgs(argv, env = process.env) {
30858
30967
  "baseline-collection-id",
30859
30968
  "smoke-collection-id",
30860
30969
  "contract-collection-id",
30970
+ "sync-examples",
30861
30971
  "collection-sync-mode",
30862
30972
  "spec-sync-mode",
30863
30973
  "release-label",
package/dist/index.cjs CHANGED
@@ -28724,6 +28724,12 @@ var openAlphaActionContract = {
28724
28724
  description: "Existing contract collection ID.",
28725
28725
  required: false
28726
28726
  },
28727
+ "sync-examples": {
28728
+ description: "Whether linked spec/collection relations should enable example syncing.",
28729
+ required: false,
28730
+ default: "true",
28731
+ allowedValues: ["true", "false"]
28732
+ },
28727
28733
  "collection-sync-mode": {
28728
28734
  description: "Collection lifecycle policy: reuse existing collections, refresh them from the latest spec, or version them by release label.",
28729
28735
  required: false,
@@ -29655,6 +29661,26 @@ var BifrostInternalIntegrationAdapter = class {
29655
29661
  options.workerBaseUrl || "https://catalog-admin.postman-account2009.workers.dev"
29656
29662
  ).replace(/\/+$/, "");
29657
29663
  }
29664
+ async proxyRequest(service, method, requestPath, body) {
29665
+ const url = "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy";
29666
+ const headers = {
29667
+ "Content-Type": "application/json",
29668
+ "x-access-token": this.accessToken
29669
+ };
29670
+ if (this.teamId) {
29671
+ headers["x-entity-team-id"] = this.teamId;
29672
+ }
29673
+ return this.fetchImpl(url, {
29674
+ method: "POST",
29675
+ headers,
29676
+ body: JSON.stringify({
29677
+ service,
29678
+ method,
29679
+ path: requestPath,
29680
+ ...body !== void 0 ? { body } : {}
29681
+ })
29682
+ });
29683
+ }
29658
29684
  async assignWorkspaceToGovernanceGroup(workspaceId, domain, mappingJson) {
29659
29685
  let mapping;
29660
29686
  try {
@@ -29748,14 +29774,6 @@ var BifrostInternalIntegrationAdapter = class {
29748
29774
  }
29749
29775
  }
29750
29776
  async connectWorkspaceToRepository(workspaceId, repoUrl) {
29751
- const url = "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy";
29752
- const headers = {
29753
- "Content-Type": "application/json",
29754
- "x-access-token": this.accessToken
29755
- };
29756
- if (this.teamId) {
29757
- headers["x-entity-team-id"] = this.teamId;
29758
- }
29759
29777
  const payload = {
29760
29778
  service: "workspaces",
29761
29779
  method: "POST",
@@ -29766,11 +29784,12 @@ var BifrostInternalIntegrationAdapter = class {
29766
29784
  versionControl: true
29767
29785
  }
29768
29786
  };
29769
- const response = await this.fetchImpl(url, {
29770
- method: "POST",
29771
- headers,
29772
- body: JSON.stringify(payload)
29773
- });
29787
+ const response = await this.proxyRequest(
29788
+ payload.service,
29789
+ payload.method,
29790
+ payload.path,
29791
+ payload.body
29792
+ );
29774
29793
  if (response.ok) return;
29775
29794
  if (response.status === 400) {
29776
29795
  const body = await response.text();
@@ -29787,29 +29806,68 @@ var BifrostInternalIntegrationAdapter = class {
29787
29806
  }
29788
29807
  throw await HttpError.fromResponse(response, {
29789
29808
  method: "POST",
29790
- requestHeaders: headers,
29809
+ requestHeaders: {
29810
+ "Content-Type": "application/json",
29811
+ "x-access-token": this.accessToken,
29812
+ ...this.teamId ? { "x-entity-team-id": this.teamId } : {}
29813
+ },
29791
29814
  secretValues: [this.accessToken],
29792
- url
29815
+ url: "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy"
29793
29816
  });
29794
29817
  }
29795
- async getWorkspaceGitRepoUrl(workspaceId) {
29796
- const url = "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy";
29797
- const headers = {
29798
- "Content-Type": "application/json",
29799
- "x-access-token": this.accessToken
29800
- };
29801
- if (this.teamId) {
29802
- headers["x-entity-team-id"] = this.teamId;
29818
+ async linkCollectionsToSpecification(specificationId, collections) {
29819
+ if (collections.length === 0) {
29820
+ return;
29803
29821
  }
29804
- const response = await this.fetchImpl(url, {
29822
+ const response = await this.proxyRequest(
29823
+ "specification",
29824
+ "put",
29825
+ `/specifications/${specificationId}/collections`,
29826
+ collections.map((collection) => ({
29827
+ collectionId: collection.collectionId,
29828
+ ...collection.syncOptions ? { syncOptions: collection.syncOptions } : {}
29829
+ }))
29830
+ );
29831
+ if (response.ok) {
29832
+ return;
29833
+ }
29834
+ throw await HttpError.fromResponse(response, {
29805
29835
  method: "POST",
29806
- headers,
29807
- body: JSON.stringify({
29808
- service: "workspaces",
29809
- method: "GET",
29810
- path: `/workspaces/${workspaceId}/filesystem`
29811
- })
29836
+ requestHeaders: {
29837
+ "Content-Type": "application/json",
29838
+ "x-access-token": this.accessToken,
29839
+ ...this.teamId ? { "x-entity-team-id": this.teamId } : {}
29840
+ },
29841
+ secretValues: [this.accessToken],
29842
+ url: "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy"
29843
+ });
29844
+ }
29845
+ async syncCollection(specificationId, collectionId) {
29846
+ const response = await this.proxyRequest(
29847
+ "specification",
29848
+ "post",
29849
+ `/specifications/${specificationId}/collections/${collectionId}/sync`
29850
+ );
29851
+ if (response.ok) {
29852
+ return;
29853
+ }
29854
+ throw await HttpError.fromResponse(response, {
29855
+ method: "POST",
29856
+ requestHeaders: {
29857
+ "Content-Type": "application/json",
29858
+ "x-access-token": this.accessToken,
29859
+ ...this.teamId ? { "x-entity-team-id": this.teamId } : {}
29860
+ },
29861
+ secretValues: [this.accessToken],
29862
+ url: "https://bifrost-premium-https-v4.gw.postman.com/ws/proxy"
29812
29863
  });
29864
+ }
29865
+ async getWorkspaceGitRepoUrl(workspaceId) {
29866
+ const response = await this.proxyRequest(
29867
+ "workspaces",
29868
+ "GET",
29869
+ `/workspaces/${workspaceId}/filesystem`
29870
+ );
29813
29871
  if (response.status === 404) return null;
29814
29872
  if (!response.ok) return null;
29815
29873
  const body = await response.text();
@@ -29992,6 +30050,13 @@ function requireInput(actionCore, name) {
29992
30050
  function optionalInput(actionCore, name) {
29993
30051
  return normalizeInputValue(actionCore.getInput(name));
29994
30052
  }
30053
+ function parseBooleanInput(value, defaultValue) {
30054
+ const normalized = String(value || "").trim().toLowerCase();
30055
+ if (!normalized) return defaultValue;
30056
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
30057
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
30058
+ return defaultValue;
30059
+ }
29995
30060
  function parseCollectionSyncMode(value) {
29996
30061
  if (value === "reuse" || value === "version") {
29997
30062
  return value;
@@ -30036,6 +30101,7 @@ function resolveInputs(env = process.env) {
30036
30101
  baselineCollectionId: getInput("baseline-collection-id", env),
30037
30102
  smokeCollectionId: getInput("smoke-collection-id", env),
30038
30103
  contractCollectionId: getInput("contract-collection-id", env),
30104
+ syncExamples: parseBooleanInput(getInput("sync-examples", env), true),
30039
30105
  collectionSyncMode: parseCollectionSyncMode(getInput("collection-sync-mode", env)),
30040
30106
  specSyncMode: parseSpecSyncMode(getInput("spec-sync-mode", env)),
30041
30107
  releaseLabel: getInput("release-label", env),
@@ -30095,6 +30161,7 @@ function readActionInputs(actionCore) {
30095
30161
  INPUT_BASELINE_COLLECTION_ID: optionalInput(actionCore, "baseline-collection-id"),
30096
30162
  INPUT_SMOKE_COLLECTION_ID: optionalInput(actionCore, "smoke-collection-id"),
30097
30163
  INPUT_CONTRACT_COLLECTION_ID: optionalInput(actionCore, "contract-collection-id"),
30164
+ INPUT_SYNC_EXAMPLES: optionalInput(actionCore, "sync-examples") ?? openAlphaActionContract.inputs["sync-examples"].default,
30098
30165
  INPUT_COLLECTION_SYNC_MODE: optionalInput(actionCore, "collection-sync-mode") ?? openAlphaActionContract.inputs["collection-sync-mode"].default,
30099
30166
  INPUT_SPEC_SYNC_MODE: optionalInput(actionCore, "spec-sync-mode") ?? openAlphaActionContract.inputs["spec-sync-mode"].default,
30100
30167
  INPUT_RELEASE_LABEL: optionalInput(actionCore, "release-label"),
@@ -30667,6 +30734,48 @@ For CLI usage, pass --workspace-team-id <id> or export POSTMAN_WORKSPACE_TEAM_ID
30667
30734
  ]);
30668
30735
  }
30669
30736
  );
30737
+ const linkedCollectionIds = [
30738
+ outputs["baseline-collection-id"],
30739
+ outputs["smoke-collection-id"],
30740
+ outputs["contract-collection-id"]
30741
+ ].filter(Boolean);
30742
+ if (linkedCollectionIds.length > 0) {
30743
+ if (dependencies.internalIntegration) {
30744
+ await runGroup(
30745
+ dependencies.core,
30746
+ "Link Collections to Specification",
30747
+ async () => {
30748
+ await dependencies.internalIntegration?.linkCollectionsToSpecification(
30749
+ outputs["spec-id"],
30750
+ linkedCollectionIds.map((collectionId) => ({
30751
+ collectionId,
30752
+ syncOptions: {
30753
+ syncExamples: inputs.syncExamples
30754
+ }
30755
+ }))
30756
+ );
30757
+ }
30758
+ );
30759
+ await runGroup(
30760
+ dependencies.core,
30761
+ "Sync Linked Collections",
30762
+ async () => {
30763
+ await Promise.all(
30764
+ linkedCollectionIds.map(
30765
+ (collectionId) => dependencies.internalIntegration.syncCollection(
30766
+ outputs["spec-id"],
30767
+ collectionId
30768
+ )
30769
+ )
30770
+ );
30771
+ }
30772
+ );
30773
+ } else {
30774
+ dependencies.core.warning(
30775
+ "Skipping cloud spec-to-collection linking and sync because postman-access-token is not configured"
30776
+ );
30777
+ }
30778
+ }
30670
30779
  for (const [name, value] of Object.entries(outputs)) {
30671
30780
  dependencies.core.setOutput(name, value);
30672
30781
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postman-cse/onboarding-bootstrap",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Public open-alpha Postman bootstrap GitHub Action.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/cli.ts CHANGED
@@ -191,6 +191,7 @@ export function parseCliArgs(argv: string[], env: NodeJS.ProcessEnv = process.en
191
191
  'baseline-collection-id',
192
192
  'smoke-collection-id',
193
193
  'contract-collection-id',
194
+ 'sync-examples',
194
195
  'collection-sync-mode',
195
196
  'spec-sync-mode',
196
197
  'release-label',
package/src/contracts.ts CHANGED
@@ -43,6 +43,12 @@ export const openAlphaActionContract: BetaActionContract = {
43
43
  description: 'Existing contract collection ID.',
44
44
  required: false
45
45
  },
46
+ 'sync-examples': {
47
+ description: 'Whether linked spec/collection relations should enable example syncing.',
48
+ required: false,
49
+ default: 'true',
50
+ allowedValues: ['true', 'false']
51
+ },
46
52
  'collection-sync-mode': {
47
53
  description:
48
54
  'Collection lifecycle policy: reuse existing collections, refresh them from the latest spec, or version them by release label.',
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export interface ResolvedInputs {
19
19
  baselineCollectionId?: string;
20
20
  smokeCollectionId?: string;
21
21
  contractCollectionId?: string;
22
+ syncExamples: boolean;
22
23
  collectionSyncMode: 'reuse' | 'refresh' | 'version';
23
24
  specSyncMode: 'update' | 'version';
24
25
  releaseLabel?: string;
@@ -101,7 +102,7 @@ export interface BootstrapExecutionDependencies {
101
102
  io: IOLike;
102
103
  internalIntegration?: Pick<
103
104
  InternalIntegrationAdapter,
104
- 'assignWorkspaceToGovernanceGroup'
105
+ 'assignWorkspaceToGovernanceGroup' | 'linkCollectionsToSpecification' | 'syncCollection'
105
106
  >;
106
107
  postman: Pick<
107
108
  PostmanAssetsClient,
@@ -250,6 +251,7 @@ export function resolveInputs(
250
251
  baselineCollectionId: getInput('baseline-collection-id', env),
251
252
  smokeCollectionId: getInput('smoke-collection-id', env),
252
253
  contractCollectionId: getInput('contract-collection-id', env),
254
+ syncExamples: parseBooleanInput(getInput('sync-examples', env), true),
253
255
  collectionSyncMode: parseCollectionSyncMode(getInput('collection-sync-mode', env)),
254
256
  specSyncMode: parseSpecSyncMode(getInput('spec-sync-mode', env)),
255
257
  releaseLabel: getInput('release-label', env),
@@ -322,6 +324,9 @@ export function readActionInputs(
322
324
  INPUT_BASELINE_COLLECTION_ID: optionalInput(actionCore, 'baseline-collection-id'),
323
325
  INPUT_SMOKE_COLLECTION_ID: optionalInput(actionCore, 'smoke-collection-id'),
324
326
  INPUT_CONTRACT_COLLECTION_ID: optionalInput(actionCore, 'contract-collection-id'),
327
+ INPUT_SYNC_EXAMPLES:
328
+ optionalInput(actionCore, 'sync-examples') ??
329
+ openAlphaActionContract.inputs['sync-examples'].default,
325
330
  INPUT_COLLECTION_SYNC_MODE:
326
331
  optionalInput(actionCore, 'collection-sync-mode') ??
327
332
  openAlphaActionContract.inputs['collection-sync-mode'].default,
@@ -1025,6 +1030,51 @@ export async function runBootstrap(
1025
1030
  }
1026
1031
  );
1027
1032
 
1033
+ const linkedCollectionIds = [
1034
+ outputs['baseline-collection-id'],
1035
+ outputs['smoke-collection-id'],
1036
+ outputs['contract-collection-id']
1037
+ ].filter(Boolean);
1038
+
1039
+ if (linkedCollectionIds.length > 0) {
1040
+ if (dependencies.internalIntegration) {
1041
+ await runGroup(
1042
+ dependencies.core,
1043
+ 'Link Collections to Specification',
1044
+ async () => {
1045
+ await dependencies.internalIntegration?.linkCollectionsToSpecification(
1046
+ outputs['spec-id'],
1047
+ linkedCollectionIds.map((collectionId) => ({
1048
+ collectionId,
1049
+ syncOptions: {
1050
+ syncExamples: inputs.syncExamples
1051
+ }
1052
+ }))
1053
+ );
1054
+ }
1055
+ );
1056
+
1057
+ await runGroup(
1058
+ dependencies.core,
1059
+ 'Sync Linked Collections',
1060
+ async () => {
1061
+ await Promise.all(
1062
+ linkedCollectionIds.map((collectionId) =>
1063
+ dependencies.internalIntegration!.syncCollection(
1064
+ outputs['spec-id'],
1065
+ collectionId
1066
+ )
1067
+ )
1068
+ );
1069
+ }
1070
+ );
1071
+ } else {
1072
+ dependencies.core.warning(
1073
+ 'Skipping cloud spec-to-collection linking and sync because postman-access-token is not configured'
1074
+ );
1075
+ }
1076
+ }
1077
+
1028
1078
  for (const [name, value] of Object.entries(outputs)) {
1029
1079
  dependencies.core.setOutput(name, value);
1030
1080
  }
@@ -9,6 +9,13 @@ export interface GovernanceAssociation {
9
9
  systemEnvId: string;
10
10
  }
11
11
 
12
+ export interface SpecificationCollectionLink {
13
+ collectionId: string;
14
+ syncOptions?: {
15
+ syncExamples: boolean;
16
+ };
17
+ }
18
+
12
19
  export interface InternalIntegrationAdapterOptions {
13
20
  accessToken: string;
14
21
  backend: string;
@@ -32,6 +39,14 @@ export interface InternalIntegrationAdapter {
32
39
  workspaceId: string,
33
40
  repoUrl: string
34
41
  ): Promise<void>;
42
+ linkCollectionsToSpecification(
43
+ specificationId: string,
44
+ collections: SpecificationCollectionLink[]
45
+ ): Promise<void>;
46
+ syncCollection(
47
+ specificationId: string,
48
+ collectionId: string
49
+ ): Promise<void>;
35
50
  }
36
51
 
37
52
  class BifrostInternalIntegrationAdapter implements InternalIntegrationAdapter {
@@ -53,6 +68,33 @@ class BifrostInternalIntegrationAdapter implements InternalIntegrationAdapter {
53
68
  ).replace(/\/+$/, '');
54
69
  }
55
70
 
71
+ private async proxyRequest(
72
+ service: string,
73
+ method: string,
74
+ requestPath: string,
75
+ body?: unknown
76
+ ): Promise<Response> {
77
+ const url = 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy';
78
+ const headers: Record<string, string> = {
79
+ 'Content-Type': 'application/json',
80
+ 'x-access-token': this.accessToken
81
+ };
82
+ if (this.teamId) {
83
+ headers['x-entity-team-id'] = this.teamId;
84
+ }
85
+
86
+ return this.fetchImpl(url, {
87
+ method: 'POST',
88
+ headers,
89
+ body: JSON.stringify({
90
+ service,
91
+ method,
92
+ path: requestPath,
93
+ ...(body !== undefined ? { body } : {})
94
+ })
95
+ });
96
+ }
97
+
56
98
  async assignWorkspaceToGovernanceGroup(
57
99
  workspaceId: string,
58
100
  domain: string,
@@ -168,14 +210,6 @@ class BifrostInternalIntegrationAdapter implements InternalIntegrationAdapter {
168
210
  workspaceId: string,
169
211
  repoUrl: string
170
212
  ): Promise<void> {
171
- const url = 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy';
172
- const headers: Record<string, string> = {
173
- 'Content-Type': 'application/json',
174
- 'x-access-token': this.accessToken
175
- };
176
- if (this.teamId) {
177
- headers['x-entity-team-id'] = this.teamId;
178
- }
179
213
  const payload = {
180
214
  service: 'workspaces',
181
215
  method: 'POST',
@@ -187,11 +221,12 @@ class BifrostInternalIntegrationAdapter implements InternalIntegrationAdapter {
187
221
  }
188
222
  };
189
223
 
190
- const response = await this.fetchImpl(url, {
191
- method: 'POST',
192
- headers,
193
- body: JSON.stringify(payload)
194
- });
224
+ const response = await this.proxyRequest(
225
+ payload.service,
226
+ payload.method,
227
+ payload.path,
228
+ payload.body
229
+ );
195
230
 
196
231
  if (response.ok) return;
197
232
 
@@ -215,31 +250,82 @@ class BifrostInternalIntegrationAdapter implements InternalIntegrationAdapter {
215
250
 
216
251
  throw await HttpError.fromResponse(response, {
217
252
  method: 'POST',
218
- requestHeaders: headers,
253
+ requestHeaders: {
254
+ 'Content-Type': 'application/json',
255
+ 'x-access-token': this.accessToken,
256
+ ...(this.teamId ? { 'x-entity-team-id': this.teamId } : {})
257
+ },
219
258
  secretValues: [this.accessToken],
220
- url
259
+ url: 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy'
221
260
  });
222
261
  }
223
262
 
224
- private async getWorkspaceGitRepoUrl(workspaceId: string): Promise<string | null> {
225
- const url = 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy';
226
- const headers: Record<string, string> = {
227
- 'Content-Type': 'application/json',
228
- 'x-access-token': this.accessToken
229
- };
230
- if (this.teamId) {
231
- headers['x-entity-team-id'] = this.teamId;
263
+ async linkCollectionsToSpecification(
264
+ specificationId: string,
265
+ collections: SpecificationCollectionLink[]
266
+ ): Promise<void> {
267
+ if (collections.length === 0) {
268
+ return;
232
269
  }
233
270
 
234
- const response = await this.fetchImpl(url, {
271
+ const response = await this.proxyRequest(
272
+ 'specification',
273
+ 'put',
274
+ `/specifications/${specificationId}/collections`,
275
+ collections.map((collection) => ({
276
+ collectionId: collection.collectionId,
277
+ ...(collection.syncOptions ? { syncOptions: collection.syncOptions } : {})
278
+ }))
279
+ );
280
+
281
+ if (response.ok) {
282
+ return;
283
+ }
284
+
285
+ throw await HttpError.fromResponse(response, {
235
286
  method: 'POST',
236
- headers,
237
- body: JSON.stringify({
238
- service: 'workspaces',
239
- method: 'GET',
240
- path: `/workspaces/${workspaceId}/filesystem`
241
- })
287
+ requestHeaders: {
288
+ 'Content-Type': 'application/json',
289
+ 'x-access-token': this.accessToken,
290
+ ...(this.teamId ? { 'x-entity-team-id': this.teamId } : {})
291
+ },
292
+ secretValues: [this.accessToken],
293
+ url: 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy'
242
294
  });
295
+ }
296
+
297
+ async syncCollection(
298
+ specificationId: string,
299
+ collectionId: string
300
+ ): Promise<void> {
301
+ const response = await this.proxyRequest(
302
+ 'specification',
303
+ 'post',
304
+ `/specifications/${specificationId}/collections/${collectionId}/sync`
305
+ );
306
+
307
+ if (response.ok) {
308
+ return;
309
+ }
310
+
311
+ throw await HttpError.fromResponse(response, {
312
+ method: 'POST',
313
+ requestHeaders: {
314
+ 'Content-Type': 'application/json',
315
+ 'x-access-token': this.accessToken,
316
+ ...(this.teamId ? { 'x-entity-team-id': this.teamId } : {})
317
+ },
318
+ secretValues: [this.accessToken],
319
+ url: 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy'
320
+ });
321
+ }
322
+
323
+ private async getWorkspaceGitRepoUrl(workspaceId: string): Promise<string | null> {
324
+ const response = await this.proxyRequest(
325
+ 'workspaces',
326
+ 'GET',
327
+ `/workspaces/${workspaceId}/filesystem`
328
+ );
243
329
 
244
330
  if (response.status === 404) return null;
245
331
  if (!response.ok) return null;
@@ -60,6 +60,7 @@ function createCoreStub(values: Record<string, string> = {}) {
60
60
  function createInputs(overrides: Partial<ResolvedInputs> = {}): ResolvedInputs {
61
61
  return {
62
62
  projectName: 'core-payments',
63
+ syncExamples: true,
63
64
  collectionSyncMode: 'refresh',
64
65
  specSyncMode: 'update',
65
66
  releaseLabel: undefined,
@@ -148,7 +149,9 @@ describe('bootstrap action', () => {
148
149
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
149
150
  };
150
151
  const internalIntegration = {
151
- assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined)
152
+ assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined),
153
+ linkCollectionsToSpecification: vi.fn().mockResolvedValue(undefined),
154
+ syncCollection: vi.fn().mockResolvedValue(undefined)
152
155
  };
153
156
  const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
154
157
  new Response('openapi: 3.1.0', {
@@ -177,6 +180,29 @@ describe('bootstrap action', () => {
177
180
  );
178
181
  expect(postman.addAdminsToWorkspace).toHaveBeenCalledWith('ws-123', '101,102');
179
182
  expect(order).toEqual(['[Baseline]', '[Smoke]', '[Contract]']);
183
+ expect(internalIntegration.linkCollectionsToSpecification).toHaveBeenCalledWith(
184
+ 'spec-123',
185
+ [
186
+ { collectionId: 'col-baseline', syncOptions: { syncExamples: true } },
187
+ { collectionId: 'col-smoke', syncOptions: { syncExamples: true } },
188
+ { collectionId: 'col-contract', syncOptions: { syncExamples: true } }
189
+ ]
190
+ );
191
+ expect(internalIntegration.syncCollection).toHaveBeenNthCalledWith(
192
+ 1,
193
+ 'spec-123',
194
+ 'col-baseline'
195
+ );
196
+ expect(internalIntegration.syncCollection).toHaveBeenNthCalledWith(
197
+ 2,
198
+ 'spec-123',
199
+ 'col-smoke'
200
+ );
201
+ expect(internalIntegration.syncCollection).toHaveBeenNthCalledWith(
202
+ 3,
203
+ 'spec-123',
204
+ 'col-contract'
205
+ );
180
206
  expect(result).toMatchObject({
181
207
  'workspace-id': 'ws-123',
182
208
  'workspace-name': '[AF] core-payments',
@@ -730,7 +756,7 @@ describe('bootstrap action', () => {
730
756
  });
731
757
 
732
758
  it('skips governance assignment when postman-access-token is absent', async () => {
733
- const { core } = createCoreStub();
759
+ const { core, warnings } = createCoreStub();
734
760
  const execStub = createExecStub();
735
761
  const ioStub = createIoStub();
736
762
  const postman = {
@@ -764,6 +790,64 @@ describe('bootstrap action', () => {
764
790
 
765
791
  expect(result['workspace-id']).toBe('ws-123');
766
792
  expect(postman.createWorkspace).toHaveBeenCalled();
793
+ expect(
794
+ warnings.some((warning) =>
795
+ warning.includes('Skipping cloud spec-to-collection linking and sync because postman-access-token is not configured')
796
+ )
797
+ ).toBe(true);
798
+ });
799
+
800
+ it('passes syncExamples=false when configured', async () => {
801
+ const { core } = createCoreStub();
802
+ const execStub = createExecStub();
803
+ const ioStub = createIoStub();
804
+ const postman = {
805
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
806
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
807
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
808
+ generateCollection: vi
809
+ .fn()
810
+ .mockResolvedValueOnce('col-baseline')
811
+ .mockResolvedValueOnce('col-smoke')
812
+ .mockResolvedValueOnce('col-contract'),
813
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
814
+ getTeams: vi.fn().mockResolvedValue([]),
815
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
816
+ injectTests: vi.fn().mockResolvedValue(undefined),
817
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
818
+ tagCollection: vi.fn().mockResolvedValue(undefined),
819
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
820
+ updateSpec: vi.fn().mockResolvedValue(undefined),
821
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
822
+ };
823
+ const internalIntegration = {
824
+ assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined),
825
+ linkCollectionsToSpecification: vi.fn().mockResolvedValue(undefined),
826
+ syncCollection: vi.fn().mockResolvedValue(undefined)
827
+ };
828
+
829
+ await runBootstrap(
830
+ createInputs({ syncExamples: false }),
831
+ {
832
+ core,
833
+ exec: execStub,
834
+ io: ioStub,
835
+ internalIntegration,
836
+ postman,
837
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
838
+ new Response('openapi: 3.1.0', { status: 200 })
839
+ )
840
+ }
841
+ );
842
+
843
+ expect(internalIntegration.linkCollectionsToSpecification).toHaveBeenCalledWith(
844
+ 'spec-123',
845
+ [
846
+ { collectionId: 'col-baseline', syncOptions: { syncExamples: false } },
847
+ { collectionId: 'col-smoke', syncOptions: { syncExamples: false } },
848
+ { collectionId: 'col-contract', syncOptions: { syncExamples: false } }
849
+ ]
850
+ );
767
851
  });
768
852
 
769
853
  it('runs without any GitHub dependency', async () => {
package/tests/cli.test.ts CHANGED
@@ -14,6 +14,8 @@ describe('parseCliArgs', () => {
14
14
  'pmak-test',
15
15
  '--workspace-admin-user-ids',
16
16
  '101,102',
17
+ '--sync-examples',
18
+ 'false',
17
19
  '--collection-sync-mode',
18
20
  'version',
19
21
  '--spec-sync-mode',
@@ -36,6 +38,7 @@ describe('parseCliArgs', () => {
36
38
  expect(config.inputEnv.INPUT_SPEC_URL).toBe('https://example.test/openapi.yaml');
37
39
  expect(config.inputEnv.INPUT_POSTMAN_API_KEY).toBe('pmak-test');
38
40
  expect(config.inputEnv.INPUT_WORKSPACE_ADMIN_USER_IDS).toBe('101,102');
41
+ expect(config.inputEnv.INPUT_SYNC_EXAMPLES).toBe('false');
39
42
  expect(config.inputEnv.INPUT_COLLECTION_SYNC_MODE).toBe('version');
40
43
  expect(config.inputEnv.INPUT_SPEC_SYNC_MODE).toBe('version');
41
44
  expect(config.inputEnv.INPUT_RELEASE_LABEL).toBe('v1.2.3');
@@ -42,6 +42,14 @@ describe('open-alpha action contract', () => {
42
42
  });
43
43
 
44
44
  it('defaults lifecycle controls in contract, manifest, and runtime', () => {
45
+ expect(openAlphaActionContract.inputs['sync-examples'].default).toBe('true');
46
+ expect(openAlphaActionContract.inputs['sync-examples'].allowedValues).toEqual([
47
+ 'true',
48
+ 'false'
49
+ ]);
50
+ expect(actionManifest.inputs['sync-examples'].default).toBe('true');
51
+ expect(resolveInputs({}).syncExamples).toBe(true);
52
+
45
53
  expect(openAlphaActionContract.inputs['collection-sync-mode'].default).toBe('refresh');
46
54
  expect(openAlphaActionContract.inputs['collection-sync-mode'].allowedValues).toEqual([
47
55
  'reuse',
@@ -184,6 +184,75 @@ describe('internal integration adapter', () => {
184
184
  );
185
185
  });
186
186
 
187
+ it('routes specification collection linking through the Bifrost proxy with sync options', async () => {
188
+ const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
189
+ jsonResponse({ data: { updated: 1 } })
190
+ );
191
+
192
+ const adapter = createInternalIntegrationAdapter({
193
+ backend: 'bifrost',
194
+ accessToken: 'token-123',
195
+ teamId: '11430732',
196
+ fetchImpl
197
+ });
198
+
199
+ await adapter.linkCollectionsToSpecification('spec-123', [
200
+ {
201
+ collectionId: 'col-1',
202
+ syncOptions: { syncExamples: true }
203
+ }
204
+ ]);
205
+
206
+ expect(fetchImpl).toHaveBeenCalledWith(
207
+ 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy',
208
+ expect.objectContaining({
209
+ method: 'POST',
210
+ headers: expect.objectContaining({
211
+ 'x-access-token': 'token-123',
212
+ 'x-entity-team-id': '11430732'
213
+ }),
214
+ body: JSON.stringify({
215
+ service: 'specification',
216
+ method: 'put',
217
+ path: '/specifications/spec-123/collections',
218
+ body: [
219
+ {
220
+ collectionId: 'col-1',
221
+ syncOptions: { syncExamples: true }
222
+ }
223
+ ]
224
+ })
225
+ })
226
+ );
227
+ });
228
+
229
+ it('routes specification collection sync through the Bifrost proxy', async () => {
230
+ const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
231
+ jsonResponse({ data: { taskId: 'task-1' } })
232
+ );
233
+
234
+ const adapter = createInternalIntegrationAdapter({
235
+ backend: 'bifrost',
236
+ accessToken: 'token-123',
237
+ teamId: '11430732',
238
+ fetchImpl
239
+ });
240
+
241
+ await adapter.syncCollection('spec-123', 'col-1');
242
+
243
+ expect(fetchImpl).toHaveBeenCalledWith(
244
+ 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy',
245
+ expect.objectContaining({
246
+ method: 'POST',
247
+ body: JSON.stringify({
248
+ service: 'specification',
249
+ method: 'post',
250
+ path: '/specifications/spec-123/collections/col-1/sync'
251
+ })
252
+ })
253
+ );
254
+ });
255
+
187
256
  it('treats projectAlreadyConnected as idempotent when the same repo is linked', async () => {
188
257
  const fetchImpl = vi.fn<typeof fetch>()
189
258
  // First call: connectWorkspaceToRepository POST returns 400 projectAlreadyConnected