@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/.github/actionlint.yaml +2 -0
- package/.github/workflows/ci.yml +36 -0
- package/.github/workflows/release.yml +62 -0
- package/README.md +261 -0
- package/action.yml +91 -0
- package/dist/cli.cjs +31207 -0
- package/dist/index.cjs +30998 -0
- package/package.json +43 -0
- package/src/cli.ts +286 -0
- package/src/contracts.ts +166 -0
- package/src/index.ts +1170 -0
- package/src/lib/github/github-api-client.ts +262 -0
- package/src/lib/http-error.ts +80 -0
- package/src/lib/postman/internal-integration-adapter.ts +272 -0
- package/src/lib/postman/postman-assets-client.ts +763 -0
- package/src/lib/postman/workspace-selection.ts +121 -0
- package/src/lib/repo/context.ts +119 -0
- package/src/lib/retry.ts +79 -0
- package/src/lib/secrets.ts +104 -0
- package/tests/bootstrap-action.test.ts +731 -0
- package/tests/cli.test.ts +144 -0
- package/tests/contract.test.ts +106 -0
- package/tests/github-api-client.test.ts +91 -0
- package/tests/internal-integration-adapter.test.ts +244 -0
- package/tests/postman-assets-client.test.ts +115 -0
- package/tests/retry.test.ts +54 -0
- package/tests/secrets.test.ts +66 -0
- package/tests/workspace-selection.test.ts +301 -0
- package/tmp/cli-run-result.json +11 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +8 -0
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
|
+
}
|