@postman-cse/onboarding-bootstrap 0.9.1 → 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/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as core from '@actions/core';
2
2
  import * as exec from '@actions/exec';
3
3
  import * as io from '@actions/io';
4
+ import { readFileSync } from 'node:fs';
4
5
  import { parse, stringify } from 'yaml';
5
6
 
6
7
  import { openAlphaActionContract } from './contracts.js';
7
- import { GitHubApiClient, type GitHubApiClientAuthMode } from './lib/github/github-api-client.js';
8
8
  import { createInternalIntegrationAdapter, type InternalIntegrationAdapter } from './lib/postman/internal-integration-adapter.js';
9
9
  import { PostmanAssetsClient } from './lib/postman/postman-assets-client.js';
10
10
  import { resolveCanonicalWorkspaceSelection } from './lib/postman/workspace-selection.js';
@@ -19,22 +19,26 @@ export interface ResolvedInputs {
19
19
  baselineCollectionId?: string;
20
20
  smokeCollectionId?: string;
21
21
  contractCollectionId?: string;
22
+ syncExamples: boolean;
23
+ collectionSyncMode: 'reuse' | 'refresh' | 'version';
24
+ specSyncMode: 'update' | 'version';
25
+ releaseLabel?: string;
22
26
  domain?: string;
23
27
  domainCode?: string;
24
28
  requesterEmail?: string;
25
29
  workspaceAdminUserIds?: string;
30
+ workspaceTeamId?: string;
26
31
  teamId?: string;
27
32
  repoUrl?: string;
28
33
  specUrl: string;
29
- environmentsJson: string;
30
- systemEnvMapJson: string;
31
34
  governanceMappingJson: string;
32
35
  postmanApiKey: string;
33
36
  postmanAccessToken?: string;
34
- githubToken?: string;
35
- ghFallbackToken?: string;
36
- githubAuthMode: string;
37
37
  integrationBackend: string;
38
+ githubRefName?: string;
39
+ githubHeadRef?: string;
40
+ githubRef?: string;
41
+ githubSha?: string;
38
42
  }
39
43
 
40
44
  export interface PlannedOutputs {
@@ -61,13 +65,6 @@ export interface LintSummary {
61
65
  warnings: number;
62
66
  }
63
67
 
64
- interface BootstrapRepositoryVariables {
65
- lintErrors: number;
66
- lintWarnings: number;
67
- }
68
-
69
- type RepoVariableClient = Pick<GitHubApiClient, 'setRepositoryVariable' | 'getRepositoryVariable'>;
70
-
71
68
  export interface CoreLike {
72
69
  error(message: string): void;
73
70
  getInput(name: string, options?: { required?: boolean }): string;
@@ -102,11 +99,10 @@ export interface BootstrapExecutionDependencies {
102
99
  'error' | 'group' | 'info' | 'setOutput' | 'warning'
103
100
  >;
104
101
  exec: ExecLike;
105
- github?: RepoVariableClient;
106
102
  io: IOLike;
107
103
  internalIntegration?: Pick<
108
104
  InternalIntegrationAdapter,
109
- 'assignWorkspaceToGovernanceGroup'
105
+ 'assignWorkspaceToGovernanceGroup' | 'linkCollectionsToSpecification' | 'syncCollection'
110
106
  >;
111
107
  postman: Pick<
112
108
  PostmanAssetsClient,
@@ -116,6 +112,7 @@ export interface BootstrapExecutionDependencies {
116
112
  | 'generateCollection'
117
113
  | 'getAutoDerivedTeamId'
118
114
  | 'getSpecContent'
115
+ | 'getTeams'
119
116
  | 'getWorkspaceGitRepoUrl'
120
117
  | 'injectTests'
121
118
  | 'inviteRequesterToWorkspace'
@@ -185,16 +182,28 @@ function asStringArray(value: unknown, inputName: string): string[] {
185
182
  return value.map((entry) => String(entry));
186
183
  }
187
184
 
188
- function asStringMap(value: unknown, inputName: string): Record<string, string> {
189
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
190
- throw new Error(`${inputName} must be a JSON object`);
185
+ function parseBooleanInput(value: string | undefined, defaultValue: boolean): boolean {
186
+ const normalized = String(value || '').trim().toLowerCase();
187
+ if (!normalized) return defaultValue;
188
+ if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
189
+ if (['false', '0', 'no', 'off'].includes(normalized)) return false;
190
+ return defaultValue;
191
+ }
192
+
193
+ function parseCollectionSyncMode(
194
+ value: string | undefined
195
+ ): 'reuse' | 'refresh' | 'version' {
196
+ if (value === 'reuse' || value === 'version') {
197
+ return value;
191
198
  }
192
- return Object.fromEntries(
193
- Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
194
- key,
195
- String(entry)
196
- ])
197
- );
199
+ return 'refresh';
200
+ }
201
+
202
+ function parseSpecSyncMode(value: string | undefined): 'update' | 'version' {
203
+ if (value === 'version') {
204
+ return value;
205
+ }
206
+ return 'update';
198
207
  }
199
208
 
200
209
  export function resolveInputs(
@@ -233,41 +242,39 @@ export function resolveInputs(
233
242
  }
234
243
 
235
244
  return {
236
- projectName: getInput('project-name', env) ?? '',
245
+ projectName: getInput('project-name', env)
246
+ ?? env.GITHUB_REPOSITORY?.split('/').pop()
247
+ ?? env.CI_PROJECT_NAME
248
+ ?? '',
237
249
  workspaceId: getInput('workspace-id', env),
238
250
  specId: getInput('spec-id', env),
239
251
  baselineCollectionId: getInput('baseline-collection-id', env),
240
252
  smokeCollectionId: getInput('smoke-collection-id', env),
241
253
  contractCollectionId: getInput('contract-collection-id', env),
254
+ syncExamples: parseBooleanInput(getInput('sync-examples', env), true),
255
+ collectionSyncMode: parseCollectionSyncMode(getInput('collection-sync-mode', env)),
256
+ specSyncMode: parseSpecSyncMode(getInput('spec-sync-mode', env)),
257
+ releaseLabel: getInput('release-label', env),
242
258
  domain: getInput('domain', env),
243
259
  domainCode: getInput('domain-code', env),
244
260
  requesterEmail: getInput('requester-email', env),
245
261
  workspaceAdminUserIds:
246
262
  getInput('workspace-admin-user-ids', env) || env.WORKSPACE_ADMIN_USER_IDS || '',
263
+ workspaceTeamId: getInput('workspace-team-id', env) || env.POSTMAN_WORKSPACE_TEAM_ID,
247
264
  teamId: getInput('team-id', env) || env.POSTMAN_TEAM_ID || '',
248
265
  repoUrl: repoContext.repoUrl || '',
249
266
  specUrl,
250
- environmentsJson:
251
- getInput('environments-json', env) ??
252
- openAlphaActionContract.inputs['environments-json'].default ??
253
- '["prod"]',
254
- systemEnvMapJson:
255
- getInput('system-env-map-json', env) ??
256
- openAlphaActionContract.inputs['system-env-map-json'].default ??
257
- '{}',
258
267
  governanceMappingJson:
259
268
  getInput('governance-mapping-json', env) ??
260
269
  openAlphaActionContract.inputs['governance-mapping-json'].default ??
261
270
  '{}',
262
271
  postmanApiKey: getInput('postman-api-key', env) ?? '',
263
272
  postmanAccessToken: getInput('postman-access-token', env),
264
- githubToken: getInput('github-token', env),
265
- ghFallbackToken: getInput('gh-fallback-token', env),
266
- githubAuthMode:
267
- getInput('github-auth-mode', env) ??
268
- openAlphaActionContract.inputs['github-auth-mode'].default ??
269
- 'github_token_first',
270
- integrationBackend
273
+ integrationBackend,
274
+ githubRefName: env.GITHUB_REF_NAME,
275
+ githubHeadRef: env.GITHUB_HEAD_REF,
276
+ githubRef: env.GITHUB_REF,
277
+ githubSha: env.GITHUB_SHA
271
278
  };
272
279
  }
273
280
 
@@ -305,13 +312,9 @@ export function readActionInputs(
305
312
  const specUrl = requireInput(actionCore, 'spec-url');
306
313
  const postmanApiKey = requireInput(actionCore, 'postman-api-key');
307
314
  const postmanAccessToken = optionalInput(actionCore, 'postman-access-token');
308
- const githubToken = optionalInput(actionCore, 'github-token');
309
- const ghFallbackToken = optionalInput(actionCore, 'gh-fallback-token');
310
315
 
311
316
  actionCore.setSecret(postmanApiKey);
312
317
  if (postmanAccessToken) actionCore.setSecret(postmanAccessToken);
313
- if (githubToken) actionCore.setSecret(githubToken);
314
- if (ghFallbackToken) actionCore.setSecret(ghFallbackToken);
315
318
 
316
319
  const inputs = resolveInputs({
317
320
  ...process.env,
@@ -321,6 +324,16 @@ export function readActionInputs(
321
324
  INPUT_BASELINE_COLLECTION_ID: optionalInput(actionCore, 'baseline-collection-id'),
322
325
  INPUT_SMOKE_COLLECTION_ID: optionalInput(actionCore, 'smoke-collection-id'),
323
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,
330
+ INPUT_COLLECTION_SYNC_MODE:
331
+ optionalInput(actionCore, 'collection-sync-mode') ??
332
+ openAlphaActionContract.inputs['collection-sync-mode'].default,
333
+ INPUT_SPEC_SYNC_MODE:
334
+ optionalInput(actionCore, 'spec-sync-mode') ??
335
+ openAlphaActionContract.inputs['spec-sync-mode'].default,
336
+ INPUT_RELEASE_LABEL: optionalInput(actionCore, 'release-label'),
324
337
  INPUT_DOMAIN: optionalInput(actionCore, 'domain'),
325
338
  INPUT_DOMAIN_CODE: optionalInput(actionCore, 'domain-code'),
326
339
  INPUT_REQUESTER_EMAIL: optionalInput(actionCore, 'requester-email'),
@@ -328,26 +341,17 @@ export function readActionInputs(
328
341
  actionCore,
329
342
  'workspace-admin-user-ids'
330
343
  ),
344
+ INPUT_WORKSPACE_TEAM_ID:
345
+ optionalInput(actionCore, 'workspace-team-id') || process.env.POSTMAN_WORKSPACE_TEAM_ID,
331
346
  INPUT_TEAM_ID:
332
347
  optionalInput(actionCore, 'postman-team-id') || process.env.POSTMAN_TEAM_ID,
333
348
  INPUT_REPO_URL: optionalInput(actionCore, 'repo-url'),
334
349
  INPUT_SPEC_URL: specUrl,
335
- INPUT_ENVIRONMENTS_JSON:
336
- optionalInput(actionCore, 'environments-json') ??
337
- openAlphaActionContract.inputs['environments-json'].default,
338
- INPUT_SYSTEM_ENV_MAP_JSON:
339
- optionalInput(actionCore, 'system-env-map-json') ??
340
- openAlphaActionContract.inputs['system-env-map-json'].default,
341
350
  INPUT_GOVERNANCE_MAPPING_JSON:
342
351
  optionalInput(actionCore, 'governance-mapping-json') ??
343
352
  openAlphaActionContract.inputs['governance-mapping-json'].default,
344
353
  INPUT_POSTMAN_API_KEY: postmanApiKey,
345
354
  INPUT_POSTMAN_ACCESS_TOKEN: postmanAccessToken,
346
- INPUT_GITHUB_TOKEN: githubToken,
347
- INPUT_GH_FALLBACK_TOKEN: ghFallbackToken,
348
- INPUT_GITHUB_AUTH_MODE:
349
- optionalInput(actionCore, 'github-auth-mode') ??
350
- openAlphaActionContract.inputs['github-auth-mode'].default,
351
355
  INPUT_INTEGRATION_BACKEND:
352
356
  optionalInput(actionCore, 'integration-backend') ??
353
357
  openAlphaActionContract.inputs['integration-backend'].default
@@ -456,6 +460,83 @@ async function fetchSpecDocument(
456
460
  );
457
461
  }
458
462
 
463
+ function normalizeReleaseLabel(value: string | undefined): string | undefined {
464
+ const trimmed = String(value || '').trim();
465
+ if (!trimmed) {
466
+ return undefined;
467
+ }
468
+
469
+ return trimmed
470
+ .replace(/^refs\/heads\//, '')
471
+ .replace(/^refs\/tags\//, '')
472
+ .replace(/^refs\/pull\//, 'pull-')
473
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
474
+ .replace(/^-+|-+$/g, '') || undefined;
475
+ }
476
+
477
+ function deriveReleaseLabel(inputs: ResolvedInputs): string | undefined {
478
+ if (inputs.releaseLabel) {
479
+ return normalizeReleaseLabel(inputs.releaseLabel);
480
+ }
481
+
482
+ return (
483
+ normalizeReleaseLabel(inputs.githubRefName) ??
484
+ normalizeReleaseLabel(inputs.githubHeadRef) ??
485
+ normalizeReleaseLabel(inputs.githubRef)
486
+ );
487
+ }
488
+
489
+ function createAssetProjectName(
490
+ inputs: ResolvedInputs,
491
+ releaseLabel?: string
492
+ ): string {
493
+ if (!releaseLabel) {
494
+ return inputs.projectName;
495
+ }
496
+
497
+ return `${inputs.projectName} ${releaseLabel}`;
498
+ }
499
+
500
+ type CloudResourceMap = Record<string, string>;
501
+
502
+ type PostmanResourcesState = {
503
+ workspace?: {
504
+ id?: string;
505
+ };
506
+ cloudResources?: {
507
+ collections?: CloudResourceMap;
508
+ environments?: CloudResourceMap;
509
+ specs?: CloudResourceMap;
510
+ };
511
+ };
512
+
513
+ function readResourcesState(): PostmanResourcesState | null {
514
+ try {
515
+ return parse(readFileSync('.postman/resources.yaml', 'utf8')) as PostmanResourcesState;
516
+ } catch {
517
+ return null;
518
+ }
519
+ }
520
+
521
+ function getFirstCloudResourceId(map: CloudResourceMap | undefined): string | undefined {
522
+ if (!map) {
523
+ return undefined;
524
+ }
525
+ return Object.values(map)[0];
526
+ }
527
+
528
+ function findCloudResourceId(
529
+ map: CloudResourceMap | undefined,
530
+ matcher: (path: string) => boolean
531
+ ): string | undefined {
532
+ if (!map) {
533
+ return undefined;
534
+ }
535
+
536
+ const match = Object.entries(map).find(([filePath]) => matcher(filePath));
537
+ return match?.[1];
538
+ }
539
+
459
540
  const SPEC_SUMMARY_MAX_LEN = 200;
460
541
  const SPEC_HTTP_METHODS = new Set([
461
542
  'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'
@@ -542,146 +623,19 @@ function validateSpecStructure(content: string): void {
542
623
  }
543
624
  }
544
625
 
545
- function varName(projectName: string, baseName: string): string {
546
- const slug = projectName.toUpperCase().replace(/[^A-Z0-9]/g, '_');
547
- return `POSTMAN_${slug}_${baseName}`;
548
- }
549
-
550
- async function readVariable(
551
- github: RepoVariableClient | undefined,
552
- projectName: string,
553
- baseName: string
554
- ): Promise<string | undefined> {
555
- if (!github) {
556
- return undefined;
557
- }
558
-
559
- const namespaced = await github
560
- .getRepositoryVariable(varName(projectName, baseName))
561
- .catch(() => undefined);
562
- if (namespaced) {
563
- return namespaced;
564
- }
565
-
566
- const legacy = await github
567
- .getRepositoryVariable(`POSTMAN_${baseName}`)
568
- .catch(() => undefined);
569
- return legacy || undefined;
570
- }
571
-
572
- async function persistVariable(
573
- github: RepoVariableClient | undefined,
574
- name: string,
575
- value: string,
576
- actionCore: Pick<CoreLike, 'warning'>
577
- ): Promise<void> {
578
- if (!github || !value) {
579
- return;
580
- }
581
-
582
- try {
583
- await github.setRepositoryVariable(name, value);
584
- } catch (err) {
585
- actionCore.warning(
586
- `Failed to persist ${name}: ${err instanceof Error ? err.message : String(err)}`
587
- );
588
- }
589
- }
590
-
591
- async function writeVariable(
592
- github: RepoVariableClient | undefined,
593
- projectName: string,
594
- baseName: string,
595
- value: string,
596
- actionCore: Pick<CoreLike, 'warning'>
597
- ): Promise<void> {
598
- if (!github || !value) {
599
- return;
600
- }
601
-
602
- await persistVariable(github, varName(projectName, baseName), value, actionCore);
603
- await persistVariable(github, `POSTMAN_${baseName}`, value, actionCore);
604
- }
605
-
606
- async function persistBootstrapRepositoryVariables(
607
- github: RepoVariableClient,
608
- projectName: string,
609
- outputs: PlannedOutputs,
610
- systemEnvMap: Record<string, string>,
611
- environments: string[],
612
- lintSummary: BootstrapRepositoryVariables,
613
- actionCore: Pick<CoreLike, 'warning'>
614
- ): Promise<void> {
615
- await persistVariable(
616
- github,
617
- 'LINT_WARNINGS',
618
- String(lintSummary.lintWarnings),
619
- actionCore
620
- );
621
- await persistVariable(
622
- github,
623
- 'LINT_ERRORS',
624
- String(lintSummary.lintErrors),
625
- actionCore
626
- );
627
- await writeVariable(
628
- github,
629
- projectName,
630
- 'WORKSPACE_ID',
631
- outputs['workspace-id'],
632
- actionCore
633
- );
634
- await writeVariable(github, projectName, 'SPEC_UID', outputs['spec-id'], actionCore);
635
- await writeVariable(
636
- github,
637
- projectName,
638
- 'BASELINE_COLLECTION_UID',
639
- outputs['baseline-collection-id'],
640
- actionCore
641
- );
642
- await writeVariable(
643
- github,
644
- projectName,
645
- 'SMOKE_COLLECTION_UID',
646
- outputs['smoke-collection-id'],
647
- actionCore
648
- );
649
- await writeVariable(
650
- github,
651
- projectName,
652
- 'CONTRACT_COLLECTION_UID',
653
- outputs['contract-collection-id'],
654
- actionCore
655
- );
656
-
657
- for (const envName of environments) {
658
- const systemEnvId = systemEnvMap[envName];
659
- if (!systemEnvId) {
660
- continue;
661
- }
662
- await writeVariable(
663
- github,
664
- projectName,
665
- `SYSTEM_ENV_${envName.toUpperCase()}`,
666
- systemEnvId,
667
- actionCore
668
- );
669
- }
670
- }
671
-
672
626
  export async function runBootstrap(
673
627
  inputs: ResolvedInputs,
674
628
  dependencies: BootstrapExecutionDependencies
675
629
  ): Promise<PlannedOutputs> {
676
630
  const outputs = createPlannedOutputs(inputs);
677
- const environments = asStringArray(
678
- parseJsonValue(inputs.environmentsJson, ['prod'], 'environments-json'),
679
- 'environments-json'
680
- );
681
- const systemEnvMap = asStringMap(
682
- parseJsonValue(inputs.systemEnvMapJson, {}, 'system-env-map-json'),
683
- 'system-env-map-json'
684
- );
631
+ const requiresReleaseLabel =
632
+ inputs.collectionSyncMode === 'version' || inputs.specSyncMode === 'version';
633
+ const releaseLabel = requiresReleaseLabel ? deriveReleaseLabel(inputs) : undefined;
634
+ if (requiresReleaseLabel && !releaseLabel) {
635
+ throw new Error(
636
+ 'Versioned spec or collection sync requires a release-label or derivable GitHub ref metadata'
637
+ );
638
+ }
685
639
  const workspaceName = createWorkspaceName(inputs);
686
640
  const aboutText = `Auto-provisioned by Postman CS open-alpha for ${inputs.projectName}`;
687
641
 
@@ -689,19 +643,17 @@ export async function runBootstrap(
689
643
  await ensurePostmanCli(dependencies, inputs.postmanApiKey);
690
644
  });
691
645
 
646
+ const resourcesState = readResourcesState();
692
647
 
693
- const explicitWorkspaceId = inputs.workspaceId;
694
- let repoWorkspaceId: string | undefined;
695
- let workspaceId = explicitWorkspaceId;
696
- if (!workspaceId && dependencies.github) {
697
- repoWorkspaceId = await readVariable(
698
- dependencies.github,
699
- inputs.projectName,
700
- 'WORKSPACE_ID'
701
- );
702
- workspaceId = repoWorkspaceId;
648
+ let explicitWorkspaceId = inputs.workspaceId;
649
+ if (!explicitWorkspaceId && resourcesState?.workspace?.id) {
650
+ explicitWorkspaceId = resourcesState.workspace.id;
651
+ dependencies.core.info('Resolved workspace-id from .postman/resources.yaml');
703
652
  }
704
653
 
654
+ const repoWorkspaceId = explicitWorkspaceId;
655
+ let workspaceId = explicitWorkspaceId;
656
+
705
657
  let teamId = inputs.teamId || '';
706
658
  if (!teamId) {
707
659
  teamId = await dependencies.postman.getAutoDerivedTeamId() || '';
@@ -738,11 +690,66 @@ export async function runBootstrap(
738
690
  dependencies.core.info(`Using existing workspace: ${workspaceId}`);
739
691
  }
740
692
 
693
+ // Parse workspace-team-id from already-resolved inputs
694
+ let workspaceTeamId: number | undefined;
695
+ if (inputs.workspaceTeamId) {
696
+ workspaceTeamId = parseInt(inputs.workspaceTeamId, 10);
697
+ if (Number.isNaN(workspaceTeamId)) {
698
+ throw new Error(`workspace-team-id must be a numeric sub-team ID, got: ${inputs.workspaceTeamId}`);
699
+ }
700
+ }
701
+
702
+ // Org-mode detection: only check if we need to create a workspace (not reuse existing)
703
+ if (!workspaceId && !workspaceTeamId) {
704
+ try {
705
+ const teams = await dependencies.postman.getTeams();
706
+ if (teams.length > 1 && teams.every(t => t.organizationId == null)) {
707
+ dependencies.core.warning(
708
+ 'GET /teams returned multiple teams but none include organizationId. ' +
709
+ 'Org-mode detection may be degraded due to an upstream API change. ' +
710
+ 'If workspace creation fails, set workspace-team-id explicitly.'
711
+ );
712
+ }
713
+ const orgIds = new Set(teams.filter(t => t.organizationId != null).map(t => t.organizationId));
714
+ const meTeamId = parseInt(teamId, 10);
715
+ const isOrgMode = teams.length > 1
716
+ && orgIds.size === 1
717
+ && orgIds.has(meTeamId);
718
+
719
+ if (isOrgMode) {
720
+ const teamList = teams
721
+ .map(t => ` ${t.id} ${t.name}`)
722
+ .join('\n');
723
+ throw new Error(
724
+ `Org-mode account detected. Workspace creation requires a specific sub-team ID.\n\n` +
725
+ `Available sub-teams:\n${teamList}\n\n` +
726
+ `To fix this, set the workspace-team-id input in your workflow:\n` +
727
+ ` workspace-team-id: '<id>'\n\n` +
728
+ `Or for reuse across runs, create a repository variable and reference it:\n` +
729
+ ` workspace-team-id: \${{ vars.POSTMAN_WORKSPACE_TEAM_ID }}\n\n` +
730
+ `For CLI usage, pass --workspace-team-id <id> or export POSTMAN_WORKSPACE_TEAM_ID=<id>.`
731
+ );
732
+ } else if (teams.length > 1) {
733
+ dependencies.core.warning(
734
+ `API key has access to ${teams.length} teams but org-mode could not be confirmed. ` +
735
+ `Proceeding without teamId. If workspace creation fails, set workspace-team-id explicitly.`
736
+ );
737
+ }
738
+ } catch (err) {
739
+ if (err instanceof Error && err.message.includes('Org-mode account detected')) {
740
+ throw err;
741
+ }
742
+ dependencies.core.warning(
743
+ `Could not check for org-mode sub-teams: ${err instanceof Error ? err.message : String(err)}`
744
+ );
745
+ }
746
+ }
747
+
741
748
  if (!workspaceId) {
742
749
  const workspace = await runGroup(
743
750
  dependencies.core,
744
751
  'Create Postman Workspace',
745
- async () => dependencies.postman.createWorkspace(workspaceName, aboutText)
752
+ async () => dependencies.postman.createWorkspace(workspaceName, aboutText, workspaceTeamId)
746
753
  );
747
754
  workspaceId = workspace.id;
748
755
  }
@@ -750,14 +757,6 @@ export async function runBootstrap(
750
757
  outputs['workspace-id'] = workspaceId || '';
751
758
  outputs['workspace-url'] = `https://go.postman.co/workspace/${workspaceId}`;
752
759
  outputs['workspace-name'] = workspaceName;
753
- await writeVariable(
754
- dependencies.github,
755
- inputs.projectName,
756
- 'WORKSPACE_ID',
757
- outputs['workspace-id'],
758
- dependencies.core
759
- );
760
-
761
760
 
762
761
  if (inputs.domain && dependencies.internalIntegration) {
763
762
  await runGroup(
@@ -818,37 +817,49 @@ export async function runBootstrap(
818
817
  );
819
818
  }
820
819
 
821
-
822
820
  let specId = inputs.specId;
823
- if (!specId && dependencies.github) {
824
- specId = await readVariable(dependencies.github, inputs.projectName, 'SPEC_UID');
821
+ if (!specId) {
822
+ specId = getFirstCloudResourceId(resourcesState?.cloudResources?.specs);
823
+ if (specId) {
824
+ dependencies.core.info('Resolved spec-id from .postman/resources.yaml');
825
+ }
825
826
  }
826
827
 
827
- let baselineCollectionId = inputs.baselineCollectionId;
828
- let smokeCollectionId = inputs.smokeCollectionId;
829
- let contractCollectionId = inputs.contractCollectionId;
828
+ let baselineCollectionId =
829
+ inputs.collectionSyncMode === 'refresh' ? undefined : inputs.baselineCollectionId;
830
+ let smokeCollectionId =
831
+ inputs.collectionSyncMode === 'refresh' ? undefined : inputs.smokeCollectionId;
832
+ let contractCollectionId =
833
+ inputs.collectionSyncMode === 'refresh' ? undefined : inputs.contractCollectionId;
830
834
 
831
- if (dependencies.github) {
835
+ if (inputs.collectionSyncMode !== 'refresh') {
836
+ const cloudCollections = resourcesState?.cloudResources?.collections;
832
837
  if (!baselineCollectionId) {
833
- baselineCollectionId = await readVariable(
834
- dependencies.github,
835
- inputs.projectName,
836
- 'BASELINE_COLLECTION_UID'
838
+ baselineCollectionId = findCloudResourceId(
839
+ cloudCollections,
840
+ (filePath) => filePath.includes('[Baseline]')
837
841
  );
842
+ if (baselineCollectionId) {
843
+ dependencies.core.info('Resolved baseline-collection-id from .postman/resources.yaml');
844
+ }
838
845
  }
839
846
  if (!smokeCollectionId) {
840
- smokeCollectionId = await readVariable(
841
- dependencies.github,
842
- inputs.projectName,
843
- 'SMOKE_COLLECTION_UID'
847
+ smokeCollectionId = findCloudResourceId(
848
+ cloudCollections,
849
+ (filePath) => filePath.includes('[Smoke]')
844
850
  );
851
+ if (smokeCollectionId) {
852
+ dependencies.core.info('Resolved smoke-collection-id from .postman/resources.yaml');
853
+ }
845
854
  }
846
855
  if (!contractCollectionId) {
847
- contractCollectionId = await readVariable(
848
- dependencies.github,
849
- inputs.projectName,
850
- 'CONTRACT_COLLECTION_UID'
856
+ contractCollectionId = findCloudResourceId(
857
+ cloudCollections,
858
+ (filePath) => filePath.includes('[Contract]')
851
859
  );
860
+ if (contractCollectionId) {
861
+ dependencies.core.info('Resolved contract-collection-id from .postman/resources.yaml');
862
+ }
852
863
  }
853
864
  }
854
865
 
@@ -874,7 +885,10 @@ export async function runBootstrap(
874
885
  } else {
875
886
  specId = await dependencies.postman.uploadSpec(
876
887
  workspaceId || '',
877
- inputs.projectName,
888
+ createAssetProjectName(
889
+ inputs,
890
+ inputs.specSyncMode === 'version' ? releaseLabel : undefined
891
+ ),
878
892
  document
879
893
  );
880
894
  }
@@ -884,13 +898,6 @@ export async function runBootstrap(
884
898
  );
885
899
 
886
900
  void specContent;
887
- await writeVariable(
888
- dependencies.github,
889
- inputs.projectName,
890
- 'SPEC_UID',
891
- outputs['spec-id'],
892
- dependencies.core
893
- );
894
901
 
895
902
  const lintSummary = await runGroup(
896
903
  dependencies.core,
@@ -936,14 +943,23 @@ export async function runBootstrap(
936
943
  dependencies.core,
937
944
  'Generate Collections from Spec',
938
945
  async () => {
939
- outputs['baseline-collection-id'] = baselineCollectionId || '';
940
- outputs['smoke-collection-id'] = smokeCollectionId || '';
941
- outputs['contract-collection-id'] = contractCollectionId || '';
946
+ const shouldReuseCollections = inputs.collectionSyncMode !== 'refresh';
947
+ const assetProjectName =
948
+ inputs.collectionSyncMode === 'version'
949
+ ? createAssetProjectName(inputs, releaseLabel)
950
+ : inputs.projectName;
951
+
952
+ outputs['baseline-collection-id'] =
953
+ shouldReuseCollections ? baselineCollectionId || '' : '';
954
+ outputs['smoke-collection-id'] =
955
+ shouldReuseCollections ? smokeCollectionId || '' : '';
956
+ outputs['contract-collection-id'] =
957
+ shouldReuseCollections ? contractCollectionId || '' : '';
942
958
 
943
959
  if (!outputs['baseline-collection-id']) {
944
960
  outputs['baseline-collection-id'] = await dependencies.postman.generateCollection(
945
961
  outputs['spec-id'],
946
- inputs.projectName,
962
+ assetProjectName,
947
963
  '[Baseline]'
948
964
  );
949
965
  } else {
@@ -951,18 +967,10 @@ export async function runBootstrap(
951
967
  `Using existing baseline collection: ${outputs['baseline-collection-id']}`
952
968
  );
953
969
  }
954
- await writeVariable(
955
- dependencies.github,
956
- inputs.projectName,
957
- 'BASELINE_COLLECTION_UID',
958
- outputs['baseline-collection-id'],
959
- dependencies.core
960
- );
961
-
962
970
  if (!outputs['smoke-collection-id']) {
963
971
  outputs['smoke-collection-id'] = await dependencies.postman.generateCollection(
964
972
  outputs['spec-id'],
965
- inputs.projectName,
973
+ assetProjectName,
966
974
  '[Smoke]'
967
975
  );
968
976
  } else {
@@ -970,18 +978,10 @@ export async function runBootstrap(
970
978
  `Using existing smoke collection: ${outputs['smoke-collection-id']}`
971
979
  );
972
980
  }
973
- await writeVariable(
974
- dependencies.github,
975
- inputs.projectName,
976
- 'SMOKE_COLLECTION_UID',
977
- outputs['smoke-collection-id'],
978
- dependencies.core
979
- );
980
-
981
981
  if (!outputs['contract-collection-id']) {
982
982
  outputs['contract-collection-id'] = await dependencies.postman.generateCollection(
983
983
  outputs['spec-id'],
984
- inputs.projectName,
984
+ assetProjectName,
985
985
  '[Contract]'
986
986
  );
987
987
  } else {
@@ -989,13 +989,6 @@ export async function runBootstrap(
989
989
  `Using existing contract collection: ${outputs['contract-collection-id']}`
990
990
  );
991
991
  }
992
- await writeVariable(
993
- dependencies.github,
994
- inputs.projectName,
995
- 'CONTRACT_COLLECTION_UID',
996
- outputs['contract-collection-id'],
997
- dependencies.core
998
- );
999
992
  }
1000
993
  );
1001
994
 
@@ -1037,26 +1030,49 @@ export async function runBootstrap(
1037
1030
  }
1038
1031
  );
1039
1032
 
1040
- if (dependencies.github) {
1041
- const github = dependencies.github;
1042
- await runGroup(
1043
- dependencies.core,
1044
- 'Store Postman UIDs as Repo Variables',
1045
- async () => {
1046
- await persistBootstrapRepositoryVariables(
1047
- github,
1048
- inputs.projectName,
1049
- outputs,
1050
- systemEnvMap,
1051
- environments,
1052
- {
1053
- lintErrors: lintSummary.errors,
1054
- lintWarnings: lintSummary.warnings
1055
- },
1056
- dependencies.core
1057
- );
1058
- }
1059
- );
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
+ }
1060
1076
  }
1061
1077
 
1062
1078
  for (const [name, value] of Object.entries(outputs)) {
@@ -1079,9 +1095,6 @@ export async function runAction(
1079
1095
  specFetcher: fetch
1080
1096
  });
1081
1097
 
1082
- if (!dependencies.github) {
1083
- actionCore.info('GitHub repository variable persistence disabled for this run');
1084
- }
1085
1098
  if (inputs.domain && !dependencies.internalIntegration) {
1086
1099
  actionCore.warning(
1087
1100
  'Skipping governance assignment because postman-access-token is not configured'
@@ -1097,25 +1110,12 @@ export function createBootstrapDependencies(
1097
1110
  ): BootstrapExecutionDependencies {
1098
1111
  const secretMasker = createSecretMasker([
1099
1112
  inputs.postmanApiKey,
1100
- inputs.postmanAccessToken,
1101
- inputs.githubToken,
1102
- inputs.ghFallbackToken
1113
+ inputs.postmanAccessToken
1103
1114
  ]);
1104
1115
  const postman = new PostmanAssetsClient({
1105
1116
  apiKey: inputs.postmanApiKey,
1106
1117
  secretMasker
1107
1118
  });
1108
- const repository = extractRepositorySlug(inputs.repoUrl);
1109
- const github =
1110
- inputs.githubToken && inputs.repoUrl && repository
1111
- ? new GitHubApiClient({
1112
- authMode: inputs.githubAuthMode as GitHubApiClientAuthMode,
1113
- fallbackToken: inputs.ghFallbackToken,
1114
- repository,
1115
- secretMasker,
1116
- token: inputs.githubToken
1117
- })
1118
- : undefined;
1119
1119
  const internalIntegration =
1120
1120
  inputs.postmanAccessToken
1121
1121
  ? createInternalIntegrationAdapter({
@@ -1129,7 +1129,6 @@ export function createBootstrapDependencies(
1129
1129
  return {
1130
1130
  core: factories.core,
1131
1131
  exec: factories.exec,
1132
- github,
1133
1132
  io: factories.io,
1134
1133
  internalIntegration,
1135
1134
  postman,
@@ -1137,25 +1136,6 @@ export function createBootstrapDependencies(
1137
1136
  };
1138
1137
  }
1139
1138
 
1140
- export function extractRepositorySlug(repoUrl: string | undefined): string | undefined {
1141
- const normalized = normalizeInputValue(repoUrl);
1142
- if (!normalized) {
1143
- return undefined;
1144
- }
1145
-
1146
- try {
1147
- const parsed = new URL(normalized);
1148
- const pathname = parsed.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '');
1149
- const segments = pathname.split('/').filter(Boolean);
1150
- if (segments.length >= 2) {
1151
- return `${segments[0]}/${segments[1]}`;
1152
- }
1153
- return undefined;
1154
- } catch {
1155
- return undefined;
1156
- }
1157
- }
1158
-
1159
1139
  const currentModulePath = typeof __filename === 'string' ? __filename : '';
1160
1140
  const entrypoint = process.argv[1];
1161
1141