@postman-cse/onboarding-bootstrap 0.11.0 → 0.13.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,1150 +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
- syncExamples: boolean;
23
- collectionSyncMode: 'reuse' | 'refresh' | 'version';
24
- specSyncMode: 'update' | 'version';
25
- releaseLabel?: string;
26
- domain?: string;
27
- domainCode?: string;
28
- requesterEmail?: string;
29
- workspaceAdminUserIds?: string;
30
- workspaceTeamId?: string;
31
- teamId?: string;
32
- repoUrl?: string;
33
- specUrl: string;
34
- governanceMappingJson: string;
35
- postmanApiKey: string;
36
- postmanAccessToken?: string;
37
- integrationBackend: string;
38
- githubRefName?: string;
39
- githubHeadRef?: string;
40
- githubRef?: string;
41
- githubSha?: string;
42
- }
43
-
44
- export interface PlannedOutputs {
45
- 'workspace-id': string;
46
- 'workspace-url': string;
47
- 'workspace-name': string;
48
- 'spec-id': string;
49
- 'baseline-collection-id': string;
50
- 'smoke-collection-id': string;
51
- 'contract-collection-id': string;
52
- 'collections-json': string;
53
- 'lint-summary-json': string;
54
- }
55
-
56
- export interface LintViolation {
57
- issue?: string;
58
- path?: string;
59
- severity?: string;
60
- }
61
-
62
- export interface LintSummary {
63
- errors: number;
64
- violations: LintViolation[];
65
- warnings: number;
66
- }
67
-
68
- export interface CoreLike {
69
- error(message: string): void;
70
- getInput(name: string, options?: { required?: boolean }): string;
71
- group<T>(name: string, fn: () => Promise<T>): Promise<T>;
72
- info(message: string): void;
73
- setFailed(message: string): void;
74
- setOutput(name: string, value: string): void;
75
- setSecret(secret: string): void;
76
- warning(message: string): void;
77
- }
78
-
79
- export interface ExecLike {
80
- exec(
81
- commandLine: string,
82
- args?: string[],
83
- options?: Parameters<typeof exec.exec>[2]
84
- ): ReturnType<typeof exec.exec>;
85
- getExecOutput(
86
- commandLine: string,
87
- args?: string[],
88
- options?: Parameters<typeof exec.getExecOutput>[2]
89
- ): ReturnType<typeof exec.getExecOutput>;
90
- }
91
-
92
- export interface IOLike {
93
- which(tool: string, check?: boolean): Promise<string>;
94
- }
95
-
96
- export interface BootstrapExecutionDependencies {
97
- core: Pick<
98
- CoreLike,
99
- 'error' | 'group' | 'info' | 'setOutput' | 'warning'
100
- >;
101
- exec: ExecLike;
102
- io: IOLike;
103
- internalIntegration?: Pick<
104
- InternalIntegrationAdapter,
105
- 'assignWorkspaceToGovernanceGroup' | 'linkCollectionsToSpecification' | 'syncCollection'
106
- >;
107
- postman: Pick<
108
- PostmanAssetsClient,
109
- | 'addAdminsToWorkspace'
110
- | 'createWorkspace'
111
- | 'findWorkspacesByName'
112
- | 'generateCollection'
113
- | 'getAutoDerivedTeamId'
114
- | 'getSpecContent'
115
- | 'getTeams'
116
- | 'getWorkspaceGitRepoUrl'
117
- | 'injectTests'
118
- | 'inviteRequesterToWorkspace'
119
- | 'tagCollection'
120
- | 'uploadSpec'
121
- | 'updateSpec'
122
- >;
123
- specFetcher: typeof fetch;
124
- }
125
-
126
- export interface BootstrapDependencyFactories {
127
- core: Pick<CoreLike, 'error' | 'group' | 'info' | 'setOutput' | 'warning'>;
128
- exec: ExecLike;
129
- io: IOLike;
130
- specFetcher?: typeof fetch;
131
- }
132
-
133
- function normalizeInputValue(value: string | undefined): string | undefined {
134
- if (value === undefined) {
135
- return undefined;
136
- }
137
- const trimmed = value.trim();
138
- return trimmed ? trimmed : undefined;
139
- }
140
-
141
- export function getInput(
142
- name: string,
143
- env: NodeJS.ProcessEnv = process.env
144
- ): string | undefined {
145
- const envName = `INPUT_${name.replace(/-/g, '_').toUpperCase()}`;
146
- return normalizeInputValue(env[envName]);
147
- }
148
-
149
- function requireInput(
150
- actionCore: Pick<CoreLike, 'getInput'>,
151
- name: string
152
- ): string {
153
- return actionCore.getInput(name, { required: true }).trim();
154
- }
155
-
156
- function optionalInput(
157
- actionCore: Pick<CoreLike, 'getInput'>,
158
- name: string
159
- ): string | undefined {
160
- return normalizeInputValue(actionCore.getInput(name));
161
- }
162
-
163
- function parseJsonValue<T>(
164
- raw: string,
165
- fallback: T,
166
- inputName: string
167
- ): T {
168
- try {
169
- return (JSON.parse(raw || JSON.stringify(fallback)) as T) ?? fallback;
170
- } catch (error) {
171
- throw new Error(
172
- `Invalid JSON for ${inputName}: ${error instanceof Error ? error.message : String(error)
173
- }`
174
- );
175
- }
176
- }
177
-
178
- function asStringArray(value: unknown, inputName: string): string[] {
179
- if (!Array.isArray(value)) {
180
- throw new Error(`${inputName} must be a JSON array`);
181
- }
182
- return value.map((entry) => String(entry));
183
- }
184
-
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;
198
- }
199
- return 'refresh';
200
- }
201
-
202
- function parseSpecSyncMode(value: string | undefined): 'update' | 'version' {
203
- if (value === 'version') {
204
- return value;
205
- }
206
- return 'update';
207
- }
208
-
209
- export function resolveInputs(
210
- env: NodeJS.ProcessEnv = process.env
211
- ): ResolvedInputs {
212
- const repoContext = detectRepoContext(
213
- {
214
- repoUrl: getInput('repo-url', env)
215
- },
216
- env
217
- );
218
-
219
- const integrationBackend =
220
- getInput('integration-backend', env) ??
221
- openAlphaActionContract.inputs['integration-backend'].default ??
222
- 'bifrost';
223
-
224
- const allowedBackends =
225
- openAlphaActionContract.inputs['integration-backend'].allowedValues ?? [];
226
- if (allowedBackends.length > 0 && !allowedBackends.includes(integrationBackend)) {
227
- throw new Error(
228
- `Unsupported integration-backend "${integrationBackend}". Supported values: ${allowedBackends.join(', ')}`
229
- );
230
- }
231
-
232
- const specUrl = getInput('spec-url', env) ?? '';
233
- if (specUrl) {
234
- try {
235
- const parsedUrl = new URL(specUrl);
236
- if (parsedUrl.protocol !== 'https:') {
237
- throw new Error('not https');
238
- }
239
- } catch {
240
- throw new Error(`spec-url must be a valid HTTPS URL, got: ${specUrl}`);
241
- }
242
- }
243
-
244
- return {
245
- projectName: getInput('project-name', env)
246
- ?? env.GITHUB_REPOSITORY?.split('/').pop()
247
- ?? env.CI_PROJECT_NAME
248
- ?? '',
249
- workspaceId: getInput('workspace-id', env),
250
- specId: getInput('spec-id', env),
251
- baselineCollectionId: getInput('baseline-collection-id', env),
252
- smokeCollectionId: getInput('smoke-collection-id', env),
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),
258
- domain: getInput('domain', env),
259
- domainCode: getInput('domain-code', env),
260
- requesterEmail: getInput('requester-email', env),
261
- workspaceAdminUserIds:
262
- getInput('workspace-admin-user-ids', env) || env.WORKSPACE_ADMIN_USER_IDS || '',
263
- workspaceTeamId: getInput('workspace-team-id', env) || env.POSTMAN_WORKSPACE_TEAM_ID,
264
- teamId: getInput('team-id', env) || env.POSTMAN_TEAM_ID || '',
265
- repoUrl: repoContext.repoUrl || '',
266
- specUrl,
267
- governanceMappingJson:
268
- getInput('governance-mapping-json', env) ??
269
- openAlphaActionContract.inputs['governance-mapping-json'].default ??
270
- '{}',
271
- postmanApiKey: getInput('postman-api-key', env) ?? '',
272
- postmanAccessToken: getInput('postman-access-token', env),
273
- integrationBackend,
274
- githubRefName: env.GITHUB_REF_NAME,
275
- githubHeadRef: env.GITHUB_HEAD_REF,
276
- githubRef: env.GITHUB_REF,
277
- githubSha: env.GITHUB_SHA
278
- };
279
- }
280
-
281
- export function createPlannedOutputs(inputs: ResolvedInputs): PlannedOutputs {
282
- const workspaceName = inputs.domainCode
283
- ? `[${inputs.domainCode}] ${inputs.projectName}`
284
- : inputs.projectName;
285
-
286
- return {
287
- 'workspace-id': '',
288
- 'workspace-url': '',
289
- 'workspace-name': workspaceName,
290
- 'spec-id': '',
291
- 'baseline-collection-id': '',
292
- 'smoke-collection-id': '',
293
- 'contract-collection-id': '',
294
- 'collections-json': JSON.stringify({
295
- baseline: '',
296
- smoke: '',
297
- contract: ''
298
- }),
299
- 'lint-summary-json': JSON.stringify({
300
- errors: 0,
301
- total: 0,
302
- violations: [],
303
- warnings: 0
304
- })
305
- };
306
- }
307
-
308
- export function readActionInputs(
309
- actionCore: Pick<CoreLike, 'getInput' | 'setSecret'>
310
- ): ResolvedInputs {
311
- const projectName = requireInput(actionCore, 'project-name');
312
- const specUrl = requireInput(actionCore, 'spec-url');
313
- const postmanApiKey = requireInput(actionCore, 'postman-api-key');
314
- const postmanAccessToken = optionalInput(actionCore, 'postman-access-token');
315
-
316
- actionCore.setSecret(postmanApiKey);
317
- if (postmanAccessToken) actionCore.setSecret(postmanAccessToken);
318
-
319
- const inputs = resolveInputs({
320
- ...process.env,
321
- INPUT_PROJECT_NAME: projectName,
322
- INPUT_WORKSPACE_ID: optionalInput(actionCore, 'workspace-id'),
323
- INPUT_SPEC_ID: optionalInput(actionCore, 'spec-id'),
324
- INPUT_BASELINE_COLLECTION_ID: optionalInput(actionCore, 'baseline-collection-id'),
325
- INPUT_SMOKE_COLLECTION_ID: optionalInput(actionCore, 'smoke-collection-id'),
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'),
337
- INPUT_DOMAIN: optionalInput(actionCore, 'domain'),
338
- INPUT_DOMAIN_CODE: optionalInput(actionCore, 'domain-code'),
339
- INPUT_REQUESTER_EMAIL: optionalInput(actionCore, 'requester-email'),
340
- INPUT_WORKSPACE_ADMIN_USER_IDS: optionalInput(
341
- actionCore,
342
- 'workspace-admin-user-ids'
343
- ),
344
- INPUT_WORKSPACE_TEAM_ID:
345
- optionalInput(actionCore, 'workspace-team-id') || process.env.POSTMAN_WORKSPACE_TEAM_ID,
346
- INPUT_TEAM_ID:
347
- optionalInput(actionCore, 'postman-team-id') || process.env.POSTMAN_TEAM_ID,
348
- INPUT_REPO_URL: optionalInput(actionCore, 'repo-url'),
349
- INPUT_SPEC_URL: specUrl,
350
- INPUT_GOVERNANCE_MAPPING_JSON:
351
- optionalInput(actionCore, 'governance-mapping-json') ??
352
- openAlphaActionContract.inputs['governance-mapping-json'].default,
353
- INPUT_POSTMAN_API_KEY: postmanApiKey,
354
- INPUT_POSTMAN_ACCESS_TOKEN: postmanAccessToken,
355
- INPUT_INTEGRATION_BACKEND:
356
- optionalInput(actionCore, 'integration-backend') ??
357
- openAlphaActionContract.inputs['integration-backend'].default
358
- });
359
-
360
- return inputs;
361
- }
362
-
363
- function createWorkspaceName(inputs: ResolvedInputs): string {
364
- return inputs.domainCode
365
- ? `[${inputs.domainCode}] ${inputs.projectName}`
366
- : inputs.projectName;
367
- }
368
-
369
- async function runGroup<T>(
370
- actionCore: Pick<CoreLike, 'group'>,
371
- name: string,
372
- fn: () => Promise<T>
373
- ): Promise<T> {
374
- return actionCore.group(name, fn);
375
- }
376
-
377
- async function ensurePostmanCli(
378
- dependencies: Pick<BootstrapExecutionDependencies, 'exec' | 'io'>,
379
- postmanApiKey: string
380
- ): Promise<void> {
381
- const existing = await dependencies.io.which('postman', false).catch(() => '');
382
- if (!existing) {
383
- await dependencies.exec.exec('sh', [
384
- '-c',
385
- 'curl -o- "https://dl-cli.pstmn.io/install/unix.sh" | sh'
386
- ]);
387
- }
388
-
389
- await dependencies.exec.exec('postman', ['login', '--with-api-key', postmanApiKey]);
390
- }
391
-
392
- export async function lintSpecViaCli(
393
- dependencies: Pick<BootstrapExecutionDependencies, 'exec'>,
394
- workspaceId: string,
395
- specId: string
396
- ): Promise<LintSummary> {
397
- const result = await dependencies.exec.getExecOutput(
398
- 'postman',
399
- [
400
- 'spec',
401
- 'lint',
402
- specId,
403
- '--workspace-id',
404
- workspaceId || '',
405
- '--report-events',
406
- '-o',
407
- 'json'
408
- ],
409
- {
410
- ignoreReturnCode: true
411
- }
412
- );
413
-
414
- if (result.exitCode !== 0 && !result.stdout.trim()) {
415
- throw new Error(`Spec lint command failed: ${result.stderr}`);
416
- }
417
-
418
- let parsed: { violations?: LintViolation[] };
419
- try {
420
- parsed = JSON.parse(result.stdout || '{}') as { violations?: LintViolation[] };
421
- } catch {
422
- throw new Error(
423
- `Spec lint output is not valid JSON. output: ${result.stdout}, err: ${result.stderr}`
424
- );
425
- }
426
-
427
- const violations = parsed.violations || [];
428
- const errors = violations.filter((entry) => entry.severity === 'ERROR').length;
429
- const warnings = violations.filter((entry) => entry.severity === 'WARNING').length;
430
-
431
- return {
432
- errors,
433
- violations,
434
- warnings
435
- };
436
- }
437
-
438
- async function fetchSpecDocument(
439
- specUrl: string,
440
- specFetcher: typeof fetch
441
- ): Promise<string> {
442
- return retry(
443
- async () => {
444
- const response = await specFetcher(specUrl, {
445
- headers: {
446
- 'User-Agent': 'postman-bootstrap-action'
447
- }
448
- });
449
-
450
- if (!response.ok) {
451
- throw new Error(`Failed to fetch spec from URL: ${response.status}`);
452
- }
453
-
454
- return response.text();
455
- },
456
- {
457
- maxAttempts: 3,
458
- delayMs: 3000
459
- }
460
- );
461
- }
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
-
540
- const SPEC_SUMMARY_MAX_LEN = 200;
541
- const SPEC_HTTP_METHODS = new Set([
542
- 'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'
543
- ]);
544
-
545
- /** OpenAPI JSON/YAML: fix missing or oversized operation summaries before Spec Hub upload. */
546
- export function normalizeSpecDocument(raw: string, warn: (msg: string) => void): string {
547
- const head = raw.trimStart();
548
- let doc: unknown;
549
- let asJson = false;
550
- try {
551
- if (head.startsWith('{') || head.startsWith('[')) {
552
- doc = JSON.parse(raw) as unknown;
553
- asJson = true;
554
- } else {
555
- doc = parse(raw) as unknown;
556
- }
557
- } catch {
558
- warn('Spec normalization skipped: document is not valid JSON or YAML.');
559
- return raw;
560
- }
561
- if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return raw;
562
- const paths = (doc as Record<string, unknown>).paths;
563
- if (!paths || typeof paths !== 'object' || Array.isArray(paths)) return raw;
564
-
565
- let changed = false;
566
- for (const [pathKey, pathItem] of Object.entries(paths as Record<string, unknown>)) {
567
- if (!pathItem || typeof pathItem !== 'object' || Array.isArray(pathItem)) continue;
568
- const item = pathItem as Record<string, unknown>;
569
- for (const method of Object.keys(item)) {
570
- if (!SPEC_HTTP_METHODS.has(method.toLowerCase())) continue;
571
- const op = item[method];
572
- if (!op || typeof op !== 'object' || Array.isArray(op)) continue;
573
- const o = op as Record<string, unknown>;
574
- const prev = o.summary;
575
- let s = typeof o.summary === 'string' ? o.summary.trim() : '';
576
- const M = method.toUpperCase();
577
- if (!s && typeof o.operationId === 'string' && o.operationId.trim()) {
578
- s = o.operationId.trim();
579
- warn(`Spec normalization: ${M} ${pathKey} — missing summary; using operationId.`);
580
- }
581
- if (!s) {
582
- s = `${M} ${pathKey}`;
583
- warn(
584
- `Spec normalization: ${M} ${pathKey} — missing summary and operationId; using method + path.`
585
- );
586
- }
587
- if (s.length > SPEC_SUMMARY_MAX_LEN) {
588
- const before = s.length;
589
- s = `${s.slice(0, SPEC_SUMMARY_MAX_LEN - 1)}…`;
590
- warn(
591
- `Spec normalization: ${M} ${pathKey} — summary truncated from ${before} to ${SPEC_SUMMARY_MAX_LEN} characters.`
592
- );
593
- }
594
- if (prev !== s && (typeof prev !== 'string' || prev.trim() !== s)) {
595
- o.summary = s;
596
- changed = true;
597
- }
598
- }
599
- }
600
- if (!changed) return raw;
601
- return asJson ? `${JSON.stringify(doc, null, 2)}\n` : `${stringify(doc, { lineWidth: 0 })}\n`;
602
- }
603
-
604
- function validateSpecStructure(content: string): void {
605
- let parsed: unknown;
606
- try {
607
- parsed = JSON.parse(content);
608
- } catch {
609
- try {
610
- parsed = parse(content);
611
- } catch {
612
- throw new Error('Spec content is not valid JSON or YAML');
613
- }
614
- }
615
-
616
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
617
- throw new Error('Spec content must be a JSON or YAML object');
618
- }
619
-
620
- const doc = parsed as Record<string, unknown>;
621
- if (!doc.openapi && !doc.swagger) {
622
- throw new Error('Spec is missing "openapi" or "swagger" version field');
623
- }
624
- }
625
-
626
- export async function runBootstrap(
627
- inputs: ResolvedInputs,
628
- dependencies: BootstrapExecutionDependencies
629
- ): Promise<PlannedOutputs> {
630
- const outputs = createPlannedOutputs(inputs);
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
- }
639
- const workspaceName = createWorkspaceName(inputs);
640
- const aboutText = `Auto-provisioned by Postman CS open-alpha for ${inputs.projectName}`;
641
-
642
- await runGroup(dependencies.core, 'Install Postman CLI', async () => {
643
- await ensurePostmanCli(dependencies, inputs.postmanApiKey);
644
- });
645
-
646
- const resourcesState = readResourcesState();
647
-
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');
652
- }
653
-
654
- const repoWorkspaceId = explicitWorkspaceId;
655
- let workspaceId = explicitWorkspaceId;
656
-
657
- let teamId = inputs.teamId || '';
658
- if (!teamId) {
659
- teamId = await dependencies.postman.getAutoDerivedTeamId() || '';
660
- }
661
- const repoUrl = inputs.repoUrl || '';
662
-
663
- if (!explicitWorkspaceId && repoUrl && inputs.postmanAccessToken && teamId) {
664
- const selection = await runGroup(
665
- dependencies.core,
666
- 'Resolve Canonical Workspace',
667
- async () => resolveCanonicalWorkspaceSelection({
668
- postman: dependencies.postman,
669
- workspaceName,
670
- repoWorkspaceId,
671
- repoUrl,
672
- teamId,
673
- accessToken: inputs.postmanAccessToken!,
674
- warn: (msg) => dependencies.core.warning(msg),
675
- })
676
- );
677
-
678
- if (selection.type === 'existing') {
679
- workspaceId = selection.workspaceId;
680
- if (selection.warning) {
681
- dependencies.core.warning(selection.warning);
682
- }
683
- dependencies.core.info(`Using canonical workspace (${selection.source}): ${workspaceId}`);
684
- } else if (selection.type === 'manual_review') {
685
- throw new Error(`Workspace selection requires manual review: ${selection.reason}`);
686
- } else {
687
- workspaceId = undefined;
688
- }
689
- } else if (workspaceId) {
690
- dependencies.core.info(`Using existing workspace: ${workspaceId}`);
691
- }
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
-
748
- if (!workspaceId) {
749
- const workspace = await runGroup(
750
- dependencies.core,
751
- 'Create Postman Workspace',
752
- async () => dependencies.postman.createWorkspace(workspaceName, aboutText, workspaceTeamId)
753
- );
754
- workspaceId = workspace.id;
755
- }
756
-
757
- outputs['workspace-id'] = workspaceId || '';
758
- outputs['workspace-url'] = `https://go.postman.co/workspace/${workspaceId}`;
759
- outputs['workspace-name'] = workspaceName;
760
-
761
- if (inputs.domain && dependencies.internalIntegration) {
762
- await runGroup(
763
- dependencies.core,
764
- 'Assign Workspace to Governance Group',
765
- async () => {
766
- try {
767
- await dependencies.internalIntegration?.assignWorkspaceToGovernanceGroup(
768
- workspaceId || '',
769
- inputs.domain || '',
770
- inputs.governanceMappingJson
771
- );
772
- } catch (error) {
773
- dependencies.core.warning(
774
- `Failed to assign governance group: ${error instanceof Error ? error.message : String(error)
775
- }`
776
- );
777
- }
778
- }
779
- );
780
- }
781
-
782
- if (inputs.requesterEmail) {
783
- await runGroup(
784
- dependencies.core,
785
- 'Invite Requester to Workspace',
786
- async () => {
787
- try {
788
- await dependencies.postman.inviteRequesterToWorkspace(
789
- workspaceId || '',
790
- inputs.requesterEmail || ''
791
- );
792
- } catch (error) {
793
- dependencies.core.warning(
794
- `Failed to invite requester: ${error instanceof Error ? error.message : String(error)
795
- }`
796
- );
797
- }
798
- }
799
- );
800
- }
801
-
802
- const adminIds = inputs.workspaceAdminUserIds || '';
803
- if (adminIds) {
804
- await runGroup(
805
- dependencies.core,
806
- 'Add Team Admins to Workspace',
807
- async () => {
808
- try {
809
- await dependencies.postman.addAdminsToWorkspace(workspaceId || '', adminIds);
810
- } catch (error) {
811
- dependencies.core.warning(
812
- `Failed to add team admins: ${error instanceof Error ? error.message : String(error)
813
- }`
814
- );
815
- }
816
- }
817
- );
818
- }
819
-
820
- let specId = inputs.specId;
821
- if (!specId) {
822
- specId = getFirstCloudResourceId(resourcesState?.cloudResources?.specs);
823
- if (specId) {
824
- dependencies.core.info('Resolved spec-id from .postman/resources.yaml');
825
- }
826
- }
827
-
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;
834
-
835
- if (inputs.collectionSyncMode !== 'refresh') {
836
- const cloudCollections = resourcesState?.cloudResources?.collections;
837
- if (!baselineCollectionId) {
838
- baselineCollectionId = findCloudResourceId(
839
- cloudCollections,
840
- (filePath) => filePath.includes('[Baseline]')
841
- );
842
- if (baselineCollectionId) {
843
- dependencies.core.info('Resolved baseline-collection-id from .postman/resources.yaml');
844
- }
845
- }
846
- if (!smokeCollectionId) {
847
- smokeCollectionId = findCloudResourceId(
848
- cloudCollections,
849
- (filePath) => filePath.includes('[Smoke]')
850
- );
851
- if (smokeCollectionId) {
852
- dependencies.core.info('Resolved smoke-collection-id from .postman/resources.yaml');
853
- }
854
- }
855
- if (!contractCollectionId) {
856
- contractCollectionId = findCloudResourceId(
857
- cloudCollections,
858
- (filePath) => filePath.includes('[Contract]')
859
- );
860
- if (contractCollectionId) {
861
- dependencies.core.info('Resolved contract-collection-id from .postman/resources.yaml');
862
- }
863
- }
864
- }
865
-
866
- if (specId) {
867
- dependencies.core.info(`Updating existing spec ${specId} from ${inputs.specUrl}`);
868
- }
869
-
870
- const isSpecUpdate = Boolean(specId);
871
- let previousSpecContent: string | undefined;
872
-
873
- const specContent = await runGroup(
874
- dependencies.core,
875
- specId ? 'Update Spec in Spec Hub' : 'Upload Spec to Spec Hub',
876
- async () => {
877
- const fetched = await fetchSpecDocument(inputs.specUrl, dependencies.specFetcher);
878
- const document = normalizeSpecDocument(fetched, (msg) =>
879
- dependencies.core.warning(msg)
880
- );
881
- validateSpecStructure(document);
882
- if (specId) {
883
- previousSpecContent = await dependencies.postman.getSpecContent(specId);
884
- await dependencies.postman.updateSpec(specId, document, workspaceId);
885
- } else {
886
- specId = await dependencies.postman.uploadSpec(
887
- workspaceId || '',
888
- createAssetProjectName(
889
- inputs,
890
- inputs.specSyncMode === 'version' ? releaseLabel : undefined
891
- ),
892
- document
893
- );
894
- }
895
- outputs['spec-id'] = specId;
896
- return document;
897
- }
898
- );
899
-
900
- void specContent;
901
-
902
- const lintSummary = await runGroup(
903
- dependencies.core,
904
- 'Lint Spec via Postman CLI',
905
- async () => lintSpecViaCli(dependencies, workspaceId || '', outputs['spec-id'])
906
- );
907
- outputs['lint-summary-json'] = JSON.stringify({
908
- errors: lintSummary.errors,
909
- total: lintSummary.violations.length,
910
- violations: lintSummary.violations,
911
- warnings: lintSummary.warnings
912
- });
913
-
914
- if (lintSummary.errors > 0) {
915
- if (isSpecUpdate && specId && previousSpecContent !== undefined) {
916
- const restoringSpecId = specId;
917
- const previous = previousSpecContent;
918
- await runGroup(
919
- dependencies.core,
920
- 'Restore Previous Spec Content',
921
- async () => {
922
- await dependencies.postman.updateSpec(restoringSpecId, previous, workspaceId);
923
- }
924
- );
925
- }
926
- lintSummary.violations
927
- .filter((entry) => entry.severity === 'ERROR')
928
- .forEach((entry) => {
929
- dependencies.core.error(` ${entry.path || '<unknown>'}: ${entry.issue || 'Unknown lint error'}`);
930
- });
931
- throw new Error(`Spec lint found ${lintSummary.errors} errors`);
932
- }
933
-
934
- lintSummary.violations
935
- .filter((entry) => entry.severity === 'WARNING')
936
- .forEach((entry) => {
937
- dependencies.core.warning(
938
- ` ${entry.path || '<unknown>'}: ${entry.issue || 'Unknown lint warning'}`
939
- );
940
- });
941
-
942
- await runGroup(
943
- dependencies.core,
944
- 'Generate Collections from Spec',
945
- async () => {
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 || '' : '';
958
-
959
- if (!outputs['baseline-collection-id']) {
960
- outputs['baseline-collection-id'] = await dependencies.postman.generateCollection(
961
- outputs['spec-id'],
962
- assetProjectName,
963
- '[Baseline]'
964
- );
965
- } else {
966
- dependencies.core.info(
967
- `Using existing baseline collection: ${outputs['baseline-collection-id']}`
968
- );
969
- }
970
- if (!outputs['smoke-collection-id']) {
971
- outputs['smoke-collection-id'] = await dependencies.postman.generateCollection(
972
- outputs['spec-id'],
973
- assetProjectName,
974
- '[Smoke]'
975
- );
976
- } else {
977
- dependencies.core.info(
978
- `Using existing smoke collection: ${outputs['smoke-collection-id']}`
979
- );
980
- }
981
- if (!outputs['contract-collection-id']) {
982
- outputs['contract-collection-id'] = await dependencies.postman.generateCollection(
983
- outputs['spec-id'],
984
- assetProjectName,
985
- '[Contract]'
986
- );
987
- } else {
988
- dependencies.core.info(
989
- `Using existing contract collection: ${outputs['contract-collection-id']}`
990
- );
991
- }
992
- }
993
- );
994
-
995
- outputs['collections-json'] = JSON.stringify({
996
- baseline: outputs['baseline-collection-id'],
997
- contract: outputs['contract-collection-id'],
998
- smoke: outputs['smoke-collection-id']
999
- });
1000
-
1001
- await runGroup(
1002
- dependencies.core,
1003
- 'Inject Test Scripts',
1004
- async () => {
1005
- await Promise.all([
1006
- dependencies.postman.injectTests(outputs['smoke-collection-id'], 'smoke'),
1007
- dependencies.postman.injectTests(
1008
- outputs['contract-collection-id'],
1009
- 'contract'
1010
- )
1011
- ]);
1012
- }
1013
- );
1014
-
1015
- await runGroup(
1016
- dependencies.core,
1017
- 'Tag Collections',
1018
- async () => {
1019
- await Promise.all([
1020
- dependencies.postman.tagCollection(outputs['baseline-collection-id'], [
1021
- 'generated-docs'
1022
- ]),
1023
- dependencies.postman.tagCollection(outputs['smoke-collection-id'], [
1024
- 'generated-smoke'
1025
- ]),
1026
- dependencies.postman.tagCollection(outputs['contract-collection-id'], [
1027
- 'generated-contract'
1028
- ])
1029
- ]);
1030
- }
1031
- );
1032
-
1033
- const linkedCollectionIds = [
1034
- outputs['baseline-collection-id'],
1035
- outputs['smoke-collection-id'],
1036
- outputs['contract-collection-id']
1037
- ].filter(Boolean);
1038
-
1039
- if (linkedCollectionIds.length > 0) {
1040
- if (dependencies.internalIntegration) {
1041
- await runGroup(
1042
- dependencies.core,
1043
- 'Link Collections to Specification',
1044
- async () => {
1045
- await dependencies.internalIntegration?.linkCollectionsToSpecification(
1046
- outputs['spec-id'],
1047
- linkedCollectionIds.map((collectionId) => ({
1048
- collectionId,
1049
- syncOptions: {
1050
- syncExamples: inputs.syncExamples
1051
- }
1052
- }))
1053
- );
1054
- }
1055
- );
1056
-
1057
- await runGroup(
1058
- dependencies.core,
1059
- 'Sync Linked Collections',
1060
- async () => {
1061
- await Promise.all(
1062
- linkedCollectionIds.map((collectionId) =>
1063
- dependencies.internalIntegration!.syncCollection(
1064
- outputs['spec-id'],
1065
- collectionId
1066
- )
1067
- )
1068
- );
1069
- }
1070
- );
1071
- } else {
1072
- dependencies.core.warning(
1073
- 'Skipping cloud spec-to-collection linking and sync because postman-access-token is not configured'
1074
- );
1075
- }
1076
- }
1077
-
1078
- for (const [name, value] of Object.entries(outputs)) {
1079
- dependencies.core.setOutput(name, value);
1080
- }
1081
-
1082
- return outputs;
1083
- }
1084
-
1085
- export async function runAction(
1086
- actionCore: CoreLike = core,
1087
- actionExec: ExecLike = exec,
1088
- actionIo: IOLike = io
1089
- ): Promise<PlannedOutputs> {
1090
- const inputs = readActionInputs(actionCore);
1091
- const dependencies = createBootstrapDependencies(inputs, {
1092
- core: actionCore,
1093
- exec: actionExec,
1094
- io: actionIo,
1095
- specFetcher: fetch
1096
- });
1097
-
1098
- if (inputs.domain && !dependencies.internalIntegration) {
1099
- actionCore.warning(
1100
- 'Skipping governance assignment because postman-access-token is not configured'
1101
- );
1102
- }
1103
-
1104
- return runBootstrap(inputs, dependencies);
1105
- }
1106
-
1107
- export function createBootstrapDependencies(
1108
- inputs: ResolvedInputs,
1109
- factories: BootstrapDependencyFactories
1110
- ): BootstrapExecutionDependencies {
1111
- const secretMasker = createSecretMasker([
1112
- inputs.postmanApiKey,
1113
- inputs.postmanAccessToken
1114
- ]);
1115
- const postman = new PostmanAssetsClient({
1116
- apiKey: inputs.postmanApiKey,
1117
- secretMasker
1118
- });
1119
- const internalIntegration =
1120
- inputs.postmanAccessToken
1121
- ? createInternalIntegrationAdapter({
1122
- accessToken: inputs.postmanAccessToken,
1123
- backend: inputs.integrationBackend,
1124
- secretMasker,
1125
- teamId: inputs.teamId || ''
1126
- })
1127
- : undefined;
1128
-
1129
- return {
1130
- core: factories.core,
1131
- exec: factories.exec,
1132
- io: factories.io,
1133
- internalIntegration,
1134
- postman,
1135
- specFetcher: factories.specFetcher ?? fetch
1136
- };
1137
- }
1138
-
1139
- const currentModulePath = typeof __filename === 'string' ? __filename : '';
1140
- const entrypoint = process.argv[1];
1141
-
1142
- if (entrypoint && currentModulePath === entrypoint) {
1143
- runAction().catch((error) => {
1144
- if (error instanceof Error) {
1145
- core.setFailed(error.message);
1146
- return;
1147
- }
1148
- core.setFailed(String(error));
1149
- });
1150
- }