@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/LICENSE +21 -0
- package/README.md +110 -10
- package/action.yml +22 -2
- package/dist/cli.cjs +34466 -2224
- package/dist/index.cjs +34358 -2195
- 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 -280
- package/src/contracts.ts +0 -166
- package/src/index.ts +0 -1100
- 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 -272
- 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 -1217
- package/tests/cli.test.ts +0 -150
- package/tests/contract.test.ts +0 -123
- package/tests/github-api-client.test.ts +0 -91
- package/tests/internal-integration-adapter.test.ts +0 -244
- 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/package.json
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postman-cse/onboarding-bootstrap",
|
|
3
|
-
"version": "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
|
-
"
|
|
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
|
},
|
package/.github/actionlint.yaml
DELETED
package/.github/workflows/ci.yml
DELETED
|
@@ -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
|
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
|
-
}
|