@shazhou/proman-core 0.9.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/CHANGELOG.md +26 -0
- package/LICENSE +18 -0
- package/dist/commands/bump.d.ts +13 -0
- package/dist/commands/bump.d.ts.map +1 -0
- package/dist/commands/bump.js +115 -0
- package/dist/commands/deploy.d.ts +9 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +42 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +175 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +262 -0
- package/dist/commands/link.d.ts +19 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +155 -0
- package/dist/commands/publish.d.ts +18 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +125 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/load-config.d.ts +6 -0
- package/dist/config/load-config.d.ts.map +1 -0
- package/dist/config/load-config.js +29 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validate-config.d.ts +7 -0
- package/dist/config/validate-config.d.ts.map +1 -0
- package/dist/config/validate-config.js +72 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/utils/changeset.d.ts +16 -0
- package/dist/utils/changeset.d.ts.map +1 -0
- package/dist/utils/changeset.js +80 -0
- package/dist/utils/fingerprint.d.ts +38 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/fingerprint.js +182 -0
- package/dist/utils/git.d.ts +23 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +105 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/npm.d.ts +30 -0
- package/dist/utils/npm.d.ts.map +1 -0
- package/dist/utils/npm.js +85 -0
- package/dist/utils/smoke-test.d.ts +7 -0
- package/dist/utils/smoke-test.d.ts.map +1 -0
- package/dist/utils/smoke-test.js +59 -0
- package/dist/utils/version.d.ts +5 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +36 -0
- package/dist/utils/workspace.d.ts +21 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +73 -0
- package/package.json +45 -0
- package/src/commands/bump.ts +131 -0
- package/src/commands/deploy.ts +52 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/index.ts +7 -0
- package/src/commands/init.integration.test.ts +59 -0
- package/src/commands/init.test.ts +179 -0
- package/src/commands/init.ts +290 -0
- package/src/commands/link.ts +195 -0
- package/src/commands/publish.ts +168 -0
- package/src/config/index.ts +8 -0
- package/src/config/load-config.ts +33 -0
- package/src/config/types.ts +19 -0
- package/src/config/validate-config.ts +81 -0
- package/src/index.ts +29 -0
- package/src/utils/changeset.ts +98 -0
- package/src/utils/fingerprint.ts +199 -0
- package/src/utils/git.ts +119 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/npm.ts +110 -0
- package/src/utils/smoke-test.ts +79 -0
- package/src/utils/version.ts +41 -0
- package/src/utils/workspace.ts +94 -0
- package/tests/build-fingerprint-integration.test.ts +403 -0
- package/tests/bump.test.ts +261 -0
- package/tests/changeset.test.ts +147 -0
- package/tests/deploy.test.ts +98 -0
- package/tests/dev.test.ts +756 -0
- package/tests/fingerprint.test.ts +316 -0
- package/tests/fixtures/api-only/packages/api/.gitkeep +0 -0
- package/tests/fixtures/api-only/proman.yaml +4 -0
- package/tests/fixtures/bad-packages/proman.yaml +1 -0
- package/tests/fixtures/bun-project/packages/a/.gitkeep +0 -0
- package/tests/fixtures/bun-project/proman.yaml +4 -0
- package/tests/fixtures/defaults/proman.yaml +3 -0
- package/tests/fixtures/no-deployable/packages/core/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/proman.yaml +7 -0
- package/tests/fixtures/node-runtime/packages/a/package.json +5 -0
- package/tests/fixtures/node-runtime/proman.yaml +3 -0
- package/tests/fixtures/pnpm-project/packages/a/package.json +1 -0
- package/tests/fixtures/pnpm-project/pnpm-lock.yaml +0 -0
- package/tests/fixtures/pnpm-project/proman.yaml +3 -0
- package/tests/fixtures/typed/packages/api/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/core/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/typed/proman.yaml +13 -0
- package/tests/fixtures/valid/packages/cli/package.json +5 -0
- package/tests/fixtures/valid/packages/core/package.json +5 -0
- package/tests/fixtures/valid/packages/fs/package.json +5 -0
- package/tests/fixtures/valid/proman.yaml +13 -0
- package/tests/fixtures/webui-only/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/webui-only/proman.yaml +4 -0
- package/tests/link.test.ts +419 -0
- package/tests/load-config.test.ts +44 -0
- package/tests/npm.test.ts +199 -0
- package/tests/publish.test.ts +599 -0
- package/tests/smoke-test.test.ts +211 -0
- package/tests/validate-config.test.ts +67 -0
- package/tests/version.test.ts +86 -0
- package/tests/workflow-schema.test.ts +72 -0
- package/tests/workspace.test.ts +160 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
4
|
+
import type { SpawnFn } from '../src/utils/npm.ts'
|
|
5
|
+
import { smokeTestTarball } from '../src/utils/smoke-test.ts'
|
|
6
|
+
|
|
7
|
+
let tmp: string
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
const { mkdtemp } = await import('node:fs/promises')
|
|
11
|
+
const { tmpdir } = await import('node:os')
|
|
12
|
+
tmp = await mkdtemp(join(tmpdir(), 'proman-smoke-'))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
const { rm } = await import('node:fs/promises')
|
|
17
|
+
await rm(tmp, { recursive: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// ── Core smoke test: package with bin entry ──
|
|
21
|
+
|
|
22
|
+
describe('smoke test with bin entry', () => {
|
|
23
|
+
test('extracts tarball and runs bin --version successfully', async () => {
|
|
24
|
+
// Setup: create a minimal package structure with bin entry
|
|
25
|
+
const pkgDir = join(tmp, 'test-pkg')
|
|
26
|
+
await mkdir(pkgDir, { recursive: true })
|
|
27
|
+
|
|
28
|
+
const pkgJson = {
|
|
29
|
+
name: '@test/cli',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
bin: { testcli: './cli.js' },
|
|
32
|
+
}
|
|
33
|
+
await writeFile(join(pkgDir, 'package.json'), JSON.stringify(pkgJson, null, 2))
|
|
34
|
+
|
|
35
|
+
// Create a minimal working CLI
|
|
36
|
+
const cliScript = `#!/usr/bin/env node
|
|
37
|
+
if (process.argv.includes('--version')) {
|
|
38
|
+
console.log('1.0.0');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
`
|
|
42
|
+
await writeFile(join(pkgDir, 'cli.js'), cliScript)
|
|
43
|
+
|
|
44
|
+
// Mock spawn that simulates npm pack + successful bin execution
|
|
45
|
+
const spawnCalls: string[] = []
|
|
46
|
+
const mockSpawn: SpawnFn = async (argv, _cwd) => {
|
|
47
|
+
const cmd = argv.join(' ')
|
|
48
|
+
spawnCalls.push(cmd)
|
|
49
|
+
|
|
50
|
+
if (cmd.startsWith('pnpm pack')) {
|
|
51
|
+
// Return tarball filename
|
|
52
|
+
return { code: 0, stdout: 'test-cli-1.0.0.tgz\n', stderr: '' }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (cmd.includes('--version')) {
|
|
56
|
+
// Simulate successful bin execution
|
|
57
|
+
return { code: 0, stdout: '1.0.0\n', stderr: '' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// When: smoke test runs
|
|
64
|
+
await smokeTestTarball(pkgDir, mockSpawn)
|
|
65
|
+
|
|
66
|
+
// Then: npm pack was called, bin --version was executed
|
|
67
|
+
expect(spawnCalls.some((c) => c.includes('pnpm pack'))).toBe(true)
|
|
68
|
+
expect(spawnCalls.some((c) => c.includes('--version'))).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('tests all bin entries when multiple exist', async () => {
|
|
72
|
+
const pkgDir = join(tmp, 'multi-bin')
|
|
73
|
+
await mkdir(pkgDir, { recursive: true })
|
|
74
|
+
|
|
75
|
+
const pkgJson = {
|
|
76
|
+
name: '@test/tools',
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
bin: {
|
|
79
|
+
'tool-a': './bin-a.js',
|
|
80
|
+
'tool-b': './bin-b.js',
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
await writeFile(join(pkgDir, 'package.json'), JSON.stringify(pkgJson, null, 2))
|
|
84
|
+
|
|
85
|
+
const spawnCalls: string[] = []
|
|
86
|
+
const mockSpawn: SpawnFn = async (argv) => {
|
|
87
|
+
spawnCalls.push(argv.join(' '))
|
|
88
|
+
if (argv.includes('pack')) {
|
|
89
|
+
return { code: 0, stdout: 'test-tools-1.0.0.tgz\n', stderr: '' }
|
|
90
|
+
}
|
|
91
|
+
return { code: 0, stdout: '1.0.0\n', stderr: '' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await smokeTestTarball(pkgDir, mockSpawn)
|
|
95
|
+
|
|
96
|
+
// Should test both binaries
|
|
97
|
+
expect(spawnCalls.some((c) => c.includes('bin-a.js'))).toBe(true)
|
|
98
|
+
expect(spawnCalls.some((c) => c.includes('bin-b.js'))).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ── Error handling: abort on failure ──
|
|
103
|
+
|
|
104
|
+
describe('abort on smoke test failure', () => {
|
|
105
|
+
test('throws error when bin command fails', async () => {
|
|
106
|
+
const pkgDir = join(tmp, 'broken-pkg')
|
|
107
|
+
await mkdir(pkgDir, { recursive: true })
|
|
108
|
+
|
|
109
|
+
const pkgJson = {
|
|
110
|
+
name: '@test/broken',
|
|
111
|
+
version: '1.0.0',
|
|
112
|
+
bin: { broken: './cli.js' },
|
|
113
|
+
}
|
|
114
|
+
await writeFile(join(pkgDir, 'package.json'), JSON.stringify(pkgJson, null, 2))
|
|
115
|
+
|
|
116
|
+
const mockSpawn: SpawnFn = async (argv) => {
|
|
117
|
+
if (argv.includes('pack')) {
|
|
118
|
+
return { code: 0, stdout: 'broken-1.0.0.tgz\n', stderr: '' }
|
|
119
|
+
}
|
|
120
|
+
// Simulate bin failure (e.g., missing file, broken import)
|
|
121
|
+
if (argv.includes('--version')) {
|
|
122
|
+
return {
|
|
123
|
+
code: 1,
|
|
124
|
+
stdout: '',
|
|
125
|
+
stderr: "Error: Cannot find module './missing-dep.js'",
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Should throw with clear error message
|
|
132
|
+
await expect(smokeTestTarball(pkgDir, mockSpawn)).rejects.toThrow('smoke test failed')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('includes error output in thrown error', async () => {
|
|
136
|
+
const pkgDir = join(tmp, 'error-pkg')
|
|
137
|
+
await mkdir(pkgDir, { recursive: true })
|
|
138
|
+
|
|
139
|
+
const pkgJson = {
|
|
140
|
+
name: '@test/error',
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
bin: { errcli: './cli.js' },
|
|
143
|
+
}
|
|
144
|
+
await writeFile(join(pkgDir, 'package.json'), JSON.stringify(pkgJson, null, 2))
|
|
145
|
+
|
|
146
|
+
const mockSpawn: SpawnFn = async (argv) => {
|
|
147
|
+
if (argv.includes('pack')) {
|
|
148
|
+
return { code: 0, stdout: 'error-1.0.0.tgz\n', stderr: '' }
|
|
149
|
+
}
|
|
150
|
+
if (argv.includes('--version')) {
|
|
151
|
+
return {
|
|
152
|
+
code: 1,
|
|
153
|
+
stdout: '',
|
|
154
|
+
stderr: 'ENOENT: no such file or directory',
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await expect(smokeTestTarball(pkgDir, mockSpawn)).rejects.toThrow('ENOENT')
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// ── Edge case: no bin entry ──
|
|
165
|
+
|
|
166
|
+
describe('skip smoke test for packages without bin', () => {
|
|
167
|
+
test('skips smoke test when no bin entry exists', async () => {
|
|
168
|
+
const pkgDir = join(tmp, 'lib-pkg')
|
|
169
|
+
await mkdir(pkgDir, { recursive: true })
|
|
170
|
+
|
|
171
|
+
const pkgJson = {
|
|
172
|
+
name: '@test/lib',
|
|
173
|
+
version: '1.0.0',
|
|
174
|
+
// No bin entry
|
|
175
|
+
}
|
|
176
|
+
await writeFile(join(pkgDir, 'package.json'), JSON.stringify(pkgJson, null, 2))
|
|
177
|
+
|
|
178
|
+
const spawnCalls: string[] = []
|
|
179
|
+
const mockSpawn: SpawnFn = async (argv) => {
|
|
180
|
+
spawnCalls.push(argv.join(' '))
|
|
181
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Should complete without error and without calling npm pack
|
|
185
|
+
await smokeTestTarball(pkgDir, mockSpawn)
|
|
186
|
+
|
|
187
|
+
expect(spawnCalls.length).toBe(0)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('skips when bin is empty object', async () => {
|
|
191
|
+
const pkgDir = join(tmp, 'empty-bin')
|
|
192
|
+
await mkdir(pkgDir, { recursive: true })
|
|
193
|
+
|
|
194
|
+
const pkgJson = {
|
|
195
|
+
name: '@test/empty',
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
bin: {},
|
|
198
|
+
}
|
|
199
|
+
await writeFile(join(pkgDir, 'package.json'), JSON.stringify(pkgJson, null, 2))
|
|
200
|
+
|
|
201
|
+
const spawnCalls: string[] = []
|
|
202
|
+
const mockSpawn: SpawnFn = async (argv) => {
|
|
203
|
+
spawnCalls.push(argv.join(' '))
|
|
204
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await smokeTestTarball(pkgDir, mockSpawn)
|
|
208
|
+
|
|
209
|
+
expect(spawnCalls.length).toBe(0)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { validateConfig } from '../src/config/index.ts'
|
|
3
|
+
|
|
4
|
+
const minimal = {
|
|
5
|
+
packages: [{ name: '@x/a', path: 'packages/a' }],
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('validateConfig', () => {
|
|
9
|
+
test('accepts minimal valid config', () => {
|
|
10
|
+
const r = validateConfig(minimal)
|
|
11
|
+
expect(r.packages).toHaveLength(1)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('rejects non-object input', () => {
|
|
15
|
+
expect(() => validateConfig(null)).toThrow(/object/i)
|
|
16
|
+
expect(() => validateConfig(undefined)).toThrow(/object/i)
|
|
17
|
+
expect(() => validateConfig(42)).toThrow(/object/i)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('ignores unknown changeset field (backward compat)', () => {
|
|
21
|
+
// Old configs with changeset.fixed should not throw
|
|
22
|
+
const r = validateConfig({ ...minimal, changeset: { fixed: true } })
|
|
23
|
+
expect(r.packages).toHaveLength(1)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('rejects invalid release.access', () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
validateConfig({
|
|
29
|
+
...minimal,
|
|
30
|
+
release: { access: 'private' as unknown as 'public' },
|
|
31
|
+
}),
|
|
32
|
+
).toThrow(/release\.access/)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('T1: accepts each package type', () => {
|
|
36
|
+
for (const t of ['lib', 'cli', 'webui', 'api'] as const) {
|
|
37
|
+
const r = validateConfig({
|
|
38
|
+
...minimal,
|
|
39
|
+
packages: [{ name: '@x/a', path: 'packages/a', type: t }],
|
|
40
|
+
})
|
|
41
|
+
expect(r.packages[0]?.type).toBe(t)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('T2: defaults type to lib when omitted', () => {
|
|
46
|
+
const r = validateConfig(minimal)
|
|
47
|
+
expect(r.packages[0]?.type).toBe('lib')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('T3: rejects unknown type string', () => {
|
|
51
|
+
expect(() =>
|
|
52
|
+
validateConfig({
|
|
53
|
+
...minimal,
|
|
54
|
+
packages: [{ name: '@x/a', path: 'packages/a', type: 'frontend' }],
|
|
55
|
+
}),
|
|
56
|
+
).toThrow(/packages\[0\]\.type/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('T4: rejects non-string type', () => {
|
|
60
|
+
expect(() =>
|
|
61
|
+
validateConfig({
|
|
62
|
+
...minimal,
|
|
63
|
+
packages: [{ name: '@x/a', path: 'packages/a', type: 1 as unknown as string }],
|
|
64
|
+
}),
|
|
65
|
+
).toThrow(/packages\[0\]\.type/)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import type { Changeset } from '../src/utils/changeset.ts'
|
|
3
|
+
import { bumpVersion, inferBump, parseTagVersion } from '../src/utils/version.ts'
|
|
4
|
+
|
|
5
|
+
describe('bumpVersion', () => {
|
|
6
|
+
test('patch/minor/major from 0.2.0', () => {
|
|
7
|
+
expect(bumpVersion('0.2.0', 'patch')).toBe('0.2.1')
|
|
8
|
+
expect(bumpVersion('0.2.0', 'minor')).toBe('0.3.0')
|
|
9
|
+
expect(bumpVersion('0.2.0', 'major')).toBe('1.0.0')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('patch/minor/major from 1.4.7', () => {
|
|
13
|
+
expect(bumpVersion('1.4.7', 'patch')).toBe('1.4.8')
|
|
14
|
+
expect(bumpVersion('1.4.7', 'minor')).toBe('1.5.0')
|
|
15
|
+
expect(bumpVersion('1.4.7', 'major')).toBe('2.0.0')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('strips pre-release suffix', () => {
|
|
19
|
+
expect(bumpVersion('0.2.0-rc.1', 'patch')).toBe('0.2.1')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('throws on invalid input', () => {
|
|
23
|
+
expect(() => bumpVersion('abc', 'patch')).toThrow(/invalid version/i)
|
|
24
|
+
expect(() => bumpVersion('', 'patch')).toThrow(/invalid version/i)
|
|
25
|
+
expect(() => bumpVersion('1.2', 'patch')).toThrow(/invalid version/i)
|
|
26
|
+
expect(() => bumpVersion('1.2.3.4', 'patch')).toThrow(/invalid version/i)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function cs(packages: Record<string, 'major' | 'minor' | 'patch'>, file = 'foo.md'): Changeset {
|
|
31
|
+
return { file, packages, body: '' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('inferBump', () => {
|
|
35
|
+
test('empty list → empty map', () => {
|
|
36
|
+
expect(inferBump([])).toEqual({})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('all-empty package records → empty map', () => {
|
|
40
|
+
expect(inferBump([cs({}), cs({}, 'b.md')])).toEqual({})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('single package patch', () => {
|
|
44
|
+
expect(inferBump([cs({ a: 'patch' })])).toEqual({ a: 'patch' })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('per-package: different bumps for different packages', () => {
|
|
48
|
+
expect(inferBump([cs({ a: 'patch' }), cs({ b: 'minor' }, 'b.md')])).toEqual({
|
|
49
|
+
a: 'patch',
|
|
50
|
+
b: 'minor',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('same package across changesets → highest wins', () => {
|
|
55
|
+
expect(inferBump([cs({ a: 'patch' }), cs({ a: 'minor' }, 'b.md')])).toEqual({ a: 'minor' })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('mixed bumps within a single changeset', () => {
|
|
59
|
+
expect(inferBump([cs({ a: 'minor', b: 'major' })])).toEqual({ a: 'minor', b: 'major' })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('complex: multiple changesets, multiple packages, highest per-package', () => {
|
|
63
|
+
expect(
|
|
64
|
+
inferBump([cs({ a: 'patch', b: 'minor' }), cs({ a: 'major', c: 'patch' }, 'b.md')]),
|
|
65
|
+
).toEqual({ a: 'major', b: 'minor', c: 'patch' })
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('parseTagVersion', () => {
|
|
70
|
+
test('v0.2.3 → 0.2.3', () => {
|
|
71
|
+
expect(parseTagVersion('v0.2.3')).toBe('0.2.3')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('0.2.3 → 0.2.3', () => {
|
|
75
|
+
expect(parseTagVersion('0.2.3')).toBe('0.2.3')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('v1.0.0-rc.1 → 1.0.0-rc.1', () => {
|
|
79
|
+
expect(parseTagVersion('v1.0.0-rc.1')).toBe('1.0.0-rc.1')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('throws on invalid tag', () => {
|
|
83
|
+
expect(() => parseTagVersion('release-foo')).toThrow(/invalid tag/i)
|
|
84
|
+
expect(() => parseTagVersion('')).toThrow(/invalid tag/i)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { parse as parseYAML } from 'yaml'
|
|
5
|
+
|
|
6
|
+
describe('Issue #154: already_approved variant in review-pr.yaml', () => {
|
|
7
|
+
const reviewPrPath = resolve(__dirname, '../../../.workflows/review-pr.yaml')
|
|
8
|
+
|
|
9
|
+
test('T154.1: already_approved variant does NOT include selfReview (synced with uwf)', () => {
|
|
10
|
+
const yaml = readFileSync(reviewPrPath, 'utf8')
|
|
11
|
+
const parsed = parseYAML(yaml)
|
|
12
|
+
const alreadyApproved = parsed.roles.fetcher.frontmatter.oneOf[1]
|
|
13
|
+
|
|
14
|
+
expect(alreadyApproved.properties.selfReview).toBeUndefined()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('T154.2: already_approved variant requires only $status, repo, prNumber', () => {
|
|
18
|
+
const yaml = readFileSync(reviewPrPath, 'utf8')
|
|
19
|
+
const parsed = parseYAML(yaml)
|
|
20
|
+
const alreadyApproved = parsed.roles.fetcher.frontmatter.oneOf[1]
|
|
21
|
+
|
|
22
|
+
expect(alreadyApproved.required).not.toContain('selfReview')
|
|
23
|
+
expect(alreadyApproved.required).toEqual(['$status', 'repo', 'prNumber'])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('T154.3: ready variant maintains selfReview field (regression)', () => {
|
|
27
|
+
const yaml = readFileSync(reviewPrPath, 'utf8')
|
|
28
|
+
const parsed = parseYAML(yaml)
|
|
29
|
+
const ready = parsed.roles.fetcher.frontmatter.oneOf[0]
|
|
30
|
+
|
|
31
|
+
expect(ready.properties.selfReview).toBeDefined()
|
|
32
|
+
expect(ready.properties.selfReview.type).toBe('boolean')
|
|
33
|
+
expect(ready.required).toContain('selfReview')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('Issue #156: Token retrieval in triage-issues.yaml', () => {
|
|
38
|
+
const triageIssuesPath = resolve(__dirname, '../../../.workflows/triage-issues.yaml')
|
|
39
|
+
|
|
40
|
+
test('T156.1: output section documents cfg get GITEA_TOKEN', () => {
|
|
41
|
+
const yaml = readFileSync(triageIssuesPath, 'utf8')
|
|
42
|
+
const parsed = parseYAML(yaml)
|
|
43
|
+
const output = parsed.roles.triager.output
|
|
44
|
+
|
|
45
|
+
// The output instruction mentions cfg-based token retrieval
|
|
46
|
+
expect(output).toBeDefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('T156.4: owner/repo sed extraction remains unchanged', () => {
|
|
50
|
+
const yaml = readFileSync(triageIssuesPath, 'utf8')
|
|
51
|
+
const parsed = parseYAML(yaml)
|
|
52
|
+
const procedure = parsed.roles.triager.procedure
|
|
53
|
+
|
|
54
|
+
// This sed pattern should remain (only token extraction is changing)
|
|
55
|
+
expect(procedure).toContain(
|
|
56
|
+
"git remote get-url origin | sed 's/.*[:/]\\([^/]*\\/[^.]*\\).*/\\1/'",
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('Integration tests', () => {
|
|
62
|
+
test('T157.1: both workflow files parse as valid YAML', () => {
|
|
63
|
+
const reviewPrPath = resolve(__dirname, '../../../.workflows/review-pr.yaml')
|
|
64
|
+
const triageIssuesPath = resolve(__dirname, '../../../.workflows/triage-issues.yaml')
|
|
65
|
+
|
|
66
|
+
const reviewPr = readFileSync(reviewPrPath, 'utf8')
|
|
67
|
+
const triageIssues = readFileSync(triageIssuesPath, 'utf8')
|
|
68
|
+
|
|
69
|
+
expect(() => parseYAML(reviewPr)).not.toThrow()
|
|
70
|
+
expect(() => parseYAML(triageIssues)).not.toThrow()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
applyWorkspaceRewrites,
|
|
7
|
+
type PkgManifest,
|
|
8
|
+
rewriteWorkspaceDeps,
|
|
9
|
+
} from '../src/utils/workspace.ts'
|
|
10
|
+
|
|
11
|
+
describe('rewriteWorkspaceDeps', () => {
|
|
12
|
+
test('rewrites workspace:* in dependencies', () => {
|
|
13
|
+
const a: PkgManifest = { name: 'A', version: '1.0.0', dependencies: { B: 'workspace:*' } }
|
|
14
|
+
const b: PkgManifest = { name: 'B', version: '2.3.4' }
|
|
15
|
+
const { rewritten, unresolved } = rewriteWorkspaceDeps([a, b])
|
|
16
|
+
expect(rewritten[0]?.dependencies?.B).toBe('2.3.4')
|
|
17
|
+
expect(unresolved).toEqual([])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('rewrites workspace:* in devDependencies', () => {
|
|
21
|
+
const a: PkgManifest = { name: 'A', version: '1.0.0', devDependencies: { B: 'workspace:*' } }
|
|
22
|
+
const b: PkgManifest = { name: 'B', version: '2.3.4' }
|
|
23
|
+
const { rewritten } = rewriteWorkspaceDeps([a, b])
|
|
24
|
+
expect(rewritten[0]?.devDependencies?.B).toBe('2.3.4')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('leaves non workspace:* deps untouched', () => {
|
|
28
|
+
const a: PkgManifest = {
|
|
29
|
+
name: 'A',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
dependencies: {
|
|
32
|
+
x: '^1.2.3',
|
|
33
|
+
y: '1.0.0',
|
|
34
|
+
z: 'npm:foo@1',
|
|
35
|
+
w: 'file:../x',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
const { rewritten } = rewriteWorkspaceDeps([a])
|
|
39
|
+
expect(rewritten[0]?.dependencies).toEqual({
|
|
40
|
+
x: '^1.2.3',
|
|
41
|
+
y: '1.0.0',
|
|
42
|
+
z: 'npm:foo@1',
|
|
43
|
+
w: 'file:../x',
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('leaves unknown workspace:* and reports unresolved', () => {
|
|
48
|
+
const a: PkgManifest = {
|
|
49
|
+
name: 'A',
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
dependencies: { unknown: 'workspace:*' },
|
|
52
|
+
}
|
|
53
|
+
const b: PkgManifest = { name: 'B', version: '2.3.4' }
|
|
54
|
+
const { rewritten, unresolved } = rewriteWorkspaceDeps([a, b])
|
|
55
|
+
expect(rewritten[0]?.dependencies?.unknown).toBe('workspace:*')
|
|
56
|
+
expect(unresolved).toEqual([{ pkg: 'A', dep: 'unknown' }])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('does not mutate input objects', () => {
|
|
60
|
+
const a: PkgManifest = { name: 'A', version: '1.0.0', dependencies: { B: 'workspace:*' } }
|
|
61
|
+
const b: PkgManifest = { name: 'B', version: '2.3.4' }
|
|
62
|
+
const aSnap = JSON.parse(JSON.stringify(a))
|
|
63
|
+
const bSnap = JSON.parse(JSON.stringify(b))
|
|
64
|
+
rewriteWorkspaceDeps([a, b])
|
|
65
|
+
expect(a).toEqual(aSnap)
|
|
66
|
+
expect(b).toEqual(bSnap)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('handles missing dependencies / devDependencies fields', () => {
|
|
70
|
+
const a: PkgManifest = { name: 'A', version: '1.0.0' }
|
|
71
|
+
const { rewritten } = rewriteWorkspaceDeps([a])
|
|
72
|
+
expect(rewritten[0]).not.toHaveProperty('dependencies')
|
|
73
|
+
expect(rewritten[0]).not.toHaveProperty('devDependencies')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('preserves unrelated fields', () => {
|
|
77
|
+
const a: PkgManifest = {
|
|
78
|
+
name: 'A',
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
scripts: { build: 'tsc' },
|
|
81
|
+
description: 'desc',
|
|
82
|
+
license: 'MIT',
|
|
83
|
+
dependencies: { B: 'workspace:*' },
|
|
84
|
+
}
|
|
85
|
+
const b: PkgManifest = { name: 'B', version: '2.3.4' }
|
|
86
|
+
const { rewritten } = rewriteWorkspaceDeps([a, b])
|
|
87
|
+
expect(rewritten[0]?.scripts).toEqual({ build: 'tsc' })
|
|
88
|
+
expect(rewritten[0]?.description).toBe('desc')
|
|
89
|
+
expect(rewritten[0]?.license).toBe('MIT')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('applyWorkspaceRewrites', () => {
|
|
94
|
+
let tmp: string
|
|
95
|
+
|
|
96
|
+
beforeEach(async () => {
|
|
97
|
+
tmp = await mkdtemp(join(tmpdir(), 'proman-ws-'))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
afterEach(async () => {
|
|
101
|
+
await rm(tmp, { recursive: true, force: true })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('writes 2-space indent + trailing newline', async () => {
|
|
105
|
+
await mkdir(join(tmp, 'packages/a'), { recursive: true })
|
|
106
|
+
await mkdir(join(tmp, 'packages/b'), { recursive: true })
|
|
107
|
+
const aPkg = { name: 'pkg-a', version: '0.2.0', dependencies: { 'pkg-b': 'workspace:*' } }
|
|
108
|
+
const bPkg = { name: 'pkg-b', version: '0.2.0' }
|
|
109
|
+
await writeFile(join(tmp, 'packages/a/package.json'), JSON.stringify(aPkg))
|
|
110
|
+
await writeFile(join(tmp, 'packages/b/package.json'), JSON.stringify(bPkg))
|
|
111
|
+
|
|
112
|
+
await applyWorkspaceRewrites(tmp, [
|
|
113
|
+
{ name: 'pkg-a', path: 'packages/a' },
|
|
114
|
+
{ name: 'pkg-b', path: 'packages/b' },
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
const aText = await readFile(join(tmp, 'packages/a/package.json'), 'utf8')
|
|
118
|
+
expect(aText.endsWith('\n')).toBe(true)
|
|
119
|
+
expect(aText).toMatch(/^ {2}"/m)
|
|
120
|
+
const aParsed = JSON.parse(aText)
|
|
121
|
+
expect(aParsed.dependencies['pkg-b']).toBe('0.2.0')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('returns only files actually changed', async () => {
|
|
125
|
+
await mkdir(join(tmp, 'packages/a'), { recursive: true })
|
|
126
|
+
await mkdir(join(tmp, 'packages/b'), { recursive: true })
|
|
127
|
+
const aPkg = { name: 'pkg-a', version: '0.2.0', dependencies: { 'pkg-b': 'workspace:*' } }
|
|
128
|
+
const bPkg = { name: 'pkg-b', version: '0.2.0', dependencies: { other: '^1.0.0' } }
|
|
129
|
+
await writeFile(join(tmp, 'packages/a/package.json'), JSON.stringify(aPkg))
|
|
130
|
+
const bText = JSON.stringify(bPkg)
|
|
131
|
+
await writeFile(join(tmp, 'packages/b/package.json'), bText)
|
|
132
|
+
const bBefore = await readFile(join(tmp, 'packages/b/package.json'))
|
|
133
|
+
|
|
134
|
+
const changed = await applyWorkspaceRewrites(tmp, [
|
|
135
|
+
{ name: 'pkg-a', path: 'packages/a' },
|
|
136
|
+
{ name: 'pkg-b', path: 'packages/b' },
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
expect(changed.some((p) => p.includes('packages/a/package.json'))).toBe(true)
|
|
140
|
+
expect(changed.some((p) => p.includes('packages/b/package.json'))).toBe(false)
|
|
141
|
+
const bAfter = await readFile(join(tmp, 'packages/b/package.json'))
|
|
142
|
+
expect(bAfter.equals(bBefore)).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('errors when a package.json is missing', async () => {
|
|
146
|
+
await mkdir(join(tmp, 'packages/a'), { recursive: true })
|
|
147
|
+
await writeFile(
|
|
148
|
+
join(tmp, 'packages/a/package.json'),
|
|
149
|
+
JSON.stringify({ name: 'pkg-a', version: '0.2.0' }),
|
|
150
|
+
)
|
|
151
|
+
await expect(
|
|
152
|
+
applyWorkspaceRewrites(tmp, [
|
|
153
|
+
{ name: 'pkg-a', path: 'packages/a' },
|
|
154
|
+
{ name: 'pkg-missing', path: 'packages/missing' },
|
|
155
|
+
]),
|
|
156
|
+
).rejects.toThrow(/packages\/missing/)
|
|
157
|
+
// also confirm a file is required
|
|
158
|
+
await stat(join(tmp, 'packages/a/package.json'))
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"allowImportingTsExtensions": false
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"],
|
|
13
|
+
"exclude": ["src/**/*.test.ts", "src/**/*.integration.test.ts"]
|
|
14
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts","./src/commands/bump.ts","./src/commands/deploy.ts","./src/commands/dev.ts","./src/commands/index.ts","./src/commands/init.integration.test.ts","./src/commands/init.test.ts","./src/commands/init.ts","./src/commands/link.ts","./src/commands/publish.ts","./src/config/index.ts","./src/config/load-config.ts","./src/config/types.ts","./src/config/validate-config.ts","./src/utils/changeset.ts","./src/utils/fingerprint.ts","./src/utils/git.ts","./src/utils/index.ts","./src/utils/npm.ts","./src/utils/smoke-test.ts","./src/utils/version.ts","./src/utils/workspace.ts"],"version":"5.9.3"}
|