@postman-cse/onboarding-bootstrap 0.9.1 → 0.10.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.
@@ -1,4 +1,6 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { stringify as stringifyYaml } from 'yaml';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
4
 
3
5
  import {
4
6
  lintSpecViaCli,
@@ -58,21 +60,23 @@ function createCoreStub(values: Record<string, string> = {}) {
58
60
  function createInputs(overrides: Partial<ResolvedInputs> = {}): ResolvedInputs {
59
61
  return {
60
62
  projectName: 'core-payments',
63
+ collectionSyncMode: 'refresh',
64
+ specSyncMode: 'update',
65
+ releaseLabel: undefined,
61
66
  domain: 'core-banking',
62
67
  domainCode: 'AF',
63
68
  requesterEmail: 'owner@example.com',
64
69
  workspaceAdminUserIds: '101,102',
65
70
  repoUrl: 'https://github.com/postman-cs/bootstrap-action-test',
66
71
  specUrl: 'https://example.test/openapi.yaml',
67
- environmentsJson: '["prod","stage"]',
68
- systemEnvMapJson: '{"prod":"sys-prod","stage":"sys-stage"}',
69
72
  governanceMappingJson: '{"core-banking":"Core Banking"}',
70
73
  postmanApiKey: 'pmak-test',
71
74
  postmanAccessToken: 'postman-access-token',
72
- githubToken: 'github-token',
73
- ghFallbackToken: 'github-fallback-token',
74
- githubAuthMode: 'github_token_first',
75
75
  integrationBackend: 'bifrost',
76
+ githubRefName: undefined,
77
+ githubHeadRef: undefined,
78
+ githubRef: undefined,
79
+ githubSha: undefined,
76
80
  ...overrides
77
81
  };
78
82
  }
@@ -95,14 +99,16 @@ function createIoStub(): IOLike {
95
99
  }
96
100
 
97
101
  describe('bootstrap action', () => {
102
+ afterEach(() => {
103
+ rmSync('.postman', { recursive: true, force: true });
104
+ });
105
+
98
106
  it('marks secrets as early as input resolution', () => {
99
107
  const { core, secrets } = createCoreStub({
100
108
  'project-name': 'core-payments',
101
109
  'spec-url': 'https://example.test/openapi.yaml',
102
110
  'postman-api-key': 'pmak-test',
103
- 'postman-access-token': 'postman-access-token',
104
- 'github-token': 'github-token',
105
- 'gh-fallback-token': 'github-fallback-token'
111
+ 'postman-access-token': 'postman-access-token'
106
112
  });
107
113
 
108
114
  const inputs = readActionInputs(core);
@@ -110,9 +116,7 @@ describe('bootstrap action', () => {
110
116
  expect(inputs.postmanApiKey).toBe('pmak-test');
111
117
  expect(secrets).toEqual([
112
118
  'pmak-test',
113
- 'postman-access-token',
114
- 'github-token',
115
- 'github-fallback-token'
119
+ 'postman-access-token'
116
120
  ]);
117
121
  });
118
122
 
@@ -134,6 +138,7 @@ describe('bootstrap action', () => {
134
138
  return 'col-contract';
135
139
  }),
136
140
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
141
+ getTeams: vi.fn().mockResolvedValue([]),
137
142
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
138
143
  injectTests: vi.fn().mockResolvedValue(undefined),
139
144
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -145,10 +150,6 @@ describe('bootstrap action', () => {
145
150
  const internalIntegration = {
146
151
  assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined)
147
152
  };
148
- const github = {
149
- setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
150
- getRepositoryVariable: vi.fn().mockResolvedValue('')
151
- };
152
153
  const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
153
154
  new Response('openapi: 3.1.0', {
154
155
  status: 200
@@ -158,7 +159,6 @@ describe('bootstrap action', () => {
158
159
  const result = await runBootstrap(createInputs(), {
159
160
  core,
160
161
  exec: execStub,
161
- github,
162
162
  io: ioStub,
163
163
  internalIntegration,
164
164
  postman,
@@ -177,22 +177,6 @@ describe('bootstrap action', () => {
177
177
  );
178
178
  expect(postman.addAdminsToWorkspace).toHaveBeenCalledWith('ws-123', '101,102');
179
179
  expect(order).toEqual(['[Baseline]', '[Smoke]', '[Contract]']);
180
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
181
- 'LINT_WARNINGS',
182
- '0'
183
- );
184
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
185
- 'LINT_ERRORS',
186
- '0'
187
- );
188
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
189
- 'POSTMAN_WORKSPACE_ID',
190
- 'ws-123'
191
- );
192
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
193
- 'POSTMAN_CORE_PAYMENTS_WORKSPACE_ID',
194
- 'ws-123'
195
- );
196
180
  expect(result).toMatchObject({
197
181
  'workspace-id': 'ws-123',
198
182
  'workspace-name': '[AF] core-payments',
@@ -238,6 +222,7 @@ describe('bootstrap action', () => {
238
222
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
239
223
  generateCollection: vi.fn(),
240
224
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
225
+ getTeams: vi.fn().mockResolvedValue([]),
241
226
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
242
227
  injectTests: vi.fn(),
243
228
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -282,6 +267,7 @@ describe('bootstrap action', () => {
282
267
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
283
268
  generateCollection: vi.fn(),
284
269
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
270
+ getTeams: vi.fn().mockResolvedValue([]),
285
271
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.0.0\ninfo:\n title: old\n'),
286
272
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
287
273
  injectTests: vi.fn(),
@@ -331,6 +317,7 @@ describe('bootstrap action', () => {
331
317
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
332
318
  generateCollection: vi.fn(),
333
319
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
320
+ getTeams: vi.fn().mockResolvedValue([]),
334
321
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0'),
335
322
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
336
323
  injectTests: vi.fn(),
@@ -366,6 +353,7 @@ describe('bootstrap action', () => {
366
353
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
367
354
  generateCollection: vi.fn(),
368
355
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
356
+ getTeams: vi.fn().mockResolvedValue([]),
369
357
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
370
358
  injectTests: vi.fn().mockResolvedValue(undefined),
371
359
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -374,23 +362,18 @@ describe('bootstrap action', () => {
374
362
  updateSpec: vi.fn().mockResolvedValue(undefined),
375
363
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
376
364
  };
377
- const github = {
378
- setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
379
- getRepositoryVariable: vi.fn()
380
- };
381
-
382
365
  const result = await runBootstrap(
383
366
  createInputs({
384
367
  workspaceId: 'ws-existing',
385
368
  specId: 'spec-existing',
386
369
  baselineCollectionId: 'col-baseline-existing',
387
370
  smokeCollectionId: 'col-smoke-existing',
388
- contractCollectionId: 'col-contract-existing'
371
+ contractCollectionId: 'col-contract-existing',
372
+ collectionSyncMode: 'reuse'
389
373
  }),
390
374
  {
391
375
  core,
392
376
  exec: execStub,
393
- github,
394
377
  io: ioStub,
395
378
  postman,
396
379
  specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
@@ -399,7 +382,6 @@ describe('bootstrap action', () => {
399
382
  }
400
383
  );
401
384
 
402
- expect(github.getRepositoryVariable).not.toHaveBeenCalled();
403
385
  expect(postman.createWorkspace).not.toHaveBeenCalled();
404
386
  expect(postman.uploadSpec).not.toHaveBeenCalled();
405
387
  expect(postman.generateCollection).not.toHaveBeenCalled();
@@ -424,7 +406,57 @@ describe('bootstrap action', () => {
424
406
  expect(infos).toContain('Using existing contract collection: col-contract-existing');
425
407
  });
426
408
 
427
- it('falls back to repository variables for reruns before creating new assets', async () => {
409
+ it('refresh mode regenerates collections even when ids already exist', async () => {
410
+ const { core } = createCoreStub();
411
+ const execStub = createExecStub();
412
+ const ioStub = createIoStub();
413
+ const generatedIds = ['col-baseline-refresh', 'col-smoke-refresh', 'col-contract-refresh'];
414
+ const postman = {
415
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
416
+ createWorkspace: vi.fn(),
417
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
418
+ generateCollection: vi
419
+ .fn()
420
+ .mockImplementation(async () => generatedIds.shift() || 'col-fallback'),
421
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
422
+ getTeams: vi.fn().mockResolvedValue([]),
423
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
424
+ injectTests: vi.fn().mockResolvedValue(undefined),
425
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
426
+ tagCollection: vi.fn().mockResolvedValue(undefined),
427
+ uploadSpec: vi.fn(),
428
+ updateSpec: vi.fn().mockResolvedValue(undefined),
429
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
430
+ };
431
+ const result = await runBootstrap(
432
+ createInputs({
433
+ workspaceId: 'ws-existing',
434
+ specId: 'spec-existing',
435
+ baselineCollectionId: 'col-baseline-existing',
436
+ smokeCollectionId: 'col-smoke-existing',
437
+ contractCollectionId: 'col-contract-existing',
438
+ collectionSyncMode: 'refresh',
439
+ }),
440
+ {
441
+ core,
442
+ exec: execStub,
443
+ io: ioStub,
444
+ postman,
445
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
446
+ new Response('openapi: 3.1.0', { status: 200 })
447
+ )
448
+ }
449
+ );
450
+
451
+ expect(postman.generateCollection).toHaveBeenCalledTimes(3);
452
+ expect(result).toMatchObject({
453
+ 'baseline-collection-id': 'col-baseline-refresh',
454
+ 'smoke-collection-id': 'col-smoke-refresh',
455
+ 'contract-collection-id': 'col-contract-refresh'
456
+ });
457
+ });
458
+
459
+ it('version mode reuses the current ref resources.yaml mappings instead of a release manifest', async () => {
428
460
  const { core } = createCoreStub();
429
461
  const execStub = createExecStub();
430
462
  const ioStub = createIoStub();
@@ -434,6 +466,7 @@ describe('bootstrap action', () => {
434
466
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
435
467
  generateCollection: vi.fn(),
436
468
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
469
+ getTeams: vi.fn().mockResolvedValue([]),
437
470
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
438
471
  injectTests: vi.fn().mockResolvedValue(undefined),
439
472
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -442,24 +475,190 @@ describe('bootstrap action', () => {
442
475
  updateSpec: vi.fn().mockResolvedValue(undefined),
443
476
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
444
477
  };
445
- const github = {
446
- setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
447
- getRepositoryVariable: vi.fn(async (name: string) => {
448
- const values: Record<string, string> = {
449
- POSTMAN_WORKSPACE_ID: 'ws-from-vars',
450
- POSTMAN_SPEC_UID: 'spec-from-vars',
451
- POSTMAN_BASELINE_COLLECTION_UID: 'col-baseline-from-vars',
452
- POSTMAN_SMOKE_COLLECTION_UID: 'col-smoke-from-vars',
453
- POSTMAN_CONTRACT_COLLECTION_UID: 'col-contract-from-vars'
454
- };
455
- return values[name] ?? '';
478
+ mkdirSync('.postman', { recursive: true });
479
+ writeFileSync(
480
+ '.postman/resources.yaml',
481
+ stringifyYaml({
482
+ workspace: { id: 'ws-existing' },
483
+ cloudResources: {
484
+ specs: { '../index.yaml': 'spec-v111' },
485
+ collections: {
486
+ '../postman/collections/[Baseline] core-payments release-v1.1.1': 'col-baseline-v111',
487
+ '../postman/collections/[Smoke] core-payments release-v1.1.1': 'col-smoke-v111',
488
+ '../postman/collections/[Contract] core-payments release-v1.1.1': 'col-contract-v111'
489
+ }
490
+ }
456
491
  })
492
+ );
493
+ const result = await runBootstrap(
494
+ createInputs({
495
+ workspaceId: 'ws-existing',
496
+ collectionSyncMode: 'version',
497
+ specSyncMode: 'version',
498
+ releaseLabel: 'release/v1.1.1',
499
+ githubRefName: 'release/v1.1.1',
500
+ githubSha: 'deadbeef'
501
+ }),
502
+ {
503
+ core,
504
+ exec: execStub,
505
+ io: ioStub,
506
+ postman,
507
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
508
+ new Response('openapi: 3.1.0', { status: 200 })
509
+ )
510
+ }
511
+ );
512
+
513
+ expect(postman.updateSpec).toHaveBeenCalledWith(
514
+ 'spec-v111',
515
+ 'openapi: 3.1.0',
516
+ 'ws-existing'
517
+ );
518
+ expect(postman.uploadSpec).not.toHaveBeenCalled();
519
+ expect(postman.generateCollection).not.toHaveBeenCalled();
520
+ expect(result['spec-id']).toBe('spec-v111');
521
+ expect(result['baseline-collection-id']).toBe('col-baseline-v111');
522
+ expect(result['smoke-collection-id']).toBe('col-smoke-v111');
523
+ expect(result['contract-collection-id']).toBe('col-contract-v111');
524
+ });
525
+
526
+ it('version mode does not persist asset identifiers to repository variables', async () => {
527
+ const { core } = createCoreStub();
528
+ const execStub = createExecStub();
529
+ const ioStub = createIoStub();
530
+ const postman = {
531
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
532
+ createWorkspace: vi.fn(),
533
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
534
+ generateCollection: vi
535
+ .fn()
536
+ .mockResolvedValueOnce('col-baseline-v111')
537
+ .mockResolvedValueOnce('col-smoke-v111')
538
+ .mockResolvedValueOnce('col-contract-v111'),
539
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
540
+ getTeams: vi.fn().mockResolvedValue([]),
541
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
542
+ injectTests: vi.fn().mockResolvedValue(undefined),
543
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
544
+ tagCollection: vi.fn().mockResolvedValue(undefined),
545
+ uploadSpec: vi.fn().mockResolvedValue('spec-v111'),
546
+ updateSpec: vi.fn().mockResolvedValue(undefined),
547
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
457
548
  };
549
+ await runBootstrap(
550
+ createInputs({
551
+ workspaceId: 'ws-existing',
552
+ collectionSyncMode: 'version',
553
+ specSyncMode: 'version',
554
+ releaseLabel: 'v1.1.1',
555
+ githubRefName: 'v1.1.1'
556
+ }),
557
+ {
558
+ core,
559
+ exec: execStub,
560
+ io: ioStub,
561
+ postman,
562
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
563
+ new Response('openapi: 3.1.0', { status: 200 })
564
+ )
565
+ }
566
+ );
567
+ });
458
568
 
459
- const result = await runBootstrap(createInputs(), {
569
+ it("version mode doesn't fall back to singleton current spec uid", async () => {
570
+ const { core } = createCoreStub();
571
+ const execStub = createExecStub();
572
+ const ioStub = createIoStub();
573
+ const postman = {
574
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
575
+ createWorkspace: vi.fn(),
576
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
577
+ generateCollection: vi
578
+ .fn()
579
+ .mockResolvedValueOnce('col-baseline-v112')
580
+ .mockResolvedValueOnce('col-smoke-v112')
581
+ .mockResolvedValueOnce('col-contract-v112'),
582
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
583
+ getTeams: vi.fn().mockResolvedValue([]),
584
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
585
+ injectTests: vi.fn().mockResolvedValue(undefined),
586
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
587
+ tagCollection: vi.fn().mockResolvedValue(undefined),
588
+ uploadSpec: vi.fn().mockResolvedValue('spec-v112'),
589
+ updateSpec: vi.fn().mockResolvedValue(undefined),
590
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
591
+ };
592
+ const result = await runBootstrap(
593
+ createInputs({
594
+ workspaceId: 'ws-existing',
595
+ collectionSyncMode: 'version',
596
+ specSyncMode: 'version',
597
+ releaseLabel: 'v1.1.2',
598
+ githubRefName: 'v1.1.2'
599
+ }),
600
+ {
601
+ core,
602
+ exec: execStub,
603
+ io: ioStub,
604
+ postman,
605
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
606
+ new Response('openapi: 3.1.0', { status: 200 })
607
+ )
608
+ }
609
+ );
610
+
611
+ expect(postman.updateSpec).not.toHaveBeenCalledWith(
612
+ 'spec-current',
613
+ 'openapi: 3.1.0',
614
+ 'ws-existing'
615
+ );
616
+ expect(postman.uploadSpec).toHaveBeenCalledWith(
617
+ 'ws-existing',
618
+ 'core-payments v1.1.2',
619
+ 'openapi: 3.1.0'
620
+ );
621
+ expect(result['spec-id']).toBe('spec-v112');
622
+ });
623
+
624
+ it('reuses .postman/resources.yaml for reruns before creating new assets', async () => {
625
+ const { core } = createCoreStub();
626
+ const execStub = createExecStub();
627
+ const ioStub = createIoStub();
628
+ const postman = {
629
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
630
+ createWorkspace: vi.fn(),
631
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
632
+ generateCollection: vi.fn(),
633
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
634
+ getTeams: vi.fn().mockResolvedValue([]),
635
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
636
+ injectTests: vi.fn().mockResolvedValue(undefined),
637
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
638
+ tagCollection: vi.fn().mockResolvedValue(undefined),
639
+ uploadSpec: vi.fn(),
640
+ updateSpec: vi.fn().mockResolvedValue(undefined),
641
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
642
+ };
643
+ mkdirSync('.postman', { recursive: true });
644
+ writeFileSync(
645
+ '.postman/resources.yaml',
646
+ stringifyYaml({
647
+ workspace: { id: 'ws-from-file' },
648
+ cloudResources: {
649
+ specs: { '../index.yaml': 'spec-from-file' },
650
+ collections: {
651
+ '../postman/collections/[Baseline] core-payments': 'col-baseline-from-file',
652
+ '../postman/collections/[Smoke] core-payments': 'col-smoke-from-file',
653
+ '../postman/collections/[Contract] core-payments': 'col-contract-from-file'
654
+ }
655
+ }
656
+ })
657
+ );
658
+
659
+ const result = await runBootstrap(createInputs({ collectionSyncMode: 'reuse' }), {
460
660
  core,
461
661
  exec: execStub,
462
- github,
463
662
  io: ioStub,
464
663
  postman,
465
664
  specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
@@ -470,19 +669,17 @@ describe('bootstrap action', () => {
470
669
  expect(postman.createWorkspace).not.toHaveBeenCalled();
471
670
  expect(postman.uploadSpec).not.toHaveBeenCalled();
472
671
  expect(postman.generateCollection).not.toHaveBeenCalled();
473
- expect(postman.updateSpec).toHaveBeenCalledWith('spec-from-vars', 'openapi: 3.1.0', 'ws-from-vars');
474
- expect(github.getRepositoryVariable).toHaveBeenCalledWith('POSTMAN_CORE_PAYMENTS_WORKSPACE_ID');
475
- expect(github.getRepositoryVariable).toHaveBeenCalledWith('POSTMAN_WORKSPACE_ID');
672
+ expect(postman.updateSpec).toHaveBeenCalledWith('spec-from-file', 'openapi: 3.1.0', 'ws-from-file');
476
673
  expect(result).toMatchObject({
477
- 'workspace-id': 'ws-from-vars',
478
- 'spec-id': 'spec-from-vars',
479
- 'baseline-collection-id': 'col-baseline-from-vars',
480
- 'smoke-collection-id': 'col-smoke-from-vars',
481
- 'contract-collection-id': 'col-contract-from-vars'
674
+ 'workspace-id': 'ws-from-file',
675
+ 'spec-id': 'spec-from-file',
676
+ 'baseline-collection-id': 'col-baseline-from-file',
677
+ 'smoke-collection-id': 'col-smoke-from-file',
678
+ 'contract-collection-id': 'col-contract-from-file'
482
679
  });
483
680
  });
484
681
 
485
- it('creates a new workspace when the repo-variable workspace is linked to a different repository', async () => {
682
+ it('ignores repository-variable asset state when .postman/resources.yaml is absent', async () => {
486
683
  const previousRepository = process.env.GITHUB_REPOSITORY;
487
684
  process.env.GITHUB_REPOSITORY = 'postman-cs/bootstrap-action-test';
488
685
 
@@ -502,6 +699,7 @@ describe('bootstrap action', () => {
502
699
  return 'col-contract';
503
700
  }),
504
701
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
702
+ getTeams: vi.fn().mockResolvedValue([]),
505
703
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue('https://github.com/postman-cs/different-repo'),
506
704
  injectTests: vi.fn().mockResolvedValue(undefined),
507
705
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -510,20 +708,9 @@ describe('bootstrap action', () => {
510
708
  updateSpec: vi.fn().mockResolvedValue(undefined),
511
709
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
512
710
  };
513
- const github = {
514
- setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
515
- getRepositoryVariable: vi.fn(async (name: string) => {
516
- const values: Record<string, string> = {
517
- POSTMAN_WORKSPACE_ID: 'ws-from-vars'
518
- };
519
- return values[name] ?? '';
520
- })
521
- };
522
-
523
711
  const result = await runBootstrap(createInputs(), {
524
712
  core,
525
713
  exec: execStub,
526
- github,
527
714
  io: ioStub,
528
715
  postman,
529
716
  specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
@@ -531,10 +718,8 @@ describe('bootstrap action', () => {
531
718
  )
532
719
  });
533
720
 
534
- expect(postman.getWorkspaceGitRepoUrl).toHaveBeenCalledWith('ws-from-vars', '12345', 'postman-access-token');
535
721
  expect(postman.createWorkspace).toHaveBeenCalled();
536
722
  expect(result['workspace-id']).toBe('ws-new');
537
- expect(postman.updateSpec).not.toHaveBeenCalled();
538
723
  } finally {
539
724
  if (previousRepository === undefined) {
540
725
  delete process.env.GITHUB_REPOSITORY;
@@ -554,6 +739,7 @@ describe('bootstrap action', () => {
554
739
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
555
740
  generateCollection: vi.fn().mockResolvedValue('col-id'),
556
741
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
742
+ getTeams: vi.fn().mockResolvedValue([]),
557
743
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
558
744
  injectTests: vi.fn().mockResolvedValue(undefined),
559
745
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -580,7 +766,7 @@ describe('bootstrap action', () => {
580
766
  expect(postman.createWorkspace).toHaveBeenCalled();
581
767
  });
582
768
 
583
- it('skips repo variable persistence when github dependency is absent', async () => {
769
+ it('runs without any GitHub dependency', async () => {
584
770
  const { core } = createCoreStub();
585
771
  const execStub = createExecStub();
586
772
  const ioStub = createIoStub();
@@ -590,6 +776,7 @@ describe('bootstrap action', () => {
590
776
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
591
777
  generateCollection: vi.fn().mockResolvedValue('col-id'),
592
778
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
779
+ getTeams: vi.fn().mockResolvedValue([]),
593
780
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
594
781
  injectTests: vi.fn().mockResolvedValue(undefined),
595
782
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -600,7 +787,7 @@ describe('bootstrap action', () => {
600
787
  };
601
788
 
602
789
  const result = await runBootstrap(
603
- createInputs({ githubToken: undefined }),
790
+ createInputs(),
604
791
  {
605
792
  core,
606
793
  exec: execStub,
@@ -616,6 +803,117 @@ describe('bootstrap action', () => {
616
803
  expect(result['spec-id']).toBe('spec-123');
617
804
  });
618
805
 
806
+ it('version mode reuses current resources.yaml collections on the checked-out ref', async () => {
807
+ const { core, infos } = createCoreStub();
808
+ const execStub = createExecStub();
809
+ const ioStub = createIoStub();
810
+ const postman = {
811
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
812
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
813
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
814
+ generateCollection: vi
815
+ .fn()
816
+ .mockImplementation(async (_specId: string, _projectName: string, prefix: string) => {
817
+ if (prefix === '[Baseline]') return 'col-baseline-v2';
818
+ if (prefix === '[Smoke]') return 'col-smoke-v2';
819
+ return 'col-contract-v2';
820
+ }),
821
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
822
+ getTeams: vi.fn().mockResolvedValue([]),
823
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
824
+ injectTests: vi.fn().mockResolvedValue(undefined),
825
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
826
+ tagCollection: vi.fn().mockResolvedValue(undefined),
827
+ uploadSpec: vi.fn().mockResolvedValue('spec-v2'),
828
+ updateSpec: vi.fn().mockResolvedValue(undefined),
829
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
830
+ };
831
+ const resources = {
832
+ workspace: { id: 'ws-current' },
833
+ cloudResources: {
834
+ collections: {
835
+ '../postman/collections/[Baseline] core-payments': 'col-baseline-current',
836
+ '../postman/collections/[Smoke] core-payments': 'col-smoke-current',
837
+ '../postman/collections/[Contract] core-payments': 'col-contract-current'
838
+ }
839
+ }
840
+ };
841
+ mkdirSync('.postman', { recursive: true });
842
+ writeFileSync('.postman/resources.yaml', stringifyYaml(resources));
843
+
844
+ const result = await runBootstrap(
845
+ createInputs({
846
+ collectionSyncMode: 'version',
847
+ specSyncMode: 'version',
848
+ releaseLabel: 'v2.0.0'
849
+ }),
850
+ {
851
+ core,
852
+ exec: execStub,
853
+ io: ioStub,
854
+ postman,
855
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
856
+ new Response('openapi: 3.1.0', { status: 200 })
857
+ )
858
+ }
859
+ );
860
+
861
+ expect(postman.generateCollection).not.toHaveBeenCalled();
862
+ expect(result['baseline-collection-id']).toBe('col-baseline-current');
863
+ expect(result['smoke-collection-id']).toBe('col-smoke-current');
864
+ expect(result['contract-collection-id']).toBe('col-contract-current');
865
+ expect(infos).toContain('Resolved baseline-collection-id from .postman/resources.yaml');
866
+ expect(infos).toContain('Resolved smoke-collection-id from .postman/resources.yaml');
867
+ expect(infos).toContain('Resolved contract-collection-id from .postman/resources.yaml');
868
+ });
869
+
870
+ it('versioned runs do not emit releases-json or write releases.yaml', async () => {
871
+ const { core } = createCoreStub();
872
+ const execStub = createExecStub();
873
+ const ioStub = createIoStub();
874
+ const postman = {
875
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
876
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
877
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
878
+ generateCollection: vi
879
+ .fn()
880
+ .mockImplementation(async (_specId: string, _projectName: string, prefix: string) => {
881
+ if (prefix === '[Baseline]') return 'col-baseline-v2';
882
+ if (prefix === '[Smoke]') return 'col-smoke-v2';
883
+ return 'col-contract-v2';
884
+ }),
885
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
886
+ getTeams: vi.fn().mockResolvedValue([]),
887
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
888
+ injectTests: vi.fn().mockResolvedValue(undefined),
889
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
890
+ tagCollection: vi.fn().mockResolvedValue(undefined),
891
+ uploadSpec: vi.fn().mockResolvedValue('spec-v2'),
892
+ updateSpec: vi.fn().mockResolvedValue(undefined),
893
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
894
+ };
895
+
896
+ const result = await runBootstrap(
897
+ createInputs({
898
+ collectionSyncMode: 'version',
899
+ specSyncMode: 'version',
900
+ releaseLabel: 'v2.0.0'
901
+ }),
902
+ {
903
+ core,
904
+ exec: execStub,
905
+ io: ioStub,
906
+ postman,
907
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
908
+ new Response('openapi: 3.1.0', { status: 200 })
909
+ )
910
+ }
911
+ );
912
+
913
+ expect('releases-json' in result).toBe(false);
914
+ expect(existsSync('.postman/releases.yaml')).toBe(false);
915
+ });
916
+
619
917
  it('emits warnings for lint violations but does not fail', async () => {
620
918
  const { core, warnings } = createCoreStub();
621
919
  const execStub = createExecStub(
@@ -632,6 +930,7 @@ describe('bootstrap action', () => {
632
930
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
633
931
  generateCollection: vi.fn().mockResolvedValue('col-id'),
634
932
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
933
+ getTeams: vi.fn().mockResolvedValue([]),
635
934
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
636
935
  injectTests: vi.fn().mockResolvedValue(undefined),
637
936
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -729,3 +1028,190 @@ describe('lintSpecViaCli', () => {
729
1028
  });
730
1029
  });
731
1030
  });
1031
+
1032
+ it('fails with team list when org-mode detected and no workspace-team-id', async () => {
1033
+ const { core } = createCoreStub();
1034
+ const execStub = createExecStub();
1035
+ const ioStub = createIoStub();
1036
+ const postman = {
1037
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1038
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1039
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1040
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1041
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('13347347'),
1042
+ getTeams: vi.fn().mockResolvedValue([
1043
+ { id: 132109, name: 'Field Services', handle: 'fs', organizationId: 13347347 },
1044
+ { id: 132118, name: 'Customer Ed', handle: 'ce', organizationId: 13347347 }
1045
+ ]),
1046
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1047
+ injectTests: vi.fn().mockResolvedValue(undefined),
1048
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1049
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1050
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1051
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1052
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1053
+ };
1054
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1055
+ new Response('openapi: 3.1.0', { status: 200 })
1056
+ );
1057
+
1058
+ await expect(
1059
+ runBootstrap(createInputs(), {
1060
+ core,
1061
+ exec: execStub,
1062
+ io: ioStub,
1063
+ postman,
1064
+ specFetcher
1065
+ })
1066
+ ).rejects.toThrow('Org-mode account detected');
1067
+
1068
+ expect(postman.createWorkspace).not.toHaveBeenCalled();
1069
+ });
1070
+
1071
+ it('passes workspace-team-id to createWorkspace for org-mode accounts', async () => {
1072
+ const { core, outputs } = createCoreStub();
1073
+ const execStub = createExecStub();
1074
+ const ioStub = createIoStub();
1075
+ const postman = {
1076
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1077
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-org' }),
1078
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1079
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1080
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('13347347'),
1081
+ getTeams: vi.fn().mockResolvedValue([]),
1082
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1083
+ injectTests: vi.fn().mockResolvedValue(undefined),
1084
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1085
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1086
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1087
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1088
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1089
+ };
1090
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1091
+ new Response('openapi: 3.1.0', { status: 200 })
1092
+ );
1093
+
1094
+ await runBootstrap(createInputs({ workspaceTeamId: '132319' }), {
1095
+ core,
1096
+ exec: execStub,
1097
+ io: ioStub,
1098
+ postman,
1099
+ specFetcher
1100
+ });
1101
+
1102
+ expect(postman.createWorkspace).toHaveBeenCalledWith(
1103
+ expect.any(String),
1104
+ expect.any(String),
1105
+ 132319
1106
+ );
1107
+ expect(outputs['workspace-id']).toBe('ws-org');
1108
+ });
1109
+
1110
+ it('skips org-mode detection when workspace-id is already provided', async () => {
1111
+ const { core, outputs } = createCoreStub();
1112
+ const execStub = createExecStub();
1113
+ const ioStub = createIoStub();
1114
+ const postman = {
1115
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1116
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1117
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1118
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1119
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('13347347'),
1120
+ getTeams: vi.fn().mockResolvedValue([
1121
+ { id: 132109, name: 'FS', handle: 'fs', organizationId: 13347347 },
1122
+ { id: 132118, name: 'CE', handle: 'ce', organizationId: 13347347 }
1123
+ ]),
1124
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1125
+ injectTests: vi.fn().mockResolvedValue(undefined),
1126
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1127
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1128
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1129
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1130
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1131
+ };
1132
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1133
+ new Response('openapi: 3.1.0', { status: 200 })
1134
+ );
1135
+
1136
+ await runBootstrap(createInputs({ workspaceId: 'ws-existing' }), {
1137
+ core,
1138
+ exec: execStub,
1139
+ io: ioStub,
1140
+ postman,
1141
+ specFetcher
1142
+ });
1143
+
1144
+ expect(postman.createWorkspace).not.toHaveBeenCalled();
1145
+ expect(outputs['workspace-id']).toBe('ws-existing');
1146
+ });
1147
+
1148
+ it('warns and proceeds when getTeams fails', async () => {
1149
+ const { core } = createCoreStub();
1150
+ const warnings: string[] = [];
1151
+ core.warning = vi.fn((msg: string) => warnings.push(msg));
1152
+ const execStub = createExecStub();
1153
+ const ioStub = createIoStub();
1154
+ const postman = {
1155
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1156
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1157
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1158
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1159
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
1160
+ getTeams: vi.fn().mockRejectedValue(new Error('Network error')),
1161
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1162
+ injectTests: vi.fn().mockResolvedValue(undefined),
1163
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1164
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1165
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1166
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1167
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1168
+ };
1169
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1170
+ new Response('openapi: 3.1.0', { status: 200 })
1171
+ );
1172
+
1173
+ await runBootstrap(createInputs(), {
1174
+ core,
1175
+ exec: execStub,
1176
+ io: ioStub,
1177
+ postman,
1178
+ specFetcher
1179
+ });
1180
+
1181
+ expect(warnings.some(w => w.includes('Could not check for org-mode'))).toBe(true);
1182
+ expect(postman.createWorkspace).toHaveBeenCalled();
1183
+ });
1184
+
1185
+ it('throws when workspace-team-id is non-numeric', async () => {
1186
+ const { core } = createCoreStub();
1187
+ const execStub = createExecStub();
1188
+ const ioStub = createIoStub();
1189
+ const postman = {
1190
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1191
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1192
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1193
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1194
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
1195
+ getTeams: vi.fn().mockResolvedValue([]),
1196
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1197
+ injectTests: vi.fn().mockResolvedValue(undefined),
1198
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1199
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1200
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1201
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1202
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1203
+ };
1204
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1205
+ new Response('openapi: 3.1.0', { status: 200 })
1206
+ );
1207
+
1208
+ await expect(
1209
+ runBootstrap(createInputs({ workspaceTeamId: 'not-a-number' }), {
1210
+ core,
1211
+ exec: execStub,
1212
+ io: ioStub,
1213
+ postman,
1214
+ specFetcher
1215
+ })
1216
+ ).rejects.toThrow('workspace-team-id must be a numeric sub-team ID');
1217
+ });