@postman-cse/onboarding-bootstrap 0.9.1

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