@postman-cse/onboarding-bootstrap 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -10
- package/action.yml +18 -2
- package/dist/cli.cjs +34450 -2318
- package/dist/index.cjs +34330 -2276
- package/package.json +22 -3
- package/.github/actionlint.yaml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/.github/workflows/contract-smoke.yml +0 -155
- package/.github/workflows/release.yml +0 -62
- package/.omc/sessions/b36ac16d-ce26-4e94-ae7e-dd817f2430a3.json +0 -8
- package/CLAUDE.md +0 -57
- package/src/cli.ts +0 -281
- package/src/contracts.ts +0 -172
- package/src/index.ts +0 -1150
- package/src/lib/github/github-api-client.ts +0 -262
- package/src/lib/http-error.ts +0 -80
- package/src/lib/postman/internal-integration-adapter.ts +0 -358
- package/src/lib/postman/postman-assets-client.ts +0 -786
- package/src/lib/postman/workspace-selection.ts +0 -121
- package/src/lib/repo/context.ts +0 -119
- package/src/lib/retry.ts +0 -79
- package/src/lib/secrets.ts +0 -104
- package/tests/bootstrap-action.test.ts +0 -1301
- package/tests/cli.test.ts +0 -153
- package/tests/contract.test.ts +0 -131
- package/tests/github-api-client.test.ts +0 -91
- package/tests/internal-integration-adapter.test.ts +0 -313
- package/tests/postman-assets-client.test.ts +0 -268
- package/tests/retry.test.ts +0 -54
- package/tests/secrets.test.ts +0 -66
- package/tests/workspace-selection.test.ts +0 -301
- package/tmp/cli-run-result.json +0 -11
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -8
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
|
-
}
|