forked from bookorbit/bookorbit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrelease.config.js
More file actions
229 lines (216 loc) · 7.52 KB
/
Copy pathrelease.config.js
File metadata and controls
229 lines (216 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"use strict";
const { spawnSync } = require("child_process");
const DOCKER_IMAGE = "ghcr.io/bookorbit/bookorbit";
const TYPES = [
{ type: "feat", section: "Features" },
{ type: "fix", section: "Bug Fixes" },
{ type: "security", section: "Security" },
{ type: "perf", section: "Performance" },
{ type: "db", section: "Database" },
{ type: "style", section: "Visual Changes" },
];
// Uses {{commitUrl}} (pre-computed per-commit in finalizeContext) instead of
// {{commitUrlFormat}} which does not resolve inside Handlebars partials.
const commitPartial = `\
*{{#if scope}} **{{scope}}:**
{{~/if}} {{#if subject}}
{{~subject}}
{{~else}}
{{~header}}
{{~/if}}
{{~!-- commit link --}}{{~#if hash}} {{#if commitUrl~}}
([{{shortHash}}]({{commitUrl}}))
{{~else}}
{{~shortHash}}
{{~/if}}{{~/if}}
{{~#if githubUser}} by @{{githubUser}}{{/if}}
{{~!-- commit references --}}
{{~#if references~}}
, closes
{{~#each references}} {{#if @root.linkReferences~}}
[
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}{{this.prefix}}{{this.issue}}]({{@root.host}}/{{@root.owner}}/{{@root.repository}}/issues/{{this.issue}})
{{~else}}
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}{{this.prefix}}{{this.issue}}
{{~/if}}{{/each}}
{{~/if}}
`;
// Main template: removes the version header (GitHub Release already shows it),
// strips the warning emoji from breaking-changes headings, and appends a
// Docker pull block after the commit groups.
const mainTemplate = [
"## Highlights",
"",
"<!-- Author: write 1 line per user-facing highlight ABOVE this comment, then delete this block.",
" Full guide: docs/whats-new-authoring-guide.md",
" Format: - **Title** - short user benefit {icon comment}",
" - Optional icon: end the line with an HTML comment GitHub hides, using a PascalCase",
" lucide.dev name, e.g. BookHeart. Omit it for the default icon. Literal syntax in the guide.",
" - Image/video: drag the file in; leave GitHub's inserted <img> tag or bare URL as-is.",
" Example: - **Kobo highlight sync** - your highlights now sync both ways. {icon: BookHeart}",
" Validate before publishing: cd server && pnpm whats-new:check <tag>",
" Leaving this comment in place (no bullets) means no What's New popup for this release.",
"{{{highlightCandidatesBlock}}}-->",
"",
"{{#if noteGroups}}",
"{{#each noteGroups}}",
"",
"### {{title}}",
"",
"{{#each notes}}",
"* {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}}",
"{{/each}}",
"{{/each}}",
"{{/if}}",
"{{#each commitGroups}}",
"",
"{{#if title}}",
"### {{title}}",
"",
"{{/if}}",
"{{#each commits}}",
"{{> commit root=@root}}",
"",
"{{/each}}",
"",
"{{/each}}",
"---",
"",
"**Docker**",
"",
"```bash",
`docker pull ${DOCKER_IMAGE}:{{version}}`,
"```",
"",
"Multi-arch: `linux/amd64` and `linux/arm64`.",
].join("\n");
// Runs after the preset transform. Looks up each commit's author via git log,
// maps git name -> GitHub username, and injects githubUser into each commit
// for the commitPartial.
function finalizeContext(ctx) {
try {
const shaToGithubUser = new Map();
try {
const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
if (ghToken && ctx.owner && ctx.repository) {
const curlResult = spawnSync(
"curl",
[
"-s",
"-H", `Authorization: Bearer ${ghToken}`,
"-H", "X-GitHub-Api-Version: 2022-11-28",
`https://api.github.com/repos/${ctx.owner}/${ctx.repository}/commits?per_page=100`
],
{ encoding: "utf8" }
);
if (curlResult.status === 0) {
const apiCommits = JSON.parse(curlResult.stdout);
if (Array.isArray(apiCommits)) {
for (const c of apiCommits) {
if (c.sha && c.author && c.author.login) {
shaToGithubUser.set(c.sha, c.author.login);
}
}
}
}
}
} catch (e) {
// Ignore API errors, fall back to local git log
}
const result = spawnSync("git", ["log", "--format=%H %aN", "--max-count=500"], {
encoding: "utf8",
cwd: process.cwd(),
});
if (result.status === 0) {
for (const line of result.stdout.split("\n").filter(Boolean)) {
const spaceIdx = line.indexOf(" ");
const hash = line.substring(0, spaceIdx);
const authorName = line.substring(spaceIdx + 1).trim();
if (!authorName || /\[bot\]/i.test(authorName)) continue;
if (!shaToGithubUser.has(hash)) {
shaToGithubUser.set(hash, authorName);
}
}
}
for (const group of ctx.commitGroups ?? []) {
for (const commit of group.commits ?? []) {
if (commit.hash) {
const githubUser = shaToGithubUser.get(commit.hash);
if (githubUser) commit.githubUser = githubUser;
}
// Pre-compute commit URL: {{commitUrlFormat}} does not resolve inside
// Handlebars partials because partials don't walk the parent context chain.
if (commit.hash && ctx.host && ctx.owner && ctx.repository) {
commit.commitUrl = `${ctx.host}/${ctx.owner}/${ctx.repository}/commit/${commit.hash}`;
}
}
}
// Collect this release's feat commits as candidate highlights for the author
// to curate. Rendered inside the scaffold's HTML comment (never shown publicly)
// as a pre-built string, so the comment never gains an inner "-->".
const candidates = [];
for (const group of ctx.commitGroups ?? []) {
if (group.title !== "Features") continue;
for (const commit of group.commits ?? []) {
const text = (commit.subject || commit.header || "").trim();
if (!text || text.includes("-->")) continue;
candidates.push(text.charAt(0).toUpperCase() + text.slice(1));
if (candidates.length >= 8) break;
}
}
ctx.highlightCandidatesBlock = candidates.length
? "\n Candidates from this release (curate the good ones into real bullets above, then delete):\n" +
candidates.map((c) => ` - **${c}**`).join("\n") +
"\n"
: "";
} catch {
// non-fatal: commits render without author attribution / candidates
}
return ctx;
}
module.exports = {
branches: ["main"],
repositoryUrl: "https://github.com/bookorbit/bookorbit",
tagFormat: "v${version}",
plugins: [
[
"@semantic-release/commit-analyzer",
{
preset: "conventionalcommits",
releaseRules: [
{ type: "feat", release: "minor" },
{ type: "fix", release: "patch" },
{ type: "perf", release: "patch" },
{ type: "security", release: "patch" },
{ type: "db", release: "patch" },
{ type: "style", release: "patch" },
{ type: "revert", release: false },
],
},
],
[
"@semantic-release/release-notes-generator",
{
preset: "conventionalcommits",
linkReferences: true,
presetConfig: { types: TYPES },
writerOpts: {
commitPartial,
mainTemplate,
finalizeContext,
},
},
],
// Create the GitHub release as a DRAFT so the maintainer can author the
// "## Highlights" section (see docs/whats-new-authoring-guide.md) before it
// goes public, then click "Publish release". The git tag and Docker images
// are still produced; BookOrbit hides draft releases until they are published.
["@semantic-release/github", { draftRelease: true }],
],
};