@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/tests/cli.test.ts DELETED
@@ -1,150 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import { ConsoleReporter, createCliDependencies, parseCliArgs, runCli, toDotenv } from '../src/cli.js';
4
- import { resolveInputs } from '../src/index.js';
5
-
6
- describe('parseCliArgs', () => {
7
- it('maps CLI flags into INPUT_* environment variables', () => {
8
- const config = parseCliArgs(
9
- [
10
- '--project-name',
11
- 'core-payments',
12
- '--spec-url=https://example.test/openapi.yaml',
13
- '--postman-api-key',
14
- 'pmak-test',
15
- '--workspace-admin-user-ids',
16
- '101,102',
17
- '--collection-sync-mode',
18
- 'version',
19
- '--spec-sync-mode',
20
- 'version',
21
- '--release-label',
22
- 'v1.2.3',
23
- '--team-id',
24
- '12345',
25
- '--repo-url',
26
- 'https://github.com/postman-cs/postman-bootstrap-action',
27
- '--result-json',
28
- 'tmp/result.json',
29
- '--dotenv-path',
30
- 'tmp/result.env'
31
- ],
32
- {}
33
- );
34
-
35
- expect(config.inputEnv.INPUT_PROJECT_NAME).toBe('core-payments');
36
- expect(config.inputEnv.INPUT_SPEC_URL).toBe('https://example.test/openapi.yaml');
37
- expect(config.inputEnv.INPUT_POSTMAN_API_KEY).toBe('pmak-test');
38
- expect(config.inputEnv.INPUT_WORKSPACE_ADMIN_USER_IDS).toBe('101,102');
39
- expect(config.inputEnv.INPUT_COLLECTION_SYNC_MODE).toBe('version');
40
- expect(config.inputEnv.INPUT_SPEC_SYNC_MODE).toBe('version');
41
- expect(config.inputEnv.INPUT_RELEASE_LABEL).toBe('v1.2.3');
42
- expect(config.inputEnv.INPUT_TEAM_ID).toBe('12345');
43
- expect(config.inputEnv.INPUT_REPO_URL).toBe('https://github.com/postman-cs/postman-bootstrap-action');
44
- expect(config.resultJsonPath).toBe('tmp/result.json');
45
- expect(config.dotenvPath).toBe('tmp/result.env');
46
- });
47
- });
48
-
49
- describe('toDotenv', () => {
50
- it('formats planned outputs as POSTMAN_BOOTSTRAP_* dotenv pairs', () => {
51
- const dotenv = toDotenv({
52
- 'workspace-id': 'ws-123',
53
- 'workspace-url': 'https://go.postman.co/workspace/ws-123',
54
- 'workspace-name': '[AF] core-payments',
55
- 'spec-id': 'spec-123',
56
- 'baseline-collection-id': 'col-baseline',
57
- 'smoke-collection-id': 'col-smoke',
58
- 'contract-collection-id': 'col-contract',
59
- 'collections-json': '{"baseline":"col-baseline"}',
60
- 'lint-summary-json': '{"errors":0}'
61
- } as any);
62
-
63
- expect(dotenv).toContain('POSTMAN_BOOTSTRAP_WORKSPACE_ID="ws-123"');
64
- expect(dotenv).toContain('POSTMAN_BOOTSTRAP_SPEC_ID="spec-123"');
65
- expect(dotenv).toContain('POSTMAN_BOOTSTRAP_LINT_SUMMARY_JSON="{\\"errors\\":0}"');
66
- });
67
- });
68
-
69
- describe('ConsoleReporter', () => {
70
- it('writes info, warning, and group logs to stderr', async () => {
71
- const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
72
- const reporter = new ConsoleReporter();
73
-
74
- reporter.info('hello');
75
- reporter.warning('careful');
76
- await reporter.group('work', async () => undefined);
77
-
78
- expect(stderrSpy).toHaveBeenNthCalledWith(1, 'hello');
79
- expect(stderrSpy).toHaveBeenNthCalledWith(2, 'warning: careful');
80
- expect(stderrSpy).toHaveBeenNthCalledWith(3, '[group] work');
81
-
82
- stderrSpy.mockRestore();
83
- });
84
- });
85
-
86
- describe('runCli', () => {
87
- it('writes only JSON payload to stdout while routing exec output to stderr', async () => {
88
- const stdoutChunks: string[] = [];
89
- const stderrSpy = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
90
-
91
- await runCli(
92
- [
93
- '--project-name',
94
- 'core-payments',
95
- '--spec-url',
96
- 'https://example.test/openapi.yaml',
97
- '--postman-api-key',
98
- 'pmak-secret',
99
- '--result-json',
100
- 'tmp/cli-run-result.json'
101
- ],
102
- {
103
- env: {},
104
- executeBootstrap: async (inputs, dependencies) => {
105
- await dependencies.exec.exec('node', ['-e', `process.stdout.write(${JSON.stringify(inputs.postmanApiKey)})`]);
106
- return {
107
- 'workspace-id': 'ws-123',
108
- 'workspace-url': 'https://go.postman.co/workspace/ws-123',
109
- 'workspace-name': '[AF] core-payments',
110
- 'spec-id': 'spec-123',
111
- 'baseline-collection-id': 'col-baseline',
112
- 'smoke-collection-id': 'col-smoke',
113
- 'contract-collection-id': 'col-contract',
114
- 'collections-json': '{"baseline":"col-baseline","smoke":"col-smoke","contract":"col-contract"}',
115
- 'lint-summary-json': '{"errors":0,"total":0,"violations":[],"warnings":0}'
116
- } as any;
117
- },
118
- writeStdout: (chunk) => {
119
- stdoutChunks.push(chunk);
120
- }
121
- }
122
- );
123
-
124
- expect(stdoutChunks).toHaveLength(1);
125
- const parsed = JSON.parse(stdoutChunks[0]);
126
- expect(parsed['workspace-id']).toBe('ws-123');
127
- const stderrCombined = stderrSpy.mock.calls.map(([entry]) => String(entry)).join('');
128
- expect(stderrCombined).toContain('[REDACTED]');
129
- expect(stderrCombined).not.toContain('pmak-secret');
130
-
131
- stderrSpy.mockRestore();
132
- });
133
- });
134
-
135
- describe('createCliDependencies', () => {
136
- it('creates internal integration dependencies under token conditions', () => {
137
- const inputs = resolveInputs({
138
- INPUT_PROJECT_NAME: 'core-payments',
139
- INPUT_SPEC_URL: 'https://example.test/openapi.yaml',
140
- INPUT_POSTMAN_API_KEY: 'pmak-test',
141
- INPUT_POSTMAN_ACCESS_TOKEN: 'pat-test',
142
- INPUT_REPO_URL: 'https://github.com/postman-cs/postman-bootstrap-action',
143
- INPUT_TEAM_ID: '12345'
144
- });
145
-
146
- const dependencies = createCliDependencies(inputs);
147
-
148
- expect(dependencies.internalIntegration).toBeDefined();
149
- });
150
- });
@@ -1,123 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { parse } from 'yaml';
4
- import { describe, expect, it } from 'vitest';
5
-
6
- import {
7
- openAlphaActionContract,
8
- contractInputNames,
9
- contractOutputNames
10
- } from '../src/contracts.js';
11
- import { createPlannedOutputs, resolveInputs } from '../src/index.js';
12
-
13
- const repoRoot = resolve(import.meta.dirname, '..');
14
- const actionManifest = parse(
15
- readFileSync(resolve(repoRoot, 'action.yml'), 'utf8')
16
- ) as {
17
- inputs: Record<string, { required?: boolean; default?: string }>;
18
- outputs: Record<string, unknown>;
19
- };
20
-
21
- describe('open-alpha action contract', () => {
22
- it('uses kebab-case input and output names', () => {
23
- const kebabCasePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
24
-
25
- for (const name of [...contractInputNames, ...contractOutputNames]) {
26
- expect(name).toMatch(kebabCasePattern);
27
- }
28
- });
29
-
30
- it('keeps action.yml aligned with the contract surface', () => {
31
- expect(Object.keys(actionManifest.inputs)).toEqual(contractInputNames);
32
- expect(Object.keys(actionManifest.outputs)).toEqual(contractOutputNames);
33
- });
34
-
35
- it('defaults integration-backend to bifrost in the contract, manifest, and runtime', () => {
36
- expect(openAlphaActionContract.inputs['integration-backend'].default).toBe('bifrost');
37
- expect(openAlphaActionContract.inputs['integration-backend'].allowedValues).toEqual([
38
- 'bifrost'
39
- ]);
40
- expect(actionManifest.inputs['integration-backend'].default).toBe('bifrost');
41
- expect(resolveInputs({}).integrationBackend).toBe('bifrost');
42
- });
43
-
44
- it('defaults lifecycle controls in contract, manifest, and runtime', () => {
45
- expect(openAlphaActionContract.inputs['collection-sync-mode'].default).toBe('refresh');
46
- expect(openAlphaActionContract.inputs['collection-sync-mode'].allowedValues).toEqual([
47
- 'reuse',
48
- 'refresh',
49
- 'version'
50
- ]);
51
- expect(actionManifest.inputs['collection-sync-mode'].default).toBe('refresh');
52
- expect(resolveInputs({}).collectionSyncMode).toBe('refresh');
53
-
54
- expect(openAlphaActionContract.inputs['spec-sync-mode'].default).toBe('update');
55
- expect(openAlphaActionContract.inputs['spec-sync-mode'].allowedValues).toEqual([
56
- 'update',
57
- 'version'
58
- ]);
59
- expect(actionManifest.inputs['spec-sync-mode'].default).toBe('update');
60
- expect(resolveInputs({}).specSyncMode).toBe('update');
61
-
62
- });
63
-
64
- it('rejects unsupported integration backends during input resolution', () => {
65
- expect(() =>
66
- resolveInputs({
67
- INPUT_INTEGRATION_BACKEND: 'custom'
68
- })
69
- ).toThrow(/Unsupported integration-backend/);
70
- });
71
-
72
- it('rejects non-HTTPS spec URLs', () => {
73
- expect(() =>
74
- resolveInputs({
75
- 'INPUT_SPEC_URL': 'http://example.com/spec.yaml'
76
- })
77
- ).toThrow(/spec-url must be a valid HTTPS URL/);
78
- });
79
-
80
- it('documents the retained bootstrap steps and removed internal-only behavior', () => {
81
- expect(openAlphaActionContract.retainedBehavior).toContain('spec linting by UID');
82
- expect(openAlphaActionContract.retainedBehavior).toContain('workspace creation');
83
- expect(openAlphaActionContract.retainedBehavior).toContain(
84
- 'governance group assignment'
85
- );
86
- expect(openAlphaActionContract.removedBehavior).toContain('step mode');
87
- expect(openAlphaActionContract.removedBehavior).toContain(
88
- 'aws, docker, and infra workflow concerns'
89
- );
90
- });
91
-
92
- it('builds placeholder outputs that match the public open-alpha output surface', () => {
93
- const outputs = createPlannedOutputs(
94
- resolveInputs({
95
- INPUT_PROJECT_NAME: 'core-payments',
96
- INPUT_DOMAIN_CODE: 'AF',
97
- INPUT_SPEC_URL: 'https://example.com/openapi.yaml',
98
- INPUT_POSTMAN_API_KEY: 'pmak-test'
99
- })
100
- );
101
-
102
- expect(outputs).toEqual({
103
- 'workspace-id': '',
104
- 'workspace-url': '',
105
- 'workspace-name': '[AF] core-payments',
106
- 'spec-id': '',
107
- 'baseline-collection-id': '',
108
- 'smoke-collection-id': '',
109
- 'contract-collection-id': '',
110
- 'collections-json': JSON.stringify({
111
- baseline: '',
112
- smoke: '',
113
- contract: ''
114
- }),
115
- 'lint-summary-json': JSON.stringify({
116
- errors: 0,
117
- total: 0,
118
- violations: [],
119
- warnings: 0
120
- })
121
- });
122
- });
123
- });
@@ -1,91 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import {
4
- GitHubApiClient,
5
- type GitHubApiClientAuthMode
6
- } from '../src/lib/github/github-api-client.js';
7
-
8
- function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
9
- return new Response(JSON.stringify(body), {
10
- headers: {
11
- 'content-type': 'application/json'
12
- },
13
- ...init
14
- });
15
- }
16
-
17
- describe('GitHubApiClient', () => {
18
- it('falls back to the fallback token for repo variable writes after a 403', async () => {
19
- const fetchMock = vi
20
- .fn<typeof fetch>()
21
- .mockResolvedValueOnce(new Response('forbidden', { status: 403 }))
22
- .mockResolvedValueOnce(new Response('', { status: 201 }));
23
-
24
- const client = new GitHubApiClient({
25
- repository: 'postman-cs/bootstrap-demo',
26
- token: 'primary-token',
27
- fallbackToken: 'fallback-token',
28
- fetch: fetchMock
29
- });
30
-
31
- await client.setRepositoryVariable('POSTMAN_WORKSPACE_ID', 'ws_123');
32
-
33
- expect(fetchMock).toHaveBeenCalledTimes(2);
34
- expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
35
- method: 'POST',
36
- headers: expect.objectContaining({
37
- Authorization: 'Bearer primary-token'
38
- })
39
- });
40
- expect(fetchMock.mock.calls[1]?.[1]).toMatchObject({
41
- method: 'POST',
42
- headers: expect.objectContaining({
43
- Authorization: 'Bearer fallback-token'
44
- })
45
- });
46
- });
47
-
48
- it.each<[GitHubApiClientAuthMode, string[]]>([
49
- ['github_token_first', ['primary-token', 'fallback-token']],
50
- ['fallback_pat_first', ['fallback-token', 'primary-token']],
51
- ['app_token', ['app-token', 'primary-token', 'fallback-token']]
52
- ])('exposes explicit token ordering for %s', (authMode, expected) => {
53
- const client = new GitHubApiClient({
54
- repository: 'postman-cs/bootstrap-demo',
55
- token: 'primary-token',
56
- fallbackToken: 'fallback-token',
57
- appToken: 'app-token',
58
- authMode
59
- });
60
-
61
- expect(client.getTokenOrder()).toEqual(expected);
62
- });
63
-
64
- it('sanitizes GitHub API error messages before surfacing them', async () => {
65
- const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
66
- jsonResponse(
67
- {
68
- message:
69
- 'workflow write denied for token fallback-token and bearer primary-token'
70
- },
71
- { status: 500, statusText: 'Internal Server Error' }
72
- )
73
- );
74
- const client = new GitHubApiClient({
75
- repository: 'postman-cs/bootstrap-demo',
76
- token: 'primary-token',
77
- fallbackToken: 'fallback-token',
78
- fetch: fetchMock
79
- });
80
-
81
- await expect(
82
- client.getRepositoryVariable('POSTMAN_WORKSPACE_ID')
83
- ).rejects.toThrow('[REDACTED]');
84
- await expect(
85
- client.getRepositoryVariable('POSTMAN_WORKSPACE_ID')
86
- ).rejects.not.toThrow('primary-token');
87
- await expect(
88
- client.getRepositoryVariable('POSTMAN_WORKSPACE_ID')
89
- ).rejects.not.toThrow('fallback-token');
90
- });
91
- });
@@ -1,244 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import {
4
- createInternalIntegrationAdapter
5
- } from '../src/lib/postman/internal-integration-adapter.js';
6
-
7
- function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
8
- return new Response(JSON.stringify(body), {
9
- headers: {
10
- 'Content-Type': 'application/json'
11
- },
12
- ...init
13
- });
14
- }
15
-
16
- describe('internal integration adapter', () => {
17
- it('routes governance assignment through the internal gateway API', async () => {
18
- const fetchImpl = vi
19
- .fn<typeof fetch>()
20
- .mockResolvedValueOnce(
21
- jsonResponse({
22
- data: [{ id: 'group-1', name: 'Core Banking' }]
23
- })
24
- )
25
- .mockResolvedValueOnce(new Response(null, { status: 204 }));
26
-
27
- const adapter = createInternalIntegrationAdapter({
28
- backend: 'bifrost',
29
- accessToken: 'token-123',
30
- teamId: '11430732',
31
- fetchImpl
32
- });
33
-
34
- await adapter.assignWorkspaceToGovernanceGroup(
35
- 'ws-123',
36
- 'core-banking',
37
- JSON.stringify({ 'core-banking': 'Core Banking' })
38
- );
39
-
40
- expect(fetchImpl).toHaveBeenCalledTimes(2);
41
- expect(fetchImpl).toHaveBeenNthCalledWith(
42
- 1,
43
- 'https://gateway.postman.com/configure/workspace-groups',
44
- expect.objectContaining({
45
- headers: expect.objectContaining({
46
- 'x-access-token': 'token-123'
47
- })
48
- })
49
- );
50
- expect(fetchImpl).toHaveBeenNthCalledWith(
51
- 2,
52
- 'https://gateway.postman.com/configure/workspace-groups/group-1',
53
- expect.objectContaining({
54
- method: 'PATCH',
55
- body: JSON.stringify({ workspaces: ['ws-123'] })
56
- })
57
- );
58
- });
59
-
60
- it('routes system-env association through the worker internal endpoint', async () => {
61
- const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
62
- jsonResponse({
63
- data: {
64
- associations: 1
65
- }
66
- })
67
- );
68
-
69
- const adapter = createInternalIntegrationAdapter({
70
- backend: 'bifrost',
71
- accessToken: 'token-123',
72
- teamId: '11430732',
73
- workerBaseUrl: 'https://catalog-admin.example.test',
74
- fetchImpl
75
- });
76
-
77
- await adapter.associateSystemEnvironments('ws-123', [
78
- { envUid: 'env-prod', systemEnvId: 'sys-prod' }
79
- ]);
80
-
81
- expect(fetchImpl).toHaveBeenCalledWith(
82
- 'https://catalog-admin.example.test/api/internal/system-envs/associate',
83
- expect.objectContaining({
84
- method: 'POST',
85
- headers: expect.objectContaining({
86
- Authorization: 'Bearer token-123',
87
- 'Content-Type': 'application/json'
88
- }),
89
- body: JSON.stringify({
90
- workspace_id: 'ws-123',
91
- associations: [{ env_uid: 'env-prod', system_env_id: 'sys-prod' }]
92
- })
93
- })
94
- );
95
- });
96
-
97
- it('uses the Bifrost proxy for workspace repository linking and rejects unsupported backends', async () => {
98
- expect(() =>
99
- createInternalIntegrationAdapter({
100
- backend: 'custom',
101
- accessToken: 'token-123',
102
- teamId: '11430732'
103
- })
104
- ).toThrow(/Unsupported integration backend/);
105
-
106
- const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
107
- jsonResponse({
108
- data: {
109
- ok: true
110
- }
111
- })
112
- );
113
-
114
- const adapter = createInternalIntegrationAdapter({
115
- backend: 'bifrost',
116
- accessToken: 'token-123',
117
- teamId: '11430732',
118
- fetchImpl
119
- });
120
-
121
- await adapter.connectWorkspaceToRepository(
122
- 'ws-123',
123
- 'https://github.com/Postman-FDE/example'
124
- );
125
-
126
- expect(fetchImpl).toHaveBeenCalledWith(
127
- 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy',
128
- expect.objectContaining({
129
- method: 'POST',
130
- headers: expect.objectContaining({
131
- 'x-access-token': 'token-123',
132
- 'x-entity-team-id': '11430732'
133
- }),
134
- body: JSON.stringify({
135
- service: 'workspaces',
136
- method: 'POST',
137
- path: '/workspaces/ws-123/filesystem',
138
- body: {
139
- path: '/',
140
- repo: 'https://github.com/Postman-FDE/example',
141
- versionControl: true
142
- }
143
- })
144
- })
145
- );
146
- });
147
-
148
- it('links GitLab repository URLs via the Bifrost proxy', async () => {
149
- const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
150
- jsonResponse({
151
- data: {
152
- ok: true
153
- }
154
- })
155
- );
156
-
157
- const adapter = createInternalIntegrationAdapter({
158
- backend: 'bifrost',
159
- accessToken: 'token-123',
160
- teamId: '11430732',
161
- fetchImpl
162
- });
163
-
164
- await adapter.connectWorkspaceToRepository(
165
- 'ws-456',
166
- 'https://gitlab.com/org/my-service'
167
- );
168
-
169
- expect(fetchImpl).toHaveBeenCalledWith(
170
- 'https://bifrost-premium-https-v4.gw.postman.com/ws/proxy',
171
- expect.objectContaining({
172
- method: 'POST',
173
- body: JSON.stringify({
174
- service: 'workspaces',
175
- method: 'POST',
176
- path: '/workspaces/ws-456/filesystem',
177
- body: {
178
- path: '/',
179
- repo: 'https://gitlab.com/org/my-service',
180
- versionControl: true
181
- }
182
- })
183
- })
184
- );
185
- });
186
-
187
- it('treats projectAlreadyConnected as idempotent when the same repo is linked', async () => {
188
- const fetchImpl = vi.fn<typeof fetch>()
189
- // First call: connectWorkspaceToRepository POST returns 400 projectAlreadyConnected
190
- .mockResolvedValueOnce(
191
- jsonResponse(
192
- { error: { status: 400, name: 'projectAlreadyConnected', message: 'Workspace already has a file system connected.' } },
193
- { status: 400 }
194
- )
195
- )
196
- // Second call: getWorkspaceGitRepoUrl GET returns the linked repo
197
- .mockResolvedValueOnce(
198
- jsonResponse({
199
- repo: 'https://gitlab.com/org/my-service'
200
- })
201
- );
202
-
203
- const adapter = createInternalIntegrationAdapter({
204
- backend: 'bifrost',
205
- accessToken: 'token-123',
206
- teamId: '11430732',
207
- fetchImpl
208
- });
209
-
210
- // Should not throw because the linked repo matches
211
- await adapter.connectWorkspaceToRepository(
212
- 'ws-456',
213
- 'https://gitlab.com/org/my-service'
214
- );
215
-
216
- expect(fetchImpl).toHaveBeenCalledTimes(2);
217
- });
218
-
219
- it('throws when projectAlreadyConnected but linked to a different repo', async () => {
220
- const fetchImpl = vi.fn<typeof fetch>()
221
- .mockResolvedValueOnce(
222
- jsonResponse(
223
- { error: { status: 400, name: 'projectAlreadyConnected', message: 'Workspace already has a file system connected.' } },
224
- { status: 400 }
225
- )
226
- )
227
- .mockResolvedValueOnce(
228
- jsonResponse({
229
- repo: 'https://gitlab.com/org/different-service'
230
- })
231
- );
232
-
233
- const adapter = createInternalIntegrationAdapter({
234
- backend: 'bifrost',
235
- accessToken: 'token-123',
236
- teamId: '11430732',
237
- fetchImpl
238
- });
239
-
240
- await expect(
241
- adapter.connectWorkspaceToRepository('ws-456', 'https://gitlab.com/org/my-service')
242
- ).rejects.toThrow(/linked to a different repo/);
243
- });
244
- });