@vv0rkz/js-template 1.7.0 → 1.8.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/README.md +224 -150
- package/bin/cli.js +227 -196
- package/bin/init.js +244 -236
- package/package.json +1 -1
- package/templates/.husky/commit-msg +3 -67
- package/templates/.husky/post-commit +3 -25
- package/templates/.husky/pre-push +3 -17
- package/templates/changelog.config.js +16 -9
- package/templates/commitlint.config.js +13 -6
- package/templates/jst.config.js +125 -0
- package/tools-gh/check-demo-for-release.js +36 -30
- package/tools-gh/config.js +126 -0
- package/tools-gh/post-commit-hook.js +40 -0
- package/tools-gh/release.js +120 -102
- package/tools-gh/report-issue.js +32 -0
- package/tools-gh/setup-deps.js +59 -0
- package/tools-gh/setup-labels.js +53 -58
- package/tools-gh/upgrade.js +72 -0
- package/tools-gh/validate-branch.js +33 -0
- package/tools-gh/validate-commit.js +86 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
1
|
+
let types = ['feat', 'fix', 'refactor', 'build', 'chore', 'docs', 'perf']
|
|
2
|
+
|
|
3
|
+
try {
|
|
4
|
+
const { default: config } = await import('./jst.config.js')
|
|
5
|
+
if (config.commits?.types) types = config.commits.types
|
|
6
|
+
} catch {}
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
extends: ['@commitlint/config-conventional'],
|
|
10
|
+
rules: {
|
|
11
|
+
'type-enum': [2, 'always', types],
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JST Configuration
|
|
3
|
+
* Документация: https://github.com/vv0rkz/js-template
|
|
4
|
+
*
|
|
5
|
+
* Все значения ниже — дефолтные. Измени нужные или закомментируй.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
/**
|
|
9
|
+
* Правила именования веток
|
|
10
|
+
*/
|
|
11
|
+
branch: {
|
|
12
|
+
/** Имя главной ветки ('main' | 'master') */
|
|
13
|
+
main: 'main',
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Допустимые форматы веток (массив шаблонов)
|
|
17
|
+
*
|
|
18
|
+
* Плейсхолдеры:
|
|
19
|
+
* {version} → семвер (X.Y.Z)
|
|
20
|
+
* {issue} → номер issue (цифры)
|
|
21
|
+
* {name} → описание (буквы, цифры, дефисы, подчёркивания)
|
|
22
|
+
* * → что угодно
|
|
23
|
+
*
|
|
24
|
+
* Примеры наборов:
|
|
25
|
+
*
|
|
26
|
+
* ['v{version}-{name}']
|
|
27
|
+
* ✅ v2.3.0-normalize-operators
|
|
28
|
+
*
|
|
29
|
+
* ['release/v{version}', 'feature/#{issue}-{name}', 'fix/#{issue}-{name}']
|
|
30
|
+
* ✅ release/v2.3.0
|
|
31
|
+
* ✅ feature/#12-dark-theme
|
|
32
|
+
* ✅ fix/#7-validation-bug
|
|
33
|
+
*
|
|
34
|
+
* ['feature/*', 'fix/*', 'release/*']
|
|
35
|
+
* ✅ feature/anything-goes-here
|
|
36
|
+
* ✅ fix/urgent-patch
|
|
37
|
+
*
|
|
38
|
+
* ['*']
|
|
39
|
+
* ✅ любое имя (без ограничений)
|
|
40
|
+
*/
|
|
41
|
+
patterns: ['v{version}-{name}'],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* GitHub Labels
|
|
46
|
+
* Используются при `jst setup-labels` и `jst create-*`
|
|
47
|
+
*
|
|
48
|
+
* Цвета — 6-символьный hex без #
|
|
49
|
+
*/
|
|
50
|
+
labels: [
|
|
51
|
+
{ name: 'task', color: '0E8A16', description: 'Новая фича', emoji: '✨' },
|
|
52
|
+
{ name: 'bug', color: 'D73A4A', description: 'Баг', emoji: '🐛' },
|
|
53
|
+
{ name: 'refactor', color: 'FEF2C0', description: 'Рефакторинг/техдолг', emoji: '♻️' },
|
|
54
|
+
{ name: 'perf', color: '007bff', description: 'Оптимизация производительности', emoji: '⚡' },
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Правила коммитов
|
|
59
|
+
*/
|
|
60
|
+
commits: {
|
|
61
|
+
/**
|
|
62
|
+
* Разрешённые типы коммитов
|
|
63
|
+
* Должны совпадать с commitlint и changelog
|
|
64
|
+
*/
|
|
65
|
+
types: ['feat', 'fix', 'refactor', 'build', 'chore', 'docs', 'perf'],
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Типы, требующие номер issue (#N)
|
|
69
|
+
* Остальные типы — свободный формат
|
|
70
|
+
*/
|
|
71
|
+
requireIssue: ['feat', 'fix'],
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Ключевое слово для закрытия issue в коммите
|
|
75
|
+
*
|
|
76
|
+
* С ним: feat: close #9 описание → закроет issue #9
|
|
77
|
+
* Без: feat: #9 описание → просто ссылка, issue остаётся открытым
|
|
78
|
+
*/
|
|
79
|
+
closeKeyword: 'close',
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Настройки релиза (`jst release`)
|
|
84
|
+
*/
|
|
85
|
+
release: {
|
|
86
|
+
/** Требовать GIF/PNG демо перед релизом (true | false) */
|
|
87
|
+
requireDemo: true,
|
|
88
|
+
|
|
89
|
+
/** Директория для демо-файлов */
|
|
90
|
+
demoDir: 'docs',
|
|
91
|
+
|
|
92
|
+
/** Допустимые форматы демо-файлов */
|
|
93
|
+
demoFormats: ['gif', 'png'],
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Настройки changelog (changelogen)
|
|
98
|
+
* title — заголовок секции в CHANGELOG.md
|
|
99
|
+
* semver — тип версионирования ('major' | 'minor' | 'patch')
|
|
100
|
+
*/
|
|
101
|
+
changelog: {
|
|
102
|
+
types: {
|
|
103
|
+
feat: { title: '✨ Новые фичи', semver: 'minor' },
|
|
104
|
+
fix: { title: '🐛 Исправления', semver: 'patch' },
|
|
105
|
+
refactor: { title: '♻️ Рефакторинг', semver: 'patch' },
|
|
106
|
+
perf: { title: '⚡ Оптимизация', semver: 'patch' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Автообновление зависимостей
|
|
112
|
+
* @type {'dependabot' | 'renovate' | false}
|
|
113
|
+
*
|
|
114
|
+
* 'dependabot' — создаст .github/dependabot.yml
|
|
115
|
+
* 'renovate' — создаст renovate.json
|
|
116
|
+
* false — отключено
|
|
117
|
+
*/
|
|
118
|
+
depUpdater: false,
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Репозиторий JST для команды `report-issue`
|
|
122
|
+
* Формат: 'owner/repo'
|
|
123
|
+
*/
|
|
124
|
+
jstRepo: 'vv0rkz/js-template',
|
|
125
|
+
}
|
|
@@ -1,30 +1,36 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { execSync } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'fs'
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
import { loadConfig } from './config.js'
|
|
5
|
+
|
|
6
|
+
const config = await loadConfig()
|
|
7
|
+
|
|
8
|
+
if (!config.release.requireDemo) {
|
|
9
|
+
console.log('⏭️ Проверка демо отключена')
|
|
10
|
+
process.exit(0)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'))
|
|
14
|
+
const [major, minor, patch] = packageJson.version.split('.').map(Number)
|
|
15
|
+
|
|
16
|
+
const commitMessages = execSync('git log --oneline -10', { encoding: 'utf8' })
|
|
17
|
+
|
|
18
|
+
let nextVersion
|
|
19
|
+
if (commitMessages.includes('feat:')) {
|
|
20
|
+
nextVersion = `v${major}.${minor + 1}.0`
|
|
21
|
+
} else {
|
|
22
|
+
nextVersion = `v${major}.${minor}.${patch + 1}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`📦 Предполагаемая следующая версия: ${nextVersion}`)
|
|
26
|
+
|
|
27
|
+
const { demoDir, demoFormats } = config.release
|
|
28
|
+
const hasDemo = demoFormats.some((fmt) => existsSync(`${demoDir}/${nextVersion}.${fmt}`))
|
|
29
|
+
|
|
30
|
+
if (!hasDemo) {
|
|
31
|
+
console.log(`❌ Релиз ${nextVersion} требует демо!`)
|
|
32
|
+
console.log(`📸 Создай: ${demoDir}/${nextVersion}.${demoFormats[0]}`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`✅ Демо для ${nextVersion} готово!`)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { pathToFileURL } from 'url'
|
|
4
|
+
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
branch: {
|
|
7
|
+
main: 'main',
|
|
8
|
+
patterns: ['v{version}-{name}'],
|
|
9
|
+
},
|
|
10
|
+
labels: [
|
|
11
|
+
{ name: 'task', color: '0E8A16', description: 'Новая фича', emoji: '✨' },
|
|
12
|
+
{ name: 'bug', color: 'D73A4A', description: 'Баг', emoji: '🐛' },
|
|
13
|
+
{ name: 'refactor', color: 'FEF2C0', description: 'Рефакторинг/техдолг', emoji: '♻️' },
|
|
14
|
+
{ name: 'perf', color: '007bff', description: 'Оптимизация производительности', emoji: '⚡' },
|
|
15
|
+
],
|
|
16
|
+
commits: {
|
|
17
|
+
types: ['feat', 'fix', 'refactor', 'build', 'chore', 'docs', 'perf'],
|
|
18
|
+
requireIssue: ['feat', 'fix'],
|
|
19
|
+
closeKeyword: 'close',
|
|
20
|
+
},
|
|
21
|
+
release: {
|
|
22
|
+
requireDemo: true,
|
|
23
|
+
demoDir: 'docs',
|
|
24
|
+
demoFormats: ['gif', 'png'],
|
|
25
|
+
},
|
|
26
|
+
changelog: {
|
|
27
|
+
types: {
|
|
28
|
+
feat: { title: '✨ Новые фичи', semver: 'minor' },
|
|
29
|
+
fix: { title: '🐛 Исправления', semver: 'patch' },
|
|
30
|
+
refactor: { title: '♻️ Рефакторинг', semver: 'patch' },
|
|
31
|
+
perf: { title: '⚡ Оптимизация', semver: 'patch' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
depUpdater: false,
|
|
35
|
+
jstRepo: 'vv0rkz/js-template',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Branch pattern template engine ---
|
|
39
|
+
|
|
40
|
+
const BRANCH_PLACEHOLDERS = {
|
|
41
|
+
'{version}': '\\d+\\.\\d+\\.\\d+',
|
|
42
|
+
'{issue}': '\\d+',
|
|
43
|
+
'{name}': '[a-zA-Z0-9_-]+',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compileTemplate(template) {
|
|
47
|
+
const parts = template.split(/(\{[a-z]+\}|\*)/g)
|
|
48
|
+
return parts
|
|
49
|
+
.map((part) => {
|
|
50
|
+
if (BRANCH_PLACEHOLDERS[part]) return BRANCH_PLACEHOLDERS[part]
|
|
51
|
+
if (part === '*') return '.+'
|
|
52
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
53
|
+
})
|
|
54
|
+
.join('')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compiles branch config into a RegExp.
|
|
59
|
+
* Supports template patterns (new) and raw regex (legacy).
|
|
60
|
+
*
|
|
61
|
+
* Templates:
|
|
62
|
+
* {version} → X.Y.Z (semver)
|
|
63
|
+
* {issue} → issue number (digits)
|
|
64
|
+
* {name} → branch description (letters, digits, hyphens, underscores)
|
|
65
|
+
* * → anything
|
|
66
|
+
*/
|
|
67
|
+
export function compileBranchRegex(branchConfig) {
|
|
68
|
+
if (branchConfig.patterns && Array.isArray(branchConfig.patterns)) {
|
|
69
|
+
const compiled = branchConfig.patterns.map(compileTemplate)
|
|
70
|
+
return new RegExp(`^(${compiled.join('|')})$`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (branchConfig.pattern) {
|
|
74
|
+
return new RegExp(branchConfig.pattern)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new RegExp('^v.*-.*')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Deep merge ---
|
|
81
|
+
|
|
82
|
+
function deepMerge(target, source) {
|
|
83
|
+
const result = { ...target }
|
|
84
|
+
for (const key of Object.keys(source)) {
|
|
85
|
+
if (
|
|
86
|
+
source[key] &&
|
|
87
|
+
typeof source[key] === 'object' &&
|
|
88
|
+
!Array.isArray(source[key]) &&
|
|
89
|
+
target[key] &&
|
|
90
|
+
typeof target[key] === 'object' &&
|
|
91
|
+
!Array.isArray(target[key])
|
|
92
|
+
) {
|
|
93
|
+
result[key] = deepMerge(target[key], source[key])
|
|
94
|
+
} else {
|
|
95
|
+
result[key] = source[key]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Loads config from jst.config.js (preferred) or jst.config.json (fallback)
|
|
103
|
+
* Deep-merges user config with defaults
|
|
104
|
+
*/
|
|
105
|
+
export async function loadConfig() {
|
|
106
|
+
const jsPath = join(process.cwd(), 'jst.config.js')
|
|
107
|
+
const jsonPath = join(process.cwd(), 'jst.config.json')
|
|
108
|
+
let userConfig = {}
|
|
109
|
+
|
|
110
|
+
if (existsSync(jsPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const mod = await import(pathToFileURL(jsPath).href)
|
|
113
|
+
userConfig = mod.default || {}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.warn('⚠️ Ошибка чтения jst.config.js:', e.message)
|
|
116
|
+
}
|
|
117
|
+
} else if (existsSync(jsonPath)) {
|
|
118
|
+
try {
|
|
119
|
+
userConfig = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.warn('⚠️ Ошибка чтения jst.config.json:', e.message)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return deepMerge(DEFAULTS, userConfig)
|
|
126
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import { loadConfig } from './config.js'
|
|
4
|
+
|
|
5
|
+
const config = await loadConfig()
|
|
6
|
+
const { closeKeyword, requireIssue } = config.commits
|
|
7
|
+
|
|
8
|
+
const commitMsg = execSync('git log -1 --pretty=%B', { encoding: 'utf8' }).trim()
|
|
9
|
+
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim()
|
|
10
|
+
|
|
11
|
+
let repoUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim()
|
|
12
|
+
if (repoUrl.startsWith('git@github.com:')) {
|
|
13
|
+
repoUrl = repoUrl.replace('git@github.com:', 'https://github.com/').replace(/\.git$/, '')
|
|
14
|
+
} else {
|
|
15
|
+
repoUrl = repoUrl.replace(/\.git$/, '')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
execSync('git push origin HEAD', { stdio: 'inherit' })
|
|
19
|
+
|
|
20
|
+
const escClose = closeKeyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
21
|
+
const typesGroup = requireIssue.join('|')
|
|
22
|
+
const closePattern = new RegExp(`^(${typesGroup})(\\(.+\\))?: ${escClose} #(\\d+)`)
|
|
23
|
+
const match = commitMsg.match(closePattern)
|
|
24
|
+
|
|
25
|
+
if (match) {
|
|
26
|
+
const issueNumber = match[3]
|
|
27
|
+
const commitLink = `${repoUrl}/commit/${commitHash}`
|
|
28
|
+
const firstLine = commitMsg.split('\n')[0]
|
|
29
|
+
|
|
30
|
+
console.log(`🔧 Закрываю issue #${issueNumber}...`)
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
execSync(
|
|
34
|
+
`gh issue close "${issueNumber}" --comment "✅ Завершено в коммите: [${commitHash}](${commitLink}) - ${firstLine}"`,
|
|
35
|
+
{ stdio: 'inherit' },
|
|
36
|
+
)
|
|
37
|
+
} catch {
|
|
38
|
+
console.log(`⚠️ Не удалось закрыть issue #${issueNumber}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
package/tools-gh/release.js
CHANGED
|
@@ -1,102 +1,120 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execSync, spawnSync } from 'child_process'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 2.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.log(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(
|
|
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
|
-
console.log(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.log('\n
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process'
|
|
3
|
+
import { existsSync, readFileSync } from 'fs'
|
|
4
|
+
import { platform } from 'os'
|
|
5
|
+
import { dirname, join } from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import { loadConfig } from './config.js'
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const isWin = platform() === 'win32'
|
|
11
|
+
const npxCmd = isWin ? 'npx.cmd' : 'npx'
|
|
12
|
+
const config = await loadConfig()
|
|
13
|
+
|
|
14
|
+
console.log('🚀 Запуск релиза...\n')
|
|
15
|
+
|
|
16
|
+
// 1. Current version
|
|
17
|
+
const currentVersion = JSON.parse(readFileSync('package.json', 'utf8')).version
|
|
18
|
+
console.log(`📦 Текущая версия: ${currentVersion}`)
|
|
19
|
+
|
|
20
|
+
// 2. Demo check (configurable)
|
|
21
|
+
if (config.release.requireDemo) {
|
|
22
|
+
const [major, minor, patch] = currentVersion.split('.').map(Number)
|
|
23
|
+
const recentCommits = execSync('git log --oneline -10', { encoding: 'utf8' })
|
|
24
|
+
const nextVersion = recentCommits.includes('feat:')
|
|
25
|
+
? `v${major}.${minor + 1}.0`
|
|
26
|
+
: `v${major}.${minor}.${patch + 1}`
|
|
27
|
+
|
|
28
|
+
console.log(`📦 Предполагаемая следующая версия: ${nextVersion}`)
|
|
29
|
+
|
|
30
|
+
const { demoDir, demoFormats } = config.release
|
|
31
|
+
const hasDemo = demoFormats.some((fmt) => existsSync(`${demoDir}/${nextVersion}.${fmt}`))
|
|
32
|
+
|
|
33
|
+
if (!hasDemo) {
|
|
34
|
+
console.log(`❌ Релиз ${nextVersion} требует демо!`)
|
|
35
|
+
console.log(`📸 Создай: ${demoDir}/${nextVersion}.${demoFormats[0]}`)
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`✅ Демо для ${nextVersion} готово!`)
|
|
40
|
+
} else {
|
|
41
|
+
console.log('⏭️ Проверка демо отключена (release.requireDemo = false)')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Warning for 0.x.x
|
|
45
|
+
if (currentVersion.startsWith('0.')) {
|
|
46
|
+
console.log('\n⚠️ ВНИМАНИЕ: Версия 0.x.x имеет особое поведение!')
|
|
47
|
+
console.log('💡 changelogen для версий < 1.0.0:')
|
|
48
|
+
console.log(' • feat: коммиты → patch bump (0.1.0 → 0.1.1)')
|
|
49
|
+
console.log(' • fix: коммиты → patch bump (0.1.0 → 0.1.1)')
|
|
50
|
+
console.log(' • BREAKING CHANGE → minor bump (0.1.0 → 0.2.0)')
|
|
51
|
+
console.log('')
|
|
52
|
+
console.log('✅ Рекомендация: используй версии ≥ 1.0.0 для правильного semver')
|
|
53
|
+
console.log(' Обнови: npm pkg set version=1.0.0 && git tag v1.0.0\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. Commit analysis
|
|
57
|
+
let lastTag = ''
|
|
58
|
+
try {
|
|
59
|
+
lastTag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim()
|
|
60
|
+
console.log(`📌 Последний тег: ${lastTag}`)
|
|
61
|
+
} catch {
|
|
62
|
+
console.log('📌 Теги не найдены (первый релиз)')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const commitLog = lastTag
|
|
66
|
+
? execSync(`git log ${lastTag}..HEAD --format=%s`, { encoding: 'utf8' })
|
|
67
|
+
: execSync('git log --format=%s -10', { encoding: 'utf8' })
|
|
68
|
+
|
|
69
|
+
const commits = commitLog.split('\n').filter(Boolean)
|
|
70
|
+
const countByType = (prefix) => commits.filter((c) => c.startsWith(`${prefix}:`)).length
|
|
71
|
+
|
|
72
|
+
const featCount = countByType('feat')
|
|
73
|
+
const fixCount = countByType('fix')
|
|
74
|
+
const refactorCount = countByType('refactor')
|
|
75
|
+
const perfCount = countByType('perf')
|
|
76
|
+
const docsCount = countByType('docs')
|
|
77
|
+
const buildCount = countByType('build')
|
|
78
|
+
|
|
79
|
+
console.log('\n📊 Анализ коммитов:')
|
|
80
|
+
if (featCount) console.log(` ✨ feat: ${featCount}`)
|
|
81
|
+
if (fixCount) console.log(` 🐛 fix: ${fixCount}`)
|
|
82
|
+
if (refactorCount) console.log(` ♻️ refactor: ${refactorCount}`)
|
|
83
|
+
if (perfCount) console.log(` ⚡ perf: ${perfCount}`)
|
|
84
|
+
if (docsCount) console.log(` 📚 docs: ${docsCount}`)
|
|
85
|
+
if (buildCount) console.log(` 🏗️ build: ${buildCount}`)
|
|
86
|
+
|
|
87
|
+
// 5. Determine bump type
|
|
88
|
+
const isV0 = currentVersion.startsWith('0.')
|
|
89
|
+
let bumpType
|
|
90
|
+
|
|
91
|
+
if (isV0) {
|
|
92
|
+
bumpType = featCount > 0 ? 'major' : 'patch'
|
|
93
|
+
console.log(`\n🔢 Bump: ${bumpType === 'major' ? 'major (для 0.x.x это даст minor)' : 'patch'}`)
|
|
94
|
+
} else {
|
|
95
|
+
bumpType = featCount > 0 ? 'minor' : 'patch'
|
|
96
|
+
console.log(`\n🔢 Bump: ${bumpType}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. Changelog
|
|
100
|
+
console.log('\n📝 Создание changelog...')
|
|
101
|
+
const changelog = spawnSync(npxCmd, ['changelogen', '--release', `--${bumpType}`], {
|
|
102
|
+
stdio: 'inherit',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (changelog.status !== 0) {
|
|
106
|
+
console.error('\n❌ Ошибка создания changelog')
|
|
107
|
+
console.log('💡 Попробуй вручную: npx changelogen --release')
|
|
108
|
+
process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 7. Update README
|
|
112
|
+
console.log('\n📝 Обновление README...')
|
|
113
|
+
const updateReadme = spawnSync('node', [join(__dirname, 'update-readme.js')], { stdio: 'inherit' })
|
|
114
|
+
if (updateReadme.status !== 0) {
|
|
115
|
+
console.log('⚠️ README не обновлён (возможно нет секции AUTOGENERATED)')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('\n✅ Релиз успешно создан!')
|
|
119
|
+
console.log('💡 Теперь можно:')
|
|
120
|
+
console.log(' npm run _ push-release # Создать PR и смерджить в main')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'child_process'
|
|
3
|
+
import { platform } from 'os'
|
|
4
|
+
import { loadConfig } from './config.js'
|
|
5
|
+
|
|
6
|
+
const config = await loadConfig()
|
|
7
|
+
const jstRepo = config.jstRepo
|
|
8
|
+
const isWin = platform() === 'win32'
|
|
9
|
+
const ghCmd = isWin ? 'gh.exe' : 'gh'
|
|
10
|
+
|
|
11
|
+
const title = process.argv.slice(2).join(' ')
|
|
12
|
+
|
|
13
|
+
console.log(`📝 Создаю issue в ${jstRepo}...`)
|
|
14
|
+
|
|
15
|
+
if (!title) {
|
|
16
|
+
const result = spawnSync(ghCmd, ['issue', 'create', '--repo', jstRepo], { stdio: 'inherit' })
|
|
17
|
+
process.exit(result.status || 0)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = spawnSync(ghCmd, ['issue', 'create', '--repo', jstRepo, '--title', title], {
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (result.status === 0) {
|
|
25
|
+
console.log('\n✅ Issue создан!')
|
|
26
|
+
} else {
|
|
27
|
+
console.error('❌ Ошибка создания issue')
|
|
28
|
+
console.log('\n💡 Убедись что:')
|
|
29
|
+
console.log(' 1. Установлен GitHub CLI: gh --version')
|
|
30
|
+
console.log(' 2. Выполнена авторизация: gh auth login')
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|