@existdb/repo-xml-changelog-generator 1.0.0 → 1.0.2
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/README.md +13 -4
- package/index.js +7 -5
- package/lib/changelog.js +69 -20
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -42,7 +42,12 @@ Add to your `.releaserc` plugin chain. Place it after the commit-analyzer / rele
|
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
The plugin works with or without `@semantic-release/git` — the choice decides where your changelog history lives:
|
|
46
|
+
|
|
47
|
+
- **Without it** (in-memory mode), the mutation happens only on the CI runner; the on-disk `repo.xml.tmpl` in your default branch stays at its development-placeholder state, and the per-release changelog history lives in git tags + GitHub Releases. Note that each built XAR then contains only the entries curated in the template plus the *current* release's generated entry — the entries generated for previous releases are not carried forward.
|
|
48
|
+
- **With it** (commit-back mode), add `@semantic-release/git` *after* this plugin with `repo.xml.tmpl` (and typically `package.json` + `package-lock.json`) as assets. Each release then commits the new `<change>` entry back to the default branch, so the changelog accumulates and every released XAR carries the full history. This is the model `eXist-db/monex` adopted by community vote ([monex#453](https://github.com/eXist-db/monex/pull/453)); it requires branch protection to allow the GitHub Actions app to push.
|
|
49
|
+
|
|
50
|
+
See [Design rationale](#design-rationale) below.
|
|
46
51
|
|
|
47
52
|
## Configuration
|
|
48
53
|
|
|
@@ -76,6 +81,8 @@ For each conventional commit since `context.lastRelease.gitTag`:
|
|
|
76
81
|
- `Revert` (for `revert:`)
|
|
77
82
|
- or the literal type for other categories.
|
|
78
83
|
- Render breaking changes as `Breaking change: <text>` from `BREAKING CHANGE:` footers.
|
|
84
|
+
- Render issue/PR references (e.g. `#398`) as XHTML `<a>` links, with URLs derived from semantic-release's `repositoryUrl`; on non-GitHub remotes they are kept as plain text.
|
|
85
|
+
- Hard-wrapped commit subjects are folded into a single line, and hard-wrapped `BREAKING CHANGE:` footers are collapsed to single-line items.
|
|
79
86
|
|
|
80
87
|
The rendered items are wrapped in a `<change version="X.Y.Z"><ul xmlns="http://www.w3.org/1999/xhtml">…</ul></change>` and inserted at the **top** of the existing `<changelog>` element (most recent first).
|
|
81
88
|
|
|
@@ -95,9 +102,11 @@ The rendered items are wrapped in a `<change version="X.Y.Z"><ul xmlns="http://w
|
|
|
95
102
|
|
|
96
103
|
The default approach for many semantic-release setups is to use `@semantic-release/git` to commit the mutated files (`package.json`, `CHANGELOG.md`, etc.) back to the default branch. For eXist-db org repos with branch protection that requires PRs, this means the GitHub Actions token can't push the release commit — you need a PAT or a GitHub App to bypass protection. That's operational overhead (PAT rotation, App installation per repo, secret management).
|
|
97
104
|
|
|
98
|
-
The alternative — used by `@existdb/xst`, `@existdb/node-exist`, `@existdb/gulp-exist`, and
|
|
105
|
+
The alternative — used by `@existdb/xst`, `@existdb/node-exist`, `@existdb/gulp-exist`, and `eXist-db/function-documentation` — is to **pin the default-branch version to `0.0.0-development` permanently**, do all the version + changelog work in-memory on the CI runner, and let the built XAR carry the real version. No push-back, no token plumbing, branch protection works as intended.
|
|
106
|
+
|
|
107
|
+
The trade-off: for npm packages the version history lives in the npm registry anyway, but for XAR packages the `<changelog>` in the shipped descriptor is itself the history — and in-memory mode drops each generated entry after its release. `eXist-db/monex` used in-memory mode from 2026-05-19 until a community vote switched it to commit-back ([monex#453](https://github.com/eXist-db/monex/pull/453)) after its 5.1.0 XAR shipped without the generated 5.0.0 entry. Branch protection there allows the GitHub Actions app to push, so no PAT or custom App is needed.
|
|
99
108
|
|
|
100
|
-
This plugin
|
|
109
|
+
This plugin supports both workflows; pick per repository.
|
|
101
110
|
|
|
102
111
|
### Why not just publish the existing script as-is?
|
|
103
112
|
|
|
@@ -114,7 +123,7 @@ semantic-release itself is a Node tool and runs in Node. The plugin API is Node-
|
|
|
114
123
|
|
|
115
124
|
## Releasing this package
|
|
116
125
|
|
|
117
|
-
This plugin releases itself with semantic-release, eating its own dog food. Merges to `main` run the [CI workflow](.github/workflows/release.yml): the test matrix runs, then `semantic-release` analyzes the Conventional Commits, computes the next version, publishes to npm (`@existdb` scope), and creates the GitHub Release. The in-tree `version` stays pinned at `0.0.0-development`; the real version is set in-memory at release time (the
|
|
126
|
+
This plugin releases itself with semantic-release, eating its own dog food. Merges to `main` run the [CI workflow](.github/workflows/release.yml): the test matrix runs, then `semantic-release` analyzes the Conventional Commits, computes the next version, publishes to npm (`@existdb` scope), and creates the GitHub Release. The in-tree `version` stays pinned at `0.0.0-development`; the real version is set in-memory at release time (the in-memory workflow described above — a natural fit here, since an npm package's version history lives in the registry).
|
|
118
127
|
|
|
119
128
|
Commit messages are linted against [Conventional Commits](https://www.conventionalcommits.org/) via commitlint — locally through a husky `commit-msg` hook, and in CI on every pull request.
|
|
120
129
|
|
package/index.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
* Reads conventional commits since `context.lastRelease.gitTag`, builds
|
|
17
17
|
* a list of human-readable change items, and inserts a new `<change
|
|
18
18
|
* version="X.Y.Z">` element with those items at the top of `<changelog>`.
|
|
19
|
-
* Writes the descriptor back to disk.
|
|
20
|
-
* CI runner
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Writes the descriptor back to disk. Whether that mutation stays
|
|
20
|
+
* in-memory on the CI runner or is committed back to the default branch
|
|
21
|
+
* (so the changelog accumulates across releases) depends on whether
|
|
22
|
+
* @semantic-release/git is in the plugin chain — both workflows are
|
|
23
|
+
* supported. See README for the trade-offs.
|
|
23
24
|
*
|
|
24
25
|
* Config options (all optional):
|
|
25
26
|
*
|
|
@@ -82,7 +83,8 @@ export async function prepare (pluginConfig, context) {
|
|
|
82
83
|
const items = await buildChangeItems({
|
|
83
84
|
prevTag: lastRelease.gitTag,
|
|
84
85
|
version: nextRelease.version,
|
|
85
|
-
cwd: context.cwd
|
|
86
|
+
cwd: context.cwd,
|
|
87
|
+
repositoryUrl: context.options?.repositoryUrl
|
|
86
88
|
})
|
|
87
89
|
|
|
88
90
|
if (items.length === 0) {
|
package/lib/changelog.js
CHANGED
|
@@ -16,6 +16,7 @@ import conventionalChangelogConventionalCommits from 'conventional-changelog-con
|
|
|
16
16
|
|
|
17
17
|
const REPO_NS = 'http://exist-db.org/xquery/repo'
|
|
18
18
|
const HTML_NS = 'http://www.w3.org/1999/xhtml'
|
|
19
|
+
const MD_LINK = /\[([^\]]+)\]\(([^)\s]+)\)/g
|
|
19
20
|
|
|
20
21
|
const SECTION_PREFIX = {
|
|
21
22
|
Features: 'New',
|
|
@@ -24,17 +25,40 @@ const SECTION_PREFIX = {
|
|
|
24
25
|
Reverts: 'Revert'
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
function escapeXml (text) {
|
|
29
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Replaces the writer's default markdown template (conventional-changelog-writer
|
|
33
|
+
// v9+ takes a template function, no longer a handlebars string).
|
|
34
|
+
function xmlTemplate (context) {
|
|
35
|
+
const lines = []
|
|
36
|
+
for (const group of context.noteGroups ?? []) {
|
|
37
|
+
for (const note of group.notes) {
|
|
38
|
+
lines.push(`<li>Breaking change: ${escapeXml(note.text)}</li>`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const group of context.commitGroups ?? []) {
|
|
42
|
+
for (const commit of group.commits) {
|
|
43
|
+
if (commit.isBreaking) continue
|
|
44
|
+
const scope = commit.scope ? `${commit.scope}: ` : ''
|
|
45
|
+
lines.push(`<li>${escapeXml(`${commit.prefix}: ${scope}${commit.subject}`)}</li>`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Derive the {host, owner, repository} link context from a repository URL
|
|
53
|
+
* (semantic-release's `context.options.repositoryUrl`). Returns null when
|
|
54
|
+
* the URL is not a recognizable GitHub remote; issue references are then
|
|
55
|
+
* left as plain text instead of rendered with broken URLs.
|
|
56
|
+
*/
|
|
57
|
+
export function githubContext (repositoryUrl) {
|
|
58
|
+
const m = (repositoryUrl ?? '').match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/)
|
|
59
|
+
if (!m) return null
|
|
60
|
+
return { host: 'https://github.com', owner: m[1], repository: m[2] }
|
|
61
|
+
}
|
|
38
62
|
|
|
39
63
|
function tagExists (tag, cwd) {
|
|
40
64
|
try {
|
|
@@ -54,12 +78,14 @@ function getRawCommits ({ prevTag, cwd }) {
|
|
|
54
78
|
const hashes = execSync(`git log ${ref}..HEAD --format=%H`, { encoding: 'utf8', cwd })
|
|
55
79
|
.trim().split('\n').filter(Boolean)
|
|
56
80
|
|
|
81
|
+
// %s folds a hard-wrapped subject paragraph into a single line; %B would
|
|
82
|
+
// keep the line break and the parser would truncate the subject there.
|
|
57
83
|
return hashes.map(hash =>
|
|
58
|
-
execSync(`git log -1 --format=%
|
|
84
|
+
execSync(`git log -1 --format=%s%n%n%b ${hash}`, { encoding: 'utf8', cwd }).trim()
|
|
59
85
|
)
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
export async function buildChangeItems ({ prevTag, version, cwd }) {
|
|
88
|
+
export async function buildChangeItems ({ prevTag, version, cwd, repositoryUrl }) {
|
|
63
89
|
const rawCommits = getRawCommits({ prevTag, cwd })
|
|
64
90
|
if (rawCommits.length === 0) return []
|
|
65
91
|
|
|
@@ -72,10 +98,7 @@ export async function buildChangeItems ({ prevTag, version, cwd }) {
|
|
|
72
98
|
const presetTransform = writerOpts.transform
|
|
73
99
|
const writerOptions = {
|
|
74
100
|
...writerOpts,
|
|
75
|
-
|
|
76
|
-
headerPartial: '',
|
|
77
|
-
commitPartial: '',
|
|
78
|
-
footerPartial: '',
|
|
101
|
+
template: xmlTemplate,
|
|
79
102
|
transform (commit, context) {
|
|
80
103
|
const transformed = presetTransform(commit, context)
|
|
81
104
|
if (!transformed) return null
|
|
@@ -88,7 +111,8 @@ export async function buildChangeItems ({ prevTag, version, cwd }) {
|
|
|
88
111
|
}
|
|
89
112
|
}
|
|
90
113
|
|
|
91
|
-
const
|
|
114
|
+
const linkContext = githubContext(repositoryUrl)
|
|
115
|
+
const output = await writeChangelogString(parsed, { version, ...linkContext }, writerOptions)
|
|
92
116
|
|
|
93
117
|
const doc = new DOMParser().parseFromString(
|
|
94
118
|
`<ul xmlns="${HTML_NS}">${output}</ul>`,
|
|
@@ -97,12 +121,37 @@ export async function buildChangeItems ({ prevTag, version, cwd }) {
|
|
|
97
121
|
const liNodes = doc.getElementsByTagNameNS(HTML_NS, 'li')
|
|
98
122
|
const items = []
|
|
99
123
|
for (let i = 0; i < liNodes.length; i++) {
|
|
100
|
-
|
|
124
|
+
// Collapse line breaks left over from hard-wrapped BREAKING CHANGE footers.
|
|
125
|
+
let text = liNodes.item(i).textContent.replace(/\s+/g, ' ').trim()
|
|
126
|
+
if (!linkContext) {
|
|
127
|
+
// Without a link context the preset renders references as markdown
|
|
128
|
+
// links with `undefined` URLs — reduce them to their label text.
|
|
129
|
+
text = text.replace(MD_LINK, '$1')
|
|
130
|
+
}
|
|
101
131
|
if (text) items.push(text)
|
|
102
132
|
}
|
|
103
133
|
return items
|
|
104
134
|
}
|
|
105
135
|
|
|
136
|
+
// Item text may contain markdown links produced by the conventional-changelog
|
|
137
|
+
// preset (issue refs, mentions); render them as XHTML anchors.
|
|
138
|
+
function appendItemContent (doc, li, text) {
|
|
139
|
+
let last = 0
|
|
140
|
+
for (const match of text.matchAll(MD_LINK)) {
|
|
141
|
+
if (match.index > last) {
|
|
142
|
+
li.appendChild(doc.createTextNode(text.slice(last, match.index)))
|
|
143
|
+
}
|
|
144
|
+
const a = doc.createElementNS(HTML_NS, 'a')
|
|
145
|
+
a.setAttribute('href', match[2])
|
|
146
|
+
a.appendChild(doc.createTextNode(match[1]))
|
|
147
|
+
li.appendChild(a)
|
|
148
|
+
last = match.index + match[0].length
|
|
149
|
+
}
|
|
150
|
+
if (last < text.length) {
|
|
151
|
+
li.appendChild(doc.createTextNode(text.slice(last)))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
106
155
|
export function insertChangeEntry ({ tmplPath, version, items }) {
|
|
107
156
|
const tmpl = readFileSync(tmplPath, 'utf8')
|
|
108
157
|
const doc = new DOMParser().parseFromString(tmpl, 'text/xml')
|
|
@@ -118,7 +167,7 @@ export function insertChangeEntry ({ tmplPath, version, items }) {
|
|
|
118
167
|
for (const item of items) {
|
|
119
168
|
ul.appendChild(doc.createTextNode('\n '))
|
|
120
169
|
const li = doc.createElementNS(HTML_NS, 'li')
|
|
121
|
-
li
|
|
170
|
+
appendItemContent(doc, li, item)
|
|
122
171
|
ul.appendChild(li)
|
|
123
172
|
}
|
|
124
173
|
ul.appendChild(doc.createTextNode('\n '))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@existdb/repo-xml-changelog-generator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "semantic-release plugin: inject conventional-commit changelog entries into EXPath package repo.xml descriptors (eXist-db / BaseX / MarkLogic XAR packages)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"semantic-release": ">=22"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@xmldom/xmldom": "^0.9.
|
|
49
|
-
"conventional-changelog-conventionalcommits": "^9.
|
|
50
|
-
"conventional-changelog-writer": "^
|
|
48
|
+
"@xmldom/xmldom": "^0.9.12",
|
|
49
|
+
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
50
|
+
"conventional-changelog-writer": "^9.0.0",
|
|
51
51
|
"conventional-commits-parser": "^6.0.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|