happyskills 0.31.0 → 0.32.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 +15 -0
- package/package.json +1 -1
- package/src/commands/check.js +28 -20
- package/src/commands/update.js +4 -5
- package/src/commands/validate.js +4 -1
- package/src/lock/reader.js +8 -2
- package/src/lock/reader.test.js +37 -1
- package/src/validation/changelog_rules.js +64 -0
- package/src/validation/changelog_rules.test.js +90 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.32.0] - 2026-04-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add changelog version validation to `validate` and `publish` — verifies that the top `## [x.y.z]` entry in CHANGELOG.md matches the `skill.json` version before publishing, preventing stale or missing changelog releases
|
|
14
|
+
|
|
15
|
+
## [0.31.1] - 2026-04-07
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fix `check` and `update --all` ignoring kit dependencies and transitive dependencies — previously only root-level skills were checked, hiding available updates for skills installed via kits or as dependencies of other skills
|
|
19
|
+
- Fix pre-existing test failure in lock reader caused by lazy `require` of `print_warn` breaking under Jest environment teardown
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Add `via` field to `check` and `update` JSON output showing which parent skill or kit pulled in each dependency (`null` for directly installed skills)
|
|
23
|
+
- Add "Via" column to `check` table output (only shown when dependencies are present)
|
|
24
|
+
|
|
10
25
|
## [0.31.0] - 2026-04-06
|
|
11
26
|
|
|
12
27
|
### Added
|
package/package.json
CHANGED
package/src/commands/check.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
|
-
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
2
|
+
const { read_lock, get_all_locked_skills, get_via } = require('../lock/reader')
|
|
3
3
|
const repos_api = require('../api/repos')
|
|
4
4
|
const { print_help, print_table, print_json, print_info, print_success, print_warn, print_hint, code } = require('../ui/output')
|
|
5
5
|
const { green, yellow, red } = require('../ui/colors')
|
|
@@ -44,15 +44,15 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
44
44
|
|
|
45
45
|
const target_skill = args._[0]
|
|
46
46
|
const to_check = target_skill
|
|
47
|
-
? entries.filter(([name]) => name === target_skill)
|
|
48
|
-
: entries.filter(([, data]) => data
|
|
47
|
+
? entries.filter(([name, data]) => name === target_skill && data !== null)
|
|
48
|
+
: entries.filter(([, data]) => data !== null)
|
|
49
49
|
|
|
50
50
|
if (to_check.length === 0) {
|
|
51
51
|
if (args.flags.json) {
|
|
52
52
|
print_json({ data: { results: [], outdated_count: 0, up_to_date_count: 0 } })
|
|
53
53
|
return
|
|
54
54
|
}
|
|
55
|
-
print_info(target_skill ? `${target_skill} is not installed.` : 'No
|
|
55
|
+
print_info(target_skill ? `${target_skill} is not installed.` : 'No installed skills found.')
|
|
56
56
|
return
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -62,25 +62,26 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
62
62
|
const results = []
|
|
63
63
|
if (batch_err) {
|
|
64
64
|
for (const [name, data] of to_check) {
|
|
65
|
-
results.push({ skill: name, installed: data.version, latest: 'error', status: 'error' })
|
|
65
|
+
results.push({ skill: name, installed: data.version, latest: 'error', status: 'error', via: get_via(data) })
|
|
66
66
|
}
|
|
67
67
|
} else {
|
|
68
68
|
for (const [name, data] of to_check) {
|
|
69
69
|
const info = batch_data?.results?.[name]
|
|
70
|
+
const via = get_via(data)
|
|
70
71
|
const has_conflicts = (data.conflict_files || []).length > 0
|
|
71
72
|
if (has_conflicts) {
|
|
72
|
-
results.push({ skill: name, installed: data.version, latest: info?.latest_version || '-', status: 'conflicts' })
|
|
73
|
+
results.push({ skill: name, installed: data.version, latest: info?.latest_version || '-', status: 'conflicts', via })
|
|
73
74
|
} else if (info?.access_denied) {
|
|
74
|
-
results.push({ skill: name, installed: data.version, latest: '-', status: 'no-access' })
|
|
75
|
+
results.push({ skill: name, installed: data.version, latest: '-', status: 'no-access', via })
|
|
75
76
|
} else if (!info || !info.latest_version) {
|
|
76
|
-
results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
|
|
77
|
+
results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown', via })
|
|
77
78
|
} else if (data.base_commit && info.commit && data.base_commit !== info.commit) {
|
|
78
|
-
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
79
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated', via })
|
|
79
80
|
} else if (!data.base_commit && info.latest_version !== data.version) {
|
|
80
81
|
// Fallback to version comparison for old lock files without base_commit
|
|
81
|
-
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
82
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated', via })
|
|
82
83
|
} else {
|
|
83
|
-
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
84
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date', via })
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
@@ -102,14 +103,21 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
102
103
|
'unknown': (s) => s
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
const has_via = results.some(r => r.via)
|
|
107
|
+
const rows = results.map(r => {
|
|
108
|
+
const row = [
|
|
109
|
+
r.skill,
|
|
110
|
+
r.installed,
|
|
111
|
+
r.latest,
|
|
112
|
+
(status_colors[r.status] || ((s) => s))(r.status)
|
|
113
|
+
]
|
|
114
|
+
if (has_via) row.push(r.via || '-')
|
|
115
|
+
return row
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const headers = ['Skill', 'Installed', 'Latest', 'Status']
|
|
119
|
+
if (has_via) headers.push('Via')
|
|
120
|
+
print_table(headers, rows)
|
|
113
121
|
|
|
114
122
|
const outdated = results.filter(r => r.status === 'outdated')
|
|
115
123
|
const conflicts = results.filter(r => r.status === 'conflicts')
|
|
@@ -121,7 +129,7 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
121
129
|
}
|
|
122
130
|
if (outdated.length > 0) {
|
|
123
131
|
console.log()
|
|
124
|
-
print_info(`Run ${code('happyskills update')} to upgrade ${outdated.length} skill(s).`)
|
|
132
|
+
print_info(`Run ${code('happyskills update --all')} to upgrade ${outdated.length} skill(s).`)
|
|
125
133
|
} else if (conflicts.length === 0 && results.every(r => r.status === 'up-to-date')) {
|
|
126
134
|
console.log()
|
|
127
135
|
print_success('All skills are up to date.')
|
package/src/commands/update.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
1
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
3
2
|
const { install } = require('../engine/installer')
|
|
4
|
-
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
3
|
+
const { read_lock, get_all_locked_skills, get_via } = require('../lock/reader')
|
|
5
4
|
const { detect_status } = require('../merge/detector')
|
|
6
5
|
const { print_help, print_success, print_info, print_warn, print_json } = require('../ui/output')
|
|
7
6
|
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
@@ -48,7 +47,7 @@ const run = (args) => catch_errors('Update failed', async () => {
|
|
|
48
47
|
|
|
49
48
|
const to_update = target_skill
|
|
50
49
|
? [[target_skill, skills[target_skill]]]
|
|
51
|
-
: Object.entries(skills).filter(([, data]) => data
|
|
50
|
+
: Object.entries(skills).filter(([, data]) => data !== null)
|
|
52
51
|
|
|
53
52
|
if (to_update.length === 0) {
|
|
54
53
|
if (args.flags.json) {
|
|
@@ -96,9 +95,9 @@ const run = (args) => catch_errors('Update failed', async () => {
|
|
|
96
95
|
const [errors, result] = await install(name, options)
|
|
97
96
|
if (errors) throw e(`Update ${name} failed`, errors)
|
|
98
97
|
if (!result.no_op) {
|
|
99
|
-
updated.push({ skill: name, from: before_version, to: result.version })
|
|
98
|
+
updated.push({ skill: name, from: before_version, to: result.version, via: get_via(data) })
|
|
100
99
|
} else {
|
|
101
|
-
already_up_to_date.push({ skill: name, version: result.version })
|
|
100
|
+
already_up_to_date.push({ skill: name, version: result.version, via: get_via(data) })
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
|
package/src/commands/validate.js
CHANGED
|
@@ -5,6 +5,7 @@ const { validate_skill_json } = require('../validation/skill_json_rules')
|
|
|
5
5
|
const { validate_cross } = require('../validation/cross_rules')
|
|
6
6
|
const { validate_no_conflict_markers } = require('../validation/conflict_marker_rules')
|
|
7
7
|
const { validate_file_sizes } = require('../validation/file_size_rules')
|
|
8
|
+
const { validate_changelog_version } = require('../validation/changelog_rules')
|
|
8
9
|
const { file_exists, read_json } = require('../utils/fs')
|
|
9
10
|
const { skills_dir, find_project_root } = require('../config/paths')
|
|
10
11
|
const { print_help, print_json } = require('../ui/output')
|
|
@@ -150,8 +151,10 @@ const run = (args) => catch_errors('Validate failed', async () => {
|
|
|
150
151
|
if (marker_err) throw marker_err
|
|
151
152
|
const [size_err, size_results] = await validate_file_sizes(skill_dir)
|
|
152
153
|
if (size_err) throw size_err
|
|
154
|
+
const [cl_err, cl_results] = await validate_changelog_version(skill_dir, json_data.manifest)
|
|
155
|
+
if (cl_err) throw cl_err
|
|
153
156
|
|
|
154
|
-
const all_results = [...md_data.results, ...json_data.results, ...cross_results, ...marker_results, ...size_results]
|
|
157
|
+
const all_results = [...md_data.results, ...json_data.results, ...cross_results, ...marker_results, ...size_results, ...cl_results]
|
|
155
158
|
const type_label = is_kit ? ' [kit]' : ''
|
|
156
159
|
|
|
157
160
|
if (args.flags.json) {
|
package/src/lock/reader.js
CHANGED
|
@@ -2,6 +2,7 @@ const { error: { catch_errors } } = require('puffy-core')
|
|
|
2
2
|
const { read_json } = require('../utils/fs')
|
|
3
3
|
const { lock_file_path } = require('../config/paths')
|
|
4
4
|
const { LOCK_VERSION } = require('../constants')
|
|
5
|
+
const { print_warn } = require('../ui/output')
|
|
5
6
|
|
|
6
7
|
const read_lock = (project_root) => catch_errors('Failed to read lock file', async () => {
|
|
7
8
|
const lock_path = lock_file_path(project_root)
|
|
@@ -9,7 +10,6 @@ const read_lock = (project_root) => catch_errors('Failed to read lock file', asy
|
|
|
9
10
|
if (errors) return null
|
|
10
11
|
|
|
11
12
|
if (data.lockVersion !== LOCK_VERSION) {
|
|
12
|
-
const { print_warn } = require('../ui/output')
|
|
13
13
|
print_warn(`Lock file version mismatch (found ${data.lockVersion}, expected ${LOCK_VERSION}). Consider running with --fresh.`)
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -26,4 +26,10 @@ const get_all_locked_skills = (lock_data) => {
|
|
|
26
26
|
return lock_data.skills
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const get_via = (data) => {
|
|
30
|
+
const requester = data?.requested_by?.[0]
|
|
31
|
+
if (!requester || requester === '__root__') return null
|
|
32
|
+
return requester
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { read_lock, get_locked_skill, get_all_locked_skills, get_via }
|
package/src/lock/reader.test.js
CHANGED
|
@@ -5,7 +5,7 @@ const os = require('os')
|
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const fs = require('fs')
|
|
7
7
|
|
|
8
|
-
const { read_lock, get_locked_skill, get_all_locked_skills } = require('./reader')
|
|
8
|
+
const { read_lock, get_locked_skill, get_all_locked_skills, get_via } = require('./reader')
|
|
9
9
|
const { LOCK_VERSION } = require('../constants')
|
|
10
10
|
|
|
11
11
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -126,3 +126,39 @@ describe('get_all_locked_skills', () => {
|
|
|
126
126
|
assert.deepEqual(get_all_locked_skills({ skills: {} }), {})
|
|
127
127
|
})
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
// ─── get_via ────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
describe('get_via', () => {
|
|
133
|
+
it('returns null for a directly installed skill (__root__)', () => {
|
|
134
|
+
assert.strictEqual(get_via({ requested_by: ['__root__'] }), null)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('returns the parent skill name for a dependency', () => {
|
|
138
|
+
assert.strictEqual(get_via({ requested_by: ['acme/deploy-aws'] }), 'acme/deploy-aws')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('returns the kit name for a kit dependency', () => {
|
|
142
|
+
assert.strictEqual(get_via({ requested_by: ['acme/_kit-react'] }), 'acme/_kit-react')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('returns the first requester when multiple exist', () => {
|
|
146
|
+
assert.strictEqual(get_via({ requested_by: ['acme/foo', 'acme/bar'] }), 'acme/foo')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('returns null when requested_by is empty', () => {
|
|
150
|
+
assert.strictEqual(get_via({ requested_by: [] }), null)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('returns null when requested_by is missing', () => {
|
|
154
|
+
assert.strictEqual(get_via({}), null)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('returns null when data is null', () => {
|
|
158
|
+
assert.strictEqual(get_via(null), null)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('returns null when data is undefined', () => {
|
|
162
|
+
assert.strictEqual(get_via(undefined), null)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
4
|
+
|
|
5
|
+
const CHANGELOG = 'CHANGELOG.md'
|
|
6
|
+
const VERSION_HEADING_RE = /^##\s+\[(\d+\.\d+\.\d+[^\]]*)\]/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates that the CHANGELOG.md top version entry matches skill.json version.
|
|
10
|
+
* Only runs when both CHANGELOG.md and skill.json exist — skills without a
|
|
11
|
+
* changelog are not penalised.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} skill_dir - Absolute path to the skill directory
|
|
14
|
+
* @param {object|null} manifest - Parsed skill.json (may be null if missing)
|
|
15
|
+
* @returns {[errors, results[]]}
|
|
16
|
+
*/
|
|
17
|
+
const validate_changelog_version = (skill_dir, manifest) => catch_errors('Failed to validate changelog', async () => {
|
|
18
|
+
const changelog_path = path.join(skill_dir, CHANGELOG)
|
|
19
|
+
let content
|
|
20
|
+
try { content = await fs.promises.readFile(changelog_path, 'utf-8') } catch { return [] }
|
|
21
|
+
|
|
22
|
+
// No manifest means skill.json is missing — other rules already flag that
|
|
23
|
+
if (!manifest || !manifest.version) return []
|
|
24
|
+
|
|
25
|
+
const lines = content.split('\n')
|
|
26
|
+
let first_version = null
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const m = line.match(VERSION_HEADING_RE)
|
|
29
|
+
if (m) {
|
|
30
|
+
first_version = m[1]
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!first_version) {
|
|
36
|
+
return [{
|
|
37
|
+
file: CHANGELOG,
|
|
38
|
+
field: null,
|
|
39
|
+
rule: 'changelog_has_version',
|
|
40
|
+
severity: 'warning',
|
|
41
|
+
message: 'CHANGELOG.md exists but has no version entry (expected ## [x.y.z])'
|
|
42
|
+
}]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (first_version !== manifest.version) {
|
|
46
|
+
return [{
|
|
47
|
+
file: CHANGELOG,
|
|
48
|
+
field: 'version',
|
|
49
|
+
rule: 'changelog_version_match',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
message: `CHANGELOG.md top version [${first_version}] does not match skill.json version ${manifest.version}`
|
|
52
|
+
}]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [{
|
|
56
|
+
file: CHANGELOG,
|
|
57
|
+
field: null,
|
|
58
|
+
rule: 'changelog_version_match',
|
|
59
|
+
severity: 'pass',
|
|
60
|
+
message: `CHANGELOG.md version matches skill.json (${manifest.version})`
|
|
61
|
+
}]
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
module.exports = { validate_changelog_version }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const { validate_changelog_version } = require('./changelog_rules')
|
|
7
|
+
|
|
8
|
+
const make_temp_dir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'changelog-test-'))
|
|
9
|
+
const clean = (dir) => fs.rmSync(dir, { recursive: true, force: true })
|
|
10
|
+
|
|
11
|
+
describe('validate_changelog_version', () => {
|
|
12
|
+
let dir
|
|
13
|
+
|
|
14
|
+
beforeEach(() => { dir = make_temp_dir() })
|
|
15
|
+
afterEach(() => { clean(dir) })
|
|
16
|
+
|
|
17
|
+
it('returns empty when no CHANGELOG.md exists', async () => {
|
|
18
|
+
const [err, results] = await validate_changelog_version(dir, { version: '1.0.0' })
|
|
19
|
+
assert.strictEqual(err, null)
|
|
20
|
+
assert.strictEqual(results.length, 0)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns empty when manifest is null', async () => {
|
|
24
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '## [1.0.0] - 2026-01-01\n')
|
|
25
|
+
const [err, results] = await validate_changelog_version(dir, null)
|
|
26
|
+
assert.strictEqual(err, null)
|
|
27
|
+
assert.strictEqual(results.length, 0)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns empty when manifest has no version', async () => {
|
|
31
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '## [1.0.0] - 2026-01-01\n')
|
|
32
|
+
const [err, results] = await validate_changelog_version(dir, {})
|
|
33
|
+
assert.strictEqual(err, null)
|
|
34
|
+
assert.strictEqual(results.length, 0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns pass when versions match', async () => {
|
|
38
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '# Changelog\n\n## [2.1.0] - 2026-04-07\n\n### Added\n- Something\n')
|
|
39
|
+
const [err, results] = await validate_changelog_version(dir, { version: '2.1.0' })
|
|
40
|
+
assert.strictEqual(err, null)
|
|
41
|
+
assert.strictEqual(results.length, 1)
|
|
42
|
+
assert.strictEqual(results[0].severity, 'pass')
|
|
43
|
+
assert.strictEqual(results[0].rule, 'changelog_version_match')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns error when versions mismatch', async () => {
|
|
47
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '# Changelog\n\n## [1.0.0] - 2026-01-01\n')
|
|
48
|
+
const [err, results] = await validate_changelog_version(dir, { version: '1.1.0' })
|
|
49
|
+
assert.strictEqual(err, null)
|
|
50
|
+
assert.strictEqual(results.length, 1)
|
|
51
|
+
assert.strictEqual(results[0].severity, 'error')
|
|
52
|
+
assert.strictEqual(results[0].rule, 'changelog_version_match')
|
|
53
|
+
assert.ok(results[0].message.includes('[1.0.0]'))
|
|
54
|
+
assert.ok(results[0].message.includes('1.1.0'))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('uses the first version heading, not later ones', async () => {
|
|
58
|
+
const content = '# Changelog\n\n## [2.0.0] - 2026-04-07\n\n## [1.0.0] - 2026-01-01\n'
|
|
59
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), content)
|
|
60
|
+
const [err, results] = await validate_changelog_version(dir, { version: '2.0.0' })
|
|
61
|
+
assert.strictEqual(err, null)
|
|
62
|
+
assert.strictEqual(results[0].severity, 'pass')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('skips [Unreleased] heading and finds the first real version', async () => {
|
|
66
|
+
const content = '# Changelog\n\n## [Unreleased]\n\n## [1.5.0] - 2026-03-01\n'
|
|
67
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), content)
|
|
68
|
+
const [err, results] = await validate_changelog_version(dir, { version: '1.5.0' })
|
|
69
|
+
assert.strictEqual(err, null)
|
|
70
|
+
assert.strictEqual(results[0].severity, 'pass')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('returns warning when changelog has no version entries', async () => {
|
|
74
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '# Changelog\n\nNothing here yet.\n')
|
|
75
|
+
const [err, results] = await validate_changelog_version(dir, { version: '1.0.0' })
|
|
76
|
+
assert.strictEqual(err, null)
|
|
77
|
+
assert.strictEqual(results.length, 1)
|
|
78
|
+
assert.strictEqual(results[0].severity, 'warning')
|
|
79
|
+
assert.strictEqual(results[0].rule, 'changelog_has_version')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('handles changelog with only [Unreleased] heading', async () => {
|
|
83
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '# Changelog\n\n## [Unreleased]\n\n- WIP stuff\n')
|
|
84
|
+
const [err, results] = await validate_changelog_version(dir, { version: '1.0.0' })
|
|
85
|
+
assert.strictEqual(err, null)
|
|
86
|
+
assert.strictEqual(results.length, 1)
|
|
87
|
+
assert.strictEqual(results[0].severity, 'warning')
|
|
88
|
+
assert.strictEqual(results[0].rule, 'changelog_has_version')
|
|
89
|
+
})
|
|
90
|
+
})
|