@postman-cse/onboarding-bootstrap 0.10.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/package.json CHANGED
@@ -1,15 +1,26 @@
1
1
  {
2
2
  "name": "@postman-cse/onboarding-bootstrap",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Public open-alpha Postman bootstrap GitHub Action.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
7
7
  "bin": {
8
8
  "postman-bootstrap": "dist/cli.cjs"
9
9
  },
10
+ "files": [
11
+ "action.yml",
12
+ "dist/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
10
19
  "scripts": {
11
20
  "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
21
  "check:dist": "npm run build && git diff --exit-code -- dist",
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
13
24
  "test": "vitest run",
14
25
  "typecheck": "tsc --noEmit -p tsconfig.json"
15
26
  },
@@ -24,16 +35,24 @@
24
35
  "dependencies": {
25
36
  "@actions/core": "^1.11.1",
26
37
  "@actions/exec": "^1.1.1",
27
- "@actions/io": "^1.1.3"
38
+ "@actions/io": "^1.1.3",
39
+ "@apidevtools/json-schema-ref-parser": "15.3.5",
40
+ "@exodus/schemasafe": "1.3.0",
41
+ "@readme/openapi-parser": "6.0.1"
28
42
  },
29
43
  "overrides": {
30
44
  "undici": "^6.24.0",
31
45
  "picomatch": ">=4.0.4"
32
46
  },
33
47
  "devDependencies": {
34
- "esbuild": "^0.27.3",
48
+ "@commitlint/cli": "^20.5.0",
49
+ "@commitlint/config-conventional": "^20.5.0",
50
+ "@eslint/js": "^10.0.1",
35
51
  "@types/node": "^25.3.5",
52
+ "esbuild": "^0.27.3",
53
+ "eslint": "^10.1.0",
36
54
  "typescript": "^5.9.3",
55
+ "typescript-eslint": "^8.57.2",
37
56
  "vitest": "^4.0.18",
38
57
  "yaml": "^2.8.2"
39
58
  },
@@ -1,2 +0,0 @@
1
- self-hosted-runner:
2
- labels: []
@@ -1,36 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches:
7
- - main
8
- - cs-beta-actions_20260305
9
-
10
- jobs:
11
- validate:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: '20'
18
- cache: npm
19
- - run: npm ci
20
- - run: npm test
21
- - run: npm run typecheck
22
- - run: npm run check:dist
23
-
24
- actionlint:
25
- runs-on: ubuntu-latest
26
- steps:
27
- - uses: actions/checkout@v4
28
- - uses: actions/setup-go@v5
29
- with:
30
- go-version: '1.23'
31
- - name: Install actionlint
32
- run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.11
33
- - name: Run actionlint
34
- run: |
35
- ACTIONLINT_BIN="$(go env GOPATH)/bin/actionlint"
36
- "$ACTIONLINT_BIN"
@@ -1,155 +0,0 @@
1
- name: Contract Smoke Tests
2
- on:
3
- schedule:
4
- - cron: '0 8 * * *' # daily at 8 AM UTC
5
- workflow_dispatch: {}
6
-
7
- jobs:
8
- bootstrap-smoke:
9
- name: Bootstrap API Contract
10
- runs-on: ubuntu-latest
11
- steps:
12
- - uses: actions/checkout@v4
13
- - uses: actions/setup-node@v4
14
- with:
15
- node-version: '20'
16
- - name: Verify GET /me (org-mode key)
17
- env:
18
- PMAK: ${{ secrets.SMOKE_ORG_API_KEY }}
19
- run: |
20
- set -euo pipefail
21
- response=$(curl -sf -H "X-Api-Key: $PMAK" https://api.getpostman.com/me)
22
- echo "$response" | jq -e '.user.teamId' > /dev/null || { echo "CONTRACT DRIFT: GET /me missing user.teamId"; exit 1; }
23
- echo "GET /me: user.teamId=$(echo "$response" | jq -r '.user.teamId')"
24
-
25
- - name: Verify GET /me (non-org key)
26
- env:
27
- PMAK: ${{ secrets.SMOKE_NON_ORG_API_KEY }}
28
- run: |
29
- set -euo pipefail
30
- response=$(curl -sf -H "X-Api-Key: $PMAK" https://api.getpostman.com/me)
31
- echo "$response" | jq -e '.user.teamId' > /dev/null || { echo "CONTRACT DRIFT: GET /me missing user.teamId"; exit 1; }
32
- echo "GET /me: user.teamId=$(echo "$response" | jq -r '.user.teamId')"
33
-
34
- - name: Verify GET /teams (org-mode key)
35
- env:
36
- PMAK: ${{ secrets.SMOKE_ORG_API_KEY }}
37
- run: |
38
- set -euo pipefail
39
- response=$(curl -sf -H "X-Api-Key: $PMAK" https://api.getpostman.com/teams)
40
- echo "$response" | jq -e '.data | type == "array"' > /dev/null || { echo "CONTRACT DRIFT: GET /teams .data is not an array"; exit 1; }
41
- count=$(echo "$response" | jq '.data | length')
42
- echo "GET /teams: $count teams returned"
43
- if [ "$count" -gt 1 ]; then
44
- # Org-mode: verify organizationId is present on at least one team
45
- org_count=$(echo "$response" | jq '[.data[] | select(.organizationId != null)] | length')
46
- if [ "$org_count" -eq 0 ]; then
47
- echo "CONTRACT DRIFT: GET /teams returned $count teams but none have organizationId"
48
- exit 1
49
- fi
50
- echo "GET /teams: $org_count teams have organizationId"
51
- # Verify required fields
52
- echo "$response" | jq -e '.data[] | select(.id and .name)' > /dev/null || { echo "CONTRACT DRIFT: team items missing id or name"; exit 1; }
53
- fi
54
-
55
- - name: Verify GET /teams (non-org key)
56
- env:
57
- PMAK: ${{ secrets.SMOKE_NON_ORG_API_KEY }}
58
- run: |
59
- set -euo pipefail
60
- response=$(curl -sf -H "X-Api-Key: $PMAK" https://api.getpostman.com/teams)
61
- echo "$response" | jq -e '.data | type == "array"' > /dev/null || { echo "CONTRACT DRIFT: GET /teams .data is not an array"; exit 1; }
62
- count=$(echo "$response" | jq '.data | length')
63
- echo "GET /teams: $count teams (non-org, expecting 0 or 1)"
64
-
65
- - name: Verify POST /workspaces with teamId (org-mode)
66
- env:
67
- PMAK: ${{ secrets.SMOKE_ORG_API_KEY }}
68
- WORKSPACE_TEAM_ID: ${{ vars.SMOKE_WORKSPACE_TEAM_ID }}
69
- run: |
70
- set -euo pipefail
71
- ts=$(date +%s)
72
- response=$(curl -sf -X POST \
73
- -H "X-Api-Key: $PMAK" \
74
- -H "Content-Type: application/json" \
75
- -d "{\"workspace\":{\"name\":\"__smoke_test_${ts}\",\"type\":\"team\",\"description\":\"Contract smoke test - safe to delete\",\"teamId\":${WORKSPACE_TEAM_ID}}}" \
76
- https://api.getpostman.com/workspaces)
77
- ws_id=$(echo "$response" | jq -r '.workspace.id')
78
- if [ -z "$ws_id" ] || [ "$ws_id" = "null" ]; then
79
- echo "CONTRACT DRIFT: POST /workspaces did not return workspace.id"
80
- echo "Response: $response"
81
- exit 1
82
- fi
83
- echo "POST /workspaces: created workspace $ws_id"
84
-
85
- # Cleanup
86
- curl -sf -X DELETE -H "X-Api-Key: $PMAK" "https://api.getpostman.com/workspaces/$ws_id" > /dev/null
87
- echo "Cleanup: deleted workspace $ws_id"
88
-
89
- - name: Verify POST /workspaces without teamId (non-org)
90
- env:
91
- PMAK: ${{ secrets.SMOKE_NON_ORG_API_KEY }}
92
- run: |
93
- set -euo pipefail
94
- ts=$(date +%s)
95
- response=$(curl -sf -X POST \
96
- -H "X-Api-Key: $PMAK" \
97
- -H "Content-Type: application/json" \
98
- -d "{\"workspace\":{\"name\":\"__smoke_test_${ts}\",\"type\":\"team\",\"description\":\"Contract smoke test - safe to delete\"}}" \
99
- https://api.getpostman.com/workspaces)
100
- ws_id=$(echo "$response" | jq -r '.workspace.id')
101
- if [ -z "$ws_id" ] || [ "$ws_id" = "null" ]; then
102
- echo "CONTRACT DRIFT: POST /workspaces (no teamId) did not return workspace.id"
103
- echo "Response: $response"
104
- exit 1
105
- fi
106
- echo "POST /workspaces (no teamId): created workspace $ws_id"
107
-
108
- # Cleanup
109
- curl -sf -X DELETE -H "X-Api-Key: $PMAK" "https://api.getpostman.com/workspaces/$ws_id" > /dev/null
110
- echo "Cleanup: deleted workspace $ws_id"
111
-
112
- bifrost-smoke:
113
- name: Bifrost API Contract
114
- runs-on: ubuntu-latest
115
- steps:
116
- - uses: actions/checkout@v4
117
- - name: Verify Bifrost API key creation
118
- env:
119
- ACCESS_TOKEN: ${{ secrets.SMOKE_ORG_ACCESS_TOKEN }}
120
- run: |
121
- set -euo pipefail
122
- ts=$(date +%s)
123
- response=$(curl -sf -X POST \
124
- -H "x-access-token: $ACCESS_TOKEN" \
125
- -H "Content-Type: application/json" \
126
- -d "{\"method\":\"POST\",\"path\":\"/scim/v2/keys\",\"body\":{\"name\":\"smoke-test-${ts}\",\"description\":\"Contract smoke - safe to delete\"}}" \
127
- https://bifrost-premium-https-v4.gw.postman.com)
128
- # Verify PMAK-shaped key returned
129
- key=$(echo "$response" | jq -r '.apikey.key // .key // empty')
130
- if [ -z "$key" ]; then
131
- echo "CONTRACT DRIFT: Bifrost API key creation did not return apikey.key"
132
- echo "Response: $(echo "$response" | head -c 500)"
133
- exit 1
134
- fi
135
- echo "Bifrost key creation: key starts with $(echo "$key" | head -c 10)..."
136
-
137
- session-smoke:
138
- name: Session Validation Contract
139
- runs-on: ubuntu-latest
140
- steps:
141
- - uses: actions/checkout@v4
142
- - name: Verify iapub session endpoint
143
- env:
144
- ACCESS_TOKEN: ${{ secrets.SMOKE_ORG_ACCESS_TOKEN }}
145
- run: |
146
- set -euo pipefail
147
- response=$(curl -sf \
148
- -H "x-access-token: $ACCESS_TOKEN" \
149
- https://iapub.postman.co/api/sessions/current)
150
- echo "$response" | jq -e '.session.identity.team' > /dev/null || {
151
- echo "CONTRACT DRIFT: iapub session endpoint missing session.identity.team"
152
- echo "Response shape: $(echo "$response" | jq 'keys')"
153
- exit 1
154
- }
155
- echo "Session validation: team=$(echo "$response" | jq -r '.session.identity.team')"
@@ -1,62 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
- workflow_dispatch:
8
-
9
- permissions:
10
- contents: write
11
- id-token: write
12
-
13
- jobs:
14
- release:
15
- runs-on: ubuntu-latest
16
- steps:
17
- - uses: actions/checkout@v4
18
- - uses: actions/setup-node@v4
19
- with:
20
- node-version: '20'
21
- cache: npm
22
- - uses: actions/setup-go@v5
23
- with:
24
- go-version: '1.23'
25
- - run: npm ci
26
- - run: npm test
27
- - run: npm run typecheck
28
- - run: npm run check:dist
29
- - name: Install actionlint
30
- run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.11
31
- - name: Run actionlint
32
- run: |
33
- ACTIONLINT_BIN="$(go env GOPATH)/bin/actionlint"
34
- "$ACTIONLINT_BIN"
35
- - name: Publish GitHub release
36
- uses: softprops/action-gh-release@v2
37
- with:
38
- generate_release_notes: true
39
- - name: Verify version matches tag
40
- run: |
41
- TAG="${GITHUB_REF#refs/tags/v}"
42
- PKG_VERSION=$(node -p "require('./package.json').version")
43
- if [ "$TAG" != "$PKG_VERSION" ]; then
44
- echo "::error::Tag v$TAG does not match package.json version $PKG_VERSION"
45
- exit 1
46
- fi
47
- - uses: actions/setup-node@v4
48
- with:
49
- node-version: '20'
50
- registry-url: 'https://registry.npmjs.org'
51
- - name: Publish to npm
52
- run: npm publish --provenance --access public
53
- env:
54
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
55
- - name: Attach npm tarball to release
56
- run: |
57
- npm pack
58
- mv ./*.tgz release.tgz
59
- - name: Upload tarball
60
- uses: softprops/action-gh-release@v2
61
- with:
62
- files: release.tgz
@@ -1,8 +0,0 @@
1
- {
2
- "session_id": "b36ac16d-ce26-4e94-ae7e-dd817f2430a3",
3
- "ended_at": "2026-03-26T17:52:36.726Z",
4
- "reason": "prompt_input_exit",
5
- "agents_spawned": 0,
6
- "agents_completed": 0,
7
- "modes_used": []
8
- }
package/CLAUDE.md DELETED
@@ -1,57 +0,0 @@
1
- # postman-bootstrap-action
2
-
3
- Creates or reuses a Postman workspace, uploads/updates an OpenAPI spec to Spec Hub, generates baseline/smoke/contract collections, assigns governance, and persists repo variables. Dual entry: GitHub Action (`dist/index.cjs`) and CLI (`dist/cli.cjs`).
4
-
5
- ## Structure
6
-
7
- ```
8
- src/
9
- index.ts # Main orchestration: inputs -> workspace -> spec -> collections -> lint -> outputs
10
- cli.ts # CLI adapter: reads flags/env, wraps runBootstrap(), writes JSON/dotenv
11
- contracts.ts # Input/output type definitions
12
- lib/
13
- postman/
14
- postman-assets-client.ts # Custom Postman API client (workspaces, specs, collections, envs, users)
15
- internal-integration-adapter.ts # Bifrost proxy adapter (governance, workspace linking, system envs)
16
- workspace-selection.ts # Canonical workspace resolution (new vs existing, fallback to repo vars)
17
- github/
18
- github-api-client.ts # GitHub repo variable read/write, workflow file APIs
19
- repo/
20
- context.ts # Auto-detect repo URL, provider, branch from CI env vars
21
- retry.ts # Exponential backoff
22
- secrets.ts # Secret masking utility
23
- http-error.ts # Typed HTTP error class
24
- tests/ # vitest unit tests
25
- ```
26
-
27
- ## Commands
28
-
29
- ```bash
30
- npm ci && npm test && npm run typecheck && npm run build
31
- npm run check:dist # build + git diff --exit-code (CI integrity)
32
- ```
33
-
34
- ## Key Behaviors
35
-
36
- - **Workspace selection**: Checks input `workspace-id` -> repo variable `POSTMAN_WORKSPACE_ID` -> creates new. Canonical workspace validation uses access-token via Bifrost.
37
- - **Spec normalization**: Before upload, fixes missing/long `summary` fields in OpenAPI operations to prevent downstream collection generation failures.
38
- - **Collection generation**: Calls `POST /specs/{id}/generations/collection` to create baseline/smoke/contract collections. Injects generated tests and applies tags.
39
- - **Lint**: Installs Postman CLI, runs `postman spec lint` against uploaded spec UID, parses JSON output for errors/warnings.
40
- - **Team ID**: Auto-derived from `GET /me` using the API key. `POSTMAN_TEAM_ID` env var overrides.
41
- - **Repo variables**: Persists `POSTMAN_WORKSPACE_ID`, `POSTMAN_SPEC_UID`, collection UIDs, lint counts as GitHub repo variables for rerun idempotency.
42
-
43
- ## Postman API Endpoints Used
44
-
45
- - `POST /workspaces`, `GET /workspaces` -- workspace CRUD
46
- - `POST /specs`, `PATCH /specs/{id}/files/{path}` -- spec upload/update
47
- - `POST /specs/{id}/generations/collection` -- collection generation
48
- - `GET /collections/{id}`, `PUT /collections/{id}` -- collection update/test injection
49
- - `PUT /collections/{id}/tags` -- collection tagging
50
- - `GET /me` -- team ID derivation
51
- - Bifrost: governance assignment, workspace-to-repo linking
52
-
53
- ## Gotchas
54
-
55
- - `postman-assets-client.ts` has a DEPRECATED URL normalization alias -- do not extend it
56
- - Spec upload re-serializes JSON/YAML; original bytes are preserved only when no normalization is needed
57
- - `@actions/core` is used directly for GitHub Actions; CLI mode uses `ConsoleReporter` (logs to stderr, JSON to stdout)
package/src/cli.ts DELETED
@@ -1,280 +0,0 @@
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
- ]);
173
- const cliExec = createCliExec(secretMasker);
174
-
175
- return createBootstrapDependencies(inputs, {
176
- core: new ConsoleReporter(),
177
- exec: cliExec,
178
- io,
179
- specFetcher: fetch
180
- });
181
- }
182
-
183
- export function parseCliArgs(argv: string[], env: NodeJS.ProcessEnv = process.env): CliConfig {
184
- const inputNames = [
185
- 'project-name',
186
- 'spec-url',
187
- 'postman-api-key',
188
- 'postman-access-token',
189
- 'workspace-id',
190
- 'spec-id',
191
- 'baseline-collection-id',
192
- 'smoke-collection-id',
193
- 'contract-collection-id',
194
- 'collection-sync-mode',
195
- 'spec-sync-mode',
196
- 'release-label',
197
- 'domain',
198
- 'domain-code',
199
- 'requester-email',
200
- 'workspace-admin-user-ids',
201
- 'governance-mapping-json',
202
- 'integration-backend',
203
- 'team-id',
204
- 'workspace-team-id',
205
- 'repo-url'
206
- ];
207
-
208
- const inputEnv: NodeJS.ProcessEnv = { ...env };
209
- for (const name of inputNames) {
210
- const value = readFlag(argv, name);
211
- if (value !== undefined) {
212
- inputEnv[normalizeCliFlag(name)] = value;
213
- }
214
- }
215
-
216
- return {
217
- inputEnv,
218
- resultJsonPath: readFlag(argv, 'result-json') ?? 'postman-bootstrap-result.json',
219
- dotenvPath: readFlag(argv, 'dotenv-path')
220
- };
221
- }
222
-
223
- export function toDotenv(outputs: PlannedOutputs): string {
224
- return Object.entries(outputs)
225
- .map(([key, value]) => [
226
- `POSTMAN_BOOTSTRAP_${key.replace(/-/g, '_').toUpperCase()}`,
227
- value
228
- ] as const)
229
- .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
230
- .join('\n');
231
- }
232
-
233
- async function writeOptionalFile(filePath: string | undefined, content: string): Promise<void> {
234
- if (!filePath) {
235
- return;
236
- }
237
- const workspaceRoot = path.resolve(process.cwd());
238
- const resolved = path.resolve(workspaceRoot, filePath);
239
- const relative = path.relative(workspaceRoot, resolved);
240
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
241
- throw new Error(`Output path must stay within workspace: ${filePath}`);
242
- }
243
- await mkdir(path.dirname(resolved), { recursive: true });
244
- await writeFile(resolved, content, 'utf8');
245
- }
246
-
247
- export async function runCli(
248
- argv: string[] = process.argv.slice(2),
249
- runtime: CliRuntime = {}
250
- ): Promise<void> {
251
- const env = runtime.env ?? process.env;
252
- const config = parseCliArgs(argv, env);
253
- const inputs = resolveInputs(config.inputEnv);
254
- const dependencies = createCliDependencies(inputs);
255
-
256
- if (inputs.domain && !dependencies.internalIntegration) {
257
- dependencies.core.warning(
258
- 'Skipping governance assignment because postman-access-token is not configured'
259
- );
260
- }
261
-
262
- const result = await (runtime.executeBootstrap ?? runBootstrap)(inputs, dependencies);
263
-
264
- await writeOptionalFile(config.resultJsonPath, JSON.stringify(result, null, 2));
265
- await writeOptionalFile(config.dotenvPath, toDotenv(result));
266
-
267
- const writeStdout = runtime.writeStdout ?? ((chunk: string) => process.stdout.write(chunk));
268
- writeStdout(`${JSON.stringify(result, null, 2)}\n`);
269
- }
270
-
271
- const currentModulePath = typeof __filename === 'string' ? __filename : '';
272
- const entrypoint = process.argv[1];
273
-
274
- if (entrypoint && currentModulePath === entrypoint) {
275
- runCli().catch((error) => {
276
- const message = error instanceof Error ? error.message : String(error);
277
- process.stderr.write(`${message}\n`);
278
- process.exitCode = 1;
279
- });
280
- }