@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/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@postman-cse/onboarding-bootstrap",
3
+ "version": "0.9.1",
4
+ "description": "Public open-alpha Postman bootstrap GitHub Action.",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "bin": {
8
+ "postman-bootstrap": "dist/cli.cjs"
9
+ },
10
+ "scripts": {
11
+ "build": "npm run typecheck && rm -rf dist && esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.cjs && esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/cli.cjs",
12
+ "check:dist": "npm run build && git diff --exit-code -- dist",
13
+ "test": "vitest run",
14
+ "typecheck": "tsc --noEmit -p tsconfig.json"
15
+ },
16
+ "keywords": [
17
+ "github-action",
18
+ "postman"
19
+ ],
20
+ "license": "MIT",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@actions/core": "^1.11.1",
26
+ "@actions/exec": "^1.1.1",
27
+ "@actions/io": "^1.1.3"
28
+ },
29
+ "overrides": {
30
+ "undici": "^6.24.0"
31
+ },
32
+ "devDependencies": {
33
+ "esbuild": "^0.27.3",
34
+ "@types/node": "^25.3.5",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.18",
37
+ "yaml": "^2.8.2"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/postman-cs/postman-bootstrap-action"
42
+ }
43
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,286 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { execFile } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ import * as io from '@actions/io';
7
+
8
+ import {
9
+ createBootstrapDependencies,
10
+ resolveInputs,
11
+ type ResolvedInputs,
12
+ runBootstrap,
13
+ type BootstrapExecutionDependencies,
14
+ type ExecLike,
15
+ type PlannedOutputs
16
+ } from './index.js';
17
+ import { createSecretMasker } from './lib/secrets.js';
18
+
19
+ interface CliConfig {
20
+ inputEnv: NodeJS.ProcessEnv;
21
+ resultJsonPath: string;
22
+ dotenvPath?: string;
23
+ }
24
+
25
+ export interface CliRuntime {
26
+ env?: NodeJS.ProcessEnv;
27
+ executeBootstrap?: typeof runBootstrap;
28
+ writeStdout?: (chunk: string) => void;
29
+ }
30
+
31
+ type ReporterCore = BootstrapExecutionDependencies['core'];
32
+
33
+ export class ConsoleReporter implements ReporterCore {
34
+ public error(message: string): void {
35
+ console.error(message);
36
+ }
37
+
38
+ public async group<T>(name: string, fn: () => Promise<T>): Promise<T> {
39
+ console.error(`[group] ${name}`);
40
+ return await fn();
41
+ }
42
+
43
+ public info(message: string): void {
44
+ console.error(message);
45
+ }
46
+
47
+ public setOutput(_name: string, _value: string): void {
48
+ }
49
+
50
+ public warning(message: string): void {
51
+ console.error(`warning: ${message}`);
52
+ }
53
+ }
54
+
55
+ function readFlag(argv: string[], name: string): string | undefined {
56
+ const prefix = `--${name}=`;
57
+ for (let index = 0; index < argv.length; index += 1) {
58
+ const arg = argv[index];
59
+ if (arg === `--${name}`) {
60
+ return argv[index + 1];
61
+ }
62
+ if (arg?.startsWith(prefix)) {
63
+ return arg.slice(prefix.length);
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+
69
+ function normalizeCliFlag(name: string): string {
70
+ return `INPUT_${name.replace(/-/g, '_').toUpperCase()}`;
71
+ }
72
+
73
+ const execFileAsync = promisify(execFile);
74
+
75
+ function toCommandLabel(commandLine: string, args: string[], secretMasker: (value: string) => string): string {
76
+ const rendered = [commandLine, ...args].join(' ');
77
+ return secretMasker(rendered);
78
+ }
79
+
80
+ export function createCliExec(secretMasker: (value: string) => string): ExecLike {
81
+ const execCommand = async (
82
+ commandLine: string,
83
+ args: string[] = [],
84
+ options?: Parameters<ExecLike['exec']>[2]
85
+ ): Promise<number> => {
86
+ const output = await getExecOutput(commandLine, args, {
87
+ ...options,
88
+ ignoreReturnCode: true
89
+ });
90
+ if (output.exitCode !== 0 && !options?.ignoreReturnCode) {
91
+ throw new Error(`Command failed with exit code ${output.exitCode}: ${toCommandLabel(commandLine, args, secretMasker)}`);
92
+ }
93
+ return output.exitCode;
94
+ };
95
+
96
+ const getExecOutput = async (
97
+ commandLine: string,
98
+ args: string[] = [],
99
+ options?: Parameters<ExecLike['getExecOutput']>[2]
100
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> => {
101
+ const cwd = options?.cwd;
102
+ const env = options?.env ? { ...process.env, ...options.env } : process.env;
103
+ const commandLabel = toCommandLabel(commandLine, args, secretMasker);
104
+ process.stderr.write(`[command] ${commandLabel}\n`);
105
+
106
+ try {
107
+ const result = await execFileAsync(commandLine, args, {
108
+ cwd,
109
+ env,
110
+ encoding: 'utf8',
111
+ maxBuffer: 20 * 1024 * 1024,
112
+ windowsHide: true
113
+ });
114
+ const stdout = String(result.stdout ?? '');
115
+ const stderr = String(result.stderr ?? '');
116
+ if (stdout) {
117
+ process.stderr.write(secretMasker(stdout));
118
+ }
119
+ if (stderr) {
120
+ process.stderr.write(secretMasker(stderr));
121
+ }
122
+ return {
123
+ exitCode: 0,
124
+ stdout,
125
+ stderr
126
+ };
127
+ } catch (error) {
128
+ const execError = error as {
129
+ code?: number | string;
130
+ stdout?: string | Buffer;
131
+ stderr?: string | Buffer;
132
+ message?: string;
133
+ };
134
+ const stdout = String(execError.stdout ?? '');
135
+ const stderr = String(execError.stderr ?? '');
136
+ const fallbackMessage = execError.message ? `${execError.message}\n` : '';
137
+ if (stdout) {
138
+ process.stderr.write(secretMasker(stdout));
139
+ }
140
+ if (stderr) {
141
+ process.stderr.write(secretMasker(stderr));
142
+ } else if (fallbackMessage) {
143
+ process.stderr.write(secretMasker(fallbackMessage));
144
+ }
145
+ const numericCode =
146
+ typeof execError.code === 'number'
147
+ ? execError.code
148
+ : Number.parseInt(String(execError.code ?? '1'), 10) || 1;
149
+ if (!options?.ignoreReturnCode) {
150
+ throw new Error(`Command failed with exit code ${numericCode}: ${commandLabel}`);
151
+ }
152
+ return {
153
+ exitCode: numericCode,
154
+ stdout,
155
+ stderr
156
+ };
157
+ }
158
+ };
159
+
160
+ return {
161
+ exec: execCommand,
162
+ getExecOutput
163
+ };
164
+ }
165
+
166
+ export function createCliDependencies(
167
+ inputs: ResolvedInputs
168
+ ): BootstrapExecutionDependencies {
169
+ const secretMasker = createSecretMasker([
170
+ inputs.postmanApiKey,
171
+ inputs.postmanAccessToken,
172
+ inputs.githubToken,
173
+ inputs.ghFallbackToken
174
+ ]);
175
+ const cliExec = createCliExec(secretMasker);
176
+
177
+ return createBootstrapDependencies(inputs, {
178
+ core: new ConsoleReporter(),
179
+ exec: cliExec,
180
+ io,
181
+ specFetcher: fetch
182
+ });
183
+ }
184
+
185
+ export function parseCliArgs(argv: string[], env: NodeJS.ProcessEnv = process.env): CliConfig {
186
+ const inputNames = [
187
+ 'project-name',
188
+ 'spec-url',
189
+ 'postman-api-key',
190
+ 'postman-access-token',
191
+ 'workspace-id',
192
+ 'spec-id',
193
+ 'baseline-collection-id',
194
+ 'smoke-collection-id',
195
+ 'contract-collection-id',
196
+ 'domain',
197
+ 'domain-code',
198
+ 'requester-email',
199
+ 'workspace-admin-user-ids',
200
+ 'environments-json',
201
+ 'system-env-map-json',
202
+ 'governance-mapping-json',
203
+ 'integration-backend',
204
+ 'github-auth-mode',
205
+ 'github-token',
206
+ 'gh-fallback-token',
207
+ 'team-id',
208
+ 'repo-url'
209
+ ];
210
+
211
+ const inputEnv: NodeJS.ProcessEnv = { ...env };
212
+ for (const name of inputNames) {
213
+ const value = readFlag(argv, name);
214
+ if (value !== undefined) {
215
+ inputEnv[normalizeCliFlag(name)] = value;
216
+ }
217
+ }
218
+
219
+ return {
220
+ inputEnv,
221
+ resultJsonPath: readFlag(argv, 'result-json') ?? 'postman-bootstrap-result.json',
222
+ dotenvPath: readFlag(argv, 'dotenv-path')
223
+ };
224
+ }
225
+
226
+ export function toDotenv(outputs: PlannedOutputs): string {
227
+ return Object.entries(outputs)
228
+ .map(([key, value]) => [
229
+ `POSTMAN_BOOTSTRAP_${key.replace(/-/g, '_').toUpperCase()}`,
230
+ value
231
+ ] as const)
232
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
233
+ .join('\n');
234
+ }
235
+
236
+ async function writeOptionalFile(filePath: string | undefined, content: string): Promise<void> {
237
+ if (!filePath) {
238
+ return;
239
+ }
240
+ const workspaceRoot = path.resolve(process.cwd());
241
+ const resolved = path.resolve(workspaceRoot, filePath);
242
+ const relative = path.relative(workspaceRoot, resolved);
243
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
244
+ throw new Error(`Output path must stay within workspace: ${filePath}`);
245
+ }
246
+ await mkdir(path.dirname(resolved), { recursive: true });
247
+ await writeFile(resolved, content, 'utf8');
248
+ }
249
+
250
+ export async function runCli(
251
+ argv: string[] = process.argv.slice(2),
252
+ runtime: CliRuntime = {}
253
+ ): Promise<void> {
254
+ const env = runtime.env ?? process.env;
255
+ const config = parseCliArgs(argv, env);
256
+ const inputs = resolveInputs(config.inputEnv);
257
+ const dependencies = createCliDependencies(inputs);
258
+
259
+ if (!dependencies.github) {
260
+ dependencies.core.info('GitHub repository variable persistence disabled for this run');
261
+ }
262
+ if (inputs.domain && !dependencies.internalIntegration) {
263
+ dependencies.core.warning(
264
+ 'Skipping governance assignment because postman-access-token is not configured'
265
+ );
266
+ }
267
+
268
+ const result = await (runtime.executeBootstrap ?? runBootstrap)(inputs, dependencies);
269
+
270
+ await writeOptionalFile(config.resultJsonPath, JSON.stringify(result, null, 2));
271
+ await writeOptionalFile(config.dotenvPath, toDotenv(result));
272
+
273
+ const writeStdout = runtime.writeStdout ?? ((chunk: string) => process.stdout.write(chunk));
274
+ writeStdout(`${JSON.stringify(result, null, 2)}\n`);
275
+ }
276
+
277
+ const currentModulePath = typeof __filename === 'string' ? __filename : '';
278
+ const entrypoint = process.argv[1];
279
+
280
+ if (entrypoint && currentModulePath === entrypoint) {
281
+ runCli().catch((error) => {
282
+ const message = error instanceof Error ? error.message : String(error);
283
+ process.stderr.write(`${message}\n`);
284
+ process.exitCode = 1;
285
+ });
286
+ }
@@ -0,0 +1,166 @@
1
+ export interface ActionInputContract {
2
+ description: string;
3
+ required: boolean;
4
+ default?: string;
5
+ allowedValues?: string[];
6
+ }
7
+
8
+ export interface ActionOutputContract {
9
+ description: string;
10
+ }
11
+
12
+ export interface BetaActionContract {
13
+ name: string;
14
+ description: string;
15
+ inputs: Record<string, ActionInputContract>;
16
+ outputs: Record<string, ActionOutputContract>;
17
+ retainedBehavior: string[];
18
+ removedBehavior: string[];
19
+ }
20
+
21
+ export const openAlphaActionContract: BetaActionContract = {
22
+ name: 'postman-bootstrap-action',
23
+ description: 'Public open-alpha contract for bootstrapping Postman assets from a registry-backed spec.',
24
+ inputs: {
25
+
26
+ 'workspace-id': {
27
+ description: 'Existing Postman workspace ID.',
28
+ required: false
29
+ },
30
+ 'spec-id': {
31
+ description: 'Existing Postman spec ID.',
32
+ required: false
33
+ },
34
+ 'baseline-collection-id': {
35
+ description: 'Existing baseline collection ID.',
36
+ required: false
37
+ },
38
+ 'smoke-collection-id': {
39
+ description: 'Existing smoke collection ID.',
40
+ required: false
41
+ },
42
+ 'contract-collection-id': {
43
+ description: 'Existing contract collection ID.',
44
+ required: false
45
+ },
46
+ 'project-name': {
47
+ description: 'Service project name.',
48
+ required: true
49
+ },
50
+ domain: {
51
+ description: 'Business domain for the service.',
52
+ required: false
53
+ },
54
+ 'domain-code': {
55
+ description: 'Short domain code used in workspace naming.',
56
+ required: false
57
+ },
58
+ 'requester-email': {
59
+ description: 'Requester email for audit context.',
60
+ required: false
61
+ },
62
+ 'workspace-admin-user-ids': {
63
+ description: 'Comma-separated workspace admin user ids.',
64
+ required: false
65
+ },
66
+ 'spec-url': {
67
+ description: 'HTTPS URL to the OpenAPI document.',
68
+ required: true
69
+ },
70
+ 'environments-json': {
71
+ description: 'JSON array of environment slugs to preserve in bootstrap outputs.',
72
+ required: false,
73
+ default: '["prod"]'
74
+ },
75
+ 'system-env-map-json': {
76
+ description: 'JSON map of environment slug to system environment id.',
77
+ required: false,
78
+ default: '{}'
79
+ },
80
+ 'governance-mapping-json': {
81
+ description: 'JSON map of business domain to governance group name.',
82
+ required: false,
83
+ default: '{}'
84
+ },
85
+ 'postman-api-key': {
86
+ description: 'Postman API key used for bootstrap operations.',
87
+ required: true
88
+ },
89
+ 'postman-access-token': {
90
+ description: 'Postman access token used for governance and workspace mutations.',
91
+ required: false
92
+ },
93
+ 'github-token': {
94
+ description: 'GitHub token for repository variable persistence.',
95
+ required: false
96
+ },
97
+ 'gh-fallback-token': {
98
+ description: 'Fallback token for repository variable APIs.',
99
+ required: false
100
+ },
101
+ 'github-auth-mode': {
102
+ description: 'GitHub auth mode for repository variable APIs.',
103
+ required: false,
104
+ default: 'github_token_first'
105
+ },
106
+ 'integration-backend': {
107
+ description: 'Integration backend for downstream workspace connectivity.',
108
+ required: false,
109
+ default: 'bifrost',
110
+ allowedValues: ['bifrost']
111
+ }
112
+ },
113
+ outputs: {
114
+ 'workspace-id': {
115
+ description: 'Postman workspace ID.'
116
+ },
117
+ 'workspace-url': {
118
+ description: 'Postman workspace URL.'
119
+ },
120
+ 'workspace-name': {
121
+ description: 'Postman workspace name.'
122
+ },
123
+ 'spec-id': {
124
+ description: 'Uploaded Postman spec ID.'
125
+ },
126
+ 'baseline-collection-id': {
127
+ description: 'Baseline collection ID.'
128
+ },
129
+ 'smoke-collection-id': {
130
+ description: 'Smoke collection ID.'
131
+ },
132
+ 'contract-collection-id': {
133
+ description: 'Contract collection ID.'
134
+ },
135
+ 'collections-json': {
136
+ description: 'JSON summary of generated collections.'
137
+ },
138
+ 'lint-summary-json': {
139
+ description: 'JSON summary of lint errors and warnings.'
140
+ }
141
+ },
142
+ retainedBehavior: [
143
+ 'workspace creation',
144
+ 'governance group assignment',
145
+ 'requester workspace invitation',
146
+ 'workspace admin assignment',
147
+ 'spec upload to Spec Hub',
148
+ 'OpenAPI operation summary normalization before upload (missing or oversized summaries)',
149
+ 'spec linting by UID',
150
+ 'baseline, smoke, and contract collection generation',
151
+ 'collection tagging',
152
+ 'GitHub repository variable persistence for downstream sync steps',
153
+ 'workspace, spec, and collection outputs'
154
+ ],
155
+ removedBehavior: [
156
+ 'snake_case input and output names',
157
+ 'step mode',
158
+ 'hardcoded runtime deployment assumptions',
159
+ 'aws, docker, and infra workflow concerns',
160
+ 'runtime-coupled workflow tuning knobs',
161
+ 'legacy placeholder inputs such as team-id'
162
+ ]
163
+ };
164
+
165
+ export const contractInputNames = Object.keys(openAlphaActionContract.inputs);
166
+ export const contractOutputNames = Object.keys(openAlphaActionContract.outputs);