@itcase/config 1.7.5 → 1.7.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -57,6 +57,7 @@
57
57
  "@rollup/plugin-terser": "^1.0.0",
58
58
  "@rollup/plugin-typescript": "^12.3.0",
59
59
  "@semantic-release/changelog": "^7.0.0",
60
+ "@semantic-release/exec": "^7.1.0",
60
61
  "@semantic-release/git": "^11.0.1",
61
62
  "@semantic-release/release-notes-generator": "14.1.1",
62
63
  "@svgr/plugin-jsx": "^8.1.0",
@@ -1,5 +1,8 @@
1
1
  /* eslint-disable perfectionist/sort-objects */
2
2
 
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
3
6
  import {
4
7
  CHANGELOG_FILE,
5
8
  COMMITS_SORT,
@@ -9,6 +12,20 @@ import {
9
12
  releaseRules,
10
13
  } from './config.js'
11
14
 
15
+ const issuesReleaseNotesScript = join(
16
+ dirname(fileURLToPath(import.meta.url)),
17
+ 'issuesReleaseNotes.js',
18
+ )
19
+
20
+ // Ссылки на задачи из PR. Репозиторий задач и токен приходят из CI:
21
+ // RELEASE_NOTES_ISSUES_REPO, RELEASE_NOTES_GH_TOKEN.
22
+ const issuesReleaseNotesPlugin = [
23
+ '@semantic-release/exec',
24
+ {
25
+ generateNotesCmd: `node ${JSON.stringify(issuesReleaseNotesScript)} "\${lastRelease.gitTag}" "\${nextRelease.gitTag}"`,
26
+ },
27
+ ]
28
+
12
29
  const releaseNotesGeneratorConfig = {
13
30
  preset: PRESET,
14
31
  parserOpts: { noteKeywords: releaseNoteKeywords },
@@ -70,7 +87,7 @@ const commonPlugins = {
70
87
  * @param {Object|null} options.npmConfig - Конфигурация для @semantic-release/npm. Если null, плагин не добавляется
71
88
  * @param {Object} options.gitConfig - Конфигурация для @semantic-release/git
72
89
  * @param {Object} [options.githubConfig] - Доп. опции для @semantic-release/github (например assets)
73
- * @param {Array} options.additionalPlugins - Дополнительные плагины перед changelog
90
+ * @param {Array} options.additionalPlugins - Дополнительные плагины перед генератором коммитов: их generateNotes идёт первым
74
91
  * @param {Array} [options.preparePlugins] - Плагины после npm и перед git (prepare-шаг)
75
92
  * @param {boolean} options.skipReleaseNotesGenerator - Пропустить @semantic-release/release-notes-generator
76
93
  * @returns {Object} Конфигурация semantic-release
@@ -86,12 +103,13 @@ function createSemanticReleaseConfig({
86
103
  }) {
87
104
  const plugins = [commonPlugins.commitAnalyzer]
88
105
 
106
+ // Задачи важнее коммитов: их текст идёт в заметках первым.
107
+ plugins.push(...additionalPlugins)
108
+
89
109
  if (!skipReleaseNotesGenerator) {
90
110
  plugins.push(commonPlugins.releaseNotesGenerator)
91
111
  }
92
112
 
93
- plugins.push(...additionalPlugins)
94
-
95
113
  plugins.push(commonPlugins.changelog)
96
114
 
97
115
  if (npmConfig !== null) {
@@ -114,5 +132,6 @@ function createSemanticReleaseConfig({
114
132
  export {
115
133
  commonPlugins,
116
134
  createSemanticReleaseConfig,
135
+ issuesReleaseNotesPlugin,
117
136
  releaseNotesGeneratorConfig,
118
137
  }
@@ -0,0 +1,494 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ /**
7
+ * process.argv:
8
+ * - 0 index: "node" executable path.
9
+ * - 1 index: Script file path.
10
+ * - 2+ indexes: Custom arguments passed from "semantic-release/exec".
11
+ *
12
+ * We slice from index 2 to keep only release-related args.
13
+ * See "common.js".
14
+ */
15
+ const [lastTagArg, nextTagArg] = process.argv.slice(2)
16
+
17
+ // Token is passed from GitHub Actions env and used for all API calls
18
+ const githubToken = process.env.RELEASE_NOTES_GH_TOKEN
19
+
20
+ const currentProjectOwner = process.env.GITHUB_REPOSITORY?.split('/')[0]
21
+ const currentProjectRepo = process.env.GITHUB_REPOSITORY?.split('/')[1]
22
+
23
+ // Repository with all issues. Not this project repository.
24
+ // Used when PR text contains "Issue: #123".
25
+ const releaseNotesIssuesRepo = process.env.RELEASE_NOTES_ISSUES_REPO
26
+
27
+ const issuesProjectOwner = releaseNotesIssuesRepo?.split('/')[0]
28
+ const issuesProjectRepo = releaseNotesIssuesRepo?.split('/')[1]
29
+
30
+ if (!currentProjectOwner || !currentProjectRepo || !githubToken) {
31
+ throw new Error('Missing "GITHUB_REPOSITORY" or "RELEASE_NOTES_GH_TOKEN"')
32
+ }
33
+
34
+ if (!issuesProjectOwner || !issuesProjectRepo) {
35
+ throw new Error('Missing "RELEASE_NOTES_ISSUES_REPO"')
36
+ }
37
+
38
+ // Only issues from this repo belong in release notes (ignore dependabot/changelog refs).
39
+ const allowedIssuesRepo = `${issuesProjectOwner}/${issuesProjectRepo}`
40
+
41
+ // Load issues in small batches to avoid API request spikes.
42
+ const ISSUE_LOAD_CONCURRENCY = 10
43
+
44
+ /**
45
+ * Allowed PR target branches used to filter associated PRs from commit lookups.
46
+ */
47
+ const allowedPrBaseBranches = [
48
+ 'develop',
49
+ 'master',
50
+ 'docker/develop',
51
+ 'docker/master',
52
+ ]
53
+
54
+ // "semantic-release" can pass "null" string when there is no previous tag.
55
+ const lastTag = lastTagArg && lastTagArg !== 'null' ? lastTagArg : null
56
+ const nextTag = nextTagArg && nextTagArg !== 'null' ? nextTagArg : null
57
+
58
+ /**
59
+ * Matches issue markers in PR body, for example:
60
+ * - `issue: itcase/agro-management-pps#123`;
61
+ * - `ref: #123`;
62
+ * - `closes owner/repo#123`;
63
+ */
64
+ const issueRefRegex =
65
+ /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|refs?|issue)\s*:?\s*(?:(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+))?#(?<number>\d+)\b/gi
66
+
67
+ // Matches full repository references anywhere in body: "owner/repo#123".
68
+ const fullRepoIssueRegex =
69
+ /\b(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+)#(?<number>\d+)\b/gi
70
+
71
+ // Matches full GitHub issue URLs, for example:
72
+ // "https://github.com/itcase/agro-management-pps/issues/123".
73
+ const githubIssueUrlRegex =
74
+ /\bhttps?:\/\/github\.com\/(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+)\/issues\/(?<number>\d+)\b/gi
75
+
76
+ let githubApiCallsCount = 0
77
+ // Track minimal remaining budget across all API calls in one run
78
+ let githubApiMinRemaining = Number.POSITIVE_INFINITY
79
+ let githubApiLastRemaining = null
80
+ let githubApiLastReset = null
81
+
82
+ async function run() {
83
+ /**
84
+ * https://www.npmjs.com/package/@semantic-release/exec
85
+ * generateNotesCmd:
86
+ * - `stdout`: Only the release note must be written to stdout.
87
+ * - `stderr`: Can be used for logging.
88
+ */
89
+ console.error(
90
+ `[release-notes] Input args: lastTagArg="${lastTagArg}", nextTagArg="${nextTagArg}", lastTag="${lastTag}", nextTag="${nextTag}"`,
91
+ )
92
+
93
+ // First release or missing tags: commit notes from semantic-release stay as-is.
94
+ if (!lastTag || !nextTag) {
95
+ console.error(
96
+ '[release-notes] Exit: Missing "lastTag" or "nextTag" arguments',
97
+ )
98
+ process.stdout.write('')
99
+ return
100
+ }
101
+
102
+ const cachePath = getCachePath(lastTag, nextTag)
103
+ const cachedNotes = readCachedNotes(cachePath)
104
+ if (cachedNotes !== null) {
105
+ console.error(`[release-notes] Cache hit: ${cachePath}`)
106
+ process.stdout.write(cachedNotes)
107
+ return
108
+ }
109
+
110
+ // "nextTag" may not exist yet during generateNotes step.
111
+ // Build range from previous release tag to current HEAD state.
112
+ const fromDateIso = getGitRefDate(lastTag)
113
+ const toDateIso = getGitRefDate('HEAD')
114
+
115
+ console.error(
116
+ `[release-notes] Processing release range: from ${lastTag} (${fromDateIso}) to HEAD (${toDateIso})`,
117
+ )
118
+
119
+ /**
120
+ * IMPORTANT:
121
+ * Release notes are based on commits that actually entered the release
122
+ * branch in range <lastTag..HEAD>, not on PR "merged_at"
123
+ * dates by search API (/search/issues).
124
+ *
125
+ * Why:
126
+ * PR can be merged into "develop" earlier, but reach "docker/master" only later.
127
+ * If we filter only by PR merge date, we can miss changes that are in release,
128
+ * or include changes that are not yet in release branch.
129
+ *
130
+ * For example:
131
+ * We merge some PR to "develop". After some time we create release
132
+ * from "docker/develop" to "docker/master". PR is merged into "develop"
133
+ * and NOT released yet, date of merge will be earlier than new release.
134
+ * On second release we will merge this PR to "docker/master" from "develop"
135
+ * and check it by included commits, not time of merge this PR.
136
+ */
137
+ const releaseCommitsList = getGitCommitsInRange(lastTag, 'HEAD')
138
+ // Deduplicate SHAs before commit->PR lookups to avoid repeated API calls
139
+ // when the git log output contains duplicated commits (perhaps in some cases)
140
+ const uniqueCommitShasList = [
141
+ ...new Set(releaseCommitsList.map((commit) => commit.sha)),
142
+ ]
143
+
144
+ console.error(
145
+ `[release-notes] Commits in release range: total=${releaseCommitsList.length}, unique=${uniqueCommitShasList.length}`,
146
+ )
147
+
148
+ const associatedPullRequestsMap = new Map()
149
+ let pullRequestLookups = 0
150
+ for (const commitSha of uniqueCommitShasList) {
151
+ // Ask GitHub which PRs are associated with each commit in the release range.
152
+ const associatedPullRequestsList =
153
+ await getAssociatedPullRequestsForCommit(commitSha)
154
+ pullRequestLookups += 1
155
+
156
+ for (const pullRequest of associatedPullRequestsList) {
157
+ // Keep PRs unique by PR number.
158
+ associatedPullRequestsMap.set(pullRequest.number, pullRequest)
159
+ }
160
+ }
161
+ const associatedPullRequestsList = [...associatedPullRequestsMap.values()]
162
+ /**
163
+ * GitHub can associate the same commit with many PRs (for example when branches
164
+ * are regularly synced with "develop"). Without filtering, this produces false
165
+ * issue links in release notes.
166
+ *
167
+ * We keep only "release-relevant" PRs:
168
+ * - merged PRs only ("closed" + "merged_at")
169
+ * - merged into an allowed base branch for this release flow
170
+ */
171
+ const pullRequestFilterStats = {
172
+ excludedByBaseBranch: 0,
173
+ excludedNotMerged: 0,
174
+ included: 0,
175
+ }
176
+ const filteredPullRequestsList = associatedPullRequestsList.filter(
177
+ (pullRequest) => {
178
+ if (!pullRequest.merged_at || pullRequest.state !== 'closed') {
179
+ pullRequestFilterStats.excludedNotMerged += 1
180
+ return false
181
+ }
182
+
183
+ const baseRef = pullRequest.base?.ref
184
+ // Keep only PRs merged into the release flow branch.
185
+ if (!baseRef || !allowedPrBaseBranches.includes(baseRef)) {
186
+ pullRequestFilterStats.excludedByBaseBranch += 1
187
+ return false
188
+ }
189
+
190
+ pullRequestFilterStats.included += 1
191
+ return true
192
+ },
193
+ )
194
+ console.error(
195
+ `[release-notes] Associated PR lookups: ${pullRequestLookups}, unique associated PRs: ${associatedPullRequestsList.length}, included after filters: ${filteredPullRequestsList.length}`,
196
+ )
197
+ console.error(
198
+ `[release-notes] PR filter stats: ${JSON.stringify(pullRequestFilterStats)};`,
199
+ )
200
+
201
+ // Use "Set" to deduplicate issue references collected from PRs and commits.
202
+ const issueRefsSet = new Set()
203
+ for (const pullRequest of filteredPullRequestsList) {
204
+ extractIssueRefs(pullRequest.title, issueRefsSet)
205
+ extractIssueRefs(pullRequest.body, issueRefsSet)
206
+ }
207
+ for (const commitInfo of releaseCommitsList) {
208
+ extractIssueRefs(commitInfo.subject, issueRefsSet)
209
+ extractIssueRefs(commitInfo.body, issueRefsSet)
210
+ }
211
+
212
+ console.error(
213
+ `[release-notes] Unique issue refs extracted from PR+commits: ${issueRefsSet.size}`,
214
+ )
215
+
216
+ if (issueRefsSet.size === 0) {
217
+ emitNotes(cachePath, '')
218
+ return
219
+ }
220
+
221
+ const allIssueRefsList = [...issueRefsSet]
222
+ const issueRefsList = allIssueRefsList.filter((ref) =>
223
+ ref.startsWith(`${allowedIssuesRepo}#`),
224
+ )
225
+ const excludedForeignRefsCount =
226
+ allIssueRefsList.length - issueRefsList.length
227
+
228
+ if (excludedForeignRefsCount > 0) {
229
+ const excludedRefsList = allIssueRefsList.filter(
230
+ (ref) => !ref.startsWith(`${allowedIssuesRepo}#`),
231
+ )
232
+ console.error(
233
+ `[release-notes] Excluded ${excludedForeignRefsCount} foreign issue refs (dependabot/changelog noise): ${excludedRefsList.join(', ')}`,
234
+ )
235
+ }
236
+
237
+ if (issueRefsList.length === 0) {
238
+ emitNotes(cachePath, '')
239
+ return
240
+ }
241
+
242
+ console.error(
243
+ `[release-notes] Issue refs to load (${allowedIssuesRepo}): ${issueRefsList.join(', ')}`,
244
+ )
245
+
246
+ const issuesList = await loadIssuesInBatches(issueRefsList)
247
+
248
+ // Issues endpoint can also return PRs("issue-like" entities)
249
+ const lines = issuesList
250
+ // Exclude pull requests from release notes
251
+ .filter((issue) => !issue.pull_request)
252
+ .sort((first, second) => first.number - second.number)
253
+ .map((issue) => `- ${issue.title} [#${issue.number}](${issue.html_url})`)
254
+
255
+ console.error(
256
+ `[release-notes] Issues loaded: ${issuesList.length}, issues included in release notes: ${lines.length}`,
257
+ )
258
+ console.error(
259
+ `[release-notes] GitHub API usage: calls=${githubApiCallsCount}, minRateRemaining=${githubApiMinRemaining === Number.POSITIVE_INFINITY ? 'n/a' : githubApiMinRemaining}, lastRateRemaining=${githubApiLastRemaining ?? 'n/a'}, lastRateReset=${githubApiLastReset ?? 'n/a'}`,
260
+ )
261
+
262
+ if (lines.length === 0) {
263
+ emitNotes(cachePath, '')
264
+ return
265
+ }
266
+
267
+ const releaseNotes = `## Что изменилось?\n\n${lines.join('\n')}\n`
268
+ emitNotes(cachePath, releaseNotes)
269
+ }
270
+
271
+ // Small helper for GitHub REST API with common headers and query serialization.
272
+ async function fetchGithubApi(pathname, queryParams = {}) {
273
+ const apiUrl = new URL(`https://api.github.com${pathname}`)
274
+
275
+ for (const [key, value] of Object.entries(queryParams)) {
276
+ if (value !== undefined && value !== null && value !== '') {
277
+ apiUrl.searchParams.set(key, String(value))
278
+ }
279
+ }
280
+
281
+ // https://docs.github.com/ru/rest/about-the-rest-api/api-versions?apiVersion=2026-03-10#supported-api-versions
282
+ const response = await fetch(apiUrl, {
283
+ headers: {
284
+ Accept: 'application/vnd.github+json',
285
+ Authorization: `Bearer ${githubToken}`,
286
+ 'User-Agent': 'semantic-release-issues-notes',
287
+ // GitHub REST API version header
288
+ 'X-GitHub-Api-Version': '2026-03-10',
289
+ },
290
+ })
291
+
292
+ if (!response.ok) {
293
+ const { status, statusText } = response
294
+ const message = await response.text()
295
+ const serializedQuery = JSON.stringify(queryParams)
296
+ throw new Error(
297
+ `GitHub API request failed: ${statusText} (${status}) - ${message}; path="${pathname}"; url="${apiUrl.toString()}"; query=${serializedQuery}`,
298
+ )
299
+ }
300
+
301
+ // Successful response: collect basic rate-limit diagnostics for CI logs
302
+ githubApiCallsCount += 1
303
+ const rateRemainingHeader = response.headers.get('x-ratelimit-remaining')
304
+ const rateResetHeader = response.headers.get('x-ratelimit-reset')
305
+ const rateRemaining = Number(rateRemainingHeader)
306
+ if (!Number.isNaN(rateRemaining)) {
307
+ githubApiLastRemaining = rateRemaining
308
+ githubApiMinRemaining = Math.min(githubApiMinRemaining, rateRemaining)
309
+ }
310
+ if (rateResetHeader) {
311
+ // API returns UNIX seconds, convert to ISO for readable logs
312
+ githubApiLastReset = new Date(Number(rateResetHeader) * 1000).toISOString()
313
+ }
314
+
315
+ return response.json()
316
+ }
317
+
318
+ function getGitRefDate(refName) {
319
+ try {
320
+ // Returns date in ISO format, e.g. "2026-03-24T12:25:32+00:00"
321
+ const dateIso = execSync(`git log -1 --format=%cI ${refName}`, {
322
+ encoding: 'utf-8',
323
+ })
324
+ return dateIso.trim()
325
+ } catch (error) {
326
+ throw new Error(`Failed to read git date for ref "${refName}"`, {
327
+ cause: error,
328
+ })
329
+ }
330
+ }
331
+
332
+ function extractIssueRefs(text, refs) {
333
+ if (!text) {
334
+ return
335
+ }
336
+
337
+ // Marker-based extraction (Issue/Refs/Fixes/Closes/etc).
338
+ for (const match of text.matchAll(issueRefRegex)) {
339
+ const issueOwner = match.groups?.owner || issuesProjectOwner
340
+ const issueRepo = match.groups?.repo || issuesProjectRepo
341
+ const issueNumber = match.groups?.number
342
+
343
+ if (!issueOwner || !issueRepo || !issueNumber) {
344
+ continue
345
+ }
346
+
347
+ refs.add(`${issueOwner}/${issueRepo}#${issueNumber}`)
348
+ }
349
+
350
+ // Fallback extraction for explicit full refs: owner/repo#number.
351
+ for (const match of text.matchAll(fullRepoIssueRegex)) {
352
+ const issueOwner = match.groups?.owner
353
+ const issueRepo = match.groups?.repo
354
+ const issueNumber = match.groups?.number
355
+
356
+ if (!issueOwner || !issueRepo || !issueNumber) {
357
+ continue
358
+ }
359
+
360
+ refs.add(`${issueOwner}/${issueRepo}#${issueNumber}`)
361
+ }
362
+
363
+ // Also support full GitHub issue URLs copied from browser address bar.
364
+ for (const match of text.matchAll(githubIssueUrlRegex)) {
365
+ const issueOwner = match.groups?.owner
366
+ const issueRepo = match.groups?.repo
367
+ const issueNumber = match.groups?.number
368
+
369
+ if (!issueOwner || !issueRepo || !issueNumber) {
370
+ continue
371
+ }
372
+
373
+ refs.add(`${issueOwner}/${issueRepo}#${issueNumber}`)
374
+ }
375
+ }
376
+
377
+ function getGitCommitsInRange(fromRef, toRef) {
378
+ /**
379
+ * We serialize each commit with `git log --format=%H%x1f%s%x1f%b%x1e`.
380
+ *
381
+ * Field mapping:
382
+ * - `%H` -> sha
383
+ * - `%s` -> subject
384
+ * - `%b` -> body
385
+ *
386
+ * Separators:
387
+ * - `\x1f` separates fields inside one commit record
388
+ * - `\x1e` separates commit records
389
+ *
390
+ * We use ASCII control separators to safely parse multiline commit bodies and avoid
391
+ * ambiguity with regular punctuation/symbols that can appear in commit messages.
392
+ *
393
+ * About commit message structure:
394
+ * - `subject` is the first line of the commit message
395
+ * - `body` is the text after an empty line, if present
396
+ *
397
+ * Example:
398
+ * - commit message: `feat(report): add table summary`
399
+ * - `subject`: `feat(report): add table summary`
400
+ * - `body`: empty string
401
+ *
402
+ * In this format `%s` is always the subject, while `%b` may be empty and that is valid.
403
+ *
404
+ * After splitting one record by `\x1f`, a healthy record should contain at least
405
+ * 3 fields: [sha, subject, body]. We keep `.filter((fields) => fields.length >= 3)`
406
+ * as a guard against malformed/partial chunks (for example, tail artifacts after split)
407
+ * so that downstream destructuring remains safe.
408
+ */
409
+ const rawOutput = execSync(
410
+ `git log --format=%H%x1f%s%x1f%b%x1e ${fromRef}..${toRef}`,
411
+ { encoding: 'utf-8' },
412
+ )
413
+
414
+ return rawOutput
415
+ .split('\x1e')
416
+ .filter(Boolean)
417
+ .map((record) => record.split('\x1f'))
418
+ .filter((fields) => fields.length >= 3)
419
+ .map(([sha, subject, body]) => ({
420
+ body: body?.trim() || '',
421
+ sha: sha.trim(),
422
+ subject: subject?.trim() || '',
423
+ }))
424
+ }
425
+
426
+ async function getAssociatedPullRequestsForCommit(commitSha) {
427
+ // GitHub endpoint that returns PRs linked to a commit SHA.
428
+ // Works for both merge-commit and regular commits associated with PRs.
429
+ const response = await fetchGithubApi(
430
+ `/repos/${currentProjectOwner}/${currentProjectRepo}/commits/${commitSha}/pulls`,
431
+ )
432
+ return Array.isArray(response) ? response : []
433
+ }
434
+
435
+ async function loadIssuesInBatches(refs) {
436
+ const issuesList = []
437
+
438
+ for (let index = 0; index < refs.length; index += ISSUE_LOAD_CONCURRENCY) {
439
+ const batch = refs.slice(index, index + ISSUE_LOAD_CONCURRENCY)
440
+ const batchResults = await Promise.all(batch.map(loadIssue))
441
+ issuesList.push(...batchResults)
442
+ }
443
+
444
+ return issuesList
445
+ }
446
+
447
+ async function loadIssue(ref) {
448
+ const [repoRef, issueNumber] = ref.split('#')
449
+ const [issueOwner, issueRepo] = repoRef.split('/')
450
+ const issuePath = `/repos/${issueOwner}/${issueRepo}/issues/${issueNumber}`
451
+ try {
452
+ return await fetchGithubApi(issuePath)
453
+ } catch (error) {
454
+ throw new Error(
455
+ `Failed to load issue for ref="${ref}" using path="${issuePath}"`,
456
+ { cause: error },
457
+ )
458
+ }
459
+ }
460
+
461
+ function getCachePath(lastTag, nextTag) {
462
+ const runId = process.env.GITHUB_RUN_ID || 'local'
463
+ const safeLastTag = lastTag.replace(/[^a-zA-Z0-9._-]/g, '_')
464
+ const safeNextTag = nextTag.replace(/[^a-zA-Z0-9._-]/g, '_')
465
+ const cacheDir = process.env.RUNNER_TEMP || tmpdir()
466
+
467
+ return join(
468
+ cacheDir,
469
+ `release-notes-${runId}-${safeLastTag}-${safeNextTag}.md`,
470
+ )
471
+ }
472
+
473
+ function readCachedNotes(cachePath) {
474
+ if (!existsSync(cachePath)) {
475
+ return null
476
+ }
477
+
478
+ return readFileSync(cachePath, 'utf-8')
479
+ }
480
+
481
+ function writeCachedNotes(cachePath, notes) {
482
+ writeFileSync(cachePath, notes, 'utf-8')
483
+ console.error(`[release-notes] Cached notes: ${cachePath}`)
484
+ }
485
+
486
+ function emitNotes(cachePath, notes) {
487
+ writeCachedNotes(cachePath, notes)
488
+ process.stdout.write(notes)
489
+ }
490
+
491
+ run().catch((error) => {
492
+ console.error(error)
493
+ process.exit(1)
494
+ })
@@ -1,4 +1,7 @@
1
- import { createSemanticReleaseConfig } from './common.js'
1
+ import {
2
+ createSemanticReleaseConfig,
3
+ issuesReleaseNotesPlugin,
4
+ } from './common.js'
2
5
 
3
6
  const DEFAULT_VERSION_ASSETS = [
4
7
  'package.json',
@@ -15,6 +18,7 @@ const DEFAULT_RELEASE_ASSETS = ['builds/*.apk']
15
18
  * - Не публикует в npm
16
19
  * - Коммитит package + Android version-файлы
17
20
  * - Прикрепляет к GitHub release APK из `builds/` (если есть; отсутствующие пропускаются)
21
+ * - Заметки релиза: сначала задачи из issuesReleaseNotes.js, если они есть, потом обычные коммиты
18
22
  *
19
23
  * Сборку APK делайте в prepare проекта или отдельным CI job до publish.
20
24
  *
@@ -35,6 +39,7 @@ function createSemanticReleaseApp({
35
39
  versionAssets = DEFAULT_VERSION_ASSETS,
36
40
  } = {}) {
37
41
  return createSemanticReleaseConfig({
42
+ additionalPlugins: [issuesReleaseNotesPlugin],
38
43
  branches: branches,
39
44
  gitConfig: {
40
45
  message:
@@ -3,10 +3,7 @@
3
3
  import { readFile, writeFile } from 'node:fs/promises'
4
4
  import { resolve } from 'node:path'
5
5
 
6
- import {
7
- createSemanticReleaseConfig,
8
- releaseNotesGeneratorConfig,
9
- } from './common.js'
6
+ import { createSemanticReleaseConfig } from './common.js'
10
7
 
11
8
  const RELEASE_ASSETS = [
12
9
  'lerna.json',
@@ -44,17 +41,14 @@ const semanticReleaseMonorepo = createSemanticReleaseConfig({
44
41
  [
45
42
  'semantic-release-lerna',
46
43
  {
47
- generateNotes: true,
44
+ // Заметки lerna для монорепо выходят пустыми. Их пишет
45
+ // @semantic-release/release-notes-generator, как в обычных пакетах.
46
+ generateNotes: false,
48
47
  latch: false,
49
- ...releaseNotesGeneratorConfig,
50
- host: 'https://github.com',
51
- commit: 'commit',
52
- issue: 'issues',
53
48
  },
54
49
  ],
55
50
  ],
56
51
  preparePlugins: [keepLernaIndependentPlugin],
57
- skipReleaseNotesGenerator: true,
58
52
  })
59
53
 
60
54
  export { semanticReleaseMonorepo }
@@ -1,6 +1,10 @@
1
- import { createSemanticReleaseConfig } from './common.js'
1
+ import {
2
+ createSemanticReleaseConfig,
3
+ issuesReleaseNotesPlugin,
4
+ } from './common.js'
2
5
 
3
6
  const semanticReleaseProject = createSemanticReleaseConfig({
7
+ additionalPlugins: [issuesReleaseNotesPlugin],
4
8
  branches: ['docker/master'],
5
9
  gitConfig: {
6
10
  message: 'release: publishing new version ${nextRelease.version} [skip ci]',