@nan0web/release 3.1.1 → 3.3.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/bin/release.test.js +81 -0
- package/package.json +20 -12
- package/src/Release/MarkdownToTest.test.js +60 -0
- package/src/Release/Scanner.close.test.js +91 -0
- package/src/Release/Scanner.test.js +134 -0
- package/src/architecture/ProjectManagementAsCode.test.js +128 -0
- package/src/domain/BumpCommand.js +3 -3
- package/src/releases/1/0/v1.0.2/test.test.js +6 -0
- package/src/releases/1/0/v1.0.3/task.test.js +16 -0
- package/types/Release/Document.d.ts +27 -20
- package/types/Release/MarkdownToTest.d.ts +1 -1
- package/types/Release/Person.d.ts +7 -9
- package/types/Release/Scanner.d.ts +39 -39
- package/types/Release.d.ts +42 -42
- package/types/architecture/ProjectManagementAsCode.d.ts +64 -59
- package/types/domain/App.d.ts +5 -6
- package/types/domain/BumpCommand.d.ts +12 -4
- package/types/domain/CheckCommand.d.ts +1 -1
- package/types/domain/CloseCommand.d.ts +6 -3
- package/types/domain/DepsCommand.d.ts +6 -4
- package/types/domain/InitCommand.d.ts +6 -3
- package/types/domain/PublishCommand.d.ts +6 -5
- package/types/domain/SpecCommand.d.ts +6 -3
- package/types/domain/StatusCommand.d.ts +5 -1
- package/types/index.d.ts +10 -13
- package/knip.json +0 -21
- package/nan0web.nan0 +0 -16
- package/project.yaml +0 -79
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import FS from '@nan0web/db-fs'
|
|
4
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { resolve, dirname } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import main from './release.js'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = dirname(__filename)
|
|
12
|
+
const tempDir = resolve(__dirname, 'temp')
|
|
13
|
+
const tempPackageJson = resolve(tempDir, 'package.json')
|
|
14
|
+
|
|
15
|
+
const fs = new FS()
|
|
16
|
+
|
|
17
|
+
describe('release.js', async () => {
|
|
18
|
+
await fs.connect()
|
|
19
|
+
const tempDir = 'tmp/release'
|
|
20
|
+
const db = fs.extract(tempDir)
|
|
21
|
+
await db.writeDocument('package.json', {
|
|
22
|
+
name: 'test-package',
|
|
23
|
+
version: '0.0.1-test',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Mock process.cwd to return tempDir
|
|
27
|
+
const originalCwd = process.cwd
|
|
28
|
+
process.cwd = () => tempDir
|
|
29
|
+
|
|
30
|
+
it('main function processes release command', async () => {
|
|
31
|
+
// Test with ignore flags to avoid actual git operations
|
|
32
|
+
const result = await main(['--ignore-uncommitted', '--ignore-fail-tests', '--ignore-tag'])
|
|
33
|
+
assert.ok(result.includes('Release completed'))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('main function processes prepare command', async () => {
|
|
37
|
+
const resultP = await main(['prepare'])
|
|
38
|
+
assert.ok(result.includes('Prepare completed'))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('ReleaseCommand handles package.json validation', async () => {
|
|
42
|
+
// Create a package.json without required scripts
|
|
43
|
+
writeFileSync(
|
|
44
|
+
tempPackageJson,
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
name: 'test-package',
|
|
47
|
+
version: '0.0.1-test',
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Should exit with error when missing scripts
|
|
52
|
+
try {
|
|
53
|
+
await main(['--ignore-uncommitted', '--ignore-fail-tests', '--ignore-tag'])
|
|
54
|
+
assert.fail('Should have exited with error')
|
|
55
|
+
} catch (err) {
|
|
56
|
+
assert.ok(err.message.includes('Missing required scripts'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Should fix when --fix flag is used
|
|
60
|
+
const result = await main([
|
|
61
|
+
'--ignore-uncommitted',
|
|
62
|
+
'--ignore-fail-tests',
|
|
63
|
+
'--ignore-tag',
|
|
64
|
+
'--fix',
|
|
65
|
+
])
|
|
66
|
+
assert.ok(result.includes('Release completed'))
|
|
67
|
+
|
|
68
|
+
// Verify package.json was fixed
|
|
69
|
+
const pkg = JSON.parse(readFileSync(tempPackageJson, 'utf8'))
|
|
70
|
+
assert.ok(pkg.scripts.dev)
|
|
71
|
+
assert.ok(pkg.scripts.build)
|
|
72
|
+
assert.ok(pkg.scripts.preview)
|
|
73
|
+
assert.ok(pkg.scripts.test)
|
|
74
|
+
assert.ok(pkg.scripts.clean)
|
|
75
|
+
assert.ok(pkg.devDependencies.husky)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Cleanup
|
|
79
|
+
process.cwd = originalCwd
|
|
80
|
+
await db.dropDocument('package.json')
|
|
81
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/release",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Project Management as Code, Git-Native, GPG-Signed, Test-Driven",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -10,23 +10,31 @@
|
|
|
10
10
|
"release": "./bin/nan0release.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@nan0web/
|
|
14
|
-
"@nan0web/
|
|
15
|
-
"@nan0web/
|
|
16
|
-
"@nan0web/
|
|
17
|
-
"@nan0web/
|
|
18
|
-
"@nan0web/
|
|
19
|
-
"@nan0web/
|
|
20
|
-
"@nan0web/
|
|
21
|
-
"@nan0web/
|
|
22
|
-
"@nan0web/
|
|
13
|
+
"@nan0web/db-fs": "3.3.0",
|
|
14
|
+
"@nan0web/db": "3.3.0",
|
|
15
|
+
"@nan0web/types": "3.3.0",
|
|
16
|
+
"@nan0web/markdown": "3.3.0",
|
|
17
|
+
"@nan0web/log": "3.3.0",
|
|
18
|
+
"@nan0web/co": "3.3.0",
|
|
19
|
+
"@nan0web/changelog": "3.3.0",
|
|
20
|
+
"@nan0web/logos": "3.3.0",
|
|
21
|
+
"@nan0web/ui": "3.3.0",
|
|
22
|
+
"@nan0web/ui-cli": "3.3.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^25.6.0",
|
|
26
26
|
"husky": "^9.1.7",
|
|
27
27
|
"knip": "^5.86.0",
|
|
28
|
-
"@nan0web/test": "3.
|
|
28
|
+
"@nan0web/test": "3.3.0"
|
|
29
29
|
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"src",
|
|
33
|
+
"types",
|
|
34
|
+
"data",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
30
38
|
"scripts": {
|
|
31
39
|
"build": "tsc",
|
|
32
40
|
"clean": "rm -rf .cache/ && rm -rf dist/",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import { strictEqual } from 'node:assert'
|
|
3
|
+
import MarkdownToTest from './MarkdownToTest.js'
|
|
4
|
+
|
|
5
|
+
describe.skip('MarkdownToTest', () => {
|
|
6
|
+
it('should generate node:test code from markdown release notes', () => {
|
|
7
|
+
const parser = new MarkdownToTest()
|
|
8
|
+
const markdown = `# v1.1.0 - 2025-08-19
|
|
9
|
+
|
|
10
|
+
## CLI application
|
|
11
|
+
### **Treats project management like code: everything is versioned, auditable, reversible.** [treats-project-management-like-code-everything-is-versioned-auditable-reversible]
|
|
12
|
+
### Draft **No external tools or databases needed — just Git.** [no-external-tools-or-databases-needed-just-git]
|
|
13
|
+
### **Natural for developers; lowers barrier to entry for dev-centric teams.** [natural-for-developers-lowers-barrier-to-entry-for-dev-centric-teams]
|
|
14
|
+
|
|
15
|
+
## UI application
|
|
16
|
+
### **Drag-and-drop interface** [drag-and-drop-interface]
|
|
17
|
+
### **Visual tree structure** [visual-tree-structure]`
|
|
18
|
+
|
|
19
|
+
const expected = `import { describe, it } from "node:test"
|
|
20
|
+
import { ok, strictEqual } from "node:assert"
|
|
21
|
+
|
|
22
|
+
describe("v1.1.0 - 2025-08-19", () => {
|
|
23
|
+
describe("CLI application", () => {
|
|
24
|
+
it("Treats project management like code: everything is versioned, auditable, reversible.", () => {
|
|
25
|
+
ok(true, "Task completed successfully")
|
|
26
|
+
})
|
|
27
|
+
it.skip("No external tools or databases needed — just Git.", () => {
|
|
28
|
+
ok(false, "Draft task - not yet implemented")
|
|
29
|
+
})
|
|
30
|
+
it("Natural for developers; lowers barrier to entry for dev-centric teams.", () => {
|
|
31
|
+
ok(true, "Task completed successfully")
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
describe("UI application", () => {
|
|
35
|
+
it("Drag-and-drop interface", () => {
|
|
36
|
+
ok(true, "Task completed successfully")
|
|
37
|
+
})
|
|
38
|
+
it("Visual tree structure", () => {
|
|
39
|
+
ok(true, "Task completed successfully")
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})`
|
|
43
|
+
|
|
44
|
+
const result = parser.generateTests(markdown)
|
|
45
|
+
strictEqual(result.trim(), expected.trim())
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("should throw error if markdown doesn't have version header", () => {
|
|
49
|
+
const parser = new MarkdownToTest()
|
|
50
|
+
const markdown = `## CLI application
|
|
51
|
+
### **Treats project management like code** [treats-project-management-like-code]`
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
parser.generateTests(markdown)
|
|
55
|
+
throw new Error('Should have thrown an error')
|
|
56
|
+
} catch (error) {
|
|
57
|
+
strictEqual(error.message, 'Markdown must start with version header: # vX.Y.Z - YYYY-MM-DD')
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import Scanner from './Scanner.js'
|
|
8
|
+
|
|
9
|
+
const TMP = join(tmpdir(), 'nan0release-close-test-' + Date.now())
|
|
10
|
+
|
|
11
|
+
describe('Scanner.close()', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mkdirSync(TMP, { recursive: true })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(TMP, { recursive: true, force: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renames spec → test for a specific version', () => {
|
|
21
|
+
const dir = join(TMP, 'releases/2/0/v2.0.0')
|
|
22
|
+
mkdirSync(dir, { recursive: true })
|
|
23
|
+
writeFileSync(join(dir, 'task.spec.js'), '// contract')
|
|
24
|
+
|
|
25
|
+
const scanner = new Scanner(TMP)
|
|
26
|
+
const result = scanner.close('v2.0.0')
|
|
27
|
+
|
|
28
|
+
assert.equal(result.closed, 1)
|
|
29
|
+
assert.ok(!existsSync(join(dir, 'task.spec.js')), 'spec should be gone')
|
|
30
|
+
assert.ok(existsSync(join(dir, 'task.test.js')), 'test should exist')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('renames multiple spec files in the same version', () => {
|
|
34
|
+
const dir = join(TMP, 'releases/2/0/v2.0.0')
|
|
35
|
+
mkdirSync(dir, { recursive: true })
|
|
36
|
+
writeFileSync(join(dir, 'task.spec.js'), '// contract 1')
|
|
37
|
+
writeFileSync(join(dir, 'api.spec.js'), '// contract 2')
|
|
38
|
+
|
|
39
|
+
const scanner = new Scanner(TMP)
|
|
40
|
+
const result = scanner.close('v2.0.0')
|
|
41
|
+
|
|
42
|
+
assert.equal(result.closed, 2)
|
|
43
|
+
assert.ok(existsSync(join(dir, 'task.test.js')))
|
|
44
|
+
assert.ok(existsSync(join(dir, 'api.test.js')))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns 0 when no specs found for version', () => {
|
|
48
|
+
mkdirSync(join(TMP, 'releases/1/0/v1.0.0'), { recursive: true })
|
|
49
|
+
writeFileSync(join(TMP, 'releases/1/0/v1.0.0/task.test.js'), '// already closed')
|
|
50
|
+
|
|
51
|
+
const scanner = new Scanner(TMP)
|
|
52
|
+
const result = scanner.close('v1.0.0')
|
|
53
|
+
|
|
54
|
+
assert.equal(result.closed, 0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('throws when version not found', () => {
|
|
58
|
+
mkdirSync(join(TMP, 'releases'), { recursive: true })
|
|
59
|
+
|
|
60
|
+
const scanner = new Scanner(TMP)
|
|
61
|
+
assert.throws(() => scanner.close('v9.9.9'), /not found/i)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('works with flat structure (releases/v2.0.0/)', () => {
|
|
65
|
+
const dir = join(TMP, 'releases/v2.0.0')
|
|
66
|
+
mkdirSync(dir, { recursive: true })
|
|
67
|
+
writeFileSync(join(dir, 'task.spec.js'), '// flat')
|
|
68
|
+
|
|
69
|
+
const scanner = new Scanner(TMP)
|
|
70
|
+
const result = scanner.close('v2.0.0')
|
|
71
|
+
|
|
72
|
+
assert.equal(result.closed, 1)
|
|
73
|
+
assert.ok(existsSync(join(dir, 'task.test.js')))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('close --all renames all specs across versions', () => {
|
|
77
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
78
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
79
|
+
mkdirSync(v1, { recursive: true })
|
|
80
|
+
mkdirSync(v2, { recursive: true })
|
|
81
|
+
writeFileSync(join(v1, 'task.spec.js'), '// v1')
|
|
82
|
+
writeFileSync(join(v2, 'task.spec.js'), '// v2')
|
|
83
|
+
|
|
84
|
+
const scanner = new Scanner(TMP)
|
|
85
|
+
const result = scanner.close()
|
|
86
|
+
|
|
87
|
+
assert.equal(result.closed, 2)
|
|
88
|
+
assert.ok(existsSync(join(v1, 'task.test.js')))
|
|
89
|
+
assert.ok(existsSync(join(v2, 'task.test.js')))
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import Scanner from './Scanner.js'
|
|
8
|
+
|
|
9
|
+
const TMP = join(tmpdir(), 'nan0release-scanner-test-' + Date.now())
|
|
10
|
+
|
|
11
|
+
describe('Scanner', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mkdirSync(TMP, { recursive: true })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(TMP, { recursive: true, force: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('nested version structure', () => {
|
|
21
|
+
it('finds spec files in nested directories (releases/1/0/v1.0.0/)', () => {
|
|
22
|
+
const dir = join(TMP, 'releases/1/0/v1.0.0')
|
|
23
|
+
mkdirSync(dir, { recursive: true })
|
|
24
|
+
writeFileSync(join(dir, 'task.spec.js'), '// spec')
|
|
25
|
+
|
|
26
|
+
const scanner = new Scanner(TMP)
|
|
27
|
+
const specs = scanner.findSpecs()
|
|
28
|
+
|
|
29
|
+
assert.equal(specs.length, 1)
|
|
30
|
+
assert.equal(specs[0].version, 'v1.0.0')
|
|
31
|
+
assert.ok(specs[0].path.endsWith('task.spec.js'))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('finds test files (closed contracts)', () => {
|
|
35
|
+
const dir = join(TMP, 'releases/1/0/v1.0.0')
|
|
36
|
+
mkdirSync(dir, { recursive: true })
|
|
37
|
+
writeFileSync(join(dir, 'task.test.js'), '// test')
|
|
38
|
+
|
|
39
|
+
const scanner = new Scanner(TMP)
|
|
40
|
+
const tests = scanner.findTests()
|
|
41
|
+
|
|
42
|
+
assert.equal(tests.length, 1)
|
|
43
|
+
assert.equal(tests[0].version, 'v1.0.0')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('finds flat structure too (releases/v2.0.0/)', () => {
|
|
47
|
+
const dir = join(TMP, 'releases/v2.0.0')
|
|
48
|
+
mkdirSync(dir, { recursive: true })
|
|
49
|
+
writeFileSync(join(dir, 'task.spec.js'), '// spec')
|
|
50
|
+
|
|
51
|
+
const scanner = new Scanner(TMP)
|
|
52
|
+
const specs = scanner.findSpecs()
|
|
53
|
+
|
|
54
|
+
assert.equal(specs.length, 1)
|
|
55
|
+
assert.equal(specs[0].version, 'v2.0.0')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('multiple versions', () => {
|
|
60
|
+
it('finds all specs across multiple versions', () => {
|
|
61
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
62
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
63
|
+
mkdirSync(v1, { recursive: true })
|
|
64
|
+
mkdirSync(v2, { recursive: true })
|
|
65
|
+
writeFileSync(join(v1, 'task.spec.js'), '// v1 spec')
|
|
66
|
+
writeFileSync(join(v2, 'task.spec.js'), '// v2 spec')
|
|
67
|
+
|
|
68
|
+
const scanner = new Scanner(TMP)
|
|
69
|
+
const specs = scanner.findSpecs()
|
|
70
|
+
|
|
71
|
+
assert.equal(specs.length, 2)
|
|
72
|
+
const versions = specs.map((s) => s.version).sort()
|
|
73
|
+
assert.deepEqual(versions, ['v1.0.0', 'v2.0.0'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('separates specs from tests', () => {
|
|
77
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
78
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
79
|
+
mkdirSync(v1, { recursive: true })
|
|
80
|
+
mkdirSync(v2, { recursive: true })
|
|
81
|
+
writeFileSync(join(v1, 'task.test.js'), '// closed')
|
|
82
|
+
writeFileSync(join(v2, 'task.spec.js'), '// wip')
|
|
83
|
+
|
|
84
|
+
const scanner = new Scanner(TMP)
|
|
85
|
+
assert.equal(scanner.findSpecs().length, 1)
|
|
86
|
+
assert.equal(scanner.findTests().length, 1)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('filter by version', () => {
|
|
91
|
+
it('finds specs for a specific version', () => {
|
|
92
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
93
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
94
|
+
mkdirSync(v1, { recursive: true })
|
|
95
|
+
mkdirSync(v2, { recursive: true })
|
|
96
|
+
writeFileSync(join(v1, 'task.spec.js'), '// v1')
|
|
97
|
+
writeFileSync(join(v2, 'task.spec.js'), '// v2')
|
|
98
|
+
|
|
99
|
+
const scanner = new Scanner(TMP)
|
|
100
|
+
const specs = scanner.findSpecs('v2.0.0')
|
|
101
|
+
|
|
102
|
+
assert.equal(specs.length, 1)
|
|
103
|
+
assert.equal(specs[0].version, 'v2.0.0')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('status', () => {
|
|
108
|
+
it('returns status for all versions', () => {
|
|
109
|
+
const v1 = join(TMP, 'releases/1/0/v1.0.0')
|
|
110
|
+
const v2 = join(TMP, 'releases/2/0/v2.0.0')
|
|
111
|
+
mkdirSync(v1, { recursive: true })
|
|
112
|
+
mkdirSync(v2, { recursive: true })
|
|
113
|
+
writeFileSync(join(v1, 'task.test.js'), '// closed')
|
|
114
|
+
writeFileSync(join(v2, 'task.spec.js'), '// wip')
|
|
115
|
+
|
|
116
|
+
const scanner = new Scanner(TMP)
|
|
117
|
+
const status = scanner.status()
|
|
118
|
+
|
|
119
|
+
assert.equal(status.length, 2)
|
|
120
|
+
const v1s = status.find((s) => s.version === 'v1.0.0')
|
|
121
|
+
const v2s = status.find((s) => s.version === 'v2.0.0')
|
|
122
|
+
assert.equal(v1s.state, 'closed')
|
|
123
|
+
assert.equal(v2s.state, 'wip')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('empty releases', () => {
|
|
128
|
+
it('returns empty arrays when no releases directory', () => {
|
|
129
|
+
const scanner = new Scanner(TMP)
|
|
130
|
+
assert.deepEqual(scanner.findSpecs(), [])
|
|
131
|
+
assert.deepEqual(scanner.findTests(), [])
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import {
|
|
4
|
+
ProjectManagement,
|
|
5
|
+
ReleaseManager,
|
|
6
|
+
TaskTestSuite,
|
|
7
|
+
ChangelogTaskManager,
|
|
8
|
+
} from './ProjectManagementAsCode.js'
|
|
9
|
+
|
|
10
|
+
describe('ProjectManagementAsCode Architecture', () => {
|
|
11
|
+
describe('ProjectManagement', () => {
|
|
12
|
+
it('should initialize with default configuration', () => {
|
|
13
|
+
const pm = new ProjectManagement()
|
|
14
|
+
assert.equal(pm.projectPath, '.')
|
|
15
|
+
assert.equal(pm.autoTrack, false)
|
|
16
|
+
assert.ok(pm.tasks instanceof Map)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should register tasks with their test files', () => {
|
|
20
|
+
const pm = new ProjectManagement()
|
|
21
|
+
pm.registerTask('task-1', './tests/task1.test.js')
|
|
22
|
+
pm.registerTask('task-2', './tests/task2.test.js')
|
|
23
|
+
|
|
24
|
+
assert.equal(pm.tasks.size, 2)
|
|
25
|
+
assert.equal(pm.tasks.get('task-1'), './tests/task1.test.js')
|
|
26
|
+
assert.equal(pm.tasks.get('task-2'), './tests/task2.test.js')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should validate project state by running task tests', async () => {
|
|
30
|
+
const pm = new ProjectManagement()
|
|
31
|
+
pm.registerTask('task-1', './tests/task1.test.js')
|
|
32
|
+
pm.registerTask('task-2', './tests/task2.test.js')
|
|
33
|
+
|
|
34
|
+
const results = await pm.validateProjectState()
|
|
35
|
+
assert.ok(results.passed.length >= 0)
|
|
36
|
+
assert.ok(results.failed.length >= 0)
|
|
37
|
+
assert.ok(results.pending.length >= 0)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('ReleaseManager', () => {
|
|
42
|
+
it('should initialize with project manager', () => {
|
|
43
|
+
const pm = new ProjectManagement()
|
|
44
|
+
const rm = new ReleaseManager(pm)
|
|
45
|
+
|
|
46
|
+
assert.equal(rm.projectManager, pm)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it.skip('should validate release with version calculation', async () => {
|
|
50
|
+
const pm = new ProjectManagement()
|
|
51
|
+
const rm = new ReleaseManager(pm)
|
|
52
|
+
|
|
53
|
+
const validation = await rm.validateRelease('1.0.0')
|
|
54
|
+
assert.equal(validation.version, '1.0.0')
|
|
55
|
+
assert.ok(validation.timestamp)
|
|
56
|
+
assert.ok(validation.tasks.total >= 0)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should execute release process with validation', async () => {
|
|
60
|
+
const pm = new ProjectManagement()
|
|
61
|
+
const rm = new ReleaseManager(pm)
|
|
62
|
+
|
|
63
|
+
const result = await rm.executeRelease('patch')
|
|
64
|
+
assert.ok(result.version)
|
|
65
|
+
assert.ok(result.validation)
|
|
66
|
+
assert.equal(typeof result.published, 'boolean')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should block release when tests fail and ignore option is not set', async () => {
|
|
70
|
+
const pm = new ProjectManagement()
|
|
71
|
+
const rm = new ReleaseManager(pm)
|
|
72
|
+
|
|
73
|
+
// Mock validation to simulate failed tasks
|
|
74
|
+
rm.projectManager.validateProjectState = async () => ({
|
|
75
|
+
passed: [],
|
|
76
|
+
failed: ['task-1'],
|
|
77
|
+
pending: [],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await rm.executeRelease('patch')
|
|
82
|
+
assert.fail('Should have thrown an error')
|
|
83
|
+
} catch (error) {
|
|
84
|
+
assert.ok(error.message.includes('Release blocked'))
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('TaskTestSuite', () => {
|
|
90
|
+
it.skip('should extend TestSuite with task metadata', () => {
|
|
91
|
+
const testFunction = () => {}
|
|
92
|
+
const taskSuite = new TaskTestSuite('task-1', 'Test task description', testFunction)
|
|
93
|
+
|
|
94
|
+
assert.equal(taskSuite.taskId, 'task-1')
|
|
95
|
+
assert.equal(taskSuite.description, 'Test task description')
|
|
96
|
+
assert.equal(taskSuite.testFunction, testFunction)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('ChangelogTaskManager', () => {
|
|
101
|
+
it('should parse changelog content to extract tasks', () => {
|
|
102
|
+
const ctm = new ChangelogTaskManager()
|
|
103
|
+
const changelogContent = `
|
|
104
|
+
# Changelog
|
|
105
|
+
|
|
106
|
+
## [1.0.0] - 2024-01-01
|
|
107
|
+
### Added
|
|
108
|
+
- Initial release features
|
|
109
|
+
- Core functionality implementation
|
|
110
|
+
`
|
|
111
|
+
|
|
112
|
+
const tasks = ctm.parseChangelog(changelogContent)
|
|
113
|
+
assert.ok(Array.isArray(tasks))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should generate task tests from changelog entries', async () => {
|
|
117
|
+
const ctm = new ChangelogTaskManager()
|
|
118
|
+
const tasks = [
|
|
119
|
+
{ id: 'task-1', description: 'Implement feature A' },
|
|
120
|
+
{ id: 'task-2', description: 'Fix bug B' },
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
// This should not throw an error
|
|
124
|
+
await ctm.generateTaskTests(tasks)
|
|
125
|
+
assert.ok(true)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -63,7 +63,7 @@ export default class BumpCommand extends ModelAsApp {
|
|
|
63
63
|
t(BumpCommand.UI.detectingChangedPackages, {
|
|
64
64
|
sinceRef,
|
|
65
65
|
targetVersion,
|
|
66
|
-
})
|
|
66
|
+
})
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
const allFiles = new Set()
|
|
@@ -113,7 +113,7 @@ export default class BumpCommand extends ModelAsApp {
|
|
|
113
113
|
version: targetVersion,
|
|
114
114
|
dir: pkgDir,
|
|
115
115
|
}),
|
|
116
|
-
'info'
|
|
116
|
+
'info'
|
|
117
117
|
)
|
|
118
118
|
continue
|
|
119
119
|
}
|
|
@@ -128,7 +128,7 @@ export default class BumpCommand extends ModelAsApp {
|
|
|
128
128
|
targetVersion,
|
|
129
129
|
dir: pkgDir,
|
|
130
130
|
}),
|
|
131
|
-
this.dryRun ? 'warn' : 'success'
|
|
131
|
+
this.dryRun ? 'warn' : 'success'
|
|
132
132
|
)
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
describe('Release v3.0.0 - Dependency Alignment', () => {
|
|
8
|
+
it('should have correct versions for @nan0web packages', async () => {
|
|
9
|
+
const pkgPath = join(process.cwd(), 'package.json')
|
|
10
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
11
|
+
|
|
12
|
+
assert.ok(/^3\.1\.\d+$/.test(pkg.version), 'Version should start with 3.1')
|
|
13
|
+
assert.ok(/^(\^1\.8\.0|workspace:\*)$/.test(pkg.dependencies['@nan0web/ui']), '@nan0web/ui should be ^1.8.0 or workspace:*')
|
|
14
|
+
assert.ok(/^(\^2\.9\.0|workspace:\*)$/.test(pkg.dependencies['@nan0web/ui-cli']), '@nan0web/ui-cli should be ^2.9.0 or workspace:*')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import Markdown, { MDElement } from '@nan0web/markdown';
|
|
2
|
+
import Person from './Person.js';
|
|
2
3
|
declare class ReleaseDocument extends Markdown {
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/** @type {Person[]} */
|
|
5
|
+
team: Person[];
|
|
6
|
+
/** @type {Map<string, Person[]>} */
|
|
7
|
+
roles: Map<string, Person[]>;
|
|
8
|
+
/** @type {string} */
|
|
9
|
+
version: string;
|
|
10
|
+
/** @type {Date | undefined} */
|
|
11
|
+
date: Date | undefined;
|
|
8
12
|
/**
|
|
9
13
|
* @param {Object} options
|
|
10
14
|
* @param {Array} [options.team] - Release team members
|
|
@@ -13,19 +17,22 @@ declare class ReleaseDocument extends Markdown {
|
|
|
13
17
|
* @param {string} [options.date] - Release date
|
|
14
18
|
*/
|
|
15
19
|
constructor(options?: {
|
|
16
|
-
team?: any[]
|
|
17
|
-
roles?:
|
|
18
|
-
version?: string
|
|
19
|
-
date?: string
|
|
20
|
+
team?: any[];
|
|
21
|
+
roles?: Map<any, any> | any[];
|
|
22
|
+
version?: string;
|
|
23
|
+
date?: string;
|
|
20
24
|
});
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Parse release document content
|
|
27
|
+
* Extracts version and date from H1 heading
|
|
28
|
+
* @param {string} input - Markdown content
|
|
29
|
+
* @returns {MDElement[]}
|
|
30
|
+
*/
|
|
31
|
+
parse(input: string): MDElement[];
|
|
32
|
+
/**
|
|
33
|
+
* @param {*} input
|
|
34
|
+
* @returns {ReleaseDocument}
|
|
35
|
+
*/
|
|
36
|
+
static from(input: any): ReleaseDocument;
|
|
29
37
|
}
|
|
30
|
-
|
|
31
|
-
import Person from './Person.js';
|
|
38
|
+
export default ReleaseDocument;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export default MarkdownToTest;
|
|
2
1
|
/**
|
|
3
2
|
* MarkdownToTest - generates node:test code from markdown release notes
|
|
4
3
|
* Enables writing release notes first, then generating executable tests
|
|
@@ -45,3 +44,4 @@ declare class MarkdownToTest {
|
|
|
45
44
|
tasks: any[];
|
|
46
45
|
}>): string;
|
|
47
46
|
}
|
|
47
|
+
export default MarkdownToTest;
|