@postman-cse/onboarding-bootstrap 0.10.0 → 0.12.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 DELETED
@@ -1,1100 +0,0 @@
1
- import * as core from '@actions/core';
2
- import * as exec from '@actions/exec';
3
- import * as io from '@actions/io';
4
- import { readFileSync } from 'node:fs';
5
- import { parse, stringify } from 'yaml';
6
-
7
- import { openAlphaActionContract } from './contracts.js';
8
- import { createInternalIntegrationAdapter, type InternalIntegrationAdapter } from './lib/postman/internal-integration-adapter.js';
9
- import { PostmanAssetsClient } from './lib/postman/postman-assets-client.js';
10
- import { resolveCanonicalWorkspaceSelection } from './lib/postman/workspace-selection.js';
11
- import { detectRepoContext } from './lib/repo/context.js';
12
- import { retry } from './lib/retry.js';
13
- import { createSecretMasker } from './lib/secrets.js';
14
-
15
- export interface ResolvedInputs {
16
- projectName: string;
17
- workspaceId?: string;
18
- specId?: string;
19
- baselineCollectionId?: string;
20
- smokeCollectionId?: string;
21
- contractCollectionId?: string;
22
- collectionSyncMode: 'reuse' | 'refresh' | 'version';
23
- specSyncMode: 'update' | 'version';
24
- releaseLabel?: string;
25
- domain?: string;
26
- domainCode?: string;
27
- requesterEmail?: string;
28
- workspaceAdminUserIds?: string;
29
- workspaceTeamId?: string;
30
- teamId?: string;
31
- repoUrl?: string;
32
- specUrl: string;
33
- governanceMappingJson: string;
34
- postmanApiKey: string;
35
- postmanAccessToken?: string;
36
- integrationBackend: string;
37
- githubRefName?: string;
38
- githubHeadRef?: string;
39
- githubRef?: string;
40
- githubSha?: string;
41
- }
42
-
43
- export interface PlannedOutputs {
44
- 'workspace-id': string;
45
- 'workspace-url': string;
46
- 'workspace-name': string;
47
- 'spec-id': string;
48
- 'baseline-collection-id': string;
49
- 'smoke-collection-id': string;
50
- 'contract-collection-id': string;
51
- 'collections-json': string;
52
- 'lint-summary-json': string;
53
- }
54
-
55
- export interface LintViolation {
56
- issue?: string;
57
- path?: string;
58
- severity?: string;
59
- }
60
-
61
- export interface LintSummary {
62
- errors: number;
63
- violations: LintViolation[];
64
- warnings: number;
65
- }
66
-
67
- export interface CoreLike {
68
- error(message: string): void;
69
- getInput(name: string, options?: { required?: boolean }): string;
70
- group<T>(name: string, fn: () => Promise<T>): Promise<T>;
71
- info(message: string): void;
72
- setFailed(message: string): void;
73
- setOutput(name: string, value: string): void;
74
- setSecret(secret: string): void;
75
- warning(message: string): void;
76
- }
77
-
78
- export interface ExecLike {
79
- exec(
80
- commandLine: string,
81
- args?: string[],
82
- options?: Parameters<typeof exec.exec>[2]
83
- ): ReturnType<typeof exec.exec>;
84
- getExecOutput(
85
- commandLine: string,
86
- args?: string[],
87
- options?: Parameters<typeof exec.getExecOutput>[2]
88
- ): ReturnType<typeof exec.getExecOutput>;
89
- }
90
-
91
- export interface IOLike {
92
- which(tool: string, check?: boolean): Promise<string>;
93
- }
94
-
95
- export interface BootstrapExecutionDependencies {
96
- core: Pick<
97
- CoreLike,
98
- 'error' | 'group' | 'info' | 'setOutput' | 'warning'
99
- >;
100
- exec: ExecLike;
101
- io: IOLike;
102
- internalIntegration?: Pick<
103
- InternalIntegrationAdapter,
104
- 'assignWorkspaceToGovernanceGroup'
105
- >;
106
- postman: Pick<
107
- PostmanAssetsClient,
108
- | 'addAdminsToWorkspace'
109
- | 'createWorkspace'
110
- | 'findWorkspacesByName'
111
- | 'generateCollection'
112
- | 'getAutoDerivedTeamId'
113
- | 'getSpecContent'
114
- | 'getTeams'
115
- | 'getWorkspaceGitRepoUrl'
116
- | 'injectTests'
117
- | 'inviteRequesterToWorkspace'
118
- | 'tagCollection'
119
- | 'uploadSpec'
120
- | 'updateSpec'
121
- >;
122
- specFetcher: typeof fetch;
123
- }
124
-
125
- export interface BootstrapDependencyFactories {
126
- core: Pick<CoreLike, 'error' | 'group' | 'info' | 'setOutput' | 'warning'>;
127
- exec: ExecLike;
128
- io: IOLike;
129
- specFetcher?: typeof fetch;
130
- }
131
-
132
- function normalizeInputValue(value: string | undefined): string | undefined {
133
- if (value === undefined) {
134
- return undefined;
135
- }
136
- const trimmed = value.trim();
137
- return trimmed ? trimmed : undefined;
138
- }
139
-
140
- export function getInput(
141
- name: string,
142
- env: NodeJS.ProcessEnv = process.env
143
- ): string | undefined {
144
- const envName = `INPUT_${name.replace(/-/g, '_').toUpperCase()}`;
145
- return normalizeInputValue(env[envName]);
146
- }
147
-
148
- function requireInput(
149
- actionCore: Pick<CoreLike, 'getInput'>,
150
- name: string
151
- ): string {
152
- return actionCore.getInput(name, { required: true }).trim();
153
- }
154
-
155
- function optionalInput(
156
- actionCore: Pick<CoreLike, 'getInput'>,
157
- name: string
158
- ): string | undefined {
159
- return normalizeInputValue(actionCore.getInput(name));
160
- }
161
-
162
- function parseJsonValue<T>(
163
- raw: string,
164
- fallback: T,
165
- inputName: string
166
- ): T {
167
- try {
168
- return (JSON.parse(raw || JSON.stringify(fallback)) as T) ?? fallback;
169
- } catch (error) {
170
- throw new Error(
171
- `Invalid JSON for ${inputName}: ${error instanceof Error ? error.message : String(error)
172
- }`
173
- );
174
- }
175
- }
176
-
177
- function asStringArray(value: unknown, inputName: string): string[] {
178
- if (!Array.isArray(value)) {
179
- throw new Error(`${inputName} must be a JSON array`);
180
- }
181
- return value.map((entry) => String(entry));
182
- }
183
-
184
- function parseBooleanInput(value: string | undefined, defaultValue: boolean): boolean {
185
- const normalized = String(value || '').trim().toLowerCase();
186
- if (!normalized) return defaultValue;
187
- if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
188
- if (['false', '0', 'no', 'off'].includes(normalized)) return false;
189
- return defaultValue;
190
- }
191
-
192
- function parseCollectionSyncMode(
193
- value: string | undefined
194
- ): 'reuse' | 'refresh' | 'version' {
195
- if (value === 'reuse' || value === 'version') {
196
- return value;
197
- }
198
- return 'refresh';
199
- }
200
-
201
- function parseSpecSyncMode(value: string | undefined): 'update' | 'version' {
202
- if (value === 'version') {
203
- return value;
204
- }
205
- return 'update';
206
- }
207
-
208
- export function resolveInputs(
209
- env: NodeJS.ProcessEnv = process.env
210
- ): ResolvedInputs {
211
- const repoContext = detectRepoContext(
212
- {
213
- repoUrl: getInput('repo-url', env)
214
- },
215
- env
216
- );
217
-
218
- const integrationBackend =
219
- getInput('integration-backend', env) ??
220
- openAlphaActionContract.inputs['integration-backend'].default ??
221
- 'bifrost';
222
-
223
- const allowedBackends =
224
- openAlphaActionContract.inputs['integration-backend'].allowedValues ?? [];
225
- if (allowedBackends.length > 0 && !allowedBackends.includes(integrationBackend)) {
226
- throw new Error(
227
- `Unsupported integration-backend "${integrationBackend}". Supported values: ${allowedBackends.join(', ')}`
228
- );
229
- }
230
-
231
- const specUrl = getInput('spec-url', env) ?? '';
232
- if (specUrl) {
233
- try {
234
- const parsedUrl = new URL(specUrl);
235
- if (parsedUrl.protocol !== 'https:') {
236
- throw new Error('not https');
237
- }
238
- } catch {
239
- throw new Error(`spec-url must be a valid HTTPS URL, got: ${specUrl}`);
240
- }
241
- }
242
-
243
- return {
244
- projectName: getInput('project-name', env)
245
- ?? env.GITHUB_REPOSITORY?.split('/').pop()
246
- ?? env.CI_PROJECT_NAME
247
- ?? '',
248
- workspaceId: getInput('workspace-id', env),
249
- specId: getInput('spec-id', env),
250
- baselineCollectionId: getInput('baseline-collection-id', env),
251
- smokeCollectionId: getInput('smoke-collection-id', env),
252
- contractCollectionId: getInput('contract-collection-id', env),
253
- collectionSyncMode: parseCollectionSyncMode(getInput('collection-sync-mode', env)),
254
- specSyncMode: parseSpecSyncMode(getInput('spec-sync-mode', env)),
255
- releaseLabel: getInput('release-label', env),
256
- domain: getInput('domain', env),
257
- domainCode: getInput('domain-code', env),
258
- requesterEmail: getInput('requester-email', env),
259
- workspaceAdminUserIds:
260
- getInput('workspace-admin-user-ids', env) || env.WORKSPACE_ADMIN_USER_IDS || '',
261
- workspaceTeamId: getInput('workspace-team-id', env) || env.POSTMAN_WORKSPACE_TEAM_ID,
262
- teamId: getInput('team-id', env) || env.POSTMAN_TEAM_ID || '',
263
- repoUrl: repoContext.repoUrl || '',
264
- specUrl,
265
- governanceMappingJson:
266
- getInput('governance-mapping-json', env) ??
267
- openAlphaActionContract.inputs['governance-mapping-json'].default ??
268
- '{}',
269
- postmanApiKey: getInput('postman-api-key', env) ?? '',
270
- postmanAccessToken: getInput('postman-access-token', env),
271
- integrationBackend,
272
- githubRefName: env.GITHUB_REF_NAME,
273
- githubHeadRef: env.GITHUB_HEAD_REF,
274
- githubRef: env.GITHUB_REF,
275
- githubSha: env.GITHUB_SHA
276
- };
277
- }
278
-
279
- export function createPlannedOutputs(inputs: ResolvedInputs): PlannedOutputs {
280
- const workspaceName = inputs.domainCode
281
- ? `[${inputs.domainCode}] ${inputs.projectName}`
282
- : inputs.projectName;
283
-
284
- return {
285
- 'workspace-id': '',
286
- 'workspace-url': '',
287
- 'workspace-name': workspaceName,
288
- 'spec-id': '',
289
- 'baseline-collection-id': '',
290
- 'smoke-collection-id': '',
291
- 'contract-collection-id': '',
292
- 'collections-json': JSON.stringify({
293
- baseline: '',
294
- smoke: '',
295
- contract: ''
296
- }),
297
- 'lint-summary-json': JSON.stringify({
298
- errors: 0,
299
- total: 0,
300
- violations: [],
301
- warnings: 0
302
- })
303
- };
304
- }
305
-
306
- export function readActionInputs(
307
- actionCore: Pick<CoreLike, 'getInput' | 'setSecret'>
308
- ): ResolvedInputs {
309
- const projectName = requireInput(actionCore, 'project-name');
310
- const specUrl = requireInput(actionCore, 'spec-url');
311
- const postmanApiKey = requireInput(actionCore, 'postman-api-key');
312
- const postmanAccessToken = optionalInput(actionCore, 'postman-access-token');
313
-
314
- actionCore.setSecret(postmanApiKey);
315
- if (postmanAccessToken) actionCore.setSecret(postmanAccessToken);
316
-
317
- const inputs = resolveInputs({
318
- ...process.env,
319
- INPUT_PROJECT_NAME: projectName,
320
- INPUT_WORKSPACE_ID: optionalInput(actionCore, 'workspace-id'),
321
- INPUT_SPEC_ID: optionalInput(actionCore, 'spec-id'),
322
- INPUT_BASELINE_COLLECTION_ID: optionalInput(actionCore, 'baseline-collection-id'),
323
- INPUT_SMOKE_COLLECTION_ID: optionalInput(actionCore, 'smoke-collection-id'),
324
- INPUT_CONTRACT_COLLECTION_ID: optionalInput(actionCore, 'contract-collection-id'),
325
- INPUT_COLLECTION_SYNC_MODE:
326
- optionalInput(actionCore, 'collection-sync-mode') ??
327
- openAlphaActionContract.inputs['collection-sync-mode'].default,
328
- INPUT_SPEC_SYNC_MODE:
329
- optionalInput(actionCore, 'spec-sync-mode') ??
330
- openAlphaActionContract.inputs['spec-sync-mode'].default,
331
- INPUT_RELEASE_LABEL: optionalInput(actionCore, 'release-label'),
332
- INPUT_DOMAIN: optionalInput(actionCore, 'domain'),
333
- INPUT_DOMAIN_CODE: optionalInput(actionCore, 'domain-code'),
334
- INPUT_REQUESTER_EMAIL: optionalInput(actionCore, 'requester-email'),
335
- INPUT_WORKSPACE_ADMIN_USER_IDS: optionalInput(
336
- actionCore,
337
- 'workspace-admin-user-ids'
338
- ),
339
- INPUT_WORKSPACE_TEAM_ID:
340
- optionalInput(actionCore, 'workspace-team-id') || process.env.POSTMAN_WORKSPACE_TEAM_ID,
341
- INPUT_TEAM_ID:
342
- optionalInput(actionCore, 'postman-team-id') || process.env.POSTMAN_TEAM_ID,
343
- INPUT_REPO_URL: optionalInput(actionCore, 'repo-url'),
344
- INPUT_SPEC_URL: specUrl,
345
- INPUT_GOVERNANCE_MAPPING_JSON:
346
- optionalInput(actionCore, 'governance-mapping-json') ??
347
- openAlphaActionContract.inputs['governance-mapping-json'].default,
348
- INPUT_POSTMAN_API_KEY: postmanApiKey,
349
- INPUT_POSTMAN_ACCESS_TOKEN: postmanAccessToken,
350
- INPUT_INTEGRATION_BACKEND:
351
- optionalInput(actionCore, 'integration-backend') ??
352
- openAlphaActionContract.inputs['integration-backend'].default
353
- });
354
-
355
- return inputs;
356
- }
357
-
358
- function createWorkspaceName(inputs: ResolvedInputs): string {
359
- return inputs.domainCode
360
- ? `[${inputs.domainCode}] ${inputs.projectName}`
361
- : inputs.projectName;
362
- }
363
-
364
- async function runGroup<T>(
365
- actionCore: Pick<CoreLike, 'group'>,
366
- name: string,
367
- fn: () => Promise<T>
368
- ): Promise<T> {
369
- return actionCore.group(name, fn);
370
- }
371
-
372
- async function ensurePostmanCli(
373
- dependencies: Pick<BootstrapExecutionDependencies, 'exec' | 'io'>,
374
- postmanApiKey: string
375
- ): Promise<void> {
376
- const existing = await dependencies.io.which('postman', false).catch(() => '');
377
- if (!existing) {
378
- await dependencies.exec.exec('sh', [
379
- '-c',
380
- 'curl -o- "https://dl-cli.pstmn.io/install/unix.sh" | sh'
381
- ]);
382
- }
383
-
384
- await dependencies.exec.exec('postman', ['login', '--with-api-key', postmanApiKey]);
385
- }
386
-
387
- export async function lintSpecViaCli(
388
- dependencies: Pick<BootstrapExecutionDependencies, 'exec'>,
389
- workspaceId: string,
390
- specId: string
391
- ): Promise<LintSummary> {
392
- const result = await dependencies.exec.getExecOutput(
393
- 'postman',
394
- [
395
- 'spec',
396
- 'lint',
397
- specId,
398
- '--workspace-id',
399
- workspaceId || '',
400
- '--report-events',
401
- '-o',
402
- 'json'
403
- ],
404
- {
405
- ignoreReturnCode: true
406
- }
407
- );
408
-
409
- if (result.exitCode !== 0 && !result.stdout.trim()) {
410
- throw new Error(`Spec lint command failed: ${result.stderr}`);
411
- }
412
-
413
- let parsed: { violations?: LintViolation[] };
414
- try {
415
- parsed = JSON.parse(result.stdout || '{}') as { violations?: LintViolation[] };
416
- } catch {
417
- throw new Error(
418
- `Spec lint output is not valid JSON. output: ${result.stdout}, err: ${result.stderr}`
419
- );
420
- }
421
-
422
- const violations = parsed.violations || [];
423
- const errors = violations.filter((entry) => entry.severity === 'ERROR').length;
424
- const warnings = violations.filter((entry) => entry.severity === 'WARNING').length;
425
-
426
- return {
427
- errors,
428
- violations,
429
- warnings
430
- };
431
- }
432
-
433
- async function fetchSpecDocument(
434
- specUrl: string,
435
- specFetcher: typeof fetch
436
- ): Promise<string> {
437
- return retry(
438
- async () => {
439
- const response = await specFetcher(specUrl, {
440
- headers: {
441
- 'User-Agent': 'postman-bootstrap-action'
442
- }
443
- });
444
-
445
- if (!response.ok) {
446
- throw new Error(`Failed to fetch spec from URL: ${response.status}`);
447
- }
448
-
449
- return response.text();
450
- },
451
- {
452
- maxAttempts: 3,
453
- delayMs: 3000
454
- }
455
- );
456
- }
457
-
458
- function normalizeReleaseLabel(value: string | undefined): string | undefined {
459
- const trimmed = String(value || '').trim();
460
- if (!trimmed) {
461
- return undefined;
462
- }
463
-
464
- return trimmed
465
- .replace(/^refs\/heads\//, '')
466
- .replace(/^refs\/tags\//, '')
467
- .replace(/^refs\/pull\//, 'pull-')
468
- .replace(/[^a-zA-Z0-9._-]+/g, '-')
469
- .replace(/^-+|-+$/g, '') || undefined;
470
- }
471
-
472
- function deriveReleaseLabel(inputs: ResolvedInputs): string | undefined {
473
- if (inputs.releaseLabel) {
474
- return normalizeReleaseLabel(inputs.releaseLabel);
475
- }
476
-
477
- return (
478
- normalizeReleaseLabel(inputs.githubRefName) ??
479
- normalizeReleaseLabel(inputs.githubHeadRef) ??
480
- normalizeReleaseLabel(inputs.githubRef)
481
- );
482
- }
483
-
484
- function createAssetProjectName(
485
- inputs: ResolvedInputs,
486
- releaseLabel?: string
487
- ): string {
488
- if (!releaseLabel) {
489
- return inputs.projectName;
490
- }
491
-
492
- return `${inputs.projectName} ${releaseLabel}`;
493
- }
494
-
495
- type CloudResourceMap = Record<string, string>;
496
-
497
- type PostmanResourcesState = {
498
- workspace?: {
499
- id?: string;
500
- };
501
- cloudResources?: {
502
- collections?: CloudResourceMap;
503
- environments?: CloudResourceMap;
504
- specs?: CloudResourceMap;
505
- };
506
- };
507
-
508
- function readResourcesState(): PostmanResourcesState | null {
509
- try {
510
- return parse(readFileSync('.postman/resources.yaml', 'utf8')) as PostmanResourcesState;
511
- } catch {
512
- return null;
513
- }
514
- }
515
-
516
- function getFirstCloudResourceId(map: CloudResourceMap | undefined): string | undefined {
517
- if (!map) {
518
- return undefined;
519
- }
520
- return Object.values(map)[0];
521
- }
522
-
523
- function findCloudResourceId(
524
- map: CloudResourceMap | undefined,
525
- matcher: (path: string) => boolean
526
- ): string | undefined {
527
- if (!map) {
528
- return undefined;
529
- }
530
-
531
- const match = Object.entries(map).find(([filePath]) => matcher(filePath));
532
- return match?.[1];
533
- }
534
-
535
- const SPEC_SUMMARY_MAX_LEN = 200;
536
- const SPEC_HTTP_METHODS = new Set([
537
- 'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'
538
- ]);
539
-
540
- /** OpenAPI JSON/YAML: fix missing or oversized operation summaries before Spec Hub upload. */
541
- export function normalizeSpecDocument(raw: string, warn: (msg: string) => void): string {
542
- const head = raw.trimStart();
543
- let doc: unknown;
544
- let asJson = false;
545
- try {
546
- if (head.startsWith('{') || head.startsWith('[')) {
547
- doc = JSON.parse(raw) as unknown;
548
- asJson = true;
549
- } else {
550
- doc = parse(raw) as unknown;
551
- }
552
- } catch {
553
- warn('Spec normalization skipped: document is not valid JSON or YAML.');
554
- return raw;
555
- }
556
- if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return raw;
557
- const paths = (doc as Record<string, unknown>).paths;
558
- if (!paths || typeof paths !== 'object' || Array.isArray(paths)) return raw;
559
-
560
- let changed = false;
561
- for (const [pathKey, pathItem] of Object.entries(paths as Record<string, unknown>)) {
562
- if (!pathItem || typeof pathItem !== 'object' || Array.isArray(pathItem)) continue;
563
- const item = pathItem as Record<string, unknown>;
564
- for (const method of Object.keys(item)) {
565
- if (!SPEC_HTTP_METHODS.has(method.toLowerCase())) continue;
566
- const op = item[method];
567
- if (!op || typeof op !== 'object' || Array.isArray(op)) continue;
568
- const o = op as Record<string, unknown>;
569
- const prev = o.summary;
570
- let s = typeof o.summary === 'string' ? o.summary.trim() : '';
571
- const M = method.toUpperCase();
572
- if (!s && typeof o.operationId === 'string' && o.operationId.trim()) {
573
- s = o.operationId.trim();
574
- warn(`Spec normalization: ${M} ${pathKey} — missing summary; using operationId.`);
575
- }
576
- if (!s) {
577
- s = `${M} ${pathKey}`;
578
- warn(
579
- `Spec normalization: ${M} ${pathKey} — missing summary and operationId; using method + path.`
580
- );
581
- }
582
- if (s.length > SPEC_SUMMARY_MAX_LEN) {
583
- const before = s.length;
584
- s = `${s.slice(0, SPEC_SUMMARY_MAX_LEN - 1)}…`;
585
- warn(
586
- `Spec normalization: ${M} ${pathKey} — summary truncated from ${before} to ${SPEC_SUMMARY_MAX_LEN} characters.`
587
- );
588
- }
589
- if (prev !== s && (typeof prev !== 'string' || prev.trim() !== s)) {
590
- o.summary = s;
591
- changed = true;
592
- }
593
- }
594
- }
595
- if (!changed) return raw;
596
- return asJson ? `${JSON.stringify(doc, null, 2)}\n` : `${stringify(doc, { lineWidth: 0 })}\n`;
597
- }
598
-
599
- function validateSpecStructure(content: string): void {
600
- let parsed: unknown;
601
- try {
602
- parsed = JSON.parse(content);
603
- } catch {
604
- try {
605
- parsed = parse(content);
606
- } catch {
607
- throw new Error('Spec content is not valid JSON or YAML');
608
- }
609
- }
610
-
611
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
612
- throw new Error('Spec content must be a JSON or YAML object');
613
- }
614
-
615
- const doc = parsed as Record<string, unknown>;
616
- if (!doc.openapi && !doc.swagger) {
617
- throw new Error('Spec is missing "openapi" or "swagger" version field');
618
- }
619
- }
620
-
621
- export async function runBootstrap(
622
- inputs: ResolvedInputs,
623
- dependencies: BootstrapExecutionDependencies
624
- ): Promise<PlannedOutputs> {
625
- const outputs = createPlannedOutputs(inputs);
626
- const requiresReleaseLabel =
627
- inputs.collectionSyncMode === 'version' || inputs.specSyncMode === 'version';
628
- const releaseLabel = requiresReleaseLabel ? deriveReleaseLabel(inputs) : undefined;
629
- if (requiresReleaseLabel && !releaseLabel) {
630
- throw new Error(
631
- 'Versioned spec or collection sync requires a release-label or derivable GitHub ref metadata'
632
- );
633
- }
634
- const workspaceName = createWorkspaceName(inputs);
635
- const aboutText = `Auto-provisioned by Postman CS open-alpha for ${inputs.projectName}`;
636
-
637
- await runGroup(dependencies.core, 'Install Postman CLI', async () => {
638
- await ensurePostmanCli(dependencies, inputs.postmanApiKey);
639
- });
640
-
641
- const resourcesState = readResourcesState();
642
-
643
- let explicitWorkspaceId = inputs.workspaceId;
644
- if (!explicitWorkspaceId && resourcesState?.workspace?.id) {
645
- explicitWorkspaceId = resourcesState.workspace.id;
646
- dependencies.core.info('Resolved workspace-id from .postman/resources.yaml');
647
- }
648
-
649
- const repoWorkspaceId = explicitWorkspaceId;
650
- let workspaceId = explicitWorkspaceId;
651
-
652
- let teamId = inputs.teamId || '';
653
- if (!teamId) {
654
- teamId = await dependencies.postman.getAutoDerivedTeamId() || '';
655
- }
656
- const repoUrl = inputs.repoUrl || '';
657
-
658
- if (!explicitWorkspaceId && repoUrl && inputs.postmanAccessToken && teamId) {
659
- const selection = await runGroup(
660
- dependencies.core,
661
- 'Resolve Canonical Workspace',
662
- async () => resolveCanonicalWorkspaceSelection({
663
- postman: dependencies.postman,
664
- workspaceName,
665
- repoWorkspaceId,
666
- repoUrl,
667
- teamId,
668
- accessToken: inputs.postmanAccessToken!,
669
- warn: (msg) => dependencies.core.warning(msg),
670
- })
671
- );
672
-
673
- if (selection.type === 'existing') {
674
- workspaceId = selection.workspaceId;
675
- if (selection.warning) {
676
- dependencies.core.warning(selection.warning);
677
- }
678
- dependencies.core.info(`Using canonical workspace (${selection.source}): ${workspaceId}`);
679
- } else if (selection.type === 'manual_review') {
680
- throw new Error(`Workspace selection requires manual review: ${selection.reason}`);
681
- } else {
682
- workspaceId = undefined;
683
- }
684
- } else if (workspaceId) {
685
- dependencies.core.info(`Using existing workspace: ${workspaceId}`);
686
- }
687
-
688
- // Parse workspace-team-id from already-resolved inputs
689
- let workspaceTeamId: number | undefined;
690
- if (inputs.workspaceTeamId) {
691
- workspaceTeamId = parseInt(inputs.workspaceTeamId, 10);
692
- if (Number.isNaN(workspaceTeamId)) {
693
- throw new Error(`workspace-team-id must be a numeric sub-team ID, got: ${inputs.workspaceTeamId}`);
694
- }
695
- }
696
-
697
- // Org-mode detection: only check if we need to create a workspace (not reuse existing)
698
- if (!workspaceId && !workspaceTeamId) {
699
- try {
700
- const teams = await dependencies.postman.getTeams();
701
- if (teams.length > 1 && teams.every(t => t.organizationId == null)) {
702
- dependencies.core.warning(
703
- 'GET /teams returned multiple teams but none include organizationId. ' +
704
- 'Org-mode detection may be degraded due to an upstream API change. ' +
705
- 'If workspace creation fails, set workspace-team-id explicitly.'
706
- );
707
- }
708
- const orgIds = new Set(teams.filter(t => t.organizationId != null).map(t => t.organizationId));
709
- const meTeamId = parseInt(teamId, 10);
710
- const isOrgMode = teams.length > 1
711
- && orgIds.size === 1
712
- && orgIds.has(meTeamId);
713
-
714
- if (isOrgMode) {
715
- const teamList = teams
716
- .map(t => ` ${t.id} ${t.name}`)
717
- .join('\n');
718
- throw new Error(
719
- `Org-mode account detected. Workspace creation requires a specific sub-team ID.\n\n` +
720
- `Available sub-teams:\n${teamList}\n\n` +
721
- `To fix this, set the workspace-team-id input in your workflow:\n` +
722
- ` workspace-team-id: '<id>'\n\n` +
723
- `Or for reuse across runs, create a repository variable and reference it:\n` +
724
- ` workspace-team-id: \${{ vars.POSTMAN_WORKSPACE_TEAM_ID }}\n\n` +
725
- `For CLI usage, pass --workspace-team-id <id> or export POSTMAN_WORKSPACE_TEAM_ID=<id>.`
726
- );
727
- } else if (teams.length > 1) {
728
- dependencies.core.warning(
729
- `API key has access to ${teams.length} teams but org-mode could not be confirmed. ` +
730
- `Proceeding without teamId. If workspace creation fails, set workspace-team-id explicitly.`
731
- );
732
- }
733
- } catch (err) {
734
- if (err instanceof Error && err.message.includes('Org-mode account detected')) {
735
- throw err;
736
- }
737
- dependencies.core.warning(
738
- `Could not check for org-mode sub-teams: ${err instanceof Error ? err.message : String(err)}`
739
- );
740
- }
741
- }
742
-
743
- if (!workspaceId) {
744
- const workspace = await runGroup(
745
- dependencies.core,
746
- 'Create Postman Workspace',
747
- async () => dependencies.postman.createWorkspace(workspaceName, aboutText, workspaceTeamId)
748
- );
749
- workspaceId = workspace.id;
750
- }
751
-
752
- outputs['workspace-id'] = workspaceId || '';
753
- outputs['workspace-url'] = `https://go.postman.co/workspace/${workspaceId}`;
754
- outputs['workspace-name'] = workspaceName;
755
-
756
- if (inputs.domain && dependencies.internalIntegration) {
757
- await runGroup(
758
- dependencies.core,
759
- 'Assign Workspace to Governance Group',
760
- async () => {
761
- try {
762
- await dependencies.internalIntegration?.assignWorkspaceToGovernanceGroup(
763
- workspaceId || '',
764
- inputs.domain || '',
765
- inputs.governanceMappingJson
766
- );
767
- } catch (error) {
768
- dependencies.core.warning(
769
- `Failed to assign governance group: ${error instanceof Error ? error.message : String(error)
770
- }`
771
- );
772
- }
773
- }
774
- );
775
- }
776
-
777
- if (inputs.requesterEmail) {
778
- await runGroup(
779
- dependencies.core,
780
- 'Invite Requester to Workspace',
781
- async () => {
782
- try {
783
- await dependencies.postman.inviteRequesterToWorkspace(
784
- workspaceId || '',
785
- inputs.requesterEmail || ''
786
- );
787
- } catch (error) {
788
- dependencies.core.warning(
789
- `Failed to invite requester: ${error instanceof Error ? error.message : String(error)
790
- }`
791
- );
792
- }
793
- }
794
- );
795
- }
796
-
797
- const adminIds = inputs.workspaceAdminUserIds || '';
798
- if (adminIds) {
799
- await runGroup(
800
- dependencies.core,
801
- 'Add Team Admins to Workspace',
802
- async () => {
803
- try {
804
- await dependencies.postman.addAdminsToWorkspace(workspaceId || '', adminIds);
805
- } catch (error) {
806
- dependencies.core.warning(
807
- `Failed to add team admins: ${error instanceof Error ? error.message : String(error)
808
- }`
809
- );
810
- }
811
- }
812
- );
813
- }
814
-
815
- let specId = inputs.specId;
816
- if (!specId) {
817
- specId = getFirstCloudResourceId(resourcesState?.cloudResources?.specs);
818
- if (specId) {
819
- dependencies.core.info('Resolved spec-id from .postman/resources.yaml');
820
- }
821
- }
822
-
823
- let baselineCollectionId =
824
- inputs.collectionSyncMode === 'refresh' ? undefined : inputs.baselineCollectionId;
825
- let smokeCollectionId =
826
- inputs.collectionSyncMode === 'refresh' ? undefined : inputs.smokeCollectionId;
827
- let contractCollectionId =
828
- inputs.collectionSyncMode === 'refresh' ? undefined : inputs.contractCollectionId;
829
-
830
- if (inputs.collectionSyncMode !== 'refresh') {
831
- const cloudCollections = resourcesState?.cloudResources?.collections;
832
- if (!baselineCollectionId) {
833
- baselineCollectionId = findCloudResourceId(
834
- cloudCollections,
835
- (filePath) => filePath.includes('[Baseline]')
836
- );
837
- if (baselineCollectionId) {
838
- dependencies.core.info('Resolved baseline-collection-id from .postman/resources.yaml');
839
- }
840
- }
841
- if (!smokeCollectionId) {
842
- smokeCollectionId = findCloudResourceId(
843
- cloudCollections,
844
- (filePath) => filePath.includes('[Smoke]')
845
- );
846
- if (smokeCollectionId) {
847
- dependencies.core.info('Resolved smoke-collection-id from .postman/resources.yaml');
848
- }
849
- }
850
- if (!contractCollectionId) {
851
- contractCollectionId = findCloudResourceId(
852
- cloudCollections,
853
- (filePath) => filePath.includes('[Contract]')
854
- );
855
- if (contractCollectionId) {
856
- dependencies.core.info('Resolved contract-collection-id from .postman/resources.yaml');
857
- }
858
- }
859
- }
860
-
861
- if (specId) {
862
- dependencies.core.info(`Updating existing spec ${specId} from ${inputs.specUrl}`);
863
- }
864
-
865
- const isSpecUpdate = Boolean(specId);
866
- let previousSpecContent: string | undefined;
867
-
868
- const specContent = await runGroup(
869
- dependencies.core,
870
- specId ? 'Update Spec in Spec Hub' : 'Upload Spec to Spec Hub',
871
- async () => {
872
- const fetched = await fetchSpecDocument(inputs.specUrl, dependencies.specFetcher);
873
- const document = normalizeSpecDocument(fetched, (msg) =>
874
- dependencies.core.warning(msg)
875
- );
876
- validateSpecStructure(document);
877
- if (specId) {
878
- previousSpecContent = await dependencies.postman.getSpecContent(specId);
879
- await dependencies.postman.updateSpec(specId, document, workspaceId);
880
- } else {
881
- specId = await dependencies.postman.uploadSpec(
882
- workspaceId || '',
883
- createAssetProjectName(
884
- inputs,
885
- inputs.specSyncMode === 'version' ? releaseLabel : undefined
886
- ),
887
- document
888
- );
889
- }
890
- outputs['spec-id'] = specId;
891
- return document;
892
- }
893
- );
894
-
895
- void specContent;
896
-
897
- const lintSummary = await runGroup(
898
- dependencies.core,
899
- 'Lint Spec via Postman CLI',
900
- async () => lintSpecViaCli(dependencies, workspaceId || '', outputs['spec-id'])
901
- );
902
- outputs['lint-summary-json'] = JSON.stringify({
903
- errors: lintSummary.errors,
904
- total: lintSummary.violations.length,
905
- violations: lintSummary.violations,
906
- warnings: lintSummary.warnings
907
- });
908
-
909
- if (lintSummary.errors > 0) {
910
- if (isSpecUpdate && specId && previousSpecContent !== undefined) {
911
- const restoringSpecId = specId;
912
- const previous = previousSpecContent;
913
- await runGroup(
914
- dependencies.core,
915
- 'Restore Previous Spec Content',
916
- async () => {
917
- await dependencies.postman.updateSpec(restoringSpecId, previous, workspaceId);
918
- }
919
- );
920
- }
921
- lintSummary.violations
922
- .filter((entry) => entry.severity === 'ERROR')
923
- .forEach((entry) => {
924
- dependencies.core.error(` ${entry.path || '<unknown>'}: ${entry.issue || 'Unknown lint error'}`);
925
- });
926
- throw new Error(`Spec lint found ${lintSummary.errors} errors`);
927
- }
928
-
929
- lintSummary.violations
930
- .filter((entry) => entry.severity === 'WARNING')
931
- .forEach((entry) => {
932
- dependencies.core.warning(
933
- ` ${entry.path || '<unknown>'}: ${entry.issue || 'Unknown lint warning'}`
934
- );
935
- });
936
-
937
- await runGroup(
938
- dependencies.core,
939
- 'Generate Collections from Spec',
940
- async () => {
941
- const shouldReuseCollections = inputs.collectionSyncMode !== 'refresh';
942
- const assetProjectName =
943
- inputs.collectionSyncMode === 'version'
944
- ? createAssetProjectName(inputs, releaseLabel)
945
- : inputs.projectName;
946
-
947
- outputs['baseline-collection-id'] =
948
- shouldReuseCollections ? baselineCollectionId || '' : '';
949
- outputs['smoke-collection-id'] =
950
- shouldReuseCollections ? smokeCollectionId || '' : '';
951
- outputs['contract-collection-id'] =
952
- shouldReuseCollections ? contractCollectionId || '' : '';
953
-
954
- if (!outputs['baseline-collection-id']) {
955
- outputs['baseline-collection-id'] = await dependencies.postman.generateCollection(
956
- outputs['spec-id'],
957
- assetProjectName,
958
- '[Baseline]'
959
- );
960
- } else {
961
- dependencies.core.info(
962
- `Using existing baseline collection: ${outputs['baseline-collection-id']}`
963
- );
964
- }
965
- if (!outputs['smoke-collection-id']) {
966
- outputs['smoke-collection-id'] = await dependencies.postman.generateCollection(
967
- outputs['spec-id'],
968
- assetProjectName,
969
- '[Smoke]'
970
- );
971
- } else {
972
- dependencies.core.info(
973
- `Using existing smoke collection: ${outputs['smoke-collection-id']}`
974
- );
975
- }
976
- if (!outputs['contract-collection-id']) {
977
- outputs['contract-collection-id'] = await dependencies.postman.generateCollection(
978
- outputs['spec-id'],
979
- assetProjectName,
980
- '[Contract]'
981
- );
982
- } else {
983
- dependencies.core.info(
984
- `Using existing contract collection: ${outputs['contract-collection-id']}`
985
- );
986
- }
987
- }
988
- );
989
-
990
- outputs['collections-json'] = JSON.stringify({
991
- baseline: outputs['baseline-collection-id'],
992
- contract: outputs['contract-collection-id'],
993
- smoke: outputs['smoke-collection-id']
994
- });
995
-
996
- await runGroup(
997
- dependencies.core,
998
- 'Inject Test Scripts',
999
- async () => {
1000
- await Promise.all([
1001
- dependencies.postman.injectTests(outputs['smoke-collection-id'], 'smoke'),
1002
- dependencies.postman.injectTests(
1003
- outputs['contract-collection-id'],
1004
- 'contract'
1005
- )
1006
- ]);
1007
- }
1008
- );
1009
-
1010
- await runGroup(
1011
- dependencies.core,
1012
- 'Tag Collections',
1013
- async () => {
1014
- await Promise.all([
1015
- dependencies.postman.tagCollection(outputs['baseline-collection-id'], [
1016
- 'generated-docs'
1017
- ]),
1018
- dependencies.postman.tagCollection(outputs['smoke-collection-id'], [
1019
- 'generated-smoke'
1020
- ]),
1021
- dependencies.postman.tagCollection(outputs['contract-collection-id'], [
1022
- 'generated-contract'
1023
- ])
1024
- ]);
1025
- }
1026
- );
1027
-
1028
- for (const [name, value] of Object.entries(outputs)) {
1029
- dependencies.core.setOutput(name, value);
1030
- }
1031
-
1032
- return outputs;
1033
- }
1034
-
1035
- export async function runAction(
1036
- actionCore: CoreLike = core,
1037
- actionExec: ExecLike = exec,
1038
- actionIo: IOLike = io
1039
- ): Promise<PlannedOutputs> {
1040
- const inputs = readActionInputs(actionCore);
1041
- const dependencies = createBootstrapDependencies(inputs, {
1042
- core: actionCore,
1043
- exec: actionExec,
1044
- io: actionIo,
1045
- specFetcher: fetch
1046
- });
1047
-
1048
- if (inputs.domain && !dependencies.internalIntegration) {
1049
- actionCore.warning(
1050
- 'Skipping governance assignment because postman-access-token is not configured'
1051
- );
1052
- }
1053
-
1054
- return runBootstrap(inputs, dependencies);
1055
- }
1056
-
1057
- export function createBootstrapDependencies(
1058
- inputs: ResolvedInputs,
1059
- factories: BootstrapDependencyFactories
1060
- ): BootstrapExecutionDependencies {
1061
- const secretMasker = createSecretMasker([
1062
- inputs.postmanApiKey,
1063
- inputs.postmanAccessToken
1064
- ]);
1065
- const postman = new PostmanAssetsClient({
1066
- apiKey: inputs.postmanApiKey,
1067
- secretMasker
1068
- });
1069
- const internalIntegration =
1070
- inputs.postmanAccessToken
1071
- ? createInternalIntegrationAdapter({
1072
- accessToken: inputs.postmanAccessToken,
1073
- backend: inputs.integrationBackend,
1074
- secretMasker,
1075
- teamId: inputs.teamId || ''
1076
- })
1077
- : undefined;
1078
-
1079
- return {
1080
- core: factories.core,
1081
- exec: factories.exec,
1082
- io: factories.io,
1083
- internalIntegration,
1084
- postman,
1085
- specFetcher: factories.specFetcher ?? fetch
1086
- };
1087
- }
1088
-
1089
- const currentModulePath = typeof __filename === 'string' ? __filename : '';
1090
- const entrypoint = process.argv[1];
1091
-
1092
- if (entrypoint && currentModulePath === entrypoint) {
1093
- runAction().catch((error) => {
1094
- if (error instanceof Error) {
1095
- core.setFailed(error.message);
1096
- return;
1097
- }
1098
- core.setFailed(String(error));
1099
- });
1100
- }